メインコンテンツまでスキップ

ローカル環境 vs クラウド環境 - 性能テストの実践

ローカル開発環境と商用環境(AWS等)における性能の違いを理解し、現実的な性能テストを実践するためのガイドです。


なぜ環境差を理解すべきか

「ローカルで10msだったのに、本番で200msもかかる」「ローカルで100ms改善できたのに、本番では誤差の範囲内だった」——こうした経験はありませんか?

環境差を理解せずにチューニングすると、無意味な最適化に時間を費やすことになります。まずは両環境のシステム構成を見比べて、何が違うのかを把握しましょう。


システム構成の比較

まず、ローカル環境とAWS本番環境の構成を並べて見てみましょう。

ローカル環境では、全てのコンポーネントが1台のマシン上で動作します。Docker Composeで起動したコンテナ同士は仮想ネットワークで接続され、通信遅延はほぼゼロです。

一方、AWS本番環境では、EKSクラスター、Aurora Global Database、ElastiCacheなどが物理的に分散配置されます。本ドキュメントで紹介する構成は、エンタープライズ向けの本格的な構成例です。

この構成が必要になる背景:

要件構成要素目的
高可用性Multi-AZ配置1つのAZが障害を起こしてもサービス継続
災害対策Aurora Global Databaseリージョン障害時に別リージョンで復旧(RTO < 1分)
グローバル展開マルチリージョンEKS北米・アジアなど地理的に近い場所からの低レイテンシアクセス
スケーラビリティEKS + HPA負荷に応じた自動スケールアウト
読み取り負荷分散Aurora Reader読み取りクエリをReaderにオフロード

スタートアップや小規模サービスであれば、シングルリージョン + Multi-AZ で十分なケースも多いです。しかし、SLA 99.99%以上を求められるエンタープライズサービスや、グローバルにユーザーを抱えるサービスでは、このような構成が必要になります。

重要なのは、この分散構成こそが性能特性の差を生み出す根本原因であるということです。可用性とパフォーマンスはトレードオフの関係にあり、高可用性を実現するための分散配置が、ネットワーク遅延という形でパフォーマンスに影響します。

ローカル環境

┌─────────────────────────────────────────────────────────────────────┐
│ 開発マシン (MacBook等) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Browser │ │ k6 / curl │ │ IDE │ │
│ └──────┬──────┘ └──────┬──────┘ └─────────────┘ │
│ │ localhost:443 │ │
│ └────────┬───────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Docker Compose │ │
│ │ ┌─────────────┐ │ │
│ │ │ nginx │ ← SSL終端、リバースプロキシ │ │
│ │ │ :443 │ worker_connections: 1024 (デフォルト) │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ┌──────▼──────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ App (JVM) │ │ PostgreSQL │ │ Redis │ │ │
│ │ │ :8080 │ │ :5432 │ │ :6379 │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
│ │ │ │ │ │ │
│ │ └────── docker network (~0.1ms) ────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ ローカルSSD (NVMe) │ │
│ │ 読み取り: ~50μs 書き込み: ~100μs │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 特徴: │
│ ・全コンポーネントが同一マシン上 │
│ ・ネットワーク遅延 ≈ 0 │
│ ・ストレージは超高速NVMe │
│ ・外部APIはモック(即座に応答) │
│ ・リソース競合なし │
│ ・nginxのリソース制限(worker数、接続数)が先に限界に達しやすい │
└─────────────────────────────────────────────────────────────────────┘

AWS本番環境 (EKS + Aurora Global Database)

