目次を表示する

共通基盤の設計軸 2026 ─ 抽象・責務・非機能要件を設計する 15 章

非機能 (2) 性能 ─ 集約トラフィックを捌く

非機能 (2) 性能 ─ 集約トラフィックを捌く

可用性が「自分が止まると全員が止まる」だとしたら、性能は「全員のリクエストが自分に集まってくる」立場。利用者ドメインなら 100 req/s で済むワークロードが、共通基盤では 10,000 req/s や 100,000 req/s になる。

この章では集約トラフィックの捌き方と、Backpressure / Cache 階層の設計を扱う。

集約トラフィックの構造

graph LR
  subgraph "利用者側"
    U1[サービス A<br/>1k req/s]
    U2[サービス B<br/>3k req/s]
    U3[サービス C<br/>500 req/s]
    U4[サービス D<br/>2k req/s]
    U5[サービス E<br/>...]
  end

  subgraph "共通基盤"
    P[認証基盤<br/>合計 50k req/s]
  end

  U1 --> P
  U2 --> P
  U3 --> P
  U4 --> P
  U5 --> P

  Note1[1 サービスの req は小さくても、<br/>全部集まると桁が違う]

  style P fill:#e1f5ff

スケール特性として:

  • トラフィックが集約される(利用者数 × 利用者ごとのトラフィック)
  • ピークが重なる(朝 9 時の出社、夜 8 時のニュース、月初のバッチ)
  • 障害時に倍増する(利用者の retry が殺到)

性能予算(Performance Budget)

可用性に Error Budget があるように、性能には Performance Budget

指標目標
p50 latency中央値、典型ユーザー
p95 latency上位 5% も満足
p99 latency上位 1% でも許容範囲
p999 latency異常な遅延の指標

共通基盤では特に p99 / p999 が重要

利用者の 1 req は基盤の 5 req になることが珍しくない。基盤の p99 が 100ms なら、利用者の p95 が 500ms になる。

長尾(tail latency)が利用者全員に倍増して波及する。Google のチーフエンジニア Jeff Dean が “The Tail at Scale” で論じた問題。

Cache 階層の設計

性能改善の第一手は Cache 階層。共通基盤は read 圧倒的なものが多いので、cache が劇的に効く。

graph TB
  C[Client] --> CDN[CDN / Edge Cache<br/>< 1ms, hot data]
  CDN -.miss.-> AppCache[App-level Cache<br/>< 5ms, in-process]
  AppCache -.miss.-> Redis[Redis Cluster<br/>< 5ms, shared]
  Redis -.miss.-> DB[(DB<br/>10-100ms)]

  Note1[各層 1 桁ずつ<br/>レイテンシが違う]

  style CDN fill:#e1ffe1
  style AppCache fill:#e1ffe1
  style Redis fill:#fff4e1
  style DB fill:#ffe1e1

認証基盤の例

// L1: アプリ in-process(< 1ms)
const cached = inProcessCache.get(token);
if (cached && !cached.expired) return cached.payload;

// L2: Redis(< 5ms)
const fromRedis = await redis.get(`token:${token}`);
if (fromRedis) {
  inProcessCache.set(token, fromRedis);
  return fromRedis;
}

// L3: DB / 暗号検証(10-100ms)
const verified = verifyJWT(token);
await redis.setex(`token:${token}`, 300, verified);
inProcessCache.set(token, verified);
return verified;

hot data ほど上の層に。cache hit 率で性能が決まる。

Cache 階層の罠

対策
Stampede(expire で殺到)jitter / lock / probabilistic refresh
Cache invalidation の漏れversioned key、TTL 強制
In-process cache のメモリ膨張size limit + LRU
依存先の同時障害stale-while-revalidate

詳細は前作 DB 設計の軸 2026 ch08-09 を参照。

Backpressure ─ 自分を守る仕組み

全員のリクエストを処理しきれない」状態に陥ったとき、処理能力を超える request を受けない のが Backpressure。

Naive な実装の問題

// ❌ 全リクエストを受けて queue に積む
async function handleRequest(req) {
  queue.push(req);  // 無限に詰める
  // → メモリ枯渇 / latency 爆発
}

これは latency が無限に伸びる。利用者は timeout して retry、retry が殺到して状況が悪化する(retry storm)。

✅ Backpressure 付き

async function handleRequest(req) {
  if (queue.size > MAX_QUEUE) {
    return { status: 503, retry_after: backoff() };  // ← 早期に拒否
  }
  queue.push(req);
}

早期に 503 を返す ことで、利用者に「retry してね(時間を空けて)」と伝える。

Adaptive な Backpressure

ハードコードされた閾値より、動的に調整 する方が現代的:

