割ると何が良くなり、何が悪くなるのか
一言でいうと
マイクロサービスは、性能のための選択ではなく、チームが互いを待たずにデプロイするための選択です。その代償として、関数呼び出しだったものがネットワーク呼び出しになります。
なぜ必要なのか
モノリスは悪いという話をよく耳にします。しかし、実際に崩れていく順序を見ると、先に崩れるのはコードではありません。デプロイが先です。1つのリポジトリに8つのチームがコミットし、リリースブランチを切るのに2日かかり、1つのチームのバグで全体のデプロイが止まる状況。ここで人々が「分割しよう」と言い始めます。
その診断は、たいてい正しいものです。問題はその先です。分割した瞬間に失うものの一覧は、たいてい用意されていません。モノリスの中でinventory.reserve(sku, 3)は、ナノ秒単位の関数呼び出しで、失敗しえませんでした。サービスに分けた瞬間、それはミリ秒単位のHTTP呼び出しになり、タイムアウトすることもあり、半分だけ成功することもあり、相手が再起動中であることもあります。そして、その呼び出しを包むトランザクションは、もはや存在しません。
どう動くのか
判断の基準は3つあります。
1つ目は、デプロイの独立性が本当に必要か。2つの機能が常に一緒にデプロイされるなら、分けて得られるものはほとんどありません。分けたのに常に一緒にリリースするなら、それは分散モノリスで、モノリスの欠点に分散システムの欠点を足したものです。
2つ目は、データが分かれるか。2つのサービスが同じテーブルへの書き込みを続けるなら、境界の引き方が間違っています。サービスの境界は、コードの境界ではなく、データの所有権の境界です。
3つ目は、チームがあるか。サービスごとにオンコールに就く人が必要です。3人のチームが9つのサービスを運用すると、デプロイは速くなる代わりに、夜中のページが3倍になります。
移行の方法は、ビッグバンではなく、ストラングラーフィグ(Strangler Fig)です。前面にファサードを置き、機能を1つずつ新しいサービスに移し、トラフィックを割合で移し、安定したらモノリスからそのコードを削除します。変換 → 共存 → 削除を繰り返します。この方式の要点は、いつでも元に戻せることです。ファサードのルーティングを1行戻すだけで済みます。
現場での姿
ファサードは、たいていAPIゲートウェイ(Kong、Envoy、NGINX)が担います。ここに腐敗防止層(Anti-Corruption Layer)を併せて置きます。レガシーのデータモデルが新しいサービスに染み込まないように、翻訳する層です。これを省略すると、新しいサービスが6か月でレガシーのスキーマにそっくりになっていきます。そうなると、分割した意味がなくなります。
最もよく見る失敗は、「在庫サービスを切り出したのに、在庫テーブルはそのまま共有している」です。こうすると、デプロイは分かれましたが、スキーマの変更には今でも2つのチームの合意が必要です。結合はコードではなくデータにあったからです。
分割する前に支払うコストの一覧
サービスを1つ増やすと、増えるのはコードだけではありません。実際に付いてくるものは、次のとおりです。
| 項目 | 増えるもの |
|---|---|
| リポジトリ・CI | パイプライン1本、ビルド時間、シークレットの管理 |
| デプロイ | マニフェスト、ロールアウト戦略、ロールバックの手順 |
| 観測 | ダッシュボード、アラート、ログのラベル、トレースの伝播 |
| オンコール | ランブック1つ、担当者の指定 |
| 通信 | ネットワークの往復、シリアライズ、リトライ・タイムアウトの設計 |
| データ | トランザクションの境界が途切れる。サーガや結果整合性 |
最後の行が最も高くつきます。1つのトランザクションの中で終わっていた処理が、補償トランザクションと状態 機械になります。コードが3倍になり、バグがその中に隠れます。
それでも分割するのが正しいというシグナル
コストが大きくても、分割すべき場合があります。3つのうち1つに当てはまれば、見合います。
デプロイの周期が大きく異なります。1日に10回出ていく部分と、四半期に1回出ていく 部分が1つのリポジトリにあると、遅いほうが速いほうを引き止めます。
リソースの特性が異なります。GPUを使う推論と普通のCRUDを同じPodに置くと、GPU ノードにCRUDも一緒に載ってしまいます。スケーリングの軸が違うなら、分けるのが正解です。
障害の分離が必要です。レポートの生成がメモリを使い果たして決済まで落ちるなら、 その2つは分離されなければなりません。
チームが違うというだけでは足りません。チームは変わりますし、組織図に沿ってシステムを 分けると、組織が変わるたびにシステムを分け直すことになります。
元に戻すことも設計のうち
分けたものをまた統合することは、思ったよりよく正解になります。ところが、ほとんどのチームは、これを 失敗と見なして先延ばしにします。
統合すべきシグナルは、次のとおりです。
- 2つのサービスが常に一緒にデプロイされる
- 1つの機能を作るとき、両方とも直さなければならない
- 間の呼び出しが同期で、必須である(1つが落ちると、もう1つも役に立たない)
- データが1つのトランザクションであるべきなのに、サーガで真似している
3つ以上に当てはまるなら、統合したほうがよいでしょう。モジュラーモノリスに戻すのは、後退ではなく 訂正です。
分割の順序。何を先に切り出すのか
分割すると決めたら、次の問いは順序です。どこでも先に切り出すと、最も難しいものを 最初にすることになります。
先に切り出す候補は、3つの条件を満たします。データをほとんど共有せず、デプロイの周期が 本体と違い、失敗しても本体が動き続けるものです。通知の送信、ファイルの変換、レポートの生成、 検索インデックスのようなものが、これに当たります。
後で切り出すのは、取引の中心です。注文・決済・在庫のように、1つのトランザクションでまとまって いたものを分けると、その瞬間に分散トランザクションの問題を抱え込みます。補償トランザクションや サーガパターンが必要になり、それはチームの準備が整ってから行うことです。
データを先に分けます。サービスだけを分けてデータベースを共有するのが最悪です。 デプロイは別々に行うのにスキーマは一緒に変わるので、両方のデプロイを同時に合わせなければならない 状態になります。それならモノリスのほうがましです。
切り出す前に、境界をコードの中で先に作ります。モジュールに分け、そのモジュール間の 呼び出しを1つのインターフェースに絞り、そのインターフェースが安定するまで待ちます。 その状態になれば、プロセスを分けるのは機械的な作業になります。境界を知らないまま プロセスから分けると、その境界をネットワークの向こう側で直すことになります。
元に戻せるように分けます。新しいサービスにトラフィックを少しずつ流し、問題があれば 本体のコードに戻します。本体からコードを削除するのは、数週間問題がなかったときに行います。
測るものを先に決めておきます。デプロイの頻度、変更の失敗率、そして1つの機能を直すのに 触るリポジトリの数です。最後のものが増えているなら、境界が間違っているので、さらに分割する 前に立ち止まって見直します。
次のラボですること
注文と在庫が1つのプロセスに入っているモノリスを受け取り、先に契約をドキュメントとして固定してから、2つのサービスに分けます。分けた後、2つの実装のレスポンスが同じかどうかを自動で比較するスクリプトを作り、最後に「これは分割すべきではなかった」に当たる根拠も1行書きます。