┌─────────────────────────────────────────────────────────────────────────────────────┐
│ AWS Cloud │
│ │
│ ┌───────────────────────────────────────────────────────────────────────────────┐ │
│ │ Primary Region (ap-northeast-1) │ │
│ │ │ │
│ │ Internet │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ API Gateway │ ← レート制限、認証、スロットリング │ │
│ │ │ (HTTP API) │ 10,000 req/sec/region (デフォルト) │ │
│ │ └────────┬────────┘ │ │
│ │ │ ~5-10ms │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ ALB (Ingress) │ ← SSL終端、ヘルスチェック │ │
│ │ └────────┬────────┘ │ │
│ │ │ ~1ms │ │
│ │ ▼ │ │
│ │ ┌────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ EKS Cluster │ │ │
│ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Availability Zone A │ │ │ │
│ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ Node (m5.large) │ │ │ │ │
│ │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │
│ │ │ │ │ │ Pod: idp-app │ │ Pod: idp-app │ ← HPA でスケール │ │ │ │ │
│ │ │ │ │ │ CPU: 500m │ │ CPU: 500m │ │ │ │ │ │
│ │ │ │ │ │ Mem: 1Gi │ │ Mem: 1Gi │ │ │ │ │ │
│ │ │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ │
│ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │
│ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Availability Zone C │ │ │ │
│ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ Node (m5.large) │ │ │ │ │
│ │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │
│ │ │ │ │ │ Pod: idp-app │ │ Pod: idp-app │ │ │ │ │ │
│ │ │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ │
│ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │
│ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │ │
│ │ │ 1-3ms │ 0.5-1ms │ │
│ │ ▼ ▼ │ │
│ │ ┌──────────────────────────┐ ┌─────────────────┐ │ │
│ │ │ Aurora Global Database │ │ ElastiCache │ │ │
│ │ │ (Primary Cluster) │ │ Redis Cluster │ │ │
│ │ │ ┌────────┐ ┌────────┐ │ └─────────────────┘ │ │
│ │ │ │ Writer │ │ Reader │ │ │ │
│ │ │ │ (AZ-A) │ │ (AZ-C) │ │ ← 同一リージョン内Reader: ~1ms │ │
│ │ │ └────────┘ └────────┘ │ │ │
│ │ └────────────┬─────────────┘ │ │
│ │ │ │ │
│ └────────────────┼───────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ レプリケーション (~1秒、ストレージレベル) │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Secondary Region (us-west-2) │ │
│ │ │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ Aurora Global Database │ ← DR用 / 読み取りオフロード │ │
│ │ │ (Secondary Cluster) │ │ │
│ │ │ ┌────────┐ ┌────────┐ │ │ │
│ │ │ │ Reader │ │ Reader │ │ フェイルオーバー時にWriterに昇格可能 │ │
│ │ │ │ (AZ-A) │ │ (AZ-B) │ │ (RTO < 1分) │ │
│ │ │ └────────┘ └────────┘ │ │ │
│ │ └──────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ EKS Cluster (us-west-2) ← 北米ユーザー向け、低レイテンシ読み取り │ │ │
│ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │
│ │ │ │ Pod: idp-app │ │ Pod: idp-app │ Reader Endpointに接続 │ │ │
│ │ │ └──────────────┘ └──────────────┘ │ │ │
│ │ └────────────────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 外部サービス連携 (NAT Gateway経由): │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 認証サービス │ │ 決済API │ │ メール送信 │ │
│ │ 50-200ms │ │ 500-2000ms │ │ 100-500ms │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 特徴: │
│ ・Pod間通信もネットワーク経由(同一Node内でも~0.1ms) │
│ ・Aurora Reader活用で読み取りスケールアウト可能 │
│ ・グローバルDBで別リージョンへの読み取りオフロード │
│ ・クロスリージョン書き込みは Primary Region 経由(レイテンシ大) │
│ ・レプリケーションラグ(~1秒)による読み取り整合性の考慮が必要 │
└─────────────────────────────────────────────────────────────────────────────────────┘

構成図から見る性能差異

1. エントリーポイント(nginx vs API Gateway)

ローカル環境ではnginx、AWS環境ではAPI Gatewayがエントリーポイントとなります。この違いが負荷テスト時に大きな差として現れます。

項目ローカル (nginx)AWS (API Gateway)
同時接続数worker_connections × workers (デフォルト1024)10,000 req/sec/region
スケーラビリティDocker内リソース制限フルマネージド、自動スケール
レート制限なし(デフォルト)バースト制限あり(設定可能)
レイテンシ~0.1ms~5-10ms
SSL終端CPUバウンドハードウェアアクセラレーション

ローカル環境の制約:

VUs増加時のボトルネック:
─────────────────────────────────────────────────────────
VUs=50: nginx余裕あり → App/DBがボトルネック
VUs=100: nginx接続数増加 → まだ余裕
VUs=150: nginx worker_connections飽和 → 502 Bad Gateway発生
─────────────────────────────────────────────────────────

VUsを150程度まで上げると、アプリケーションやDBより先にnginxが限界を迎えることがあります。これはローカル特有の問題で、本番環境では発生しません。

ローカルで高負荷テストを行う場合の対策:

# nginx.conf でworker_connectionsを増やす
events {
worker_connections 4096; # デフォルト1024から増加
}

# upstreamのkeepalive設定
upstream app {
server app:8080;
keepalive 100; # 接続を再利用
}

