LLM 推論 API 服務 (Design LLM Inference API Service)

一句話定位 + TL;DR(含 35 分鐘心智時鐘)

設計一個 HTTP API 包裝題目給定的 batched_sample(List[str]) -> List[str]:它在昂貴 GPU 上跑、一次吃 1-100 筆 prompt任何 batch 大小都固定 ~100ms一台 GPU 同時只能跑一個 batch。使用者卻是單一同步請求POST /sample,body 是一段 raw prompt)。核心挑戰 = 把『一個請求』橋接到『一個 batch』,同時兼顧延遲(端到端 < 200ms)、吞吐(尖峰不丟請求)、成本(GPU 填充率最大化、低流量自動縮容)。
招牌取捨dual-trigger batching——用 size trigger(湊滿 100 筆立刻送) + time trigger(第一筆等超過 max_wait≈50ms 就送) 兩個觸發條件。max_wait 是一個刻意的人為延遲:等越久 batch 越滿、GPU 利用率越高、單位成本越低,但使用者也等越久。time trigger 給延遲設天花板、size trigger 給吞吐設地板——這條 latency↔throughput/cost 曲線就是整題的靈魂。其餘決勝點:async request-reply(request_id 關聯 + Result Store 解耦)Redis Streams 當 Request Queue(consumer group + PEL 兜 at-least-once)Pub/Sub 通知省連線queue 緩衝 + GPU auto-scaling + backpressure 三層擋過載
配速(35 min 預算):~4 min 釐清需求 → ~3 min 容量估算 → ~3 min API + ~3 min 資料/元件選型 → ~6 min 高階架構(含 request_id 關聯)→ ~11 min deep dive(batching 策略 / queue 選型 / 通知機制 / 過載 / crash / Redis SPOF)→ ~3 min 擴展瓶頸 → ~2 min 收尾。開場先一句話複述「單請求 ↔ batch 的橋接」這個核心張力,宣告配速,並把 batching 細節標記為「待 deep dive」——宣告配速本身就是高分動作。

1. 釐清需求與範圍 (Clarify & Scope ~4 min)

開場後先鎖住邊界。本題的關鍵是先確認 batched_sample 的硬約束(它就是設計的物理定律),再問 SLA 與規模。

釐清問題 為何要問 / 影響到哪個決策 本題假設(依來源)
batched_sample 的延遲跟 batch 大小有關嗎? 決定 batching 是否「免費」 無關,1 筆或 100 筆都 ~100ms(固定延遲)
一台 GPU 能同時跑幾個 batch? 決定 worker 與 GPU 的並行模型 一次只能一個 batch(執行時獨佔 GPU)
batch 大小上下限? 決定 size trigger 的閾值 1-100 筆,超過 100 拋錯
端到端延遲 SLA? 決定 max_wait 預算與通知機制 < 200ms(含網路;推論本身就吃 100ms)
使用者請求長怎樣?同步還非同步? 決定 API 形態與是否要 async reply 同步POST /sample body 一段 prompt、回一段 completion
尖峰可以丟請求嗎? 決定要不要 queue 緩衝 / backpressure 不可丟(高吞吐、尖峰不丟)
成本敏感嗎?低流量要省 GPU 嗎? 決定 auto-scaling / warm pool ,GPU 貴,低流量要縮容、batch 要填滿
同一 prompt 重複處理可接受嗎? 決定重試是否要 idempotency / 去重 可接受(sampling 無副作用,重跑只是換個合理答案)
defer 話術

batching 策略怎麼平衡延遲與吞吐 我先標記,等 deep dive 展開——現在先把 GPU Worker 當成一個『會從 queue 拉一批 prompt、塞進 batched_sample、把結果寫回去』的 black box,先把端到端流程串起來。」這招把整題最難的張力推遲,保住架構敘事連貫。

2. 功能性與非功能性需求 (FR / NFR 與設計北極星)

設計北極星(NFR 直接導出後續所有選型):

