キチログ

Keep it simple, stupid.

Helmの光と闇

https://helm.sh/

Helm使ってとある実装をしたので、メリットとデメリットを整理しておきたい。

Pros - 光

動的にパラメーターを埋め込める

KubernetesはManifestと呼ばれるyamlファイルを記述することで、コンテナの状態を宣言的に管理することができる。

しかし、複数の環境(テスト環境、本番環境など)があるとき、共通部分を多く持ち合わせることになるため、似たようなManifestをいくつも書かなければいけないケースがある。

Helmを使うと、テンプレートファイル(KubernetesのManifestファイル)に任意の値をセットしてデプロイすることができる。

ちなみにこのテンプレートファイルを含む独自のパッケージング機構のことをHelmではChartと呼ぶ。

例えば、以下のようなテンプレートファイルを定義する

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: {{ .Values.replicas }}
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

このとき、 テスト環境ではレプリカ数2、本番環境では4としたいとして、その部分を変数定義してくと、デプロイ(リリース)する際にそれぞれの環境でパラメーターを渡すことで(--set--values を使って)、マニフェストはそのままにデプロイ変数を変えることができる。

コマンドは以下例

// テスト環境
helm install --set replicas=2 test

// 本番環境
helm install --set replicas=4 production

こういうことしたいときに類似のツールとしてKustomizeがある。これはGoogleさんお手製で、去年 kubectl に取り込まれて標準となった。 kubectl -k で使える。

パッケージマネジャーとしてのHelm

また、アプリケーションをデプロイするとなるとたいていの場合、Deployments, Service, Configmapなどと、色々なKubernetesリソースが複合的に組み合わさって、一つのアプリケーションとして動作する。

Helmはこの複数のKubernetesリソースを、Packageという機能を使ってパッケージングしてくれる。

いや、Kubernetesマニフェストでも複数リソースの定義はできるじゃないか?

確かにできる。

一つのマニフェストのなかに --- で区切り線を使って定義するか、もしくはディレクトリに複数マニフェストファイルを入れて、 kubectl -f ./dir すればいい。

しかし、Helmはパッケージマネジャーとしてより強力な機能を提供している。

リリースバージョニング

Releaseを使うことで、Helmは複数のリソースからなる一つのアプリケーション単位で世代管理してくれる。

これはすなわち、サービスの単位でロールバックが可能ということだ。

ちなみに、KubernetesのDeploymentsがやってくれる世代管理はリソース単位。

ちなみに、先ほど紹介したKustomizeにはこの機能はなく、現状Helmの大きなアドバンテージと言っていいだろう。

Helm Chart Repository

例えば、datadogをクラスタにデプロイしたいとする。

自分でDeploymentを書いて、ServiceAccountやConfigMapを追加して、、とやるのが通常。

Helmにはみんながよく使うツールやライブラリをパッケージ化したチャートが豊富に用意されている。チャートはもちろん自作もできる。

なので、datadog入れたいってなったらチャートを持ってきて、動的にパラメータ変えたいとこ変えて install してしまえばdoneとなる。

要は、人気のツール使うときにごにょごにょ書かなきゃいけないのをほとんど書かなくてよくなって、設定値を渡すだけでよくなる。

これがPackageの恩恵。


ここまでは、Helmのありがたい機能を紹介したけど、実際使ってみて闇を感じたところもあったのでそちらも紹介していく。

Cons - 闇

"宣言的"ではない

動的に値を埋め込めるのはいい。パッケージングできるのもいい。

ただ、Helmを使った結果、宣言的な管理が難しくなる。

helm install --sethelm install --values という形のコマンドインタフェースしか持っていないため、パラメーター+テンプレートや複数のReleaseを宣言的に管理できない。

これを解決するためにhelmfileというOSSがある。

helmfileを使うと、状態のあるべき姿helmfile.yamlに記述することで、あとはhelmfile applyするだけでいい。

番外編: helmfile

確かに宣言的になるんだけど、一つ残念だったのが、すでにあるリリースを削除するときは明示的に宣言する必要がある。

つまりhelmfileからリリースの定義を消したからといって、環境から削除されるわけではない、ということ。

削除対象のリリースに以下を指定しなくてはならない。

# set `false` to uninstall on sync
installed: true

なお、一度これを適用して消えたのちにreleaseの定義ごと削除すればいい。

この辺の動作でGitOps的な実装のハンドリングが面倒だった。

黒魔術を使ったテンプレートファイル

Helmのチャートテンプレートのなかを覗くと突然 {{ .Values.hoge }} とか {{ include "temaki.labels" . | indent 4 }}とか出てくる。

これの正体は、GoのTemplateという標準ライブラリの記法だ。

HelmのソースコードはGoで書かれていて、パラメーターの変数埋め込みの機能の部分はこれがそのまま採用されている。

なので、良い感じのチャートを作るためには頑張ってこちらを学習する必要がある。

もともとGoを書いている人ならまだ良いけど、初見だとなんだこの記法は・・となるんじゃないだろうか?

というわけでプログラミング言語の標準そのままなので、やろうと思えばやりたいことは割となんでもできる。forとかifとか。

最初は・・・

マニフェストの定義をしていたつもりでも気づいたらあなたはプログラミングをしていることだろう。。。

Helmを触っていたつもりでも気づいたらGoを書いている人になっている。。。

その柔軟性ゆえに、Helmチャートは黒魔術化しやすい。

正直人が書いたチャート読みたくない・・・

でも自分でチャート描きたくない・・・・・

公開チャートは万能ではない

上で、datadogの例でチャートをインストールすればよしという話を下が、いつでもこの方法が全てを解決してくれるとは限らない。

なぜか?

パラメーター渡せば済むから簡単じゃないのか?

理由はそこにある。

本来のKubernetesマニフェストを書くことにHelmが独自の制約を作ることで、パラメータを渡すだけでよくなり、開発者の考えることを減らしてくれている。

裏返せば、そうして柔軟性を奪われているために、ここもパラメータで渡したいんだけど・・というケースには対応しないからだ。

提供されているチャートが自分のユースケースには合わない場合は、独自にチャートを変更する必要がある。

複数のRelease間であるリソースを共有できない

これはnicheな悩みだったので、具体的に困ったことをのべる。

以下のような構成をとりたかった。

Podには同じアプリケーションの違うバージョンが乗る。

このときはじめに作成したチャートのテンプレートには以下のリソースを含んだ。

Ingressは全リリースで共通にして、ルーティングルールで向き先を変える運用にしたかった。

が、結果的にこれは期待して期待していた挙動にはならなかった。

何が起こったかというと、例えばv1のリリースをデプロイする。そのあとにv2のリリースをデプロイすると、もともと存在したIngressリソースは削除され新たにIngressが再作成される動作になる。

そうなると何が起こるかというと、クラウドベンダー側ではロードバランサーが再作成されてしまい、IPアドレスとIDが変化するため、DNSのAレコードが外れてしまい名前解決されなくなってしまう事になる。

