Closed1
モノリスからマイクロサービス読書メモ
マイクロサービスに関すること
- 技術的な凝集度とビジネス的な凝集度がある
- 技術的な凝集度によっているのがモノリスというのは面白かった
- 凝集度の高い・低いだけではなく、何に対する凝集なのかを意識する
- システム結合とドメイン結合も区別する必要がある
- 何より独立デプロイ可能であることを大切にしたい
- DBを共有すると、公開する情報・しない情報を選べなくなる
- パブリックなインターフェースなら選べる
- spotifyではネイティブアプリであっても審査なしにUIを変更できるコンポーネント化を達成している
- どういうことだろう? React NativeとBit組み合わせてる的なこと...?
本当にマイクロサービスが必要なのか
-
自律性を与えるのにアーキテクチャの変更は必須ではない
- SQLが得意な人には「クエリに関する判断は全部この人に任せよう」とか
- 能力による責任分担でも十分機能する
- ==これは明日からでも活かせそう==
- そこがボトルネックなのか?
- 著者が調べたところ、アイデアを思いついてから開発可能な状態に持っていくまでの方が遥かに時間がかかっていたこともあった
-
アイデアを思いついてから本番環境にリリースされるまでのステップを全て書き出してみる
- ==明日からできそう==
どうやって移行していくのか
- 小さく達成できて、インパクトは大きいものから着手する
- ==短期的な成果を出していかないとビジョンへの信頼が失われる==
- 小さく進めるとサンクコストバイアスも軽減できる
- 小さく進めると、その間に他の機能が追加されるのを防げる
- ドメインモデルに基づいてサービスを分割する。依存関係の上流から着手する
- 下流は参照が多いので変えづらいことが多い
- 定性的に振りかえる
- メンバーはプロセスを楽しんでいるか、力を与えられたと感じているか、圧倒されていないか、新しい責任やスキルを身につけるのに必要なサポートを受けていると感じているか
- ==デプロイとリリースを分ける==
- 本番環境に存在するけど呼び出せない状態(デプロイしているけどリリースはしていない)
- デプロイプロセスに慣れることが出来る
- 切り替え容易になる
具体的なアプローチ
以下はデータベースに触れずアプリケーションコードだけを修正するパターン
ストラングラーパターン
- ラッパーを設ける(nginx的なイメージ)
- プロキシをスマートにしすぎないように注意
- ==スマートエンドポイント・ダムパイプ==
- http->grpcへのプロトコルマッピングなども共有プロキシで行うのではなく、各マイクロサービスの中で責務として持たせておく
- プロキシを変更する度にデプロイに複数のチームが関わるようになってくるとマイクロサービスの旨みが失われていく
抽象化によるブランチ
- 呼び出しと実装の間にインターフェースを噛ませる
- インターフェースを実装した新実装を用意する
- 新実装はテストしたりデプロイしても構わない
- 十分チェックできたら新実装と旧実装を入れ替える
- ストラングラーとの違い
- ストラングラーは、切り出す対象までの通信を傍受できる場合に適用しやすい
- 傍受できない内部的実装の時に抽象化を使うイメージ
- github scientist
同時実行
- 抽象化によるブランチを使って新旧機能が共存している状態で、両方とも実行し続ける
- 出力を定期的に比較して新機能が問題ないことを検証する
- 非機能要件(実行時間とか)を計測する上でも役立つ
- ただコードは複雑になるので(同じ実行が2つ存在するとメンテも大変だし)使用は控えめに
- fly by wireの飛行機も複数システムを同時実行しておくことでバグを検知できる
- メール送信のように2通送信されたら困る時はスパイに置き換えて呼び出しの事実だけ記録しておく
デコレーティングコラボレーター
- プロキシを挟んでおいて、モノリスからのレスポンスを受けたプロキシが別サービスを呼び出す
- 別サービスが必要とする情報をモノリスレスポンスが含んでいるとは限らないため、別途モノリスに呼び出しが発生することもある
- いまいちユースケースが思い浮かばなかった
変更データキャプチャ
- デコレーターを差し込むのが現実的でない場合はdbで一部代替できる
- DBのトリガーなどを使う。dbが変更された時に外部サービスを呼び出す
- ただトリガーは暗黙的になりやすいので利用は計画的に
- トランザクションログを読み取る方が良いかもしれない。ツールは豊富にあるらしい
- スキーマ設計によってはデータの差分をバッチで検知したり
- でも差分検知するの難しいケースでは使えない
データベースの分離
データベースが共有されていると何がまずいか
- 何を隠して何を公開するか制御できない
- データを操作するビジネスロジックがどこにあるかわからない
- 変更のない参照専用dbぐらいしか許容できない
- 全てのサービスが同じ認証情報を使っていると、そもそもどのサービスがdbを参照しているのかわからない
- シークレットマネージャ使ってサービスごとにちゃんと分けておいた方が良さそう
ビューを活用する
- 参照用途のサービスの間にビューを挟んでおく(抽象化によるブランチに似てる)
- なぜびゅーを挟むのか
- ビューの下のデータ構造を変更できるようになる(ビューのIFさえ保たれていれば)
- できることならビューとdb自体を別のインスタンスにデプロイしたい(デプロイ疎結合)
ビューの代わりにDB専用アプリケーションでラッピングする
- アプリケーション自体を分割したいケース
- 権限管理DBがあるとしたら、権限管理マイクロサービスを作る
- 他のサービスは直接権限管理DBに接続するのではなくAPI通信を行う
- 少なくともdbに対するオーナーシップが明確になる
- 問題の切り分けがしやすくなる
- BIツール系を使いたいケース
- 直接DB内部を参照させるのではなくBIツール専用のdbを提供する
- debeziumとか使って変更データを参照用dbに連携する
- bigqueryとcloud data fusionとか近い用途で使ってるな
- 呼び出し側の実装をだいぶ変えることになるし、そこまで一気にできるケースどこまであるんだろう
-
直接データベースを参照させるのは最終手段。挙動を把握しやすいサービスを噛ませることを常に優先して考える。サービスの境界を見つけることに役立つこともあるし、よりdbを変更しやすくなる
- dbをどうしても参照したい場合は最低でもビューを参照させることを考える。ビューのインターフェースさえあっていれば中身を変えられるため
- やりたいことは状態(dbのデータ)と振る舞い(appロジック)を同じところにおくこと。データと所有権を明確にすること
- 注文というエンティティが「顧客がレストランに行うこと」「レストランが食材を仕入れること」「配達員がレストランから受け取って顧客に届けること」の全てを内包していたのでdbがグチャグチャになっていた。かつ本来分離されるべき複数のサービスがdbレベルで結合しているためデプロイ分離性が損なわれていた
- まずはコンテキストに応じて注文を分離することから始めた
- 境界づけられたコンテキストがコードレベルではなくdbレベルでも破綻しているケース
段階的に分離していくアプローチ
- dbインスタンスを分けなくても、1つのインスタンス内でスキーマを分けるところから始めても良いかもしれない。特にスタートアップの場合は有効。将来的にマイクロサービスに移行するのが簡単
データの同期
- dbを複数稼働させる選択をした場合にどうやって同期させていくのか
- まずテーブルの全カラムを同期する必要はないので、必要なカラムから徐々に同期していけば良い
データから分離するかコードから分離するか
- コードから先にやった方が成果が出やすいしデータがどのように使われているのか見通しが得られることが多い
- コードを分離して新しいサービスを提供し始めたら、その時にあえて古い(悪い)スキーマにデータを入れる必要はない。そのタイミングで新しいデータソースを参照するようにしても良い
マイクロサービスによって生じるコスト
- フロートレーシング
- レイテンシ
- 参照整合性
- 外部キー制約が効かない問題にどう対処するか
- どうしても外部キーが必要な時は、そもそもサービスの切り方を疑う。それほど密に結合しているデータなら分離しない方が適切なこともある
- 参照先が見つからなかったとしても呼び出し側が破綻しないようなコードを書く。nullableな外部キーが貼れるdbなら基本的には考慮済みだからさほど追加コストにはならなそう
- 外部キー制約が効かない問題にどう対処するか
- トランザクション
- ものすごく強烈にトランザクション整合性が求められる場合はサービスの分離が間違っているヒントかもしれない
- dbの外部制約と似てる話だ
- 二つのオプションがある。分離コミットとサーガ
- 分離コミット(2-phase-commit)
- 関連サービスからの投票結果が得られてから初めてコミットする
- 問題点
- 投票結果を返してからコミットまでにラグがあるのでロック時間が長くなる
- 各サービスでコミットが実行されるまでにラグがあるので、一部サービスには反映済みだけど一部には未反映みたいな状態になりかねない。acid特製のうち分離性が担保されない(トランザクションが互いに影響を及ぼしているので分離できない)
- サーガ
- 大きなトランザクションを細かなトランザクションに分離して実行していく。失敗したら小さいトランザクションの範囲で補償アクションを実行する。意味的なロールバックとも呼ばれる
- 意味的なロールバックを減らすには処理の順番が大事。メール送信は最後にするとか、ポイント付与は商品の発送が完了してからとか。
- オーケストレーションとコレオグラフィーに分類される
- オーケストレーション: 一つのサービスにサーガ全体の管理を任せる。ビジネスロジックは集まりやすくなるが状態を把握したり、ビジネスプロセス全体を把握しやすくなる
- コレオグラフィー: akkaとかelixir的なやつ。サービスはイベントを発行するだけで、誰が購読しているかとか、システム上の他のサービスを意識する必要はない。ただしビジネスプロセスの明示が失われるのと、補償アクションを実行する機会が失われる可能性が高い(プロセスから見落としてしまうってことだろうか)。
- サーガ全体でユニークなidを持っておくことで状態を表現するビューが生成できるので、状態がわからない問題はこれで解決できる(akkaでもタスクオーケストレーションアクターにaktor-id的なやつ持たせるしな)
- メリット
- 綺麗な打ち消しはできないが、失敗の記録も残るので調査しやすい
- そもそもビジネスプロセス全体の処理を綺麗に打ち消すことはできないので最終的には意味的なロールバックが必ず入ってくるので、最初から全部そちらに寄せてしまった方がシンプル(例えばプロセスの途中で注文完了メール送信するなら、送信を取り消すことはできない。「何らかの理由でさっきの注文が失敗しました」と意味的な取り消しをすることしかできない)
- そもそもビジネスプロセス全体ではロールバックではなくロールフォワードが適切なケースも多々ある。発送に失敗したからといって注文全体を取り消すのは自然ではない。発送を何度かやり直す方が自然。こういう処理が入ってくると自然と意味的なロールバック/フォワードがどの道避けられない
- 分離コミット(2-phase-commit)
- ものすごく強烈にトランザクション整合性が求められる場合はサービスの分離が間違っているヒントかもしれない
- カスケード障害
マスター的なデータをどう扱うか
- 例えば国コードとか
- 変更頻度が低くてデータ量も少ないのであれば列挙型としてコード上に表現しちゃう
- あとはライブラリで配布するなり、cloud functionsとかで専用サービスを作るなり
- これだけのためにhttpリクエストが増えるのに抵抗がある場合はキャッシュしておけば良い
- 技術スタックが縛られるからライブラリ配布よりは後者の方が良さそう
- あとはライブラリで配布するなり、cloud functionsとかで専用サービスを作るなり
それ以外のこと
- 全ての事業は人だ。自信と動機と自由と願望を提供しなさい(john timpson)
- 社内ルールは3つだけ。身だしなみを整える、レジにお金を入れておく、最善のサービスを提供するためなら何でもする
- ==火事の後の火の用心==
- 解決策を提案するベストなタイミングは問題によってダメージを被った直後
- 新しい技術は目に見える問題を解決するために導入しよう
- それだと場当たり的にならないのかな?と気になったけど、場当たり的な対応で済むくらい疎結合になっていれば問題ないよね、ってことなのかも
- 成功と失敗の後は立ち止まって振り返ることが大切
- ==答えではなく問いを模倣しよう==
- ==うまくいっているかどうかではなく、何か他のことを試してみるべきか自問する==
- これポジティブシンキングの良い代替アプローチになりそう
- まずはチームで何を解決しようとしているのか共有する
- 解決策を説明するより課題を説明する方を厚くしたほうが良いのかもしれない
- チームの取り組むべき課題をリストアップして、各メンバーがそれぞれの課題についてどこまで知っているか1~5で評価してもらう
この先解消されていく疑問
- e2eテストの書きづらさ
- 監視のやりづらさ
- データ整合性の取り方
- 自前で作るよりerlangとかakka使った方が早くね?
- 技術的な概念に基づく分類をしているとビジネス的凝集にしづらい
- これはrailsとか見てて感じることと同じかも。screaming architectureに通ずるものを感じる
その他のリソース
- testing on the toilet
- test mercenaries
- road to envoy service mesh
このスクラップは2022/07/27にクローズされました