NFR 導出的設計北極星 後續對映選型
< 200ms + 100ms 推論 batching 等待預算極小(~50ms),通知不能慢、queue 要快 max_wait≈50ms、Redis Streams(sub-ms)、Pub/Sub
高吞吐不丟請求 請求進來先落 queue 緩衝,永不直接打 GPU Request Queue + at-least-once + auto-scaling
成本效率 每個 batch 盡量填滿、低流量縮 GPU dual-trigger / adaptive batching、warm pool、scale-down
正確性(不錯配) 單請求要能在 batch 海裡找回自己的結果 request_id 關聯 + result:{request_id} KV
一句話北極星

「這是一個 同步請求外殼、非同步 batch 內核 的系統」——把『同步 API』與『batch GPU』兩個世界縫起來,縫線就是 request_id,縫的張力就是 max_wait

3. 容量估算 (Back-of-Envelope ~3 min)

本題來源未給使用者數,估算重點不是流量絕對值,而是 GPU 吞吐上限單請求的延遲預算分解——這兩者直接驅動 batching 與 scaling 決策。回連 02-Distributed-Systems/05-Numbers-to-Know

單台 GPU 的吞吐上限

一台 GPU:一次一個 batch、每 batch ~100ms、最多 100 筆
→ 每秒最多 10 個 batch
→ 單台 GPU 吞吐上限 = 10 batch/s × 100 筆 = 1,000 prompts/sec(batch 全滿時)
→ 若 batch 只裝到 50% → 有效吞吐砍半 = 500 prompts/sec(填充率直接決定成本)

承載 N 的 GPU 數(推導值)

目標吞吐 T prompts/sec ÷ (1,000 × 平均填充率) = 需要的 GPU 台數
例:T=10,000、填充率 80% → 10,000 ÷ 800 ≈ 13 台 GPU(推導值)
→ 填充率每掉 10%,所需 GPU 台數同比上升 → 「填滿 batch」= 直接省錢

端到端延遲預算分解(< 200ms)

網路往返           ~?ms(client ↔ LB ↔ API server)
batching 等待      0 ~ max_wait(≈ 0-50ms)
GPU 推論           ~100ms(固定,不可壓縮)
通知 + 讀 Result   ~ 數 ms(Redis sub-ms)
─────────────────────────────────
合計目標           < 200ms
→ 推論吃掉一半預算 → max_wait 只能給到 ~50ms,否則尖峰外的請求就破 SLA

下列 GPU 吞吐 / 台數 / 預算分解皆為 推導值(由 batched_sample 的固定約束「~100ms、≤100 筆、一台一 batch」與 SLA「<200ms」算出;來源僅給出這些輸入與「建議 max_wait≈50ms」)。

維度 數字 對後續決策的意義
單 GPU 吞吐上限 ≈ 1,000 prompts/sec(推導值,batch 全滿) batch 填充率 = 成本槓桿;湊滿才划算
填充率影響 填充率 ↓ → GPU 台數 ∝ ↑(推導值) 低流量也要盡量湊批 → time trigger 存在的理由
推論延遲 ~100ms(固定,不可壓縮) 吃掉一半 SLA → max_wait 預算被卡在 ~50ms
max_wait ≈ 50ms(來源建議起始值) 延遲天花板;可依 P99 / GPU util 動態調
估算的最大收穫

推論 100ms 是物理定律、不可壓縮,它吃掉了 200ms 預算的一半 → 留給 batching 的等待只剩 ~50ms。這一條就解釋了為何 max_wait 不能設大、為何通知機制不能慢、為何 queue 要選 Redis(sub-ms)而非 SQS(5-20ms/call)。

4. 用到的積木 / Building Blocks Used

架構即目錄:先給導覽圖,再展開。下表每個決策都對映既有 concept note(無 alias 連結);本表即「本案例練到哪些積木」的單一事實來源(single source of truth)。