external-dnsを使ってDNSのレコードセットも同期させればいいじゃないとも思うが、新しいアプリケーション環境が追加されるたびにロードバランサが再作成されるのは、なんか不愉快だ。

というわけで、この問題については、IngressだけHelm Releaseを分けることで解決した。

以上!

僕は今回要件的にKustomizeだと難しそうだったのでHelmを採用しました。

標準でシンプルなKustomizeと比べるとなかなか闇深い面を備えているけれども、バージョニングしてくれるところとかチャートレポジトリにあやかれるところとかはHelmならではの良さですね。

Helm便利ですね。結構難しい要件にも対応できる。

GKEがEKSより優れている点をまとめる

最近EKSもGKEも触ってみた中で感じたGKEの優位性について書き留めておく。

逆はどうなのかという点、EKSが優っているメリットは今の所特に思いつかない。

GCP vs AWSというクラウドベンダーの比較の観点からすると、それぞれの分野で甲乙出てくるだろうが、今回はあくまでGKE vs EKS。

なので、実際に導入・選定するときには周りの他のサービスやエコシステム、セキュリティや安定性、コンプライアンスといった色々な観点で選ぶ必要がある。

安い

GKEはMaster Nodeが無料。

一方でEKSでは$0.20/hが固定でかかる。

例えば、テスト環境・ステージング環境・本番環境という3つの環境があって、それぞれクラスタを分ける構成をとっていたとする。

$0.20 x 24hour x 30day x 12year x 3env = $5184 x JPY108.22 = 年間561,012.48円

結果、EKSを採用した時点で、年間56万円の費用が上乗せされる。

GKEだとこの分の固定費用が一切かからない。

クラスタ構築が爆速

  • GKEは3分
  • EKSは20分

なんでここまで差があるのか。

GKE標準ダッシュボード

PodやServiceの稼働状況やコンテナログ(Stackdriver)への導線などがあるダッシュボードがGKEには標準でついている。

これはメトリクス情報のことではなくて、普段 kubectl get pods などのコマンドを叩いた結果みることのできるPodやServiceの状態がUI上で眺められる。

EKSにはこれがなく、そもそもEKSのコンソールには開発やデバッグで参考になる情報が何もない。

実際EKSを使ってみるとわかるが、このコンソール画面にアクセスする機会はないに等しい。

一方、GKEを使うととりあえずこの標準ダッシュボードを見ていれば基本的な情報があるし、ノードのメモリやCPUが不足しているせいで一部のPodが起動していない状況をUI上で教えてくれたりするので常にこの画面を見ながら開発やデバッグを快適に行うことができる。

EKSでこれが欲しければ公式のダッシュボードをインストールする感じになる。

https://github.com/kubernetes/dashboard

Cluster Autoscalerがワンポチ

GKEではクラスタサイズのオートスケーリングの設定がGKEダッシュボードから簡単に行える。

desired, max, min を入力してボタンを押すだけ。

EKSでは詳しくは https://aws.amazon.com/jp/premiumsupport/knowledge-center/eks-cluster-autoscaler-setup/ をご覧いただきたい。

Stackdriverがデフォルト

まずはStackdriverがどんな機能を持っているかを簡単に挙げる。

  • ログ
  • モニタリング
  • 分散トレーシング
  • デバッグ
  • エラーレポート

何も考えずにクラスタを作ると、このうちログとモニタリングはデフォルトで有効になっていて、すぐに開発に取りかかれる。

EKSは今年5月にようやくこれらの情報が見れるCloud Watch Insightsが発表された。現在はOpen Previewのステータスとなっている。

これを利用することで無事ログとモニタリングが見れるようになる。

それまでは、そのノードやコンテナのメトリクスやログを収集するためには、別途FluentdやDatadogなどのオープンソースSaaSなどを利用する必要があった。

分散トレーシングについては、AWSのマネージドサービスを使う場合はAWS X-Rayとなる。

Istio on GKE(Beta)

https://cloud.google.com/istio/docs/istio-on-gke/overview

Anthosのベータ機能。まだ使ったことはない。

普通にIstioインストールするといくらか手順を踏む必要があるのだが、これだと設定画面で有効/無効があるので、有効にするだけでIstioが使えるようになるらしい。

easily manage the installation and upgrade

インスコとアプデがラクチン

If you need to use a more recent open source version of Istio, or want greater control over your Istio control plane configuration (which may happen in some production use cases), we recommend that you use the open source version of Istio

最新バージョン使いたかったりIstioのコントロールプレーン完全制御したかったら、OSSの方つかってね

OSSのIstioのインストール自体はそんなに大変じゃないので、普通に入れるでも良さそう。後々Pilotとかの設定いじりたくなった時困るし。

Spinnaker on GKE

https://www.infoq.com/jp/news/2019/09/netflix-google-spinnaker-gcp/

これも割と最近発表されたやつ。

SpinnakerもOSSだが、こちらはインストールするまでの手順が煩雑で正直めんどくさい。全てコマンドベースでできるようにはなっているけども。

この動画みるとわかるけど、これは俄然楽そう。

Cloud Shell便利すぎ

本件はEKS/GKEには関係ない。

ただただ、AWSいつも使っている身で、このCloud Shellを使うとありがたすぎてツライ。

ブラウザ上でエフェメラルなコンテナ環境が起動しその中で、様々なターミナル作業ができる。手元のローカル環境で kubectlhelm コマンドインスコしたりしなくていい。大方のものはすでに入っている。認証もGoogle認証だから必要ない。

詳細は省略。

ノードの自動修復

https://cloud.google.com/kubernetes-engine/docs/how-to/node-auto-repair?hl=ja

GKE のノードの自動修復は、クラスタ内のノードの実行状態を正常に保つための機能です。この機能を有効にすると、GKE はクラスタ内の各ノードのヘルス状態を定期的にチェックします。ノードが長期にわたって連続してヘルスチェックに失敗すると、GKE はそのノードの修復プロセスを開始します。

EKSにはない。

ノードの任意自動アップグレード

https://cloud.google.com/kubernetes-engine/docs/how-to/node-auto-upgrades?hl=ja

ノードの自動アップグレードを有効にすると、マスターが更新されたときに、クラスタ マスター バージョンを使用してクラスタ内のノードを最新の状態に維持できます。

EKSにはない。

高可用性 99.9999...%

これはAWSGCPのネットワークの仕組みの差で、GCPはネットワーク(VPC)が世界の全リージョンにまたがることができるので、理論上どこまでもHigh Availableなのだ。(実際ここまで必要なケースは少ないかも)

Kubernetes生みの親Google

GKEにのっかっておけばその分、Kubernetesと親密でズブズブなエコシステムの中に身を置けるはずだ。 EKSはあとをついてくる感じになっている(最近出たCloud Watch Insightsなどもそう)

DDDでモデリングをするときに知っておくべき構成要素

