負載平衡器從第一天起就只有一個目標:不要讓任何一台特別忙。

round-robin 是這樣,least connection 是這樣,量 EWMA 延遲挑最快那台也是這樣。招式不同,底下的心智是同一個。後端是一群可替換、無狀態、彼此沒有差別的機器,你的工作是把水倒得平均一點。這個假設好用到我們幾乎不會去想它:擴容就是多倒一杯,某台掛了就從名單上劃掉,從頭到尾沒有人需要知道那台機器的肚子裡正在發生什麼事。

Kubernetes SIG 底下有個專案叫 Gateway API Inference Extension,最新的 v1.6.1 是 UTC 時間 2026 年 9 月 11 日發的。它做的事情,跟上面那段話每一句都反著來。

以前後端是黑箱,現在它得自報內臟

傳統健康檢查問的問題很簡單:你還活著嗎。回 200 就算活著。後端就算想多講一點,LB 這邊也沒有欄位可以聽——它的世界觀裡本來就沒有「這台現在有多累」這種資訊。

這個專案的 003 號提案叫「模型伺服器協定」,它把問法整個換掉了。模型伺服器要開一個 Prometheus 端點,至少吐出三件事:排隊中有幾個請求、正在跑的有幾個、KV cache 用掉幾成。

語意 型別 vLLM Triton TensorRT-LLM trtllm-serve SGLang
TotalQueuedRequests Gauge vllm:num_requests_waiting nv_trt_llm_request_metrics{request_type=waiting} trtllm_num_requests_waiting sglang:num_queue_reqs
TotalRunningRequests Gauge vllm:num_requests_running nv_trt_llm_request_metrics{request_type=scheduled} trtllm_num_requests_running sglang:num_running_reqs
KVCacheUtilization Gauge vllm:kv_cache_usage_perc nv_trt_llm_kv_cache_block_metrics{kv_cache_block_type=fraction} trtllm_kv_cache_utilization sglang:token_usage

這張表很容易讀錯,我一開始也讀錯了。規格原文寫得很小心:「The exact metric names don’t necessarily need to be the same as the recommended names here, however the metric types and semantics MUST follow this doc.」被規定死的是型別跟語意,名稱只是推薦值。上面那四欄只是四家目前的實作長相,你自己寫的模型伺服器可以叫別的名字。

另外兩個選配指標是 BlockSize 跟 NumGPUBlocks,給前綴快取的評分器算格子用的;沒吐的話就退回去讀外掛設定裡的值。

LoRA 那段的設計特別髒,髒得很誠實。規格要求吐一個叫 vllm:lora_requests_info 的 Gauge,而這個 Gauge 的數值本身是一個時間戳,用途只是讓閘道分辨哪一筆比較新。真正的資料塞在三個 label 裡:max_lora 是這張卡最多能同時載幾個 adapter,running_lora_adapterswaiting_lora_adapters 各是一串逗號分隔的名字。Prometheus 的 label 本來就不是拿來裝清單的,但要在一個既有的抓取端點上傳遞「這台現在記憶體裡有哪些東西」,也真的沒有更好的位置。

規格自己在開頭標了狀態:Partially implemented,並且附了一句「外掛化之後,這份協定按定義就不可能那麼嚴格」。這句話擺在文件第一行。

Filter 跟 Score 可以都不掛,Pick 必須恰好一個

0845 號提案定義的那套外掛框架裡,有一條約束寫得比階段本身有意思:Filter 外掛可以掛零個,Score 外掛也可以掛零個,但 Pick 外掛必須恰好一個

階段本身是三個:Filter 把不合格的端點刷掉、Score 給剩下的每一台打分、Pick 從打完分的名單裡選。提案原文直接寫了靈感來源是 kube-scheduler 的外掛系統。評分器應該把分數正規化到 0 到 1 之間,加權不在評分器裡做,統一放到 profile 的設定層——你負責觀察,我負責取捨。

對照舊做法才知道這是另一個量級。以前挑機器要花多久?一次取餘數,或者一個 atomic 計數器比大小,都是 O(1) 的事。不需要記憶體,不需要記得上一個請求去了哪,也不需要知道被挑中的那台是誰。