本番環境(API Gateway)の特性:

  • 自動スケールで接続数上限を気にする必要がほぼない
  • ただしリージョン単位のレート制限(デフォルト10,000 req/sec)に注意
  • 必要に応じてService Quotasで上限引き上げ可能
  • レイテンシが5-10ms追加されるため、総レイテンシに影響

教訓:

  • ローカルでVUs=150でエラーが出ても、本番では問題ない可能性が高い
  • ローカルの負荷テストはnginxの制約を考慮して解釈する
  • 本番の限界値を知りたい場合はステージング環境でテストする

2. ネットワークレイテンシ

構成図を見ると、ローカルでは全てが同一マシン内で完結しているのに対し、AWSでは各コンポーネント間にネットワークが介在しています。

通信経路ローカルAWS (同一AZ)AWS (クロスAZ)AWS (クロスリージョン)
App ↔ DB (Writer)~0.1ms1-3ms2-5ms70-150ms
App ↔ DB (Reader)~0.1ms1-3ms1-3ms1-3ms (ローカルReader)
App ↔ Redis~0.1ms0.5-1ms1-2ms-
App ↔ 外部API0ms (モック)20-200ms20-200ms20-200ms

ポイント:

  • ローカルのdocker networkは実質ゼロ遅延
  • AWSでは同一AZ内でも1-3msの遅延
  • Aurora Global DBではリージョン内Readerで読み取りスケールアウト
  • クロスリージョン書き込みはPrimary経由で高レイテンシ

3. ストレージ性能

要素ローカル (NVMe SSD)AWS (EBS gp3)
読み取りレイテンシ~50μs~1ms
書き込みレイテンシ~100μs~1-2ms
IOPS制限なし3000-16000 (設定依存)
スループット~3GB/s125-1000 MB/s

ローカルSSDは桁違いに速いため、I/O待ちの問題がローカルでは見えません。

4. リソース制限

要素ローカルAWS (EKS)
CPU常にフル性能requests/limitsで制限、スロットリング発生
メモリ十分な容量limitsを超えるとOOMKill
接続数制限なしRDSはメモリ依存で制限
帯域制限なしNodeのインスタンスタイプで制限

EKS特有の考慮点:

# Pod のリソース制限例
resources:
requests:
cpu: "500m" # 0.5 CPU確保
memory: "1Gi"
limits:
cpu: "1000m" # 最大1 CPU
memory: "2Gi" # 超過でOOMKill
  • requests: 最低保証リソース。Nodeへの配置判断に使用
  • limits: 上限。CPU超過→スロットリング、Memory超過→OOMKill
  • ローカルでは制限なしで動くコードが、本番ではスロットリングで遅延

5. マルチテナント影響

ローカルでは自分だけがリソースを使用しますが、EKSでは:

  • 同一Node上の他Podとリソース競合
  • CPUスロットリングによるレイテンシ増加
  • RDSのバックアップ実行時に性能低下
  • HPAスケールアウト時のコールドスタート遅延
  • 結果として性能に変動(ジッター)が発生し、P99が安定しない

6. EKSスケーリングの遅延

リクエスト急増時:
─────────────────────────────────────────────────────────────────
t=0s 負荷増加検知
t=15s HPA がメトリクス取得(デフォルト15秒間隔)
t=15s スケールアウト判断
t=20s 新Pod起動開始
t=30s JVMウォームアップ中(まだ本来の性能が出ない)
t=60s JIT最適化完了、本来の性能に到達
─────────────────────────────────────────────────────────────────
→ 約1分間は既存Podに負荷集中

ローカルでは単一インスタンスで動かすため、このスケーリング遅延は見えません。

7. Aurora Global Database の特性

構成図のPrimary/Secondary Region間のレプリケーションに注目してください。

┌─────────────────────────────────────────────────────────────────────────────┐
│ Aurora Global Database の読み書き │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 書き込み (Write): │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ App (Tokyo) │ ─1-3ms─→│ Writer │ ← 常にPrimary Regionのみ │
│ └─────────────┘ │ (Tokyo) │ │
│ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ App (US) │ ─150ms─→│ Writer │ ─1-3ms─→│ 実際の │ │
│ └─────────────┘ │ (Tokyo) │ │ 書き込み │ │
│ ↑ └─────────────┘ └─────────────┘ │
│ USからの書き込みは │
│ Tokyoまで往復が必要 │
│ │
│ 読み取り (Read): │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ App (Tokyo) │ ─1-3ms─→│ Reader │ ← リージョン内Readerで高速 │
│ └─────────────┘ │ (Tokyo) │ │
│ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ App (US) │ ─1-3ms─→│ Reader │ ← USリージョンのReaderで高速 │
│ └─────────────┘ │ (US) │ ただしレプリケーションラグあり │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