設計決策 積木 在本題的作用(≤1 行)
延遲預算分解驅動選型 02-Distributed-Systems/05-Numbers-to-Know 算出單 GPU 吞吐與 ~50ms 等待預算,定出 batching / queue 選型
同步 API 外殼 03-API-Design/02-REST 單一 POST /sample,body raw prompt、回 raw completion
同步請求 ↔ 非同步處理橋接 10-Design-Patterns/03-Long-Running-Tasks async request-reply:丟 queue → 等通知 → 回應,連線 hold 住
請求緩衝 + at-least-once 09-Messaging-Coord/02-Queue Request Queue 吸收尖峰、不丟請求、解耦 API 與 GPU
Queue / Result Store 技術選型 07-Caching-Storage/04-Redis Redis Streams(consumer group/PEL)+ Redis KV(result)
結果關聯(不錯配) 04-Database-Indexing/04-Hash-Index result:{request_id} 按 key 精確查回自己的 completion
結果就緒通知 10-Design-Patterns/05-Real-time-Updates polling / blocking / Pub/Sub 三方案,選 Pub/Sub 省連線
GPU crash 不丟請求 12-Ops-and-Reliability/03-Reliable-Delivery 未 ACK 訊息 idle timeout 後被 re-claim;sampling 無副作用可重跑
尖峰過載保護 12-Ops-and-Reliability/02-Overload-Protection queue 緩衝 + backpressure(429)+ per-user rate limit
GPU pool 動態調度 11-Infrastructure/03-Container auto-scaler 依 queue depth 增減 GPU worker、warm pool 抗 cold start
流量分散到多台 API Server 01-Networking/06-Load-Balancing LB 把 POST /sample 平均分到無狀態 API Server
Redis 單點容錯 05-Database-Advanced/03-Replication Redis Cluster + replica + Sentinel failover

5. API 設計 (API Design ~3 min)

外部極簡(題目就一個 endpoint),複雜度全藏在後端。採 REST,理據見 03-API-Design/02-REST

Method Path 用途 重點
POST /sample 提交單一 prompt 取 completion body = raw text prompt;回 raw text completion;連線 hold 住直到結果就緒或 timeout
curl llm-api.buildmoat.com/sample -d "E equals "
# → MC squared
細節話術

雖然對外是同步阻塞請求,後端其實走 async request-reply:API Server 收到後產 request_id、入 queue、hold 住 HTTP 連線等通知,拿到結果才回。要主動補一句「超過 SLA(如 timeout / 過載)會回 429 / 503 + Retry-After」——展示你想過失敗路徑,不是只畫 happy path。

6. 元件選型 (Components & Storage Choice ~3 min)

本題沒有持久化的業務資料表,核心是兩個短命的 Redis 結構(Request Queue + Result Store)+ GPU worker pool。它們的選型就是整題地基,故獨立成段。

資料形態(皆短命,生命週期僅數百毫秒)

Request Queue(Redis Streams)
  entry: (request_id, prompt)      ← consumer group 分給 GPU worker,PEL 兜 at-least-once

Result Store(Redis KV)
  key:   result:{request_id}
  value: completion 字串
  TTL:   數秒自動過期(讀完即丟、防洩漏)

通知(Redis Pub/Sub)
  channel: result:{request_id}     ← worker 寫 KV 後 PUBLISH,API server subscribe 收

元件選型比較:

元件 / 需求 選項 適配本題嗎 理由
Request Queue Redis Streams ✅ 主選 consumer group 安全並行消費、PEL 兜 at-least-once、COUNT 一次拉 1-100 筆、sub-ms
SQS 每次最多拉 10 條 → 湊滿 100 要多次呼叫;每 call 5-20ms,逼近 100ms 推論時間
Kafka 磁碟寫入延遲高、為高吞吐 log 設計,對 sub-ms 即時 API 過重
Result Store Redis KV ✅ 主選 request_id 精確查一筆 = key-value lookup,正是 KV 的存取模式
Kafka / SQS(log/queue) log-based / queue-based 不支援「按 key 隨機讀某一筆」
通知機制 Pub/Sub + KV ✅(高併發) 連線數從「每請求一條」降為「每台 API server 一條」(見 Deep Dive 3)
GPU 調度 container + auto-scaler 依 queue depth 增減 GPU worker、維 warm pool 抗 cold start

