旧システムは 6 つのマイクロサービスで、HTTP と URL 設定を使って互いに呼び出し合っていました。聞こえは極めて標準的ですが、実際のコストは二箇所に集中していました。一つはサービス間呼び出しのネットワークオーバーヘッドと失敗処理、もう一つは「ある機能を変えるのに三つのリポジトリに手を入れ、三つのサービスをデプロイする」ことです。
呼び出しを関数呼び出しに戻す
モジュラーモノリスの核心は「分割しない」ではなく「プロセス内で分割する」です。8 つの業務ドメインが一つのプロセスに集まり、ドメイン間の呼び出しは普通の関数呼び出し —— ネットワークも、シリアライズも、サービスディスカバリもありません。
01// 每个域拆成两个 module:02// cms/api → 契约,只依赖 base03// cms/internal → 实现,外部不可 import0405// edge 层做跨域编排06func (h *Handler) SubmitAttempt(c *gin.Context) {07 node := cms.MustGetNode(id) // 域内调用08 result := quiz.Grade(node, answers) // 跨域:只经 api09 cms.Records.Save(node, result) // 再调回 cms10 c.JSON(200, result)11}境界は lint ではなくコンパイラが守る
Go の internal の仕組みにより、ドメインを跨ぐ import はビルドが通らなくなります。この制約はどんな lint ルールやアーキテクチャ文書よりも硬い —— 文書は古くなりますが、コンパイラは古くなりません。
- ドメイン間で import できるのは相手の api(契約)だけで、internal(実装)には触れない
- api は base だけに依存し、base は外部依存ゼロ
- 複数のドメインを編成するロジックは edge に統一して書き、依存グラフは二層の木になる
- 新しいドメインは api と internal を足すだけで追加でき、境界ルールを議論し直す必要はない
私たちが失ったもの
代償は現実に存在します。包み隠しません。
- 独立したスケール調整は失われる。どのドメインに負荷がかかっても、全体をスケールさせるしかない
- 障害分離は弱くなる。あるドメインの panic がプロセス全体に影響しかねない(recover とヘルスチェックで支える)
- リリース粒度は粗くなる。あるドメインの変更に、バイナリ全体の再リリースが付きまとう
- 技術スタックの固定。あるドメインを Go にして別のドメインを Java にする、というわけにはいかない
なぜこの取引が割に合うのか
このシステムの本当のボトルネックは単一ドメインのスループットではなく、イテレーション速度と運用の複雑さだったからです。6 つのマイクロサービスは、十数個のコンテナ、サービスディスカバリ、設定センター、分散トレーシングを意味します —— そうしたインフラのコストは、スケール調整で失ったわずかな弾力性よりはるかに高い。
01之前:6 个服务 · 十余个容器 · 服务发现 · 配置中心02现在:1 个二进制 · 1 个容器 · 启动时自动迁移0304$ make docker && docker run -p 8080:808005[migrate] cms v12 → v14 ✓06[migrate] quiz v07 → v09 ✓07[server] listening on :8080アーキテクチャ決定の本質はコストの比較であって、何か「より先進的な」形態を追いかけることではありません。もしいつか単一ドメインのスループットが本当にボトルネックになったら、そのドメインを切り出すのは api 契約を一度コピーするだけで済みます。