ストーリー
佐
佐藤CTO
NexPayは月間10億件のトランザクションを処理する。ピーク時は10,000 TPS。この規模を安定して捌くには、パフォーマンスとスケーラビリティを”設計”として組み込む必要がある
佐藤CTOがモニターにグラフを表示しました。横軸は時間、縦軸はトランザクション数。12時台と18時台に急峻なピークが見えます。
佐
佐藤CTO
さらに、月末の給料日やキャンペーン時には通常の3-5倍のスパイクが来る。年末年始は10倍もありえる。これに耐えるスケーラビリティをどう設計するか?
あなた
Month 2のスケーラブルシステム設計とMonth 8のパフォーマンスエンジニアリングを統合して設計します
あ
佐
佐藤CTO
よし。“理論を知っている”から”設計に落とせる”に進化しよう
多層キャッシュ戦略
NexPayのキャッシュレイヤー
キャッシュ階層:
[クライアント] ─── CDN Cache ─── API Gateway Cache ─── App Cache ─── DB
│ │ │
┌───┴────┐ ┌────┴─────┐ ┌────┴─────┐
│ 静的 │ │ 認証 │ │ Redis │
│ アセット │ │ トークン │ │ Cluster │
│ 1日 │ │ 検証結果 │ │ │
└────────┘ │ 5分 │ │ ・残高 │
└──────────┘ │ 30秒 │
│ ・為替 │
│ 10秒 │
│ ・店舗 │
│ 5分 │
└──────────┘
キャッシュ設計の詳細
| データ種別 | キャッシュ層 | TTL | 更新戦略 | 整合性要件 |
|---|
| 静的アセット | CloudFront CDN | 24時間 | バージョニング | 結果整合性 |
| APIレスポンス(公開) | API Gateway | 60秒 | TTLベース | 結果整合性 |
| 認証トークン検証 | API Gateway | 5分 | TTLベース | 結果整合性 |
| ユーザー残高 | Redis | 30秒 | Write-Through | 結果整合性(注意) |
| 為替レート | Redis | 10秒 | TTLベース | 結果整合性 |
| 加盟店情報 | Redis | 5分 | Cache-Aside | 結果整合性 |
| 取引履歴 | Redis | 2分 | Cache-Aside | 結果整合性 |
| セッション | Redis | 15分 | Write-Through | 強整合性 |
残高キャッシュの注意事項
// 残高キャッシュの設計(整合性を考慮)
interface BalanceCacheDesign {
// 方針: 残高の「表示用」はキャッシュ、「決済用」はDB直接参照
strategy: {
// 残高照会API(表示用): キャッシュ許容
balanceInquiry: {
source: "Redis Cache (TTL: 30s)";
consistency: "結果整合性";
note: "最大30秒の遅延を許容。UI上に'数十秒の遅延あり'と表示";
};
// 決済処理(引落し用): キャッシュ不可
paymentDebit: {
source: "Aurora DB (SELECT FOR UPDATE)";
consistency: "強整合性";
note: "残高不足の誤判定を防ぐため、必ずDBから最新値を取得";
};
};
// キャッシュ更新フロー
updateFlow: {
onPayment: "決済完了 → DB更新 → Redisキャッシュ無効化 → 次回照会時にDBから取得";
onTransfer: "送金完了 → DB更新 → 送金元・送金先の両方のキャッシュを無効化";
};
}
コネクションプーリング
データベース接続の最適化
# NexPayコネクションプール設計
connection_pooling:
# サービス側プーリング(HikariCP / node-pg-pool)
application_pool:
payment_service:
min_connections: 10
max_connections: 50
idle_timeout: "30s"
connection_timeout: "3s"
validation_query: "SELECT 1"
account_service:
min_connections: 20
max_connections: 100 # 残高照会が多いため大きめ
idle_timeout: "30s"
connection_timeout: "3s"
# RDS Proxy(接続の多重化)
rds_proxy:
enabled: true
purpose: "Lambda/短命Pod/大量Podからの接続を集約"
max_connections_percent: 80 # DBの最大接続数の80%まで
idle_client_timeout: "30m"
# RDS Proxyを使う理由
benefits:
- "接続のプーリングとシェアリング"
- "フェイルオーバー時の自動接続切替"
- "IAM認証による接続管理"
# Aurora接続上限の設計
aurora:
writer:
instance_class: "db.r6g.2xlarge"
max_connections: 2000
reserved_for_admin: 50
available_for_apps: 1950
reader:
instance_count: 3
max_connections_per_reader: 2000
read_routing: "ラウンドロビン(RDS Proxy経由)"
オートスケーリング設計
スケーリング戦略
NexPayオートスケーリング:
┌─────────────────────────────┐
│ Predictive Scaling │
│ (予測ベース: 過去データから │
│ ピーク時間帯を予測) │
└──────────┬──────────────────┘
│
┌─────────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Payment Svc │ │ Account Svc │ │ Transfer Svc │
│ │ │ │ │ │
│ Min: 6 Pods │ │ Min: 4 Pods │ │ Min: 3 Pods │
│ Max: 60 Pods │ │ Max: 40 Pods │ │ Max: 30 Pods │
│ │ │ │ │ │
│ CPU: 60% │ │ CPU: 65% │ │ CPU: 60% │
│ RPS: 1500/pod│ │ RPS: 2000/pod│ │ RPS: 1000/pod│
└──────────────┘ └──────────────┘ └──────────────┘
HPAとKEDAの使い分け
# Payment Service HPA設定
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: payment-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: payment-service
minReplicas: 6
maxReplicas: 60
metrics:
# CPU使用率ベース
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
# カスタムメトリクス(RPS)ベース
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "1500"
behavior:
scaleUp:
stabilizationWindowSeconds: 30 # 30秒で判断
policies:
- type: Percent
value: 100 # 一度に100%増加可能
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300 # 5分間安定後に縮小
policies:
- type: Percent
value: 25 # 一度に25%まで縮小
periodSeconds: 60
スパイク対応
| シナリオ | 予想倍率 | 対応戦略 |
|---|
| ランチタイム(12-13時) | 2x | 予測スケーリング(11) |
| 給料日(毎月25日前後) | 3x | 予測スケーリング + 手動プリウォーム |
| キャンペーン開始 | 5x | 事前通知ベースの手動スケールアウト |
| 年末年始 | 10x | Warm Pool + 専用のバースト構成 |
| 障害回復後のサージ | 3-5x | Circuit Breakerとバックプレッシャーで制御 |
レイテンシ最適化
レイテンシバジェットの配分
決済API レイテンシバジェット(p99 < 800ms):
┌─────────────────────────────────────────────────────────────┐
│ 800ms │
├────────┬──────┬──────────────┬───────┬──────┬──────┬────────┤
│API GW │Auth │Risk Check │Card NW│Account│Notify│Margin │
│ 20ms │30ms │ 50ms │300ms │ 50ms │30ms │320ms │
│ │ │ │ (外部) │ │(非同期)│ │
└────────┴──────┴──────────────┴───────┴──────┴──────┴────────┘
最適化ポイント:
①Auth: JWTキャッシュで30ms→5msに短縮
②Risk Check: ルールキャッシュで50ms→20msに短縮
③Card NW: 外部のため制御不能。タイムアウト500msを設定
④Notify: 非同期化で決済クリティカルパスから除外
最適化テクニック一覧
| テクニック | 適用箇所 | 効果 |
|---|
| 非同期化 | 通知、監査ログ、分析イベント | クリティカルパスから除外 |
| 並列化 | リスクチェック + 残高確認 | 直列→並列で待ち時間短縮 |
| プリフェッチ | 加盟店情報、為替レート | リクエスト時の取得を回避 |
| コネクションプーリング | DB、Redis、外部API | 接続確立コストの排除 |
| プロトコル最適化 | サービス間をgRPCに | JSON/REST比で30-50%削減 |
| データ圧縮 | APIレスポンス(gzip/brotli) | 転送データ量60-80%削減 |
負荷テスト戦略
テスト計画
// NexPay負荷テスト設計
interface LoadTestDesign {
// ツール
tool: "k6 + Grafana Cloud k6";
// テストシナリオ
scenarios: {
// ベースラインテスト
baseline: {
description: "通常トラフィックの再現";
vus: 1000;
duration: "30分";
target: "決済API: p99 < 800ms, エラー率 < 0.05%";
};
// ピークテスト
peak: {
description: "ランチタイムピークの再現";
vus: 3000;
duration: "15分";
target: "決済API: p99 < 1200ms, エラー率 < 0.1%";
};
// スパイクテスト
spike: {
description: "キャンペーン開始時の急激な負荷";
vus: "0 → 5000 (1分間で)";
duration: "10分";
target: "スケールアウト完了まで3分以内。エラー率 < 1%";
};
// ソークテスト
soak: {
description: "長時間の安定性確認";
vus: 2000;
duration: "4時間";
target: "メモリリークなし、レイテンシ劣化なし";
};
// ブレイクポイントテスト
breakpoint: {
description: "限界値の特定";
vus: "100 → 10000 (段階的増加)";
target: "SLO違反が発生するVU数を特定";
};
};
// テスト環境
environment: {
type: "本番同等のステージング環境";
data: "匿名化された本番データのサブセット";
externalDeps: "モック化(Card Network等)";
};
// 定期実行
schedule: {
baseline: "毎日(CI/CDパイプライン内)";
peak: "週次(水曜深夜)";
spike: "リリース前";
soak: "月次";
breakpoint: "四半期";
};
}
まとめ
| ポイント | 内容 |
|---|
| キャッシュ | 多層キャッシュ戦略。残高は表示用と決済用で分離 |
| コネクションプーリング | アプリ層プール + RDS Proxyの2層構成 |
| オートスケーリング | 予測ベース + メトリクスベースの組み合わせ |
| レイテンシ | バジェット配分方式。非同期化と並列化で最適化 |
| 負荷テスト | 5種類のシナリオを定期的に実行。CI/CDに統合 |
チェックリスト
次のステップへ
次は「災害復旧と事業継続設計」に進みます。フィンテックプラットフォームのRPO/RTOとマルチリージョン構成を設計しましょう。
推定読了時間: 40分