TL;DR
- AWSで東京/大阪をAct-Actにし、DBは東京単一という構成のレイテンシコストを実測した
- 代償はリージョンをまたぐ呼び出し1回あたり約17.6ms。これがリクエスト内の呼び出し回数だけ積み上がる
- 同じ15回の呼び出しでも、直列なら +243ms、並列なら +17ms。差は約28倍
- Act-Act化の可否は距離ではなく、アプリがDB呼び出しを並列化できているかで決まる
やりたかったこと
AWSで東京・大阪の2リージョンにAct-Actでシステムを提供したい。可用性を上げ、障害時の切り替え時間も短くしたい。
一方でDBは東京単一にする方針にした。両リージョンにDBを持つとレプリケーション構成や書き込み競合の考慮が必要になりコストも運用も複雑になる。DBを東京だけに寄せればその複雑さを避けられ、構成もシンプルに保てると考えた。
ただしこの構成だと、大阪のAPから見るとDBが常に別リージョン(東京)にある状態になる。東京リージョンに接続した場合と大阪リージョンに接続した場合とで、当然性能差が出る。知りたかったのは 「その差がどれくらいで、許容できるレベルかどうか」。
レイテンシは改善したい対象ではなく、可用性のために払う代償として見る。これが評価軸。
測定の前提となる構成
- Web/APレイヤは東京・大阪の両方にActive-Activeで配置。DB(Aurora)とモックAPIは東京のみ
- リージョン間の接続は Transit Gateway(inter-region peering)経由
- app(FastAPI, Python 3.12): 1vCPU/2GB × 2タスク(Fargate)を東京・大阪の両方に配置
- mock-api: 0.5vCPU/1GB × 1タスク、東京のみ
- DB: Aurora PostgreSQL、db.r6g.large × 1(東京のみ)。バースト型はCPUクレジット変動で
測定の再現性が崩れるため非バースト型を選定 - DB接続は asyncpg + コネクションプール(min=max=50)
- NAT Gatewayは各リージョン1つ
- 負荷試験クライアント(k6)は東京・大阪の両方にEC2(t3.small)を配置し、SSM Session Manager経由で操作
スペックは要件である「100 rpsでDB/APが飽和しない」ことを優先して決めている。 最小構成だとリソース競合とリージョン間RTTの影響が混ざって切り分けられなくなるため、先にリソース面の余裕を確保した上でリージョン間の代償だけを取り出せるようにした。
測定方法
クエリは主キー等値検索を基本にした
-- 実行コストを ~0.4ms に固定する
SELECT id, payload, created_at FROM items WHERE id = $1;
単発読取のサーバ側処理時間を 2.82ms まで抑えることで、リージョン間RTTを支配項にした。比較として、クエリコストが支配的なケース(mode=heavy、1万行全件スキャン+ソート)も意図的に用意している(後述)。
東京と大阪を交互に測る
これが一番効いた。ラウンドの最内側で東京と大阪を交互に叩き、ラウンドごとに順序も反転させる。
for round in $(seq 1 "$ROUNDS"); do
# 奇数ラウンドは東京→大阪、偶数ラウンドは大阪→東京。順序効果も相殺する
if [ $((round % 2)) -eq 1 ]; then
region_order=("tokyo:$TOKYO_HOST" "osaka:$OSAKA_HOST")
else
region_order=("osaka:$OSAKA_HOST" "tokyo:$TOKYO_HOST")
fi
for case_id in $LATENCY_CASES; do
for region_host in "${region_order[@]}"; do
run_one ...
done
done
done
分析では同一ラウンド内の東京/大阪ペア差分を主指標にする。こうすると時間帯によるドリフトが相殺される。
「飽和していないこと」をレイテンシで判断しない
レイテンシだけ見ていると、「リージョン間RTTが積み上がって遅い」のか「リソースが足りなくて遅い」のかが区別できない。
なので測定前に、リソースメトリクス側でゲートを設けた。
| 項目 | 基準 | 実測(東京 / 大阪) |
|---|---|---|
k6 dropped_iterations
|
0 | 0 / 2(21002試行中) |
| ECS CPU (app) | < 70% | 40.3% / 42.8% |
| Aurora CPU | < 50% | 42.4% / 46.2% |
Aurora DatabaseConnections
|
上限に張り付かない | 200(期待値どおり) |
Aurora BufferCacheHitRatio
|
安定 | 100% / 100% |
これを通らないうちは本測定に進まない、というルールにした。
測定して分かったこと
代償の単価は 17.6 ms/call
呼び出し回数 N に対する総遅延を回帰した結果:
| 経路 | reads | writes |
|---|---|---|
| 東京AP → 東京DB | 3.54 ms/call | 5.88 ms/call |
| 大阪AP → 東京DB | 21.22 ms/call | 23.36 ms/call |
| 差(=代償) | +17.7 ms/call | +17.5 ms/call |
読取でも書込でもほぼ同じ、約17.6ms。これがNに比例して積み上がるのがコスト構造の本質。
直列か並列かで28倍違う
ここが一番の収穫。呼び出し内容が完全に同一(読取10・書込3・API 2の計15回)で、直列実行か並列実行かだけが違う2ケースを比較した。
| 東京AP経由 | 大阪AP経由 | 代償 | |
|---|---|---|---|
| 直列で15回 | 65.8 ms | 308.8 ms | +243.0 ms |
| 並列で15回 | 11.5 ms | 28.8 ms | +17.3 ms |
直列だと17.6msが15回分積み上がる。並列だと往復が重なって1回分に畳まれる。
同一ラウンド内のペア差分で見ても同じで、直列 +234.6 ± 15.3ms に対し並列 +8.5 ± 0.6ms。3ラウンドを通じて安定している。
# 並列側の実装(FastAPI + asyncpg)
tasks = (
[_read() for _ in range(reads)]
+ [_write() for _ in range(writes)]
+ [_api() for _ in range(apis)]
)
for key, ms in await asyncio.gather(*tasks):
phases[key].append(ms)
Act-Act化できるかどうかは、リージョン間の距離の問題ではなく、アプリの実装様式の問題だった。
地理的距離そのものの影響は小さい
「大阪にAPを置くこと」と「ユーザーが西日本にいること」は別の話なので、分離して測った。
同じ「東京AP→東京DB」を、東京と大阪の両方のクライアントから叩いた差:
| ケース | 東京クライアント | 大阪クライアント | 差 |
|---|---|---|---|
| 単発読取 | 6.4 ms | 14.1 ms | +7.7 |
| 複合15回(直列) | 65.8 ms | 74.2 ms | +8.4 |
| 複合15回(並列) | 11.5 ms | 20.3 ms | +8.8 |
約8msで一定。呼び出し回数に比例しない(クライアント→ALB間は1往復しかないので当然)。
つまり西日本のユーザーが東京のAPに直接つないでも、体感できるほどの差にはならない。効いてくるのはAP→DB間の往復がリクエストあたり何回発生するかだけ。
裏を返すと、レイテンシだけを見れば大阪にAPを立てる積極的な理由はない。損益分岐点を実測回帰から求めると N* = 0.46 で、1回を下回る。DBが東京にある以上、大阪APを経由してもAP→DB間の跨ぎは避けられず、「クライアント→APが近い」メリット(8ms)が「AP→DBが遠い」デメリット(17.6ms/call)に勝てない。
ただし今回の目的は可用性なので、この数字は「レイテンシ面での積極的理由はない」ことを示すに留まる。Act-Act化の是非を決めるものではない。
おまけ: クエリコストが支配的だとどうなるか
比較のため、意図的に重いクエリのケース(mode=heavy、1万行全件スキャン+ソート)も測った。
結果は経路によらず 520〜1033ms。しかも同じ経路を測っても、測定時刻によって2倍近くぶれた(東京AP→東京DBのサーバ側処理時間が、あるセッションで516ms、別のセッションで936ms)。
経路差はペア差分で取り出せば +86.2ms と出るのだが、総遅延1000msに対して8%程度で埋もれる。
軽いクエリ(左)では経路ごとの水準がラウンドを通して安定して分離できているのに対し、重いクエリ(右)では同じ経路・同じクライアントの組でも測定時刻(ラウンド)ごとに数百ms単位でドリフトし、経路差が測定時刻のドリフトに埋もれてしまっている。
クエリコストが支配的な状態では、リージョン間の経路差を議論しても意味がないことが確認できた。
結局どうするか
測定結果から、2つの運用モードのトレードオフが定量化できた。
A: 平常時から両リージョンに振り分ける
- 西日本ユーザーは常時、代償(直列回数 × 17.6ms)を払う
- 並列型のアプリなら +17ms で済む。直列15回のアプリだと +243ms で厳しい
B: 平常時は東京に寄せ、障害時に大阪へ切り替える
- 平常時の代償はゼロ
- 大阪側は常時ウォームなので、切り替えはルーティングの重み変更だけ。
インフラの起動待ちが発生しない(=切り替え時間短縮という目的は達成できる) - 障害時は全ユーザーが大阪AP経由の縮退運転に入る(直列型で318.8ms、並列型で37.5ms)
どちらを選ぶにせよ、先にやるべきはアプリの棚卸しだという結論になった。リクエストあたりのDB/API呼び出し回数と、それが直列か並列かを数える。「直列呼び出し回数 × 17.6ms」が許容範囲に収まらないなら、Act-Act化より先に並列化・バッチ化をやる方が費用対効果が高い。
正直に書いておく限界
DBが単一障害点として残る。 今回のAct-Act化で吸収できるのはAZ障害、デプロイ事故、APレイヤのキャパシティ不足まで。
東京リージョン全体の障害には耐えられない(Auroraが失われるので大阪APも機能しない)。リージョン障害への耐性が要るなら、Aurora Global Database等が別途必要になる。
切り替え時間そのものは測っていない。 「常時ウォームだから速い」は構成上の性質であって、実際のフェイルオーバー時間(DNS切替、ヘルスチェック検知、ルーティング反映)は未測定。
クライアントは大阪リージョン内のEC2であり、実際の西日本エンドユーザー(モバイル回線、ISP経由)より条件が良い。「距離の影響は8ms」は下限値。
呼び出し1回あたりの代償17.6msが、生RTT 11.3msを上回る理由は特定できていない。
HTTP(API呼び出し)の代償は9.6msで生RTTとほぼ整合するので、DBのクエリ往復がHTTPより多くの往復を要している可能性があるが、確証はない。見積もりには実測値の17.6ms/callを使い、生RTTからの単純計算は避けるべき。
まとめ
- AWSのマルチリージョン構成でのレイテンシは「距離」ではなく「距離 × 直列往復回数」で効く
- 今回の構成(DBは東京単一)では 17.6 ms/call。並列化できれば往復1回分に畳める
- 測定は、測りたい信号より大きいノイズを先に潰すところから始める
- 比較対象は同一セッション内で交互に測る。別々に測ると時間帯の差と交絡して分離できない