換句話說,以前餐廳門口那個人的工作是發號碼牌,現在他每來一個客人就要把全店的桌子重新評一次分。這件事本來荒謬,但當一桌客人可能坐兩分鐘也可能坐四十分鐘,而且你事先看得出來是哪一種的時候,重算就變得划算了。

限流的四種演算法那篇談的是同一個位置的另一個決定:要不要讓這個請求進來。這裡談的是進來之後給誰。兩個問題在 LLM 上都比在一般 HTTP 服務上難算,因為成本的變異太大。

以前黏著是不得已,現在黏著是手段

session affinity 在傳統 LB 的地位很尷尬。它存在是因為你的後端沒寫好、狀態留在記憶體裡,只好用客戶端 IP 把人綁回原本那台。大家都知道這是債,而且是遲早要還的那種。

推論服務把這個債變成了資產。理由很單純:LLM 處理一段 prompt 的第一步是把它算成 KV cache,這步叫 prefill,很貴。如果兩個請求共用同一段開頭,而它們剛好落在同一張卡上,第二個請求那段開頭就不用重算。多輪對話、同一份系統提示詞、同一份被反覆問的文件,全都是這種形狀。把它們刻意堆到同一台去,省下來的 prefill 遠大於負載不均的損失。

0602 號提案的做法我很喜歡,因為它繞過了一個明顯但走不通的路。

最直覺的方案是問模型伺服器:你 cache 裡現在有哪些前綴。提案評估過,也寫下了否決理由:要改模型伺服器的 API,而且即時回報這些索引會吃掉不小的頻寬。於是它選了另一條,叫「近似前綴索引」。

閘道把請求按字元切成等長的塊,注意是字元不是 token,提案原文特地說了不必真的 tokenize。然後對每塊算雜湊,公式是這樣:

1
hash(chunk i) = hash(chunk i content + hash(chunk i-1))

每一塊的雜湊都把前一塊的雜湊吃進去。這個設計有個漂亮的性質:只要第 N 塊對得上,前面 N-1 塊一定也對得上。像連署書上的簽名,第五個人簽的時候簽的是「前面四個人的簽名加上我自己」,所以你只要驗第五個,前四個自動成立。查表的時候就是找哪一台的前綴匹配得最長。

關鍵在於這張表的來源。閘道從來沒有問過任何一台模型伺服器它的快取裡有什麼。它記的是自己的流水帳:我把哪個請求送去了哪一台,就假設那台現在有那段前綴。整張索引是推論出來的,不是查證出來的。

而且前綴親和性不能單獨用。提案寫明它要跟負載一起算,做法是把前綴命中率、佇列長度、KV cache 用量湊成一個加權分數。前綴匹配還必須認 adapter,因為不同的 LoRA 不共用 KV cache,配錯了等於白比。

那如果挑機器的那個元件自己掛了

這是我讀到一半才想到、然後被答案嚇到的問題。

InferencePool 這個 CRD 的 endpointPickerRef 底下有個欄位叫 failureMode,合法值只有兩個:FailOpen 是閘道自己隨便挑一台送出去,FailClose直接把請求丟掉。預設值是 FailClose

健康檢查那邊還有一層。協定要求端點挑選器實作 gRPC 健康檢查,而 readiness 的判準是:資料同步完成,而且在多副本部署下它是被選出來的 leader。是 follower 就回 NOT_SERVING。

把這兩件事放在一起看,責任的位置就清楚了。傳統 LB 的路由決策是一個旁路的啟發式,算錯了頂多慢一點,那段程式碼掛了還有一堆 fallback。現在它是請求路徑上的一個有狀態、要選主、預設失敗就丟請求的服務。你在資料面多養了一個必須自己做高可用的元件。

Kong 與 API Gateway那篇的問題是「該不該擺一個守門員」。這裡的守門員不只擺了,他手上還有一本只有他自己看得到的帳本,而且他請假的時候整家店預設是不開門的。

那本帳本會過期,而且過期得很快

寫到這裡為止都是設計的好處。現在講那條真的會咬人的。

端點挑選器已經照 README 的公告搬到 llm-d/llm-d-router 了,飽和偵測器的原始碼在那邊。把它的預設值攤開寫成設定,長這樣:

1
2
3
4
5
6
7
{
"queueDepthThreshold": 5,
"kvCacheUtilThreshold": 0.8,
"metricsStalenessThreshold": "200ms",
"stalenessPolicy": "saturated",
"headroom": 0.0
}

metricsStalenessThreshold 預設 200 毫秒。超過這個歲數的指標就算過期。一個兩百毫秒前抓到的數字已經不值得相信,這件事本身就說明了 GPU 裡的狀態變得多快。

而程式碼裡那個欄位的註解,是整個設計最誠實的一段話。原文寫著:「In continuous batching architectures, the KV cache grows rapidly during the decode phase. A high staleness threshold combined with a high KV cache utilization limit can lead to routing traffic to an endpoint whose memory is silently expanding within the telemetry blind spot.」

拆開來講就是:連續批次推論在解碼階段會讓 KV cache 快速長大,而你手上的數字是上一次抓的。如果你把過期門檻調寬一點、又把 KV cache 上限設高一點,你就有機會把流量一路送進一台記憶體正在無聲膨脹的機器,而它在你的遙測裡看起來還很健康。這段話的出處才是重點:它就寫在那組預設值常數旁邊,作者自己留的。

指標真的收不到的時候要怎麼辦,做成一個可以選的策略,只有兩個值。預設的 saturated 把沒有指標的端點當成完全飽和,所以一旦全場指標蒐集掛掉,派工會整個停下來,寧可不發也不盲發。另一個 ignore 把它們排除在平均之外,於是全場都過期時分數算出來是零、流量照發,賭的是「指標掛掉的可用性損失」大於「送進可能過載的機器的風險」。

近似索引那邊也有它自己的帳要還。提案的缺點欄列得很清楚:它依賴對模型伺服器逐出策略的理解,換個環境可能要重調;索引放在記憶體裡,重啟就得重建;而且端點挑選器一旦跑多份,各自只看得到自己經手的請求,命中率會下降。

我只讀了規格、提案與原始碼,沒有實際部署或跑過這套系統。官方 README 宣稱它能改善尾延遲與吞吐,但那句話裡沒有附任何數字,我也沒有找到官方的基準測試報告,所以這篇不引用任何效能數據。上面提到的欄位名、預設值與警語,全部是回 repo 原文逐項核對過的。

什麼時候這整套對你是純成本

我的立場是這樣:前綴親和性這部分,值不值得完全取決於你的流量長什麼樣。

如果你的請求彼此之間幾乎沒有共用開頭,每次都是全新的短 prompt,那前綴索引每次都會算出一個很短的匹配,你付了維護那張表的代價,換回來的 prefill 節省接近零,卻同時把負載推得比 round-robin 更不平均。這種情況下該做的事是把前綴評分器關掉,退回去只看佇列深度跟 KV cache 用量。

會讓我改變這個判斷的條件也很具體:只要你的系統裡有一份夠長、夠多請求共用的系統提示詞或文件脈絡,計算就翻過來了,因為那段前綴的 prefill 成本是固定支出,而它被省下的次數跟請求量成正比。

搬完家之後,k8s-sigs 這邊還剩下什麼

那則搬遷公告值得再讀一次。端點挑選器、InferenceObjective、InferenceModelRewrite 跟 Body Based Router 全部搬去 llm-d 家族的 repo,k8s-sigs 這邊只留下輕量版的挑選器、InferencePool API,以及一致性測試。標準組織把最搶眼的那塊讓出去,自己守著介面跟「你說你實作了,那我來驗」的測試套件。

Gateway API 本體走的也是這條路:repo 裡有一整包 conformance/ 測試,實作交給 Envoy Gateway、kgateway、GKE Gateway 那些人各做各的。這個定位比任何功能清單都更能說明它想成為什麼。

「後端是一群可替換的機器」——這個模型到這裡就不成立了。以前你調負載平衡是在轉一個旋鈕,現在你是在寫一個排程器的評分函式,而這個函式的輸入是別人 GPU 裡的內臟,還附帶一個兩百毫秒的保鮮期。

下次有人跟你說某某系統的負載沒有平均分散,先別急著當成問題。問一句:不平均,是因為它算錯了,還是因為它算對了。