昨天服務跑起來了。今天回答:它跑得好不好?
面試時很容易被問到的題目,包含「你的服務延遲多少」、「怎麼提升吞吐」及「P99 為什麼重要」?這三題答得好,入職的機率大大提升。
一段夢到的情境對話:
PM:「我們的推薦 API 快嗎?」
工程師:「平均 45 毫秒,很快。」
PM:「可是有客戶抱怨等很久。」
工程師:「他可能遇到極端值,畢竟平均才 45 毫秒。」
雖然工程師沒說錯,是這樣但不是這樣,原因是平均值會被大量的快速請求拉低,而抱怨的人來自那被平均值掩蓋的長尾。
今天要建立的能力是:用百分位說話,並且知道怎麼把它變好。
📊 職缺訊號
「模型部署與推論服務」出現在 49.2% 的 MLOps 職缺中,是第二高的任務訊號。而在生成式 AI 職缺裡,它以「LLM 服務化」的形式出現於 38.7%。這是雙主軸交會的第一個具體地點——Day 26 會用同一組觀念處理 LLM 服務。

圖 13-1:推論服務的延遲分佈與百分位曲線(10,000 筆模擬請求,固定隨機種子)。左圖顯示平均值 (約 49 ms) 遠低於 P99 (約 219 ms);右圖顯示百分位曲線在 95% 之後陡升。
這張圖的兩個讀法:
左圖:分佈是右偏的——大多數請求很快,但有一條長尾。平均值落在主體,P99 落在長尾。平均值告訴你「典型情況」,P99 告訴你「最不幸的 1% 使用者的體驗」。
右圖:百分位曲線在 P95 之後陡升。這代表訂 SLO Service Level Objective,服務水準目標)時,P95 與 P99 的難度差距很大——從 P95 提升到 P99 的工程成本,通常遠高於從 P50 提升到 P95。
長尾從哪來? 四個常見來源:冷啟動(新 Pod 剛起、模型剛載入)、GC 或記憶體回收停頓、特別大的輸入(長文件、大批次)、排隊(尖峰時請求在佇列等待)。排查長尾要先分清是哪一種,因為解法不同。
第二張圖是 MLOps 工程師可以多留意的:

