LLM 推論 API 服務 (Design LLM Inference API Service)
設計一個 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 無副作用,重跑只是換個合理答案) |
「batching 策略怎麼平衡延遲與吞吐 我先標記,等 deep dive 展開——現在先把 GPU Worker 當成一個『會從 queue 拉一批 prompt、塞進 batched_sample、把結果寫回去』的 black box,先把端到端流程串起來。」這招把整題最難的張力推遲,保住架構敘事連貫。
2. 功能性與非功能性需求 (FR / NFR 與設計北極星)
-
功能性需求 (FR)
- 使用者透過 HTTP 提交單一 prompt,取得對應的 LLM completion。
- 系統把多位使用者的獨立請求聚合成 batch,最大化 GPU 使用效率。
- 系統管理一個 GPU instance pool,依流量動態調度資源。
-
非功能性需求 (NFR)
- 低延遲:端到端 < 200ms(推論本身已佔 ~100ms,留給 batching + 網路 + 通知的預算很緊)。
- 高吞吐:尖峰高效 batching,不丟任何請求。
- 成本效率:最大化 batch 填充率,低流量自動縮減 GPU。
- 正確性:每筆請求拿到對應自己 prompt 的 completion,不可張冠李戴(batch 內結果順序 / request_id 關聯必須對得上)。
設計北極星(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)
通用機制(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%、成本爆炸。這是整題的核心張力。
- 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 處理模型)
因為第一筆是 batch 裡等最久的那筆,它最接近破 SLA。盯住「最老請求的等待時間」= 給整批的延遲設了上界;只要它沒超過 max_wait,後到的更不會超。若改盯「整批平均」或「最後一筆」,最老那筆可能已經超時。
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-Redis、09-Messaging-Coord/02-Queue、04-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
Race:worker 在 API server SUBSCRIBE 之前就 PUBLISH,那則通知沒人收、永遠遺失,API server 乾等到 timeout。治法靠兩個保證疊加:(1) worker 先寫 KV 再發通知 → 結果一定已落地;(2) API server 訂閱成功後主動 GET 一次 → 若通知在訂閱前已發(漏接),這次 GET 直接撈到結果,不必等通知。通知只是「加速」,KV 才是「真相來源」。
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-After;per-user rate limit 防單一使用者獨佔 |
depth>10,000 → 429 |
選擇與理由:三層由內到外——queue 先吸(免費、零延遲增容),auto-scaling 再補 GPU(治本但慢,故靠 warm pool / predictive 提前),backpressure 兜底(GPU 真的補不上時,主動拒絕 + 引導 client backoff,總比靜默丟請求或拖垮全系統好)。
→ 積木:12-Ops-and-Reliability/02-Overload-Protection、11-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 內完成、不必引外部服務。
安全,不需要 idempotency key。 LLM sampling 是 side-effect free——batched_sample 不改 DB、不扣款、不發訊息。重跑最壞結果是使用者拿到「另一個同樣合理的 completion」(sampling 有隨機性),對系統與使用者都無害。這正是 at-least-once(而非昂貴的 exactly-once)在這裡夠用的原因。相較之下,扣款 / 下單類請求重跑就必須 idempotency——見 12-Ops-and-Reliability/03-Reliable-Delivery。
→ 積木:12-Ops-and-Reliability/03-Reliable-Delivery、07-Caching-Storage/04-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-Replication、12-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-Container、12-Ops-and-Reliability/02-Overload-Protection |
| Request Queue 暴漲 | queue 緩衝吸短峰;超極端閾值 backpressure(429)+ per-user rate limit | 09-Messaging-Coord/02-Queue、12-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-Scalability、01-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 |
「系統會 先在 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…")
batched_sample 任何 batch 大小都固定 ~100ms、一台 GPU 一次只跑一個 batch。來一筆送一筆 → 填充率 1%、GPU 吞吐只剩 10 prompts/s(vs 全滿 1,000/s),成本暴漲 100 倍。batching 是把「固定延遲」攤到多筆請求上。見 09-Messaging-Coord/02-Queue。
size trigger(湊滿 100 送)= 吞吐地板,高流量幾乎 0 等待;time trigger(第一筆等超過 max_wait 送)= 延遲天花板,低流量也不會無限等。兩者取先到。max_wait≈50ms 起步,依 P99/GPU util 調。
worker 從 queue 拉請求時不知道哪台 API Server 在等。直推要維護 request_id → API Server mapping(耦合),且持有連線的 server 中途 crash 推送會失敗。經 Result Store:worker 只寫共享 store + 發通知(職責單一),API server 自取(解耦),互不知對方存在。
GPU 吞吐先裂(最貴、cold start 數十秒~分鐘)。依序:(1) queue 吸短峰(零延遲增容);(2) auto-scaling 加 GPU worker,靠 warm pool + predictive scaling 抵 cold start;(3) adaptive batching(queue 深時 max_wait=0 直接拉滿)榨乾每塊 GPU;(4) 真補不上 → backpressure 429 + Retry-After + per-user rate limit。API server stateless 直接加機器(02-Distributed-Systems/03-Scalability)。見 12-Ops-and-Reliability/02-Overload-Protection。
不會。Redis Streams consumer group:worker 讀取後訊息進 PEL,未 XACK 前不算完成;crash 後其他 worker 在 idle timeout(如 5s)後 XCLAIM 接管重跑。重跑安全,因為 sampling 無副作用——最壞只是換個同樣合理的 completion。見 12-Ops-and-Reliability/03-Reliable-Delivery。
從 blocking-on-list(每請求一條連線) 遷到 Pub/Sub(每台 API server 一條訂閱連線收所有 channel)。處理 Pub/Sub fire-and-forget 的 race:worker 先 SET KV 再 PUBLISH,API server subscribe 後補 GET 一次 兜底。見 10-Design-Patterns/05-Real-time-Updates。
queue 待處理請求 + 未讀結果全失,但它們短命無狀態(生命週期數百毫秒)。API server timeout → 回 503/504 + Retry-After,使用者 retry 即恢復,無資料庫被改、無扣款、無不可逆副作用。防護:Cluster + replica + Sentinel failover(~1-2s)。見 05-Database-Advanced/03-Replication。
來源明說的判準:即時 API → 延遲第一 → Redis Streams;離線 / 批量 → 可靠性 > 延遲 → SQS。離線版會改用 SQS(拿它的 DLQ、跨 AZ 持久化、全託管低維運),max_wait 可放大到秒級湊更滿的 batch(吞吐 / 成本最優),通知改為 callback / 輪詢任務狀態(不必同步 hold 連線)。整體從「同步阻塞」變「提交 + 之後取結果」。
因為 batched_sample side-effect free。exactly-once 昂貴(要去重 / idempotency / 分散式協調);本題重複處理最壞只是換個合理答案,零損害,所以選便宜的 at-least-once(PEL re-claim)。判準是「重複執行是否有副作用」——若是扣款 / 下單就必須 idempotency。見 12-Ops-and-Reliability/03-Reliable-Delivery。
固定 max_wait 在高流量下浪費(明明 queue 已滿還等)、低流量下嫌短(湊不滿)。adaptive:queue depth>100 → max_wait=0(不等、直接拉滿 100 追吞吐);queue depth<10 → max_wait=50ms(多等湊批、提升填充率)。代價是要持續監控 queue depth、多一組調參邏輯與抖動風險。
主動回想:白板重建 (Whiteboard from Scratch)
不看上文,照下面提示把整個設計重建一次。每格都該能 30 秒內口述。
常見陷阱 (Exam Traps for This Problem)
POST /sample 直接同步呼叫 batched_sample(一請求一推論)
What:API Server 收到請求就立刻把這一筆送進 GPU。
Why:填充率 1%、單 GPU 吞吐從 1,000 掉到 10 prompts/s、成本暴漲 100 倍,且 GPU 一次只跑一個 batch → 並發直接排隊。
正確做法:請求先入 Request Queue,由 GPU Worker 用 dual-trigger batching 湊批 再送。見 09-Messaging-Coord/02-Queue。
What:worker 拿到 completion 後直連持有 HTTP 連線的那台 API Server 推送。
Why:worker 不知道哪台 server 在等 → 要維護 request_id→server mapping(耦合);server 中途 crash 推送失敗要額外處理。
正確做法:worker 只 寫 Result Store + 發通知,API server 自取(解耦)。見 10-Design-Patterns/05-Real-time-Updates。
What:覺得「全託管 queue 比較穩」就用 SQS。
Why:SQS 每次最多拉 10 條(湊 100 要多次呼叫)、每 call 5-20ms(並行仍 20-50ms),逼近推論本身的 100ms,即時 SLA<200ms 扛不住。
正確做法:即時 API 用 Redis Streams(sub-ms、一次拉 1-100);離線 Batch API 才用 SQS。見 07-Caching-Storage/04-Redis。
What:API Server 狂輪詢 KV,或每個 in-flight 請求佔一條 Redis 連線阻塞等。
Why:polling 造成海量無效 GET;blocking 讓連線數 = 並發數(1,000×10 台=10,000)撞 Redis 上限。
正確做法:高併發用 Pub/Sub(每台 API server 1 條訂閱連線),先 SET 再 PUBLISH、subscribe 後補 GET 防 race。見 10-Design-Patterns/05-Real-time-Updates。
What:擔心 crash 重跑,加一堆去重 / 分散式協調。
Why:batched_sample 無副作用,重跑最壞只是換個合理答案,零損害;exactly-once 昂貴且多餘。
正確做法:用 at-least-once(Redis Streams PEL re-claim)即可。判準是「重複執行有無副作用」。見 12-Ops-and-Reliability/03-Reliable-Delivery。
What:流量一漲就假設能瞬間擴出 GPU 吃下所有請求。
Why:GPU cold start 數十秒~分鐘,來不及救急;不設上限會拖垮整系統。
正確做法:queue 吸短峰 + warm pool/predictive 抗 cold start + 極端閾值 backpressure(429+Retry-After)+ per-user rate limit。見 12-Ops-and-Reliability/02-Overload-Protection。
Related Notes
- 02-Distributed-Systems/05-Numbers-to-Know
- 02-Distributed-Systems/03-Scalability
- 03-API-Design/02-REST
- 04-Database-Indexing/04-Hash-Index
- 05-Database-Advanced/03-Replication
- 07-Caching-Storage/04-Redis
- 09-Messaging-Coord/02-Queue
- 10-Design-Patterns/03-Long-Running-Tasks
- 10-Design-Patterns/05-Real-time-Updates
- 11-Infrastructure/03-Container
- 01-Networking/06-Load-Balancing
- 12-Ops-and-Reliability/02-Overload-Protection
- 12-Ops-and-Reliability/03-Reliable-Delivery