レプリケーションラグの影響:

シナリオラグ影響
同一リージョン内Reader~数msほぼ即座に反映
クロスリージョンReader~1秒書き込み直後の読み取りで古いデータ

ローカルでは見えない問題:

  • Write-After-Read整合性の問題(書き込み直後に別リージョンで読むと古い値)
  • Secondary Regionでの書き込みは常にPrimary経由(高レイテンシ)
  • フェイルオーバー時の書き込み先切り替え

アプリケーション設計での考慮:

// 書き込み直後の読み取りはWriterエンドポイントを使用
@Transactional
public User createUser(UserCreateRequest request) {
User user = userRepository.save(request.toEntity());
// ここでReaderから読むとラグで見つからない可能性
return user; // 保存したエンティティをそのまま返す
}

影響の具体例

構成図を見ながら、具体的にどのような影響が出るか確認しましょう。

N+1クエリ問題

構成図の App ↔ DB の通信に注目してください。100件のユーザー一覧取得(関連データ含む)を例に考えます。

ローカル環境:
┌─────────┐ ┌───────────┐
│ App │ ─0.1ms→│ PostgreSQL│ 1回のクエリ: 0.1ms
└─────────┘ ×100 └───────────┘ N+1 (100回): 10ms
docker network → 「許容範囲かな」

AWS環境:
┌─────────┐ ┌───────────┐
│ App │ ─2ms──→│ RDS │ 1回のクエリ: 2ms
└─────────┘ ×100 └───────────┘ N+1 (100回): 200ms
VPC network → SLO違反の可能性

教訓:

  • ローカルで10msの処理は本番で200msになりうる(20倍)
  • N+1は「クエリ数 × ネットワークレイテンシ」で影響が倍増
  • ローカルではJOINの効果が見えにくい

外部API呼び出し

構成図下部の「外部サービス連携」に注目してください。ローカルではモックで即応答ですが、本番ではインターネット越しの通信になります。

ローカル環境:
┌─────────┐ ┌───────────┐
│ App │ ─0ms──→│ Mock │ 外部依存の影響が
└─────────┘ └───────────┘ 完全に隠蔽される

本番環境:
┌─────────┐ ┌─── Internet ───┐ ┌───────────┐
│ App │ ──────→│ 20-2000ms │───────→│ 外部API │
└─────────┘ └────────────────┘ └───────────┘
タイムアウト、リトライ、レートリミット発生

本番で発生する遅延:

  • 認証サービス: 50-200ms
  • 決済API: 500-2000ms
  • メール送信: 100-500ms

対策:

  • 遅延シミュレーション付きモックを使う
  • ステージング環境では実APIに接続
  • タイムアウト、リトライのテストを行う

接続プール枯渇

構成図の Pod ↔ Aurora 間の接続に注目。ローカルでは接続数制限がほぼありませんが、EKS + Auroraでは制限があります。

ローカル環境:
┌─────────┐ ┌───────────┐
│ App │ ─ 接続数制限なし ─→│ PostgreSQL│
└─────────┘ └───────────┘

EKS + Aurora本番環境:
┌─────────────────────────────┐
┌──────────┐ │ Aurora Cluster │
│ Pod (×4) │──┐ │ ┌────────┐ ┌────────┐ │
└──────────┘ │ Writer Pool: 10 │ │ Writer │ │ Reader │ │
┌──────────┐ ├─────────────────────┼─→│ (2000) │ │ (2000) │ │
│ Pod (×4) │──┤ Reader Pool: 20 │ └────────┘ └────────┘ │
└──────────┘ │ ↓ │ ↑ ↑ │
┌──────────┐ │ Writer: 40接続 │ │ │ │
│ Pod (×4) │──┘ Reader: 80接続 │ 書き込み 読み取り │
└──────────┘ └─────────────────────────────┘

HPAでPod増加 → 接続数も増加 → 上限に近づく

Aurora特有の考慮点:

  • Writer/Readerで別々の接続プールを持つ
  • Readerはオートスケーリングで増やせる(最大15インスタンス)
  • RDS Proxyで接続プーリングを効率化可能

問題が起きるシナリオ:

  • HPA でPod数が増加すると、接続プール × Pod数 で接続数が急増
  • Writerへの接続集中(読み取りもWriterに向けてしまう設定ミス)
  • ローカルの単一インスタンスでは発生しない問題

