【検証】マイクロサービス化で通信遅延は倍増?Python/FastAPIでオーバーヘッドを実測比較してみた
はじめに
「ソフトウェアアーキテクチャの第一法則」をご存知でしょうか。
"ソフトウェアアーキテクチャはトレードオフがすべてだ"
『ソフトウェアアーキテクチャの基礎』より
最近、アーキテクチャの学習を進める中で、この言葉の重みを実感することが増えました。特に「マイクロサービス」はスケーラビリティや開発の柔軟性をもたらす一方で、「パフォーマンス(レイテンシ)」や「複雑性」という対価を支払う必要があると言われています。
頭では理解していても、「実際、そのオーバーヘッドはどれくらい重いのか?」を肌で感じたことがありませんでした。
そこで今回は、Python (FastAPI) と Docker を使い、「モノリス」と「マイクロサービス」でどれくらい応答速度に差が出るのかを検証(PoC)してみました。
検証環境の構成
検証のために、シンプルな EC サイトの「注文処理」を模した API を構築しました。
技術スタック
- 言語: Python 3.11
- フレームワーク: FastAPI
- インフラ: Docker Compose
- 負荷試験: Locust
- 可視化: Jaeger (OpenTelemetry)
比較するアーキテクチャ

services:
# --- モノリス ---
monolith:
build: .
volumes:
- ./monolith:/app
- ./telemetry.py:/telemetry.py
ports:
- "8001:8000"
# --- マイクロサービス群 ---
order:
build: .
volumes:
- ./microservices/order:/app
- ./telemetry.py:/telemetry.py
ports:
- "8002:8000"
depends_on:
- payment
- inventory
- jaeger # (あれば)
payment:
build: .
volumes:
- ./microservices/payment:/app
- ./telemetry.py:/telemetry.py
expose:
- "8000"
inventory:
build: .
volumes:
- ./microservices/inventory:/app
- ./telemetry.py:/telemetry.py
expose:
- "8000"
# 4. 負荷試験ツール (Locust)
load-test:
image: locustio/locust
volumes:
- ./locustfile.py:/mnt/locust/locustfile.py
ports:
- "8089:8089"
command: -f /mnt/locust/locustfile.py
depends_on:
- monolith
- order
# --- Jaeger (All-in-one) ---
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # 管理画面 (UI)
- "4317:4317" # データ受信 (OTLP gRPC)
environment:
- COLLECTOR_OTLP_ENABLED=true
-
モノリス構成 (Monolith)
- 1 つのコンテナ内で、「注文」「決済」「在庫」の処理を関数呼び出しとして実行する。
-
マイクロサービス構成 (Microservices)
- 「注文」「決済」「在庫」をそれぞれ別のコンテナに分割。
- サービス間の連携は HTTP 通信 (httpx) で行う。
測定のポイント
純粋な「通信オーバーヘッド」だけを浮き彫りにするため、ビジネスロジック(DB 処理など)の時間は極限まで短く設定しました。
- 各処理の擬似的な待ち時間:
time.sleep(0.001)(1ms) - モノリス: 関数を呼ぶだけなので、オーバーヘッドはほぼゼロ。
- マイクロサービス: HTTP 通信が発生するため、シリアライズや TCP 接続のコストが乗るはず。
実装の抜粋
違いはこれだけです。
モノリスの場合(メモリ内完結)
@app.get("/buy")
def buy_monolith():
# 関数を呼ぶだけ
payment = process_payment() # 内部関数
inventory = check_inventory() # 内部関数
return {"status": "complete"}
マイクロサービスの場合(ネットワーク通信)
@app.get("/buy")
def buy_microservice():
# わざわざHTTPリクエストを飛ばす
with httpx.Client() as client:
payment = client.get("http://payment:8000/pay").json()
inventory = client.get("http://inventory:8000/check").json()
return {"status": "complete"}
結果:残酷なまでの「3 倍」の差
Locust を使って、同時接続数 500 ユーザーで負荷をかけた結果がこちらです。

考察
平均レイテンシが約 3 倍悪化:
モノリスが約 5.5ms に対し、マイクロサービスは約 16ms でした。
処理自体はほぼ何もしない(1ms 程度)設定なのに、「分割した」という事実だけで +10ms 以上の税金 が発生しています。
安定性の低下 (95%ile):
マイクロサービス側は、高負荷時に最大 120ms まで跳ね上がっています。ネットワークの詰まりやコネクション確立のコストが、レイテンシのばらつき(Jitter)を生んでいることがわかります。
Jaeger で「見えない壁」を可視化する
「なぜ遅いのか?」を視覚的に確認するために、Jaeger(分散トレーシング)を導入しました。 これが、マイクロサービスにおけるリクエストの裏側です。

このウォーターフォールチャートから、以下のことが読み取れます。
階段状の遅延:
モノリスなら垂直に落ちるはずの処理フローが、右下に向かって階段状になっています。これが「行って、帰ってくる」のを待っている時間です。
Span の隙間(オーバーヘッド):
親のバー(青色:呼び出し側)と、子のバー(茶色:実処理)の間に、左右のズレがあります。
これこそが、JSON のシリアライズ/デシリアライズ、TCP ハンドシェイク、ネットワーク転送にかかっている「純粋な通信コスト」です。
まとめ:アーキテクトとしての学び
今回の PoC を通じて、教科書的な知識だった「トレードオフ」を実測値として体感することができました。
マイクロサービスは「速さ」のための技術ではない。
むしろ、単一リクエストのレイテンシは構造的に悪化する。
分割はコストである。
安易にサービスを分けると、そこには必ず「ネットワーク」という不安定で遅い媒体が介在する。
それでも分割する理由は?
この「遅さ」という税金を払ってでも、「チームの独立性」や「特定機能のスケーラビリティ」が欲しい時だけ、マイクロサービスを選択すべき。
「流行っているからマイクロサービス」ではなく、「この対価を払う価値が、このシステムにあるか?」 を問うのが、アーキテクトの仕事なのだと痛感しました。
Discussion