→ 積木:queue 語意見 09-Messaging-Coord/02-Queue;Redis 細節(Streams / consumer group / PEL)見 07-Caching-Storage/04-Redis;按 key 精確查回結果見 04-Database-Indexing/04-Hash-Index

邊界

「全押 Redis」是本題在『資料短命、延遲第一』前提下的結論。若改成離線 / 批量 Batch API(延遲不敏感、可靠性第一),SQS 的 DLQ / 跨 AZ 持久化反而更划算——這條「即時 → Redis、離線 → SQS」的判準是來源明說的選型依據。

7. 高階架構 (High-Level Design ~6 min)

把積木串成端到端流程。本題唯一真正的難點是**「單請求如何在 batch 海裡找回自己的結果」**,故先講 request_id 關聯,再看 scaled 圖。

(A) 端到端 flow(含 request_id 關聯)

Client
  │  POST /sample  body="E equals "
  ▼
Load Balancer ──► API Server (stateless, 多台)
  │ 1. 產生唯一 request_id
  │ 2. XADD 到 Request Queue:  (request_id, prompt)
  │ 3. SUBSCRIBE channel result:{request_id}   ← 先訂閱(避免 race)
  │ 4. hold 住 HTTP 連線等通知
  ▼
Request Queue (Redis Streams)
  │  GPU Worker 用 consumer group 拉批(XREADGROUP COUNT≤100)
  ▼
GPU Worker(持續迴圈,dual-trigger 決定何時送批)
  │ A. 收集請求;湊滿 100(size)或第一筆等超過 max_wait(time)→ 送批
  │ B. prompts = [p1, p2, ...]
  │ C. completions = batched_sample(prompts)   ~100ms(GPU 獨佔)
  │ D. for 每筆: SET result:{request_id} = completion (TTL)
  │             PUBLISH result:{request_id}  notify
  │ E. ACK 該批所有 stream entry(移出 PEL)
  ▼
API Server 收到 channel 通知
  │ GET result:{request_id}  →  completion
  ▼
回 HTTP 200 body=completion,關閉連線

關鍵正確性點:batch 內順序 = 輸入順序batched_sample 保證 list 對位回傳),worker 用「輸入 index」把每筆 completion 對回各自的 request_id,再寫進 result:{request_id}——這就是「不錯配」的實作。

(B) 為什麼經 Result Store,而不是 GPU Worker 直推 API Server?

若直推:worker 必須維護  request_id → 哪台 API Server  的 mapping
  ✗ 增加元件耦合
  ✗ 持有連線的 API Server 中途 crash → 推送失敗,要額外錯誤處理
改用 Result Store 中介:
  ✓ worker 只管「寫共享 store + 發通知」(職責單一)
  ✓ API Server 自行訂閱取結果(解耦,兩者互不知對方存在)
  ✓ API Server 換一台 / 重試也不影響 worker

(C) Scaled 架構(端到端)

                          ┌───────────────────────────────┐
   Client ──► Load Balancer├──► API Server #1  (stateless) │
              (流量分散)    │    API Server #2  ...          │ 每台 1 條 Pub/Sub 訂閱連線
                          └──────────┬────────────────────┘
                            XADD     │ ▲ PUBLISH 通知 + GET result
                                     ▼ │
                       ┌─────────────────────────────────────┐
                       │ Redis (Cluster + replica + Sentinel) │
                       │  ├ Request Queue  (Streams + group)  │ ← 緩衝尖峰、at-least-once
                       │  └ Result Store   (KV, TTL)          │
                       └───────┬──────────────────────────▲──┘
              XREADGROUP COUNT≤100  │                      │ SET result + PUBLISH
                                    ▼                      │
                       ┌─────────────────────────────────────┐
                       │      GPU Worker Pool (N 台 GPU)      │
                       │  dual-trigger batching → batched_sample│
                       └─────────────────▲───────────────────┘
                                         │ scale up/down
                       ┌─────────────────┴───────────────────┐
                       │ Auto-Scaler(監控 queue depth)       │
                       │  + Warm Pool(預熱 GPU,抗 cold start)│
                       └─────────────────────────────────────┘