Netflix’s Concurrency Limit Algorithm

Latency が悪化したら自動的に concurrency limit を下げる。改善したら上げる。

TCP の congestion control と似たアイデアを application layer に持ってくる。

graph LR
  T[トラフィック増]
  T --> L[Latency 悪化検知]
  L --> R[concurrency limit を下げる]
  R --> S[503 を返す request 増]
  S --> R2[Latency 回復]
  R2 --> U[concurrency limit を戻す]

  style L fill:#fff4e1
  style R2 fill:#e1ffe1

N+1 問題と batch API

利用者が ループ内で API を呼ぶ と、共通基盤側で N+1 が起きる:

// ❌ 利用者のコード(N+1)
for (const userId of userIds) {  // 1000 user
  const profile = await profileService.get(userId);  // 1000 req
}

共通基盤側が 1000 req を捌くことになる。Batch API を提供する ことで救える:

// ✅ Batch API
const profiles = await profileService.getBatch(userIds);  // 1 req

サーバー側は 1 req で済み、内部的に DB に WHERE id IN (...) で 1 query。

graph TB
  subgraph "N+1: ❌"
    U1[Client] -->|1000 req| S1[Server]
    S1 -->|1000 query| D1[(DB)]
  end

  subgraph "Batch: ✅"
    U2[Client] -->|1 req| S2[Server]
    S2 -->|1 query| D2[(DB)]
  end

  style D1 fill:#ffe1e1
  style D2 fill:#e1ffe1

Batch API を提供しないと、利用者は N+1 を書く。提供する側の責任。

DataLoader パターン

Batch API の自動化として DataLoader(Facebook 発祥のパターン):

const loader = new DataLoader(async (userIds) => {
  return profileService.getBatch(userIds);
});

// 利用者は普通に loader を呼ぶ
const profile1 = await loader.load(userId1);
const profile2 = await loader.load(userId2);
// → 同じ tick 内のリクエストが自動で batch される

GraphQL の文脈で広まったが、任意の batch 化に使える

Streaming で latency を分散する

長時間の処理を stream で返す ことで、p99 を改善できる:

# ❌ 全結果を待ってから返す
GET /v1/large-export
HTTP/1.1 200 OK
[5 秒待ってから 100MB JSON]

# ✅ Streaming で順次返す
GET /v1/large-export
HTTP/1.1 200 OK
Transfer-Encoding: chunked
[最初の 100KB 50ms で返す、残りは順次]

利用者が 最初のデータを見るまでの時間(TTFB) が劇的に改善する。これは LLM API の typical な設計(token streaming)と同じ思想。

Async / Job 化

同期的に応答する必要がない 処理は async に逃がす:

// ❌ 同期で全部やる
POST /v1/reports
HTTP/1.1 200 OK
{ "report": "...100MB..." }  // 30 秒

// ✅ Job として受け付ける
POST /v1/reports
HTTP/1.1 202 Accepted
{ "job_id": "j_xxx", "status_url": "/v1/jobs/j_xxx" }

// 利用者は polling or callback で結果取得

202 Accepted で受付、後で polling か webhook で結果通知。同期 latency を切り離す ことで p99 が劇的に改善。

性能予算のチェックリスト

graph TB
  Q1[Latency 目標] --> Q2[Cache 階層]
  Q2 --> Q3[Backpressure]
  Q3 --> Q4[Batch API]
  Q4 --> Q5[Async / Job]
  Q5 --> OK[捌ける設計]

  Q1 -->|未設定| FixSet[p50/p95/p99 の目標を決める]
  Q2 -->|なし| FixCache[L1/L2/L3 構築]
  Q3 -->|なし| FixBP[Adaptive limit]
  Q4 -->|なし| FixBatch[batch endpoint 追加]
  Q5 -->|同期のみ| FixAsync[長時間処理は 202]

  style OK fill:#e1ffe1

この章の要点

  • 共通基盤は 集約トラフィック を捌く立場、桁違いの req/s
  • 性能予算:p50/p95/p99/p999 で計測、p99 が利用者に倍増して波及
  • Cache 階層:CDN / In-process / Redis / DB の 4 層、各層 1 桁ずつ違う
  • Backpressure:早期に 503 で守る、Adaptive concurrency limit
  • N+1 対策:Batch API を提供、DataLoader パターン
  • Streaming で TTFB 改善、Async / Job で同期 latency 切り離し

次章への問いかけ

性能で利用者を守れた。だが 悪意ある利用横断的な情報漏洩 から守る必要もある。

次章で 非機能要件 (3) セキュリティ ── Zero Trust と Tenant 分離、Policy as Code。