対策:

  • RDS Proxyの導入(接続の多重化)
  • 読み取りは明示的にReader Endpointを使用
  • Pod単位の接続プールサイズを適切に設定
  • @Transactional(readOnly = true) でReaderにルーティング

環境差を考慮した計測手法

ローカルで有効な計測

ローカル環境での計測は、絶対値ではなく「相対値」や「回数」に注目します。

有効な計測:

  • アルゴリズムの計算量(O(n) vs O(n²) の差)
  • メモリ使用量の変化
  • クエリ発行回数の変化
  • CPU処理時間の相対的な比較

計測ポイント:

  • クエリ数を数える(N+1の検出)
  • 処理時間の「比」を見る(絶対値ではなく)
  • メモリプロファイリング
  • GC発生回数

注意: ローカルの絶対値(ms)は参考程度に。ローカルで10ms → 本番で100msは普通です。

本番相当環境で必要な計測

ステージング環境で計測すべきこと:

  • End-to-Endのレイテンシ
  • ネットワーク遅延込みのスループット
  • 外部サービス連携の実際の応答時間
  • 高負荷時の振る舞い
  • P99の値(外れ値の影響)

ステージング環境の要件:

  • 本番と同じリージョン/AZ構成
  • 本番と同等のインスタンスタイプ(またはスケール係数を把握)
  • 本番相当のデータ量
  • 外部サービスは実接続(またはサンドボックス)

実践: 計測からチューニングまで

Step 1: ベースラインの確立

現状を正確に把握することから始めます。

ローカルでのベースライン:

# k6でローカルテスト
k6 run --vus 10 --duration 30s test.js

記録する項目: P50, P95, P99、スループット (req/s)、エラーレート、SQLクエリ数

ステージングでのベースライン:

# 同じテストをステージングで実行
k6 run --vus 10 --duration 30s \
-e BASE_URL=https://staging.example.com test.js

追加で記録: ネットワーク遅延の内訳、外部API呼び出し時間、DBコネクション使用率

比較表を作成:

指標ローカルステージング倍率
P5015ms45ms3x
P9950ms200ms4x
スループット500/s200/s0.4x

この「倍率」が環境差の目安になります。

Step 2: ボトルネックの特定

時間の内訳を可視化して、どこに時間がかかっているかを把握します。

ローカル (50ms):                    ステージング (200ms):
├─ DB: 10ms (20%) ├─ DB: 80ms (40%) ← 支配的
├─ App処理: 35ms (70%) ├─ App処理: 40ms (20%)
└─ その他: 5ms (10%) ├─ 外部API: 60ms (30%)
└─ その他: 20ms (10%)

この例では:

  • 本番ではDBアクセスの最適化が効果大
  • ローカルでApp処理を最適化しても効果小

トレーシングツールを活用:

  • OpenTelemetry + Jaeger/Zipkin
  • AWS X-Ray
  • Datadog APM

Step 3: 環境に適した最適化

ボトルネックの種類に応じて、最適化の優先度を決めます。

ネットワーク遅延が支配的な場合(クラウド典型):

効果施策
N+1クエリの解消(JOIN、バッチ取得)
キャッシュの導入(Redis、アプリキャッシュ)
外部API呼び出しの並列化
DB接続プールの最適化
アルゴリズムの微最適化
オブジェクト生成の削減

CPU処理が支配的な場合(計算集約型):

効果施策
アルゴリズムの改善
計算結果のキャッシュ
並列処理の活用
DB最適化(ボトルネックでないため)

リソース制限が支配的な場合(スケール問題):

効果施策
スケールアップ/アウト
IOPSの増加(gp2→gp3、io1)
接続プールサイズの調整
コードの最適化だけではリソース不足は解消しない

Step 4: 効果検証

良くない評価方法:

  • ローカルだけで「30%改善!」と判断
  • 1回の計測で判断
  • 平均値だけで判断

正しい評価方法:

  • Before/After を同じ条件で比較
  • ローカルとステージング両方で計測
  • 複数回実行して統計的に評価
  • P50だけでなくP99も確認

評価シートの例:

改善内容: N+1クエリをJOINに変更

環境指標BeforeAfter改善率
ローカルP5015ms12ms20%
ローカルP9950ms40ms20%
ステージングP50180ms60ms67%
ステージングP99400ms120ms70%

→ ステージング環境での改善を重視して判断