圖 13-2:動態批次的取捨曲線(依「單筆 24 ms、每多一筆 +1.15 ms」的模型推算的示範值)。批次從 1 加到 128,吞吐提升近 20 倍,但單筆延遲也從 24 ms 膨脹到 170 ms。
關鍵因素:GPU 擅長平行運算,一次處理 32 筆的時間遠少於分 32 次處理。所以把請求湊成批,總吞吐會大幅提升。代價是先到的請求要等後面的請求湊齊。
這張圖真正的用法,其實不是先看吞吐量,而是反過來從 SLO 往回推:
1. 先訂定 SLO
→ P95 延遲 ≤ 100 ms
2. 從圖上找出仍能滿足 SLO 的最大批次
→ batch = 64
3. 讀出這個設定下的單機吞吐上限
→ 約 664 req/s
4. 再用整體流量需求反推機器數
→ 2,000 req/s ÷ 664 req/s ≈ 3 台
5. 最後預留容錯與尖峰空間
→ 實際部署可規劃 4 台,其中 1 台作為冗餘
先訂 SLO、再回推批次上限、最後算機器數——這條推論路徑就是容量規劃的標準動作,也是面試系統設計題的標準答法。
"""app/main.py —— 模型推論服務的最小完整版。"""
from contextlib import asynccontextmanager
import mlflow.pyfunc
import pandas as pd
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
MODEL = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# 模型在啟動時載入一次——不要在每次請求時載入(這是新手最常見的效能殺手)
MODEL["m"] = mlflow.pyfunc.load_model("models:/fraud-model@champion")
yield
MODEL.clear()
app = FastAPI(lifespan=lifespan)
class Req(BaseModel):
amount: float = Field(gt=0, description="交易金額")
channel: str = Field(pattern="^(web|app|store|line)$")
age: int = Field(ge=0, le=120)
@app.get("/health")
def health():
"""給 K8s 探針用:模型沒載入完就回 503,避免流量太早進來(Day 12)。"""
if "m" not in MODEL:
raise HTTPException(status_code=503, detail="model not loaded")
return {"status": "ok"}
@app.post("/predict")
def predict(req: Req):
df = pd.DataFrame([req.model_dump()])
score = float(MODEL["m"].predict(df)[0])
return {"score": round(score, 4), "model_alias": "champion"}
三個重點:模型在啟動時載入(不是每次請求)、用 pydantic 驗證輸入(擋掉髒資料,也是文件)、健康檢查反映真實狀態(別無腦回 200)。
"""locustfile.py —— 逐步加壓,觀察延遲隨負載的變化。"""
from locust import HttpUser, task, between
import random
class ModelUser(HttpUser):
wait_time = between(0.05, 0.2)
@task
def predict(self):
self.client.post("/predict", json={
"amount": round(random.uniform(100, 50000), 2),
"channel": random.choice(["web", "app", "store", "line"]),
"age": random.randint(18, 80),
})
uv add locust
locust -f locustfile.py --host http://localhost:8000 \
--users 200 --spawn-rate 10 --run-time 3m
看三件事,順序不能亂:
| 觀察 | 意義 | 下一步 |
|---|---|---|
| 延遲隨負載線性上升 | 還沒到瓶頸 | 繼續加壓找出真正的極限 |
| 延遲突然陡升、吞吐不再增加 | 到達飽和點——這就是你的容量 | 記下這個數字,它是擴容的依據 |
| 錯誤率開始上升 | 已經超載 | SLO 應訂在飽和點之前,不是之後 |
⚠️ 壓測最常見的錯誤:壓到自己
用筆電對雲端服務壓測,結果測到的是你家的網路頻寬。壓測端的資源必須遠大於被測端,或至少確認壓測端的 CPU 與網路沒有先飽和。看到「吞吐上不去但被測服務 CPU 只有 30%」時,先懷疑壓測端。
優化的正確順序(由投報率高到低):
部署形態的選擇:
| 形態 | 適合 | 例子 |
|---|---|---|
| 批次推論 | 無即時需求、量大、可預先算 | 每日凌晨算全客戶風險分數 |
| 即時 API | 需要當下回應 | 交易當下的詐欺判斷 |
| 串流 | 事件驅動、持續處理 | IoT 感測異常偵測 |
| 邊緣/端側 | 網路受限、隱私要求 | 產線瑕疵檢測、手機端模型 |
推論伺服器要不要用專用的? FastAPI 手刻與 Triton/TorchServe 這類專用伺服器的分界線是:
| 用 FastAPI 手刻 | 用專用推論伺服器 |
|---|---|
| 模型少、邏輯簡單 | 多模型、需要版本管理與熱更新 |
| 前後處理複雜、需要自訂邏輯 | 需要動態批次、模型組合(ensemble) |
| 團隊熟 Python 生態 | 需要壓榨 GPU 效能到極限 |
| 延遲需求寬鬆 | 高吞吐、低延遲的嚴苛要求 |
建議是先手刻。因為手刻讓你完全掌握發生什麼事,而專用伺服器的設定檔在你還不理解瓶頸在哪時,只會變成另一個黑盒子。等能判讀出「我的瓶頸在動態批次的湊批延遲」時,再換過去。
💡 一個常被忽略的選項:能批次就別即時
如果團隊為了「即時性」把每日只查詢一次的分數做成即時 API,結果扛著沒必要的 SLO 壓力與成本。先問清楚:使用者真的需要「當下」算出來,還是只要「查得到」就好? 後者用批次推論預先算好寫進資料庫,成本與複雜度都低一個量級。
本篇未深入探討變長輸入(Variable Input Length)對 LLM 的影響:批次吞吐曲線主要基於傳統結構化模型或固定維度特徵;在生成式 AI(LLM)中,Prompt 與 Output Token 長度高度浮動,單純靜態 Batching 會造成嚴重的 Padding 浪費,需額外依賴 Continuous Batching / PagedAttention(如 vLLM)才能精確衡量。
忽略了「動態流量抖動」與「冷啟動彈性」的延遲衝擊:容量推算採用了理想的線性均勻流量假設,但在真實生產環境中,流量常呈現突發波峰(Burst),單靠回推批次上限可能會在冷啟動擴容未完成前直接打垮佇列。
模型量化(Quantization)的精度損失與回歸成本被輕描淡寫:文中雖提到要驗證數值一致性,但在高風險場景(如反洗錢、金融風控)中,量化帶來的微小分佈漂移往往需要龐大的 Golden Set 回歸測試成本,並非單純的效能切換。
服務跑得又快又穩,但——它算得對嗎? 明天談這個系列的重要主題:監控與漂移。用時間軸說明模型「無聲壞掉」的那 14 天發生了什麼,以及為什麼縮短這 14 天,就是這份工作的全部價值。
max_queue_delay_microseconds 就是圖 13-2 那條取捨曲線的實作旋鈕