關鍵 hand-off:LB → 無狀態 API Server(產 request_id、訂閱、hold 連線)→ Request Queue(緩衝 + 分批)→ GPU Worker(dual-trigger 湊批 + 推論 + 寫回)→ Pub/Sub 通知 → API Server 讀 KV 回應。Auto-Scaler 旁路監看 queue depth 調 GPU 數。

8. 深入探討 (Deep Dives ~11 min)

不重複原則(link-don't-restate)

通用機制(queue 語意、rate limiting、replication failover、async request-reply)只在 concept note 解釋;這裡只講「本題怎麼用、選哪個、為什麼」。任一機制超過 ~3 行就砍掉改連結。本題無 home note 的新材料(dual-trigger / adaptive batching、Redis Streams 的 consumer-group+PEL 用法、GPU warm pool)才在此 inline 教學並登記到 new_concepts_introduced

8.1 batching 策略:如何平衡延遲與吞吐(招牌取捨)

問題:使用者要 batch 越滿越省,但等湊批會增加延遲。怎麼決定「何時送批」?
為何是瓶頸:純等湊滿 100 → 低流量永遠湊不滿、使用者乾等;純來一筆送一筆 → GPU 填充率 1%、成本爆炸。這是整題的核心張力。

新材料:dual-trigger batching(本案例首次 introduce,尚無 atomic note → 已登記 new_concepts_introduced)

  • Size trigger:收集到 100 筆batched_sample 上限)立刻送 → 吞吐地板,高流量下幾乎 0 等待。
  • Time trigger:batch 中第一筆等超過 max_wait(≈50ms)就送,不管當前幾筆延遲天花板,保證低流量也不會無限等。
  • max_wait 的取捨:太短(5ms)→ 中低流量 batch 只裝 1-5 筆,GPU util 低、燒錢;太長(500ms)→ 延遲破 SLA。建議 50ms 起步,依 P99 latency 與 GPU utilization 動態調

不同流量下 dual-trigger 的表現(來源數據):

