モノリスは10年前のもので、リリースのたびに長いリグレッションサイクルが必要です。そこで誰かが書き直しを提案します。新システムを並行して作り、機能の同等性に達したら、週末に切り替えるというものです。その間も旧システムは変わり続け、切り替え日は延びていきます。Martin Fowler は、自分と同僚がこの計画を「たいていは炎上して終わる」のを見てきたと書いています。
ストラングラーフィグパターンは、代わりにシステムを少しずつ置き換えます。モノリスの前に置いたファサードが各リクエストを旧コードか新サービスへ振り分け、モノリスへ向かうものがなくなるまで、機能を1つずつ移します。ルーティングは簡単な部分です。データの所有、同等性の証明、そして終わらせることが難しい部分です。
この記事ではそのすべてを、進行中の移行の図とともに扱います。
名前の由来
2001年、Fowler はクイーンズランドの熱帯雨林でストラングラーフィグ(絞め殺しのイチジク)を見ました。宿主の木の上で発芽し、地面に根を張り、木より長生きして、木の形のままイチジクだけが残ることもあるつる植物です。2004年6月、彼はこれをレガシーシステムを段階的に置き換えるためのたとえに使いました。2019年4月には植物を指し示すため、記事名を「Strangler Application」から「Strangler Fig Application」に改め、2024年8月に書き直しました。現行版は、チームが抵抗しがちな点を強調しています。それは移行期のアーキテクチャ、つまり新旧を共存させるためだけに作るコードです。
ルーティングはファサードが担う
ファサードは、モノリスの前に置くリバースプロキシまたは API ゲートウェイです。NGINX、Envoy、クラウドの API ゲートウェイ、パスルール付きのロードバランサーなどが使えます。最初はすべてをモノリスへ送ります。その後は、移行の各ステップがルーティングの変更になります。次の基準で振り分けます。
- パス:
/catalog/*はカタログサービスへ送ります。 - メソッド:
POST /ordersより先にGET /orders/*を移し、読み取りから始めます。 - 割合: セッション ID またはユーザー ID のハッシュで、各ユーザーを同じ側に固定します。
- ヘッダーまたはテナント: 社内ユーザーやパイロット顧客を先に移します。
# upstream blocks catalog, orders and monolith defined elsewhere
split_clients "${cookie_session}" $orders_read {
25% orders; # canary share for reads
* monolith;
}
map $request_method $orders_target {
GET $orders_read;
default monolith; # order writes stay in the monolith
}
server {
location /catalog/ { proxy_pass http://catalog; }
location /orders/ { proxy_pass http://$orders_target; }
location / { proxy_pass http://monolith; }
}
ロールバックは1行の編集で済みます。25% の行を monolith に向けてください(nginx は 0% のエントリを受け付けません)。ハッシュには、すべてのリクエストが持つキーを使ってください。Cookie がないリクエストは、すべて同じバケットに入ります。
ファサードは薄く保ちます。ルーティング、TLS、認証、ログ記録だけを担い、ビジネスルールは入れません。すべてのリクエストが通るので、複数のインスタンスで動かし、追加されるレイテンシを測ってください。ゲートウェイに何を置くべきかは、API ゲートウェイのアーキテクチャ図で扱っています。
移行の4つのフェーズ
| フェーズ | ファサードのルーティング | データ | 完了の条件 |
|---|---|---|---|
| 0. まずファサード | すべてモノリスへ | 変更なし | ファサードが稼働し、追加されるレイテンシを計測済み |
| 1. 最初の機能 | /catalog/* など1つのルートを新サービスへ |
サービスはモノリスのテーブルか CDC のコピーを読み、その後は自前のデータを使う | ルートが100%で、業務サイクルを1周した |
| 2. 大半のトラフィックが新サービス | ほとんどのルート。モノリスには既知の一覧だけが残る | 各サービスが自分のテーブルを所有する | 残りが1ページに収まる |
| 3. モノリス廃止 | モノリスへは何も送らない | レガシーデータベースをアーカイブしてから削除 | モノリスを停止した |
Microsoft の Strangler Fig パターンのページは最後のステップを加えています。ファサードを取り除くか、レガシークライアント向けのアダプターとして残すかです。
最初に切り出すものを選ぶ
最初の候補として適しているのは、次の3つの特徴を持つものです。
- 明確な継ぎ目: Michael Feathers の言葉では、その場所を編集せずに振る舞いを変えられる場所です。URL のプレフィックス、キュー、モジュールの境界などがあたります。
- 変更頻度が高い: 移行の効果がすぐ出ます。バージョン管理で確認できます。
- 結合が弱い: 他のテーブルへの join が少なく、モノリスへ戻る呼び出しも少ないものです。
git log --since="12 months ago" --format= --name-only -- src/ \
| sort | uniq -c | sort -rn | head -20
オンラインストアでの順位付けの例です。
| 候補 | 継ぎ目 | 変更頻度 | 結合 | 判断 |
|---|---|---|---|---|
| カタログ閲覧 | /catalog/* |
高 | 低 | 最初 |
| 注文履歴 | GET /orders/* |
中 | 中 | 2番目、読み取りから |
| チェックアウト | 価格、在庫、決済を呼び出す | 高 | 高 | 依存先の後 |
| 夜間レポート | スキーマ全体にわたる SQL | 低 | 非常に高い | 最後、またはウェアハウスへのフィード |
切り出しは技術的なレイヤーではなく、ビジネス機能ごとに行います。共有の「データアクセスサービス」は何も動かさず、すべてを結合させます。
データを移す
ルーティングが動かすのはリクエストであってデータではなく、移行が止まるのはデータの部分です。各ステップで、どのテーブルを誰が所有するかを決めてください。
| 戦略 | 仕組み | 向いている場面 | リスク |
|---|---|---|---|
| 共有データベース | 新サービスがモノリスのテーブルを使う | 最初の数週間 | モノリスのスキーマ変更で壊れる |
| Change data capture | コネクターがモノリスのログから行の変更をストリーミングする | 読み取りを先に移すとき | 遅延、レガシースキーマ、一方向のフロー |
| サービスごとのデータベース | サービスが自分のテーブルを所有し、他は API を呼ぶ | 最終形 | サービスをまたぐ join が API 呼び出しになる |
| コードからのダブルライト | アプリケーションが両方のデータベースに書く | 使わない | 2つの書き込みの間でクラッシュすると同期がずれる |
Microsoft のガイダンスは、通常の順序を示しています。新サービスはまずモノリスのテーブルで始まり、次に自前のデータベースを持ちます。これは一度ロードして CDC で最新に保ち、検証後にシステムオブレコードになります。レガシーのテーブルを削除するまでは、ロールバックはルートの変更で済みます。削除後はリストアになります。
ダブルライトは避けてください。2つの別々の書き込みの間でクラッシュするとデータベース同士が食い違い、同時更新が異なる順序で届くこともあります。CDC かトランザクショナルアウトボックスを使い、1つのトランザクションをすべての変更の起点にします。Debezium とレプリケーションスロットについては、change data capture の図で扱っています。
腐敗防止層
モノリスのモデルは漏れ出します。1文字のステータスコード、1つのテーブルに同居する請求と配送のフィールド、ストアドプロシージャの中のルールなどです。これらの行を使うサービスは、離れようとしているモデルを引き継いでしまいます。Eric Evans の Domain-Driven Design(2003年)に由来するパターンである腐敗防止層は、境界で変換します。
- レガシーから新へ: レガシーの行を運ぶ CDC イベントが、新モデルの注文になります。
- 新からレガシーへ: 注文サービスは、アダプターを通じてモノリスから顧客データを読みます。
- モノリスから新へ: カタログを移した後は、モノリスのコードが古いテーブルの代わりにカタログ API を呼びます。
ビジネスルールは層に入れないでください。ヘキサゴナルなサービスでは、この層はアダプターの中にあり、コアがレガシーの型を目にすることはありません。
ルートを移す前に同等性を証明する
どのルートも同じ手順を踏みます。
- シャドウ。 ファサードがリクエストを新サービスへコピーし、そのレスポンスは破棄します。NGINX のミラーモジュールや Envoy のリクエストミラーリングがこれにあたります。ミラーするのは読み取りだけです。ミラーした
POST /ordersは2件目の注文を作ってしまいます。 - 比較。 タイムスタンプ、ID、並び順を正規化してから、両方のレスポンスを比較します。2015年にオープンソース化された Twitter の Diffy は、旧コードを2つ動かし、その間で異なる部分をノイズとして無視します。モノリスの内部では、GitHub の Scientist ライブラリ(Ruby 製で、他言語への移植もあります)が旧パスと新パスを並べて実行し、旧パスの結果を返します。
- カナリア。 実トラフィックの1%、5%、25%、50%、100% の順に移し、同じルートについてエラー率と p99 レイテンシをモノリスと比べ、各ステップを日次のピークをまたいで保ちます。仕組みはデプロイ戦略の図で扱っています。
- 維持。 100% の状態で、旧パスをデプロイ可能なまま、そのデータも月次の締めのようなイベントをまたいで最新に保ちます。
- 旧コード、そのテーブル、ルートを削除します。
ルートを元に戻せるのは、モノリスのデータが最新である間だけです。新サービスが書き込みを所有したら、逆方向の CDC やイベントで書き戻してください。そうしないと、ロールバックで書き込みが失われます。
ストラングラーフィグ移行が失敗するパターン
- 終わらない。 簡単な8割は移りますが、レポート、バッチジョブ、管理画面が残り、2つのシステムを運用することになります。毎スプリント残りを数え、停止日を決めてください。
- 分散モノリス。 テーブルを共有する、一緒にデプロイされる、長い同期呼び出しの連鎖で互いを呼ぶサービスです。判定基準は、テーブルごとに書き込み側が1つであることです。
- ボトルネックになるファサード: ゲートウェイにビジネスロジックがある、または1つのインスタンスが全トラフィックを担っている状態です。
- 見えない利用者。 レポートや他チームがモノリスのデータベースを直接読んでいて、ファサードからは見えません。テーブルを移す前に接続ログを確認してください。
- 機能の同等性を目標にする。 Fowler は、レガシーの振る舞いの多くは「本当は求められていない」と指摘しています。Patterns of Legacy Displacement は、これを含む落とし穴を扱っています。
ストラングラーフィグ移行の図
ダイアグラムの内容
- Customers
- Facade (API gateway)
- Catalog service
- Orders service
- Legacy monolith
- Catalog DB
- Orders DB
- Monolith DB
- Debezium + Kafka
- Anti-corruption layer
- HTTPS
- /catalog/* · 100%
- GET /orders/* · 25%
- everything else + 75% of GET /orders
- reads, writes
- reads
- WAL changes
- order row events
- upserts in the new model
図の読み方は次のとおりです。
- 左から右: 顧客がファサードを呼び、ファサードがルートごとにトラフィックを振り分けます。データベースは右側にあります。
- 緑、移行済み:
/catalog/*のすべてが、自前のデータを持つカタログサービスへ向かいます。 - 琥珀色、カナリア:
GET /orders/*の 25% が注文サービスへ向かいます。書き込みと残りの読み取りは、まだモノリスへ向かいます。 - 灰色、レガシー: モノリスが残りを処理し、注文データも引き続き所有します。
- 紫、破線: Debezium がモノリスの WAL から注文の変更を Kafka 経由でストリーミングし、腐敗防止層がそれを新モデルに変換します。
- ルート表には、ルートごとの送信先、割合、フェーズが載っているので、ロールバックは1行の編集で済みます。
ストラングラーフィグ移行をわかりやすく描く
- フェーズごとに1枚の図を描くか、日付入りのタイトルを付けます。古い図は方向性を示すために残してください。
- ルートにはパスと割合を書きます。 たとえば「GET /orders/* · 25%」です。
- データの所有を示します。 システムオブレコードと、各同期の向きです。
- 色はチームではなくステータスで分けます(移行済み、カナリア、レガシー)。
- 内部の詳細は省きます。 これは移行マップであり、コンポーネント図ではありません。
- 見えない利用者も描きます。 たとえば、モノリスのデータベースを読む夜間レポートです。
最終形を読みやすく保つ方法は、マイクロサービスのアーキテクチャ図で紹介しています。
Archboard で作る
- Google または GitHub でサインインし、ダイアグラムを作成します。
- 顧客用に C4 Model パックの人物を、ファサード用にクラウドパックの APIゲートウェイを追加します。
- C4 Model パックのコンテナノードを3つ追加します。Catalog service、Orders service、Legacy monolith です。それぞれ緑、琥珀色、灰色に塗ります。
- 各コンテナの右側に、データパックの Postgres ノードを追加します。
- モノリスのデータベースと注文データベースの間に、データパックの Kafka ノード(Debezium + Kafka)と、C4 Model パックのコンポーネント(Anti-corruption layer)を重ねて置き、破線のエッジで上向きにつなぎます。
- ファサードからルートを描き、パスと割合をラベルに付けます。各コンテナの上に、基本パックのテキストでステータスラベルを追加します。
- ルート表を入れた Markdown ブロックを、基本パックから追加します。
ルートマップが Mermaid のフローチャートにある場合は、Mermaid コンバーターで編集可能な最初の下書きに変換できます。
この例で使うダイアグラムノードは 11 個で、Free プランの範囲に収まります。
よくある質問
ストラングラーフィグパターンとは何ですか? レガシーシステムを段階的に置き換える方法です。ファサードが各リクエストを旧コードか新サービスへ振り分け、機能を1つずつ移し、どこからも振り分けられなくなった時点で旧システムを停止します。
なぜストラングラーフィグパターンと呼ばれるのですか? Martin Fowler が2004年に、クイーンズランドで見たストラングラーフィグにちなんで名付けました。この植物は宿主の木の周りに育ち、その木に取って代わることがあります。2019年には、植物を強調するため名前に Fig を加えました。
ストラングラーフィグ移行では、データベースをどう扱いますか? 段階を踏みます。新サービスはまずモノリスのテーブルを読み、次に change data capture で更新される自前のデータベースを持ち、検証後に書き込みを引き継ぎます。アプリケーションコードからのダブルライトは避けてください。
ストラングラーフィグと branch by abstraction の違いは何ですか? ストラングラーフィグはシステムの境界でリクエストを振り分けます。Branch by abstraction はコードの内部で働きます。インターフェースによって、すべての呼び出し元が切り替わるまで新旧の実装が共存できます。多くの移行は両方を使います。
ストラングラーフィグパターンを使うべきでないのはどんなときですか? 旧システムへのリクエストを横取りできないとき、システムが小さく一度に置き換えられるとき、または早く停止しなければならないときです。