開発・技術選定

モノリスのままスケールさせる — マイクロサービス化の前にやること

規模が大きくなると「マイクロサービス化」の話が出ますが、
分割は運用コストを大幅に増やします。その前にできることを整理します。

分割で増えるもの

  • ネットワーク越しの通信(遅延・失敗を扱う必要が出る)
  • トランザクションが跨げない(整合性の担保が難しくなる)
  • デプロイ対象と監視対象の増加
  • 障害の切り分けが複雑になる

チームが分かれていないのに、システムだけ分けるのは、
利点が出ないままコストだけ増える典型です。

効果の大きい順

1. クエリの改善(最大)

多くの「遅い」は、単純に N+1 かインデックス不足です。

# 遅いエンドポイントを特定して、クエリ数を数える
reset_queries()
response = client.get("/orders/")
print(len(connection.queries))     # 200 とか出たら N+1

サーバーを増やす前に、必ずここを確認します。
効果は数倍から数十倍になることがあり、コストはゼロです。

2. キャッシュ

集計値や、更新頻度の低いデータをキャッシュします。
これも構成を変えずに済みます。

3. 重い処理の非同期化

レポート生成やメール一括送信をリクエストから外します。
ワーカーが専有されなくなり、同時接続数の余力が生まれます。

4. 読み取り専用レプリカ

読み取りが多いなら、参照用のDBを分けます。
アプリのコードはほぼ変えずに負荷を分散できます。

DATABASES = {"default": {...}, "replica": {...}}

class ReadReplicaRouter:
    def db_for_read(self, model, **hints):
        return "replica"
    def db_for_write(self, model, **hints):
        return "default"

レプリケーション遅延があるため、
書き込み直後の読み取りはプライマリに向ける必要があります。

5. アプリサーバーの水平展開

モノリスでも、ステートレスなら台数を増やせます。

  • セッションをDBかキャッシュに置く(メモリに持たない)
  • アップロードファイルを共有ストレージに置く
  • 定期処理が二重実行されないようにする

この3つを満たせば、モノリスのまま台数を増やせます。

それでも分割するとき

次の条件が揃ったときに、初めて分割を検討します。

  • 特定の機能だけ極端に負荷が高く、独立してスケールさせたい
  • チームが分かれていて、デプロイの独立性が必要
  • 技術要件が根本的に違う(例:機械学習の推論だけ別基盤)

まず1つだけ切り出すのが安全です。全体を一度に分けると、
問題が起きたときに原因が特定できなくなります。

モジュール分割は先にやっておく

サービスとして分割しなくても、
コードの中でモジュール境界を明確にしておくことには価値があります。

apps/
  orders/      … 注文
  billing/     … 請求
  inventory/   … 在庫

モジュール間の呼び出しを限定しておくと、
将来分割する必要が出たときの切り出しが容易になります。

まとめ

  1. クエリ改善(効果最大・コストゼロ)
  2. キャッシュ
  3. 重い処理の非同期化
  4. 読み取りレプリカ
  5. アプリサーバーの水平展開

ここまでやってもなお足りない場合に、初めて分割を検討します。
分割はスケールの手段であって、目的ではありません。