流量場景 批次等待 GPU 處理 總延遲(不含網路)
高(batch 迅速填滿) ~0-10ms 100ms ~100-110ms
~20-40ms 100ms ~120-140ms
低(觸發 time trigger) 50ms 100ms ~150ms
方案 做法 優點 缺點
A. 純 size(湊滿 100 才送) 只看數量 GPU 永遠全滿、最省 低流量永遠湊不滿 → 使用者無限等
B. 純 time(固定間隔送) 每 X ms 清一次 延遲可控 高流量下 batch 仍可能不滿 / 等多餘
C. dual-trigger(兩者取先到) size OR time 任一觸發 高流量低延遲、低流量保底 多一個 max_wait 要調
D. adaptive(依 queue depth 調 max_wait queue>100→max_wait=0;queue<10→max_wait=50ms 高流量拉滿吞吐、低流量多等湊批 實作更複雜,要監控 queue depth

選擇與理由:採 C(dual-trigger)為基線、D(adaptive)為進階。dual-trigger 一個策略同時給延遲天花板與吞吐地板;進階再依 queue depth 動態調 max_wait(深 queue 不等、淺 queue 多等湊批),把 latency↔cost 曲線壓到最優。這就是本題的 core_tradeoff。
→ 積木:09-Messaging-Coord/02-Queue(湊批的緩衝來源)、10-Design-Patterns/03-Long-Running-Tasks(async 處理模型)

8.2 Request Queue 與 Result Store 的技術選型

問題:queue 要選什麼?result 要存哪?
為何是瓶頸:選錯(如 SQS)會讓「湊批」這個動作本身就吃掉大半延遲預算。

需求點 Redis Streams SQS Kafka
一次拉 1-100 筆湊批 ✅ COUNT 任意數,一次到位 ❌ 每次最多 10 條,要多次(並行)呼叫 ✅ 但偏批量
拉取延遲 sub-ms(in-memory) 5-20ms/call(並行 10 次仍 20-50ms) ❌ 磁碟寫入延遲
並行消費不重複 ✅ consumer group ✅ 原生 ✅ partition
at-least-once / crash 重處理 ✅ PEL + idle timeout re-claim ✅ visibility timeout ✅ offset
維運成本 / 持久化 △ 自管,資料短命可接受 ✅ 全託管、DLQ、跨 AZ △ 重

選擇與理由Request Queue 用 Redis Streams——COUNT 一次拉滿 1-100 筆完美匹配 batched_sample,sub-ms 延遲在 200ms 預算裡至關重要;consumer group 讓多 GPU worker 安全並行、PEL 內建 at-least-once(等同 SQS visibility timeout 但不必引外部服務)。Result Store 用 Redis KV——按 request_id 精確查一筆是 key-value lookup,log/queue 系統做不到。為何不用 SQS:每次只拉 10 條 + 每 call 5-20ms,逼近甚至超過推論本身的 100ms,即時 API 不能接受;但離線 Batch API 反而該選 SQS(可靠性 > 延遲)。
→ 積木:07-Caching-Storage/04-Redis09-Messaging-Coord/02-Queue04-Database-Indexing/04-Hash-Index

8.3 API Server 如何得知結果已就緒(通知機制選型)

問題:worker 把結果寫進 KV 後,怎麼通知正在 hold 連線的 API Server?
為何是瓶頸:高併發下「通知」會變成連線數瓶頸——這是這段的真正主角。

方案 做法 優點 缺點 / 瓶頸
A. Polling API Server 每 ~50ms GET 一次 KV 實作最簡單 海量無效 GET(1,000 並發 × 20次/s = 20,000 GET/s);延遲 +一個 interval
B. Blocking on List worker push 到 result:{id} List;API server 阻塞讀 語意清晰、無 race 每 in-flight 請求佔 1 條 Redis 連線(1,000×10 台=10,000 連線,撞上限)
C. Pub/Sub + KV worker SET KV 再 PUBLISH;API server 每台 1 條訂閱連線收所有 channel 連線數 = 每台 1 條,解決擴展瓶頸 Pub/Sub 是 fire-and-forget,publish 早於 subscribe 會丟訊息

選擇與理由:演進路徑——起步用 B(簡單可靠,中小規模首選),並發高到 Redis 連線成瓶頸時遷 C。C 的 race(先 publish 後 subscribe 丟訊息)的解法是固定順序:worker 先 SET KV 再 PUBLISH;API server 先 SUBSCRIBE,再(subscribe 後 / timeout 前)GET 一次 KV 兜底——這樣即使通知早到 / 漏接,也能從 KV 撈回結果。
→ 積木:10-Design-Patterns/05-Real-time-Updates(通知 / 推送模式)、07-Caching-Storage/04-Redis

8.4 尖峰過載:如何不丟任何請求

問題:流量暴增、GPU 消化跟不上進入速度,queue 暴漲、延遲飆升。
為何是瓶頸:GPU 是最貴也最慢擴的資源(cold start 數十秒~數分鐘)。

三層防線:

手段 觸發 / 參數(來源範例)
第一層緩衝 Request Queue 天然緩衝:短暫尖峰全進 queue,GPU 慢慢消化即自然恢復 queue 本身
第二層擴容 GPU Auto-Scaling:依 queue depth 增減 worker;warm pool 預熱抗 cold start;predictive scaling 依歷史尖峰提前開;至少維持 1 台 baseline depth>high watermark(500)→scale up;<low(50)→scale down(含 cooldown)
第三層卸載 Backpressure + Rate Limiting:queue 超極端閾值即超載 → API Server 回 429 + Retry-Afterper-user rate limit 防單一使用者獨佔 depth>10,000 → 429

選擇與理由:三層由內到外——queue 先吸(免費、零延遲增容),auto-scaling 再補 GPU(治本但慢,故靠 warm pool / predictive 提前),backpressure 兜底(GPU 真的補不上時,主動拒絕 + 引導 client backoff,總比靜默丟請求或拖垮全系統好)。
→ 積木:12-Ops-and-Reliability/02-Overload-Protection11-Infrastructure/03-Container(auto-scaling / warm pool)、09-Messaging-Coord/02-Queue

8.5 GPU Worker crash 時如何不丟正在處理的請求

問題:worker 在 batched_sample 途中 crash,已拉出 queue 但未 ACK 的請求怎麼辦?
為何是瓶頸:GPU 推論 100ms 內 crash,這批 prompt 已離開 queue,若無補救就靜默丟失。

做法(直接複用 Deep Dive 8.2 的 Redis Streams 保證):worker XREADGROUP 讀取後訊息進 PEL(Pending Entries List);正常處理完 XACK 移出。若 worker crash 未 ACK,其他健康 worker 在 idle timeout(如 5s)後 XCLAIM 接管重跑——等同 SQS visibility timeout,但全在 Redis 內完成、不必引外部服務。

8.6 Redis 同時當 Queue 與 Result Store,如何避免單點故障

問題:Redis 全掛 → queue 待處理請求 + result store 未讀結果全失。
為何是瓶頸:Redis 是本題的中樞,是最危險的 SPOF。

先評估影響嚴重度:Redis 存的是短命無狀態資料(request / result 生命週期僅數百毫秒)。Redis 掛 → API Server timeout → 回 503/504使用者 retry 即恢復——沒資料庫被改、沒錢被扣、無不可逆副作用。所以這是「可用性問題」而非「資料完整性災難」。

手段 說明
第一層 Redis Cluster + Replica(每 master 1-2 replica)+ Sentinel master 掛 → Sentinel 自動提升 replica,failover ~1-2s,解決多數生產故障
第二層 API Server graceful degradation:Redis 全不可用 → 回 503 + Retry-After 引導 client backoff retry
第三層(可選) 分離 Queue 與 Result Store 到不同 Redis Cluster 一方掛不波及另一方;但多數場景單一 cluster + replica 已足夠,過度分離徒增維運複雜度

選擇與理由:因資料短命、可 retry,不追求強持久化;用 Cluster + replica + Sentinel 擋掉大多數故障,graceful degradation 兜底,第三層只在確有需要時才上——避免過早分離。
→ 積木:05-Database-Advanced/03-Replication12-Ops-and-Reliability/02-Overload-Protection

9. 擴展與瓶頸 (Scaling & Bottlenecks ~3 min)

主動找出系統會先在哪裂開並補強。

瓶頸 / 風險 補強手段 積木
GPU 吞吐(最貴、最慢擴)先裂 auto-scaling 依 queue depth 加 worker;warm pool / predictive 抗 cold start;adaptive batching 拉滿填充率 11-Infrastructure/03-Container12-Ops-and-Reliability/02-Overload-Protection
Request Queue 暴漲 queue 緩衝吸短峰;超極端閾值 backpressure(429)+ per-user rate limit 09-Messaging-Coord/02-Queue12-Ops-and-Reliability/02-Overload-Protection
通知連線數(每請求一條)撞 Redis 上限 從 blocking-list 遷移到 Pub/Sub(每台 API server 1 條訂閱連線) 10-Design-Patterns/05-Real-time-Updates
API Server 算力不足 stateless → 加機器水平擴展,前面擺 LB 02-Distributed-Systems/03-Scalability01-Networking/06-Load-Balancing
Redis SPOF Cluster + replica + Sentinel failover;必要時分離 queue / result cluster 05-Database-Advanced/03-Replication
GPU Worker crash 丟批 Redis Streams PEL + idle timeout re-claim;sampling 無副作用可安全重跑 12-Ops-and-Reliability/03-Reliable-Delivery
drive 的關鍵句

「系統會 先在 GPU 吞吐裂開(最貴、cold start 最慢)→ 所以我用 queue 先吸短峰、auto-scaling + warm pool 補 GPU、再用 adaptive batching 把每塊 GPU 榨乾;真的補不上才 backpressure 拒絕。API server / Redis 都能水平擴或 failover,不是當前瓶頸。」先指出最可能裂開處再補強,比逐項羅列更有說服力。

10. 取捨總表與收尾 (Trade-off Recap & Wrap-up ~2 min)

決策點 選擇 放棄了什麼 為什麼值得
Batching dual-trigger(size OR time)+ adaptive 單請求的最低延遲 換 GPU 填充率(吞吐 / 成本)——招牌取捨
請求 ↔ 處理 async request-reply(request_id + Result Store) 直推的少一跳 解耦、worker 不必知 API server、crash 容錯
Request Queue Redis Streams SQS 的全託管 / DLQ sub-ms + 一次拉 1-100 筆,匹配即時 SLA
Result Store Redis KV log/queue 的順序語意 按 request_id 精確查 = KV lookup
通知 Pub/Sub + KV 兜底 blocking-list 的零 race 連線數降到每台 1 條,解決擴展瓶頸
Crash 處理 at-least-once(PEL re-claim),不做 exactly-once 嚴格不重複的保證 sampling 無副作用,重跑只是換個合理答案,省複雜度
過載 queue 緩衝 + auto-scaling + backpressure 「無限擴 GPU」的幻想 GPU 貴且 cold start 慢,三層由內到外最划算
Redis SPOF Cluster + replica + Sentinel,不追強持久化 資料零丟失保證 資料短命可 retry,過度持久化不值
收尾話術

「整題的單一錨點是 dual-trigger batching:我用 max_wait≈50ms 這個刻意的人為延遲去湊批——time trigger 給延遲設天花板、size trigger 給吞吐設地板,把『單同步請求』和『100ms 固定的 batch GPU』縫起來,縫線是 request_id。其餘都是這條主軸的支撐:Redis Streams(sub-ms、一次拉滿 100)讓湊批不吃延遲、Pub/Sub 讓通知不爆連線、PEL 讓 crash 可安全重跑(因為 sampling 無副作用)、queue+auto-scaling+backpressure 三層擋尖峰。GPU 是最貴最慢擴的資源,所以整套設計的終極目標就是讓每塊 GPU 的 batch 盡量填滿。」引用既有決策用連結,不重新推導。

後續追問演練 (Interviewer Follow-ups — "What if…")

為什麼要 batching?來一筆送一筆不行嗎?

dual-trigger 的兩個觸發條件各保證什麼?

為什麼結果經 Result Store 中介,而不是 GPU Worker 直推 API Server?

流量突然放大 100x,架構哪裡先撐不住?怎麼補?

某個 GPU Worker 在 batched_sample 途中 crash,那批請求會丟嗎?

高併發下通知機制變成連線瓶頸怎麼辦?

Redis 整個掛了會怎樣?使用者體驗如何?

如果改設計成「離線 Batch API」(延遲不敏感、大量任務),技術選型會怎麼變?

為什麼這題用 at-least-once 就夠,不需要 exactly-once?

adaptive batching 依 queue depth 調 max_wait,相比固定 max_wait 好在哪?代價是什麼?

主動回想:白板重建 (Whiteboard from Scratch)

不看上文,照下面提示把整個設計重建一次。每格都該能 30 秒內口述。

常見陷阱 (Exam Traps for This Problem)