保守性に欠けたコードを読むのは骨が折れるし、変更を加えて何が起こるかわからないコードには恐怖と不安が付いて回るから、モデリングを頑張りたい。

そんなところで、今回はドメインモデリングをする上で最低限理解しておかなければならない登場キャラクターたちについて、チートシートとして使えるようにした。

ドメイン駆動開発についての書籍を読み返しながら。

ちょいちょい僕個人の表現や考えも入っていますのでご了承ください。あと、書き出したりメモしてるところも僕自身が興味あるとこだったり、わからないとこだったりしてるので、内容に偏りがあるかも。

あくまで個人メモ。ちゃんと原著読んでください。

ちなみに

今回は、コンテキストやコアドメイン/汎用ドメインアーキテクチャの内容は含んでいない。アーキテクチャについては、ちょいちょい入っちゃっているところもあります。

ファクトリ・レポジトリに関しては、入れてもよかったけど、理解がそんなに難しくなく、レイヤーアーキテクチャデザインパターンで見知った人も多そうなことから今回は省略しています。(めんどくさかっただけ)

一旦初期のモデリング活動においては外しても怒られないでしょう。エヴァンズもそれっぽいこと言ってる

リポジトリとファクトリは、それ自体はドメインに由来しないが、ドメイン設計においては意味のある役割をもっている (P. 122)