ケーススタディ

事例1: マスタデータのキャッシュ導入

問題: リクエストのたびに設定情報をDBから取得していた。

ローカル:  App ──0.5ms──→ DB (設定取得)  → 気にならない
本番: Pod ──2-3ms──→ Aurora → リクエスト多発時に累積
  • ローカル: 0.5ms/回 → 許容範囲に見える
  • 本番: 2-3ms/回 × 1000リクエスト/秒 = 2-3秒/秒をDB待ちに消費

解決:

  • アプリケーション層でキャッシュ導入(TTL 5分)
  • 設定変更時にキャッシュ無効化

効果:

  • キャッシュヒット時: ~0ms(メモリアクセスのみ)
  • 設定変更頻度が低いため、99%以上ヒット

教訓:

  • ローカルでは問題に見えなくても、本番では影響大
  • 変更頻度の低いマスタデータはキャッシュ効果が高い
  • 「毎回DBから取得しても0.5msだから大丈夫」は本番では通用しない

事例2: 集計処理のクエリ最適化

Before:

-- JSONBカラムを読み取り、アプリで更新して書き戻し
SELECT stats FROM statistics WHERE tenant_id = ?;
-- アプリ側でJSONを更新
UPDATE statistics SET stats = ? WHERE tenant_id = ?;
  • 処理時間: 約10ms/回(READ + WRITE + ロック待ち)

After:

-- 行ベースのupsert(1クエリで完結)
INSERT INTO statistics_events (tenant_id, event_type, count, date)
VALUES (?, ?, 1, CURRENT_DATE)
ON CONFLICT (tenant_id, event_type, date)
DO UPDATE SET count = statistics_events.count + 1;
  • 処理時間: 約0.5ms/回

改善率: 約20倍(10ms → 0.5ms)

教訓:

  • READ → 加工 → WRITE パターンはネットワーク往復が2回発生
  • DB側で完結できる処理はDBに任せる
  • ローカルでは往復2回でも2ms程度だが、本番では20ms以上になりうる

事例3: 外部API呼び出しの並列化

Before:

// 直列呼び出し
UserInfo user = userService.getUser(id); // 50ms
List<Order> orders = orderService.getOrders(id); // 80ms
PaymentInfo payment = paymentService.getInfo(id); // 60ms
// 合計: 190ms

After:

// 並列呼び出し
CompletableFuture<UserInfo> userFuture =
CompletableFuture.supplyAsync(() -> userService.getUser(id));
CompletableFuture<List<Order>> ordersFuture =
CompletableFuture.supplyAsync(() -> orderService.getOrders(id));
CompletableFuture<PaymentInfo> paymentFuture =
CompletableFuture.supplyAsync(() -> paymentService.getInfo(id));

CompletableFuture.allOf(userFuture, ordersFuture, paymentFuture).join();
// 合計: 80ms(最も遅いAPIの時間)

改善率: 2.4倍(190ms → 80ms)

教訓:

  • ローカルのモックでは全て0msなので並列化の効果が見えない
  • 依存関係のない外部呼び出しは並列化で大幅改善
  • 本番環境でのみ効果が発揮される最適化パターン

チェックリスト

ローカルテスト時

  • クエリ発行回数を確認したか(N+1検出)
  • メモリ使用量の増加傾向を確認したか
  • 処理時間は「相対値」で評価しているか
  • 絶対値を過信していないか

ステージングテスト時

  • 本番と同等のネットワーク構成か
  • 本番相当のデータ量があるか
  • 外部サービスは実接続(またはサンドボックス)か
  • P99まで計測しているか
  • 複数回実行して平均を取っているか

チューニング判断時

  • ボトルネックの内訳を把握しているか
  • ネットワーク遅延 vs CPU処理 vs I/O 待ちの比率を知っているか
  • 最適化対象が本当にボトルネックか
  • ローカルとステージング両方で効果を検証したか

まとめ

ローカル環境の役割:

  • クエリ数、アルゴリズムの問題発見
  • 迅速な試行錯誤
  • 相対的な改善確認
  • ただし絶対的な性能値は参考程度

本番相当環境の役割:

  • 実際の性能値の計測
  • ネットワーク遅延込みの評価
  • 高負荷時の振る舞い確認
  • SLO達成可否の判断
  • 最終判断は必ずここで行う

心構え:

  • 「ローカルで速い」は「本番で速い」ではない
  • 環境差の「倍率」を把握しておく
  • 本番でのボトルネックに集中する

次のステップ