エンティティ(Entities)

  • 状態が異なったり実装を跨いだりしても追跡されるような、連続性と一意性を持つ
  • 別名:参照オブジェクト(Reference Objects)
  • 各オブジェクトを識別する手段を定義すること
  • 同じものであるということが何を意味するかを定義しなければならない
  • 同一性は世の中に本来備わっているものではない。これは「意味」である。現実には同じものであってもあるドメインモデルにおいてはエンティティとして表現されるかもしれないし、そうでないかもしれない
    • スタジアムの座席予約アプリケーションの例
      • 指定席の場合、席はエンティティ
      • 自由席の場合、席はエンティティではない
  • クラスの定義をシンプルに保ち、ライフサイクルの連続性と同一性に集中すること
  • 属性や振る舞いに集中するよりは、定義をもっとも本質的な特徴にまで削ぎ落とすこと
  • 同一性を持つため、双方向の関連があると、保守が難しくなるかもしれない
  • -
  • データベースを主軸に考えると、データモデルがオブジェクトになり、Getter/Setterだらけのエンティティが出来上がってしまう(ドメインモデル貧血症
  • 一意な識別子があって、オブジェクトが変化する
  • ユビキタス言語に沿った意図の明白なインタフェース(ふるまい)を持たせる
  • オブジェクトのバリデーション
    • 三つのレベル
      • 個別の属性/プロパティ
      • オブジェクト全体
      • オブジェクトどうしの合成
  • オブジェクトのバリデーションと呼びたくない。それ単体で個別の関心ごとであり、ドメインオブジェクトではなくバリデーション専任のクラスが受け持つ責務だから。エンティティの責務はドメインおふるまいを扱うこと
  • 変更の追跡にはドメインイベントとイベントストアが有効

値オブジェクト(Value Objects)

  • 何かの状態を記述する属性
  • 概念的な同一性を持たない
  • 追跡を要求しないようなものに同一性を持たせてしまうとシステムの性能を失い分析作業が増す
  • 何であるかだけが問題。どれだとか誰であるかとかは問わない
  • 値オブジェクトはエンティティを参照することもできる
    • オンライン地図サービスの例
      • ドライブルートの検索において、2都市と道路はエンティティであっても、その経路オブジェクトは値オブジェクト
  • 「住所」は値オブジェクト?その質ものをしているのは誰か?
    • 通販ソフトの宛先→値オブジェクト
    • 郵便サービスの配達先→エンティティ
    • 電力会社ソフトの電力サービスの目的地→エンティティ、電力サービスを住居のような住所を持つエンティティと関連づけた場合、住所は値オブジェクト
  • 操作のために生成されては破棄される
  • エンティティの属性としても使用される
  • 自分が伝える属性の意味を表現させ、関係した機能を与える
  • 不変なものとして扱う。完全に置き換える以外変更はできない
    • 所有者の手を離れている間、さすらうオブジェクトにはどんなことでも起こりうる
    • パフォーマンスの観点で参照(ポインタ)を使うことはありえる(フライウェイと、FLYWEIGHT
      • 数が多くなる場合は一つの値オブジェクトを参照した方が効率がいい
    • いつ可変性を認めるべきか?
      • 値が頻繁に変化する場合
      • オブジェクトの生成や削除が高くつく場合
  • 複雑な設計を避ける
  • 値オブジェクトを構成する属性は、概念的な統一体を形成すべき(完結した値、WHOLE VALUEパターン)
    • 町、都市、郵便番号は、人オブジェクトの別々な属性であってはならない。ある住所全体の一部である
    • 50,000,000ドルは50,000,000とドルから成る金銭的な計測値
  • 同じ値オブジェクト同士は相互に交換・コピーできる
  • 同一性がないため、双方向の関連があっても、意味をなさない。しかし、双方向の関連は完全に取り除くよう試みること。どうしてもこれが必要となったら、そもそもそれを値オブジェクトとして宣言することを見直す。隠れた同一性があるはず
  • -
  • 値型は使うのも最適化するのも保守するのも楽
  • 可能な限り、エンティティより値オブジェクトを使ってモデリングすべき
  • 値の特徴
    • 計測・定量化・説明
    • 不変
    • 概念的な統一体
    • 交換可能性
    • 値の等価性
    • 副作用のないふるまい

サービス(Services)

  • アクションや操作として表現した方が明確になるもの
  • オブジェクト指向モデリングの伝統からはやや外れる
  • 操作を行う責務をエンティティや値オブジェクトに押し付けない方が適切なとき
  • 要求に応じてクライアントのために行われる何か
  • ソフトウェアの技術的なレイヤには多くのサービスがある
  • あんまり多用すると手続き型のコードになりドメインモデル貧血症に陥る
  • 単純に「もの」とはできないことがある
  • 概念的にどのオブジェクトにも属さないような操作
  • ドメインの重要な操作でありながら、エンティティにも値オブジェクトにも自然な落ち着き場所を見つけることができないもの。モデルに基づくオブジェクトの定義を歪めたり、意味のない不自然なオブジェクトを追加することになるもの
  • 複雑な操作はシンプルなオブジェクトを侵食しその役割を曖昧にしかねない。そういう操作は多くのドメインオブジェクトを一緒に調整したり動かしたりすることが多い
  • サービスがモデルオブジェクトになりすます時、こういう実行者は「マネジャー」などで終わる名前を持つことになる(デジャヴ…)
  • 実態よりも活動、名詞よりも動詞
  • 操作名はユビキタス言語に由来しなければならない
  • 引数と結果はドメインオブジェクトであるべき
  • 実際には何も表していない偽のオブジェクトとして宣言するよりも、モデルの中でサービスとして宣言されている方が、独立した操作によって誰かが誤解することはない
  • 優れたサービスの三つの特徴
    • 操作がドメインの概念に関係していて、その概念がエンティティや値オブジェクトの自然な一部ではない
    • ドメインモデルの他の要素の観点からインタフェースが定義されている
    • 操作に状態がない(ステートレス)
  • エンティティと値オブジェクトで構成される集合体の上に構築され、ドメインに本来備わっている能力をまとめ上げて、実際に何らかの処理を行うスクリプトのように振る舞う
  • 細粒度のドメインオブジェクトを用いると、ドメイン層からアプリケーション層へ知識が流出するかもしれない(ドメインモデル貧血症を引き起こす)。アプリケーション層は、ドメインオブジェクトの振る舞いが組み合わされる場所だから。ドメインサービスを慎重に導入すれば、複数のレイヤ間で境界を鮮明に維持できる
  • インタフェースの単純さが優先され、便利な中粒度の機能が提供される
  • -
  • サービスといってもSOAとかRPCとかMoMのそれとは異なる
  • アプリケーションサービスとは違う。アプリケーションサービスはドメインモデルのクライアント
  • サービスインタフェースの宣言は関わる集約と同じモジュール内で行う
  • セパレートインタフェースは必須なのかというと必ずしもそうではない。大抵実装が一つなのだから
  • ドメインサービスでパッケージングするとドメインモデル貧血症を引き起こしがち

モジュール(Modules)

  • モジュール間では低結合
    • 人が一度に考えられる物事の数には限りがある
  • モジュール内では高凝集
    • 首尾一貫していない思考の断片は、思考がバラバラに溶け合ったスープのようなもの
  • 別名:パッケージ(Packages)
  • モデルにおいて意味のある一部として現れ、より大きな尺度でドメインを語らなければならない
  • コードと概念の分割
  • 粒度の大きいモデリング
  • コミュニケーションの仕組み
    • いくつかのクラスを一つのモジュールに入れるということは、その設計を見る開発者に、それらをひとまとめに考えるよう伝えていることになる
    • モデルが物語だとすれば、モジュールは章
  • モジュールにはユビキタス言語の一部になる名前をつける
  • モデルと同様にモジュールも進化すべきだが、モジュールのリファクタリングは影響範囲が大きくなる
  • インフラストラクチャ駆動パッケージングの落とし穴
    • 別々のサーバーにコードを分散させようという糸が実際にない限り、単一の概念オブジェクトを実装するコードは全て、同一のオブジェクトにはならなくても、同一のモジュールにまとめた方がいい
    • 抽象と具象の結合度が高いなら同じパッケージでいいじゃないか
  • 概念の凝集した集合を含んでいるものを選ぶこと
  • モジュール間が低結合にならないなら、概念のもつれをほぐすようにモデルを変更する方法を探す
  • -
  • 概念の境界↔︎「集約」
  • ドメインオブジェクト内のひとまとまりのクラス群をまとめる、名前付きのコンテナとして機能する
  • いい例え
    • キッチンの引き出しには銀製のナイフとフォーク、スプーンがきちんとまとめられていて、ガレージの工具箱にはドライバーや電源タップ、ハンマーがちゃんと入っていてほしいよね。それぞれ混じっている状態って誰も嬉しくないよね。
      • placesettings.{Fork,Spoon,Knife,Serviette}
    • 一方、キッチンの整理整頓を機械的にやってしまおうとは思わないよね。頑丈なものは全部一つの引き出しにほうりこんで、壊れやすいものは戸棚の上の方にまとめるといった整理はしないよね。
      • pronged, scooping, blunt ...
  • 対等なモジュールどうしの結合が必須に成る場合は、循環依存にならないようにすべし
  • 境界づけられたコンテキストの前にモジュールを検討する。境界づけられたコンテキストはモジュールの代わりに使うべきではない

集約(Aggregates)

  • 明確な所有権と境界を定義しモデルを引き締める
  • 集約が区切るスコープによって、不変条件が維持されなければならない範囲が示される
  • ファクトリとリポジトリは集約を対象にとする
  • 複雑な関連を伴うモデルでは、オブジェクトに対する変更の一貫性を保証するのは難しい
  • 集約とは、関連するオブジェクトの集まりであり、データを変更するための単位として扱われる
  • 各集約にはルートと境界がある
    • 境界:集約の内部に何があるかを定義するもの。境界内のオブジェクトは互いに参照を保持しあっても良い
    • ルート:集約に含まれている特定の1エンティティ(ルートエンティティ)。集約のメンバの中で、外部のオブジェクトが参照を保持していいのはルートだけ
    • ルート以外のエンティティは局所的な同一性を持っているが、その同一性は集約内部でのみ識別できれば良い
  • 自動車修理店向けのソフトの例
    • 自動車:集約ルートエンティティ
    • タイヤ:集約の境界内にあるエンティティ。特定の自動車というコンテキストの外部でタイヤの同一性が気にされることはない
    • エンジンブロック:集約ルート かもしれないしそうじゃないかもしれない。自動車とは独立して追跡されることがあればそれは集約ルートになる
  • 集約のルートエンティティはグローバルな同一性を持ち、不変条件をチェックする最終的な責務を負う
  • エンティティの同一性のスコープ
    • ルートエンティティ:グローバルな同一性を持つ
    • 境界内部のエンティティ:ローカルな同一性を持つ
  • 集約の境界外部にあるオブジェクトは、ルートエンティティを除き、境界内部への参照を保持できない
  • ルートエンティティは内部のエンティティへの参照を他のオブジェクトに渡せるが、受け手側は参照を一時的に利用することができるだけで、その参照を保持してはならない。
  • ルートがアクセスを制御するので、内部が知らないうちに変更されることはなくなる
  • データベースに問い合わせて直接取得できるのは、集約ルートだけ。それ以外のオブジェクトは全て、関連を辿ることで取得しなければならない
  • 削除操作は、集約境界内部に存在するあらゆるものを一度に削除しなければならない
  • 集約境界の内部に存在するオブジェクトに対する変更がコミットされるときには、集約全体の不変条件が全て満たされていなければならない
  • トランザクション効率にも繋がる。集約の境界が不適切に広いとロックがかかる時間も長くなるため
  • -
  • トランザクション整合性の境界↔︎「モジュール」
  • トランザクションの分析をしてからでないと、集約の設計の良し悪しを正しく判断することはできない
  • 小さな集約を設計する
    • パフォーマンスやスケーラビリティの問題に関わる
    • 究極、一意な識別子とそれ以外の属性を一つだけ持つ形
    • 結果整合性を保つことで逃げられないか考える
  • 集約から別の集約を参照する際に、その識別子を使うように設計すべき
    • 切り離されたドメインモデル
    • リポジトリあるいはドメインサービスを使って、集約のふるまいを実行する前に依存オブジェクトを検索する。クライアントであるアプリケーションサービスがこれを制御し、その後、集約に処理を回す
  • 集約の境界の外部で結果整合性を使う
  • 「命じろ、たずねるな」(デメテルの法則)
    • 情報隠蔽を重視した原則。最小知識の原則。クライアントは必要以上にサーバーの構造を知ってはならない。
  • (アプリケーションの)利便性を重視するとどんどん合成して大きくなるし、かといって簡素にすると真の不変条件を守れなってしまう
  • トランザクション整合性か結果整合性か?→それは誰の役割かを考える
  • 集約のパーツを設計するときは、可能な限り、エンティティではなく値オブジェクトを使う
  • 依存性の注入を避ける

ファクトリ(Factories)

省略

リポジトリ(Repositories)

省略

とりあえず、これをチートシートとして見ながらモデリング進めて行けば道を踏み外すことはないかと。

個人的な悩みは、モジュールの境界と集約の境界の切り方がむずいなーと感じてる。

一説では、モジュール=集約もありというようなのを見かけたが、そうなるとモジュール小さくなりすぎないかねと思うところではある。

ちなみにモデリング会やる上で、今の所考えている流れはこんな感じです。

  1. ユースケースを見つける(アプリケーション層のインタフェース)
  2. エンティティを見つける
  3. エンティティの属性(値オブジェクト)を見つける
  4. エンティティのふるまいを見つける
  5. サービスを見つける
  6. モジュールを見つける
  7. トランザクション整合性の境界(集約)を見つける

特に、2〜4を繰り返してモデルを洗練させるのが大事だと思ってる。

GoのSplit関数を使うときの注意 Field関数との挙動の違い

文字列をスペースや,や.をデリミタとして分割してスライスに入れたいこと、あると思います。

そんな時によく使われる標準パッケージの関数として、

  • Split
  • Fields

があるかと思います。 (Fields は空白文字をデリミタとして使用します。それ以外を指定したい場合は FieldsFunc を使います。)

突然ですが、以下のコードをみてください。

s := "abc def xyz"

fmt.Println(len(strings.Split(s, " "))
fmt.Println(len(strings.Fields(s))

こちらの出力結果にはどうなるでしょう?

正解はどちらも「3」です。

続いてこちらをご覧ください。

s := ""

fmt.Println(len(strings.Split(s, " "))
fmt.Println(len(strings.Fields(s))

対象を空の文字にしてみました。

どんな出力結果を期待しますか?

答えはこちらです。

1 0

これが今回言いたかったことなんですが、

Split 関数を使った場合空文字を指定すると返却されたスライスの長さは0ではなく1となります。

これは分割文字を , にしようが、 . にしようが同じ挙動をします。

ややこしい挙動をするなーと思ったのでメモでした。

例えばサブドメインを抽出するために . とかで分割しようとするケースで、

さて SplitFieldsFunc となった時に Split の方がインタフェースがお手軽なのでこっち使っちゃうことは多いと思うので注意です。

コードはこちら:

https://play.golang.org/p/Hl1xUHhhqDW

GoのSliceのCapacity Allocationを効率よくやるために

GoのSliceを使う時にどのタイミングでCapacityが際割り当てされるかを調べます。

package main

import (
    "fmtが"
)

func main() {
    const max int = 1000000
    var lc int
    slc := make([]int, 0, 0)
    for i := 0; i < max; i++ {
        slc = append(slc, i)
        if lc < cap(slc) {
            fmt.Println(len(slc), "->", cap(slc))
        }
        lc = cap(slc)
    }
}

上のコードの実行結果がこちらです。

1 -> 2
3 -> 4
5 -> 8
9 -> 16
17 -> 32
33 -> 64
65 -> 128
129 -> 256
257 -> 512
513 -> 1024
1025 -> 1344
1345 -> 1696
1697 -> 2368
2369 -> 3072
3073 -> 4096
4097 -> 5120
5121 -> 6816
6817 -> 10240
10241 -> 14336
14337 -> 18432
18433 -> 24576
24577 -> 30720
30721 -> 38912
38913 -> 49152
49153 -> 61440
61441 -> 77824
77825 -> 98304
98305 -> 122880
122881 -> 153600
153601 -> 192512
192513 -> 241664
241665 -> 303104
303105 -> 378880
378881 -> 475136
475137 -> 593920
593921 -> 743424
743425 -> 929792
929793 -> 1163264

1000までは倍のcapをallocateしてるけど、1000を越えるとぼちぼちって感じに変わってるのがわかります。

GolangにおけるGenericsについて考えながらGoらしさについて考えてみる

tl;dr

Go 2 Draft が発表され、その中にはgenericsの機能追加が検討されています。この機会にジェネリクスとは何かについて考えてみたい。

以前はJavaを書いていて、当時使いながら便利だなあと感じていた。

けど、いわば抽象化テクニックであるgenericsを多用するとコードの可読性は著しく下がります。

そして、これが「Golang」というプログラミング言語の思想として正しい方向に向かうのかということもこっそり考えてみたい。

Golangジェネリクスを使いたい

現状はできない。 interface{} を使って強引に書くことはできるが、結果的には不完全な実装になる。これについては、あとでコードを紹介します。

genericsの使い所と勘所

よくある、というかもしgenericsがあったとしたら真っ先に思いつくのがリスト操作。じゃないですか?

例えば、mapとかkeysとかsortとか、そういう奴らですね。

どういうものかは実装をみたほうが早いので、例としてfilter処理を実装することで比較考察してみることにします。

GolangGenericsっぽく書いてみる実験

filter関数は、リストの中からある条件を満たす値だけを探します。

filter関数についての詳細はこちら https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

以下のようなパターンの実装をもとに比較します。

  • go言語(interface{}を使わない)
  • go言語(interface{}を使う)
  • fo言語 - An experimental language which adds functional programming features to Go
  • go言語(Go 2 Draftで提案されているもの)

今回説明で載せるコードは全部githubにアプしております。

https://github.com/uqichi/go-filter-generics

interface{}を使わずに実装

まずはシンプルにpure goで書いてみる。

func filter(ls []int, f func(i int) bool) []int {
  ret := make([]int, 0)
  for _, v := range ls {
    if f(v) {
      ret = append(ret, v)
    }
  }
  return ret
}

これを使ってみると、こんな感じ。

ls := []int{1, 2, 3, 4, 5, 4, 3, 2, 1}

res := filter(ls, func(i int) bool {
  if i == 3 {
    return true
  }
  return false
})

結果は以下になる。

[3 3]

さて、このfilter関数を使ってstringのリストを処理したいとなったらどうなるだろう。残念ながら現在のgoではできない。

素直に筋肉を使って新たにstring用のfilterを書くことになりおます。

func filterInt(ls []int, f func(i int) bool) []int {
  ret := make([]int, 0)
    for _, v := range ls {
    if f(v) {
      ret = append(ret, v)
    }
  }
  return ret
}
func filterString(ls []int, f func(s string) bool) []string {
  ret := make([]string, 0)
    for _, v := range ls {
    if f(v) {
      ret = append(ret, v)
    }
  }
  return ret
}

こうして、使いたい型の数だけ関数を実直に増やしていかなければならない。

どうにかならないものか!

interface{}を使って実装

さて、こうなってくると共通化したい欲がムラムラと湧いてきます。

interface{} でどうにかならないか。

func filterWithIface(ls interface{}, f interface{}) interface{} {
  lsVal := reflect.ValueOf(ls)
  fVal := reflect.ValueOf(f)

  ret := reflect.MakeSlice(reflect.TypeOf(ls), 0, lsVal.Len())

  for i := 0; i < lsVal.Len(); i++ {
    b := fVal.Call([]reflect.Value{lsVal.Index(i)})[0]
    if b.Bool() {
      ret = reflect.Append(ret, lsVal.Index(i))
    }
  }

  return ret.Interface()
}

通化はできた模様ですが、みるからにinterface{}だらけでこれどうなのよというかんじ。

これを使ってみると、

li := []int{1, 2, 3, 4, 5, 4, 3, 2, 1}
ls := []string{"a", "b", "c", "d", "e", "d", "c", "b", "a"}

// filter list of int
{
  res := filterWithIface(li, func(i int) bool {
    if i == 3 {
      return true
    }
    return false
  })
  resInt := res.([]int)
}

// filter list of string
{
  res := filterWithIface(ls, func(s string) bool {
    if s == "c" {
      return true
    }
    return false
  })
  resString := res.([]string)
}

なんかそれらしくまとめられました。しかし、これでは以下のような理由から実用的とは言えません。

  • interface{}が出てきすぎて何が起こっているのやら。
  • interface{}が抽象的すぎるため、関数が引数として渡されることを想定しているのに、なんでも入ってきうる。
  • 上と同じ問題で、渡された引数の型が関数内で処理できるとは限らない。上の例だと = でcomparableな型がくるかどうかわからない。
  • 関数の取得結果を対象の型でキャストしなければならない。

わざわざ説明しなくてもなんか無理そうな空気がプンプンします。interface{}多すぎ。

これを型安全に使えるようにするためには、結局 filterWithIface のなかで、switch文を駆使してハンドリングするしかなさそうです。

にしても、結果のキャストとかしなきゃいけないので、これは使えない。

どうにもならないものか!

fo言語を使って実装

先日 golang.tokyo という勉強会でエウレカの臼井さんという方が紹介していたライブラリです。

https://github.com/albrow/fo - An experimental language which adds functional programming features to Go

関数プログラミングの機能をGoに追加すべく作られた実験的な言語

ということなので、こちらも実用段階ではありません。これがgenericsの機能を備えてるんですね。

簡単に使い方を説明すると、main関数書いて、 fo run <filename> するだけ。

これだけ。fo run すると、同じ階層に.goファイルが生成され、同時に実行されます。fo runしないと使えません。

これだけのfolangですが、playgroundが用意されています。

https://play.folang.org/

既視感が半端ないですね←

ということで、早速コードを使ってみることにします。まずはインストール。

なんとなくGOPATHを汚染したくいのでカレントディレクトリで済ませる。(folangさん、汚染って言ってすみません。)

dep init
dep ensure -add github.com/albrow/fo
go build ./vendor/github.com/albrow/fo

foのバイナリが生成されます。

次はfolangのgenericsを使ってfilter関数を書いていきます。

main.fo というファイルを作ります。拡張子は .fo !!

func filter[T](ls []T, f func(T) bool) []T {
  ret := make([]T, 0)
  for _, v := range ls {
    if f(v) {
      ret = append(ret, v)
    }
  }
  return ret
}

これぞジェネリクス!直感的でいい感じ。

関数名の直後に [type] を入れることで、genericsを定義(ここでは [T] )します。複数ある場合はコンマ区切りでかける。

以下はこれを実行したサンプル。

li := []int{1, 2, 3, 4, 5, 4, 3, 2, 1}
ls := []string{"a", "b", "c", "d", "e", "d", "c", "b", "a"}

// int filter generics with fo lang
{
  res := filter[int](li, func(i int) bool {
    if i == 3 {
      return true
    }
    return false
  })
}

// string filter generics with fo lang
{
  res := filter[string](ls, func(s string) bool {
    if s == "c" {
      return true
    }
    return false
  })
}

結果のキャストも不要です。

この main.fo を先ほどビルドした fo commandでrunします。

./fo run main.fo

と、main.go が生成され、実行されます。

可読性も悪くなく、なかなか実用に耐えるシンプルなジェネリクスな気がします。

しかし、こちらのfolang.. あくまで”experimental”なので使うのは厳しいですね。エディタで書いてもsyntaxもcompile checkもはいりません。流石に無理だ。

なお、生成された main.go の中身はどうなっているかというと、、、

func filter__int(ls []int, f func(int) bool) []int {
  ret := make([]int, 0)
  for _, v := range ls {
    if f(v) {
      ret = append(ret, v)
    }
  }
  return ret
}

func filter__string(ls []string, f func(string) bool) []string {
  ret := make([]string, 0)
  for _, v := range ls {
    if f(v) {
      ret = append(ret, v)
    }
  }
  return ret
}

結局、冒頭のinterface{}を使わず愚直に筋肉を使ったように、こちらも、関数利用する際に使用されているそれぞれの型に合わせてその分の関数が作られていますね。

つまり、golanggenericsを表現するには結局地道に関数を書いていくことが正ということでしょう。

しかし!

まあめんどくさいよ。という声が多数挙がったのを聞いて、Go 2 Draftのなかでgenericsが上がったわけですね。

Go 2 Draftのgenericsを使って実装

GenericsのDraft Designはこちら https://go.googlesource.com/proposal/+/master/design/go2draft-generics-overview.md

ここらでちょっともう疲れてきたので、早速書いてみます。Go 2にGenericsが入ることで世界はどう変わるのか。

※もちろんのことですが、ドラフト段階なので、動作確認はできません。ドラフトデザインに沿ったサンプル書けてるかもあやしい。。。

contract Comparable(t T) {
  t == t
}

func filterInGo2Draft(type T Comparable(T))(ls []T, f func(T) bool) []T {
  ret := make([]T, 0)
  for _, v := range ls {
    if f(v) {
      ret = append(ret, v)
    }
  }
  return ret
}

むむー。合ってるのかな?

個人的に、folangの方がわかりやすかった感じあります。

まず、Generics型を定義するには type を使って明示する必要があります。golangの定義をするときはすでにみんなそうしていますね。今回もそうしましょうといったところか。

そして次に、どうみても違和感のある新参者 contract

これは、folangではなかった機能で、実際に使われる型に制約を設けることができます。 の 上例では、「Tの型は==で比較できますよ」ってことになります。Comparableって名前のは僕が自由に定義したcontractであって、重要なのは t == tの部分になります。

つまり、これまで書いた i == 3s == "``c``" のような比較がおkですよ〜ってこと。型によってはこの==の比較ができないものがあるので、あらかじめcontractで縛ってやろうという算段ですね。

なるほど、folangよりも緻密にtype-safeなGenericsが書けることになります。

なお、 type T Comparable(T) の部分は以下のように省略が可能らしい。

type T Comparable

使うときは以下のような感じ(になると思います自信ない)

li := []int{1, 2, 3, 4, 5, 4, 3, 2, 1}

res := filterInGo2Draft(int)(li, func(i int) bool {
  if i == 3 {
    return true
  }
  return false
})

はーい。以上。

Go 2 Draft Generics Designは、contract によって、Generics型のその後の振る舞いまで縛れるためより安全に記述することが可能になっているみたいですね。

なんかもはやGoっぽくはないと僕の直感は言っているのですが、Generics入ってきたらきたでめっちゃ使うだろうしめっちゃ謝謝ってなる気はします。

終わりに

Generics使えると便利!ですね。それを再認識しました。

一方で、Goがそれをやるか?やるか?という疑問もあります。うまく言えないけどシンプルなGoが..hmm

folangを教えてもらったgolang.tokyoの勉強会で、別で話してたスピーカーの kaneshinさん が、トーク終了間際に聴衆にこんなこと問うていました。

「皆さんはGoらしさってなんだと思います?」

僕なりには、シンプルで使いやすい。っていう非凡な答えに落ち着くのですが。。これにGenericsがどう組みいるのか、今後もwatchしていきたい所存です。

コードは全部githubにアプしております。

https://github.com/uqichi/go-filter-generics

ご意見ありましたら、ツイートに反応したりDMしてください。

長々と、ありがとうございました。

Mercari Tech Conf 2018 に参加して見てきたテックカンパニーのマイクロサービス

だい2回めとなる、Mercari Tech Conf 2018 が昨日の木曜に開催され、平日にも関わらず500人くらい来てました。

ちなみに去年は300人って言ってた。

だいぶいい刺激をいただいたので、せっかくなので記事にします。

tl;dr

  • テックカンパニーとしての熱量がすごい。僕がCAに入った時と同じようなものを感じた
  • メルカリがすごいのはそれをオープンにやる。から、人が集まるんだろう
  • 会場とかデザインとかスライドとか凝っててかっこいい
  • 参加者全員にモバイルバッテリーと今半弁当くれるマネーパワー
  • セッション途中に滲み出る採用宣伝、勧誘
  • セッションはマイクロサービスと機械学習の話が大半
  • マイクロサービスについては色々肌で感じられてよかった。この施策は、社内エンジニアの成長育成や採用フックという副次効果ともかなりあるとおもう。
  • マイクロサービスは数百人規模のいまだからこそやる価値がありそう。逆に少ないフェイズだとやらないほうがいいとさえ感じる茨の道
  • micro decisionという言葉もでてきて、各自が小さな決断を繰り返すことで成長してこうという話。名村さんが喋ってた
  • 小さくわける、小さく保つというのは、それだけでいいことだということには共感できる。ただ、設計するのが意外と難しい
  • 機械学習はかなりニッチな優秀エンジニアがしっかりやっている印象。CAのラボみたいな。
  • 「mercari X」という、ブロックチェーン技術を使ったプロダクトを新発表。なういことはすべてやってる

気になったセッションについて

全部に言及すると、多すぎるので、つまむ。

なお、

当日のタイムテーブルはこちら: https://techconf.mercari.com/

資料はこちら: https://speakerdeck.com/mercari

スライドの一枚目が全部同じで見辛いw

★基調講演(名村さん)

テックカンパニーになりたいmercari。そのためにはどうすればいいか?

キーワードは「micro」。以下のような形でまとめられてたわけではないけど、聞こえてきたmicro hogehogeを羅列してみる。

  • micro service
  • micro service for client side
  • micro deploy
  • micro decision

スケールするための技術戦略として、micro decisionというワードも。

決断の粒度を可能な限り小さくし、スムーズにどこでもいつでも決断する機会が生まれるようにする。そのうち、これを繰り返すととで大きな決断もできるようになる。ということ。

言わずもがなのマイクロサービスを進めているmercariだけど、それもこの一環。

ちなみに、フロントエンドも同様にその方針で進めているらしい。

  • Micro Frontends / web
  • Micro View Controller / ios
  • Layered Architecture / android

デプロイについてもmicro化。

一つのタスクをできるだけ小さくし、多数のタスクの結果シグナルを受信して、おkならば次に進めるという感じでやっていきたい所存らしい。

沈没したタイタニック船のバルクヘッドが機能しなかった話。こういうシステムにお客を載せるわけにはいかないので、一つの独立系を小さくし、不安定な連鎖反応を防ぐ。

以上のmicroさんたち。進化し続けるテックカンパニーを目指す。自走する個人、チームに向けてのいい目標、いい標語だと思う。

★基調講演(曾川 景介さん)

mercari Xという新プロダクトの発表があった。

ブロックチェーン技術を使った価値交換をやるためのものらしい。

機械学習の技術はすでに結構使われてるみたいだし、ナウいところは全部攻めてますね。

★Microservices Platform at Mercari

中島 大一さん

  • 現在うごいているmicroservice: 19
  • next: 73

めっちゃ増える…

microserviceのつらみ?

モノリスシステムだと、専門性でチームが分かれる。(例えば、backend, frontend, sre

microserviceやるにはそれぞれにowner(担当者)をおく。組織から変える必要がある。

それぞれのmicroserviceでtest,deployを担当する。

チームメンバーが少ないスタートアップだと、microserviceをやっていく決断をするかどうかはじっくり検討したほうがいいと思う。

少ないままに始めたら結局同一人物が複数のマイクロサービスをみることになり、本来の効能が得られないばかりか、作業能率は悪化しそうな気さえします。

ちなみにそのあと会場で会ったSREの友人は、マイクロサービスつらみという話を聞きました。理由は上記に似た感じのもの。

microserviceに関わるデベロッパーは、自然みんなが本来やらなくていいことを覚えなきゃだし、SREのような意識になり、周辺技術を身につけていかなければならない。

そういう意味では技術者としての成長角度はかなり上がりそう。

gRPCとproto

最近個人的にgRPCに興味ある。そんな中、.protoの扱い方の話が少しあったのでメモ。

メルカリでは全てprotocol buffer定義を一つのレポジトリで管理しているよう。そこをbackend, frontendが使う感じ。

これはmicroservice横断で一つということだと思う。micro A → micro Bみたいなこともあるし、多分認識あってるはず。

ドキュメントの機能も果たすのですごくよさげ。まとめたらまとめたで短所もありそうな気もするが。

provisioning

もちろんinfrastructure as code 的なことはちゃんとやってました。terraform。

逆にあの開発規模でやってないのは首締まるだろうなー。

starter-kit as templateってのを用意してるらしく、一瞬でプロジェクトを始められるそう。

deployに関してもterraform同様の形で、kubernates v2 providerを使って infra as code 進めているらしい。現在のデプロイにはSpinnakerを利用とのこと。

★from Monolithic to Microservices

李 同輝さん

既存システムからmicroserviceへの移行のtips

新機能追加とか既存の改修とかどうするか

既存APIと新しいserviceA, serviceBの前面にAPI Gatewayを置いて、振り分けるようにしたそう。細々とリリースできるし、少しずつリアルタイムにnewにシフトできるからいいですね。

★どうして僕らは決済処理をマイクロサービス化しようとしているのか

斎藤 祐一郎 さん

メルペイチームの決済処理移行。

踏み切った経緯

立て直しを重ねた温泉旅館のようなモノリシック

  • スケールしない既存の決済処理
  • 一方で新しい電子決済を加えなければならない

巨大なswitch分を読み解く 過酷な業務分析w これじゃスケールしない。

得られた効果

  • 保守性向上
  • 責務の分離
  • 決済手段を簡単に追加できるようになった

同時に決済処理の際実装を行うことで決済処理を理解している人間が増えた。

これ意外と大事かも。僕も以前iOS/Android周りの購入処理を書いたんですが、当時新規チームで忙しく、仕様が複雑(特に iOS)なせいで、レビューもままならぬままMerge。怖かった。

分散トランザクション

モノリシックなシステムでは、db transactionを使うことでrollbackできるので簡単だた microserviceの分散システムだと...NO

ステートレスだと問題ないが、決済処理のようなステートフルな処理は、問題が起こる

問題1. カード処理とポイント処理。片方が失敗するケースがありうる。

解決策1:呼び出し側でtransactionの段階を分ける

  1. 支払い枠を抑える
  2. 支払い能力が担保された状態で購入処理を続ける
  3. 問題なければ支払い確定
  4. 問題あれば支払い枠を解放

とはいえ、結局、

支払い枠を解放

ここの実装が難しそう。

問題2. paymentServiceに障害が起きたら?

メソッド内で、networkを超えて実行しているので、通信経路に障害があるとどうなるか。次の処理が始まらない。

解決策2. ステートマシン

特徴:

  • 非同期に処理が可能。呼び出し元を待たせない。
  • 失敗じも再度キューイングして、自動リトライ。可用性が高い
  • 復帰不能な失敗は状態を巻き戻しできる

ステートマシンという単語小難しくてよくわからない。勉強不足。

分散トランザクショショナルな処理を直列的にやると、通信詰まると死ぬから非同期にして全部終わったら次行こ(確定)ってことでしょうか。

1も2も解決してるけど、この分散トランザクションの問題は、送信先にそういう実装(機能)がないと実現できないんじゃないのかなー。例えば

  • キャンセル処理
  • dry-run的なチェック処理

まあ、削除処理的なのはあるだろうから最終的にはなんとかなるのかもだけど。

仮に、決済代行A、決済代行Bがあったとして、これが一つの分散トランザクション上にあるとして、上記のようなものを持っていなかったら、巻き戻しを実現するのって無理なきしかしない。

この辺教えて欲しいです。

会計システムへの影響をいかに減らすか。

いきなり移行できないので、平行期間が存在することを留意すべき。

  1. 決済システム移行じは記録は現システムと新システムで平行書き込み
  2. paymentserviceに完全移行次第、平行書き込みは終了。paymentserviceのみ。

★Listing Service: モノリスからマイクロサービスへ

森國 泰平さん

先ほども全く同じテーマのセッションを紹介しましたが、こちらの話はとても辛そうな話ばかりでした。

感じたツラミ1. gcp microからさくらインターネットDBへの接続

  • リリースまではdb共有
  • mysql portを外部公開したくない

solution

databaseだけは北海道においてけぼり。。しかしmysqlのportを晒したくないと。

結果どうしたかというと、dao layerもこちらにおいており、google cloudからそちらを呼んでいるようです。

そして移行のためだけに使うためのgRPCサービスを実装したという。インタフェースはシンプルで使いやすそう。

早く一緒にしてあげて欲しい。。

感じたツラミ2. 機能・追加修正への追随

解決策として、

  • phpリポジトリに変更追跡用ブランチを作りgoに移行
  • merge conflictが起こるので気づける

感じたツラミ3. PHP例外との互換性

問題

  • api responseエラーコードとしてphpの例外名が含まれている。
  • クライアントはその例外名を使用している

解決

  • php例外との互換性を保つためのgoのライブラリを開発

これきついだろうなー。これも先ほどと同じく移行専用のツール。

この問題はまさにモノリシックなやつで。負の遺産とはまさにこれのことでそうか。

★Web Application as a Microservice

杉浦 颯太さん

web版メルカリ、冒頭に出たmicroservice for client sideの話がまさにこのプロジェクト。

  • 環境の変化
  • web周りの技術の変化

このため、Web Re-architectというチームが発足

Goals:

  1. 変更に強い柔軟なアーキテクチャ
  2. 開発チームのスケーラビリティ向上

このためにやりたいこと

1 monolithic service to 4 microservices

いきなりこれやるの厳しいから、

1 monolithic service with 4 microservices

並行してやるってこと。これ、李さんの移行のセッションでも同じ手法を取っていましたね。

why?

  • not renewal but re-architect
  • 小さいスコープで移行する
  • 先ずは小さなチャレンジと失敗を積み重ねる

microの思想というか方針が浸透してるんですね〜すごいなあ。

monolithic service + microservicesの共存

CDNはfastly使ってるらしい。

web gatewayを実装

  • 各サービスへのbalancingを行う
  • 初期は共存のため
  • パスによる制御
  • 公開範囲の制御
  • session persistence

backendのmicroservice化はfrontendにとてどんな意味があるか

  • 日々増えるmicroserviceの仕様の把握
  • パフォーマンスを意識したコーディング

BFFというバッファリングサーバーを置く。それが複数のマイクロサービスを束ねる。 要するにAPI gateway

ちなみにbacend for frontendの略らしい

セッション同期問題

  • old monolithicにログインしたユーザーからnew microserviceにコール。どうする?

→session管理を行うmicroserviceを立てる

全てmicroserviceで解決!侍!

レイテンシ問題

物理的に距離がある場合ここがネックになる。

そして、mercariは先にも言ったように、ネックになる。

メルカリの北海道ー東京問題はなかなか大変そう。

Storateを前段に置いて、レスポンスキャッシュ。


以上!最後駆け足になった感ありますが。

上にあげたセッション以外にもたくさん面白そうなのありました。

セッションのほとんどはmicroserviceについてで、そうじゃなくても必ず触れると言ったように社内では今メインの関心ごととなっているようです。

また、流行りの機械学習ブロックチェーンについても早々と事業を打ち出していてさすがはと感じたところです。 誤解されないように、流行りだから、ではなくmercariの事業・サービスを実現に必要な手段・技術として適切に選択したといったところだと思います。

にしても恐ろしいテックカンパニーと畏敬の念を抱いたところであります。

来年もまたぜひ参加させていただきたい。