尾延遲治理:從超時(shí)、重試到排隊(duì)控制的實(shí)戰(zhàn)指南)
這次聊一個(gè)生產(chǎn)環(huán)境會(huì)很快暴露的問題LLM 推理服務(wù)的平均延遲很好看P99 卻會(huì)突然起飛緊接著就是接口超時(shí)、用戶重試、服務(wù)雪崩。很多團(tuán)隊(duì)把 LLM 接口接進(jìn)業(yè)務(wù)后第一個(gè)關(guān)注的是“平均響應(yīng)多快”真正上線后才會(huì)被上一課平均延遲不高但總有一部分請(qǐng)求比平均慢好幾倍。這些慢請(qǐng)求就是尾延遲。它不一定是模型變笨了更多時(shí)候是排隊(duì)、批處理、調(diào)度、共享資源共同作用后的結(jié)果。從分布式系統(tǒng)角度看尾延遲是經(jīng)典問題。LLM 推理場(chǎng)景又給它加了幾個(gè)放大器解碼階段必須逐 token 生成單個(gè)長(zhǎng)請(qǐng)求會(huì)占住推理資源動(dòng)態(tài)批次里的長(zhǎng)短請(qǐng)求還會(huì)互相干擾。所以“換更強(qiáng)的顯卡”不一定能解決問題真正的修法往往在模型執(zhí)行層、調(diào)度層和客戶端這一整條鏈路上。這篇文章給出一個(gè)可以照著落地的“簡(jiǎn)單修復(fù)”思路先量化尾延遲再在客戶端與網(wǎng)關(guān)之間做三層控制——按業(yè)務(wù) SLO 設(shè)置超時(shí)、對(duì)關(guān)鍵請(qǐng)求引入 hedge requests競(jìng)速請(qǐng)求先到先得、在服務(wù)入口控制排隊(duì)深度并做容量隔離。如果這些還不夠再往推理框架的執(zhí)行層推進(jìn)。全程會(huì)給出可改的代碼和驗(yàn)證流程不涉及改模型權(quán)重也不需要重訓(xùn)模型。1. 這篇要解決的問題與核心結(jié)論速覽先給結(jié)論后面再拆步驟。維度說明討論對(duì)象LLM 推理服務(wù)的尾延遲重點(diǎn)看 p95 / p99 表現(xiàn)典型癥狀平均延遲正常少量請(qǐng)求高達(dá)均值數(shù)倍超時(shí)集中在高負(fù)載時(shí)段簡(jiǎn)單修復(fù)客戶端/網(wǎng)關(guān)超時(shí) hedge requests 服務(wù)端排隊(duì)與容量控制進(jìn)階修復(fù)動(dòng)態(tài)批處理、chunked prefill、prefill/decode 分離、推測(cè)解碼等執(zhí)行層優(yōu)化涉及環(huán)節(jié)客戶端 SDK、網(wǎng)關(guān)、推理服務(wù)、GPU 資源、可觀測(cè)指標(biāo)依賴環(huán)境Python 3.9、異步 HTTP 客戶端、推理服務(wù)日志或指標(biāo)系統(tǒng)驗(yàn)證方式壓測(cè)前后對(duì)比 p50 / p95 / p99、成功率、超時(shí)率和排隊(duì)長(zhǎng)度適合讀者負(fù)責(zé) LLM 接口穩(wěn)定性、推理服務(wù)性能優(yōu)化、分布式系統(tǒng)設(shè)計(jì)的研發(fā)這里把“簡(jiǎn)單”二字說清楚它不是“調(diào)一個(gè)參數(shù)就徹底解決所有人尾延遲”的魔法而是先處理成本最低、見效最快的那幾層。做分布式系統(tǒng)的人都熟悉一句話不能讓一個(gè)慢副本或一個(gè)慢請(qǐng)求拖垮整體 SLA。LLM 服務(wù)也一樣。2. LLM 尾延遲是怎么產(chǎn)生的要理解修復(fù)方案得先看清 LLM 推理和普通 HTTP 服務(wù)的差別。普通服務(wù)處理一個(gè)請(qǐng)求通常是一個(gè)有限的計(jì)算過程讀取參數(shù)、查詢數(shù)據(jù)、返回結(jié)果。耗時(shí)大概率是穩(wěn)定的尾延遲主要來自排隊(duì)和機(jī)器抖動(dòng)。LLM 推理請(qǐng)求不是這樣。一個(gè)生成請(qǐng)求內(nèi)部有兩個(gè)明顯階段Prefill 階段處理完整 prompt計(jì)算量很大但可以并行計(jì)算。Decode 階段逐個(gè)生成 token當(dāng)前 token 依賴前一個(gè) token天然串行。Decode 階段對(duì)顯存帶寬很敏感而且生成多少個(gè) token 完全不確定。同樣是“寫一段總結(jié)”有的請(qǐng)求生成 100 個(gè) token有的生成 1000 個(gè) token。輸出長(zhǎng)度方差一大請(qǐng)求的占用時(shí)間方差就大尾延遲自然出現(xiàn)。再疊加一個(gè)因素推理服務(wù)為了提升吞吐通常會(huì)把多個(gè)請(qǐng)求動(dòng)態(tài)打包到同一個(gè) GPU 上執(zhí)行。一個(gè) batch 里如果混入一個(gè)超長(zhǎng)請(qǐng)求它占用的 compute 和 KV cache 就會(huì)拖住其他請(qǐng)求。即使框架做了 continuous batching讓短請(qǐng)求可以提前退出長(zhǎng)請(qǐng)求仍然會(huì)在某一段時(shí)間內(nèi)獨(dú)占或擠壓資源。所以 LLM 服務(wù)的尾延遲至少由四段組成請(qǐng)求在隊(duì)列里的等待時(shí)間。Prefill 階段的計(jì)算時(shí)間。Decode 階段的逐 token 生成時(shí)間。網(wǎng)絡(luò)傳輸、反序列化、框架調(diào)度等額外開銷。只看總延遲很難定位問題。后面所有修復(fù)手段都要先能拆出這幾段。3. 從六個(gè)來源排查尾延遲3.1 排隊(duì)效應(yīng)和隊(duì)頭阻塞推理服務(wù)通常有一個(gè)線程池或任務(wù)隊(duì)列。服務(wù)一忙新請(qǐng)求先排隊(duì)。隊(duì)列越深等待越久。如果隊(duì)首是一個(gè)超長(zhǎng)請(qǐng)求后面的短請(qǐng)求全部被壓住就會(huì)出現(xiàn)“前面那個(gè)任務(wù)不走后面全堵住”的隊(duì)頭阻塞。排查時(shí)不要只看 GPU 是否打滿。隊(duì)列長(zhǎng)度高但 GPU 利用率不飽和往往說明瓶頸在調(diào)度而不是算力。3.2 動(dòng)態(tài)批次里的長(zhǎng)短請(qǐng)求互相干擾現(xiàn)在主流推理框架都支持動(dòng)態(tài)批處理但長(zhǎng)度差異的影響仍然存在。長(zhǎng)請(qǐng)求的 decode 會(huì)持續(xù)占用顯存帶寬和計(jì)算資源。一個(gè) 2048 token 的請(qǐng)求和一個(gè) 64 token 的請(qǐng)求混在同一個(gè) batch 中框架要等前者逐步完成資源空轉(zhuǎn)在短請(qǐng)求上會(huì)很明顯。3.3 Prefill 和 Decode 混跑很多推理框架默認(rèn)把 prefill 和 decode 放在同一批里執(zhí)行。prefill 是計(jì)算密集型decode 是訪存密集型。兩者混在一起會(huì)產(chǎn)生資源互踩prefill 搶計(jì)算decode 搶顯存帶寬。如果調(diào)度策略不好個(gè)別請(qǐng)求的首 token 延遲就會(huì)被 prefill 頂上去。3.4 請(qǐng)求長(zhǎng)度本身就是長(zhǎng)尾分布用戶輸入長(zhǎng)度極不均勻。有人發(fā)一句話有人粘貼一整篇文檔。Prompt 越長(zhǎng)prefill 時(shí)間越長(zhǎng)輸出 max_tokens 設(shè)置越大單請(qǐng)求占住資源的時(shí)間越長(zhǎng)。這個(gè)變量很容易被忽略但它常常是 p99 突然飆高的原因。3.5 硬件異構(gòu)和資源爭(zhēng)搶多副本部署時(shí)不同機(jī)器的 GPU 型號(hào)、驅(qū)動(dòng)、鄰居負(fù)載不一定相同。共享節(jié)點(diǎn)上如果還有別的任務(wù)在搶 CPU 或內(nèi)存帶寬某些副本就是會(huì)比別人慢??蛻舳嗣看温酚傻铰北镜母怕孰m然不高但 p99 恰恰就是這部分請(qǐng)求。3.6 框架調(diào)度和日志鏈路開銷Python 推理服務(wù)在高并發(fā)下會(huì)有 GIL 爭(zhēng)搶、鎖競(jìng)爭(zhēng)、序列化開銷。日志如果同步寫磁盤也可能成為延遲黑洞。這類問題不是模型造成的但會(huì)直接體現(xiàn)在總延遲上。4. 修復(fù)前先做好觀測(cè)指標(biāo)和日志要能拆階段沒有指標(biāo)就動(dòng)優(yōu)化等于閉眼開車。建議在接入任何修復(fù)手段之前先為每個(gè)推理請(qǐng)求記錄結(jié)構(gòu)化日志至少包含這些字段字段含義request_id請(qǐng)求唯一 ID用于鏈路追蹤start_ts請(qǐng)求到達(dá)網(wǎng)關(guān)/服務(wù)的時(shí)間queue_start_ts進(jìn)入推理隊(duì)列的時(shí)間prefill_start_tsprefill 開始時(shí)間first_token_ts首 token 返回時(shí)間finish_ts請(qǐng)求完成時(shí)間prompt_tokens輸入 token 數(shù)completion_tokens輸出 token 數(shù)status成功、超時(shí)、失敗、被限流有了這些字段至少能算三個(gè)關(guān)鍵指標(biāo)TTFTtime to first token從請(qǐng)求發(fā)出到第一個(gè) token 返回的時(shí)間主要反映排隊(duì)和 prefill。TPOTtime per output token/ decode 速度反映 decode 階段是否穩(wěn)定。端到端總延遲影響用戶整體體驗(yàn)。采集示例如下import time import uuid def make_record(): return { request_id: str(uuid.uuid4()), start_ts: time.perf_counter(), } def mark(record, key): record[key] time.perf_counter() def save_record(record): # 生產(chǎn)環(huán)境建議寫入 Prometheus / ClickHouse / Loki不要同步寫本地磁盤 print(record)日志字段先不追求全但 request_id 和 finish_ts 必須有。沒有 request_id后面所有耗時(shí)分析都無法串聯(lián)。GPU 資源觀察可以在壓測(cè)時(shí)開一個(gè)終端watch -n 1 nvidia-smi如果部署在 Kubernetes 里可以看節(jié)點(diǎn)維度的資源占用kubectl top node kubectl top pod -l appllm-server這里的重點(diǎn)是“先量化再修復(fù)”。你接下來做的每一個(gè)改動(dòng)都要回到 p50、p95、p99 上去驗(yàn)證。5. 簡(jiǎn)單修復(fù)一給客戶端和網(wǎng)關(guān)加上超時(shí)第一個(gè)修復(fù)不需要?jiǎng)油评矸?wù)先把用戶側(cè)的等待上限卡住。很多服務(wù)被拖垮是因?yàn)榭蛻舳藳]有超時(shí)請(qǐng)求一直掛在遠(yuǎn)端不放。一個(gè)請(qǐng)求在服務(wù)端排隊(duì) 30 秒客戶端也等 30 秒這一個(gè)慢請(qǐng)求就會(huì)占住一個(gè)連接、一個(gè)線程池任務(wù)、一個(gè)協(xié)程。并發(fā)一高整個(gè)服務(wù)連接池被占滿所有請(qǐng)求都開始排隊(duì)。所以第一步是按業(yè)務(wù) SLO 設(shè)置請(qǐng)求超時(shí)。如果業(yè)務(wù)要求 5 秒內(nèi)返回就不要等服務(wù)端自己把任務(wù)跑完。超過預(yù)算就立即降級(jí)或返回超時(shí)錯(cuò)誤。下面是一個(gè)異步客戶端超時(shí)示例import httpx async def call_llm( client: httpx.AsyncClient, payload: dict, timeout_seconds: float 5.0, ): try: response await client.post( http://127.0.0.1:8000/v1/completions, jsonpayload, timeouttimeout_seconds, ) return response.status_code, response.json() except httpx.ReadTimeout: # 超時(shí)后走降級(jí)邏輯返回緩存、降級(jí)文案或標(biāo)記失敗 return 504, {error: llm request timed out}這里有一個(gè)常見誤區(qū)客戶端超時(shí)只是不讓調(diào)用方繼續(xù)等待不代表服務(wù)端請(qǐng)求被取消。很多推理框架并不會(huì)因?yàn)榭蛻舳藬嚅_連接就主動(dòng)停止生成任務(wù)仍然在 GPU 上跑資源照樣被占住。所以超時(shí)要配合服務(wù)端取消能力。網(wǎng)關(guān)如果支持把客戶端的取消信號(hào)傳遞到推理服務(wù)就用不支持的話超時(shí)只解決“調(diào)用方體驗(yàn)”不解決“服務(wù)端負(fù)載”。必要時(shí)還需要在推理服務(wù)側(cè)加請(qǐng)求級(jí)取消或最大排隊(duì)時(shí)間避免請(qǐng)求進(jìn)來后無限等待。建議超時(shí)設(shè)置要有梯度讀緩存或降級(jí)接口給較小超時(shí)。LLM 生成接口按歷史 p95 再加一點(diǎn)余量。不要給所有接口同一個(gè)超時(shí)時(shí)間。超時(shí)本身不能降尾延遲但它能把尾延遲對(duì)業(yè)務(wù)的影響限制在可控范圍內(nèi)。6. 簡(jiǎn)單修復(fù)二Hedged Requests讓請(qǐng)求競(jìng)速如果你已經(jīng)排除了排隊(duì)問題但個(gè)別請(qǐng)求就是會(huì)因?yàn)闄C(jī)器抖動(dòng)、網(wǎng)絡(luò)抖動(dòng)而特別慢可以考慮第二個(gè)手段hedged requests。這個(gè)思路來自大規(guī)模分布式系統(tǒng)中的經(jīng)典實(shí)踐不要只發(fā)一個(gè)請(qǐng)求死等而是在等待超過某個(gè)閾值后向另一個(gè)副本發(fā)起同樣的請(qǐng)求哪個(gè)先返回就用哪個(gè)。放到 LLM 場(chǎng)景使用方式不是每次請(qǐng)求都雙發(fā)那樣成本會(huì)翻倍。更合理的是先向首選副本發(fā)請(qǐng)求。請(qǐng)求超過一個(gè)閾值后仍未返回默認(rèn)選擇從歷史 p95 或 SLO 預(yù)算推導(dǎo)出該閾值。再向另一副本發(fā)起同樣請(qǐng)求。先返回的成功結(jié)果勝出取消其他未完成請(qǐng)求。代碼示意如下import asyncio import httpx async def call_one(url: str, payload: dict) - dict: async with httpx.AsyncClient(timeout10.0) as client: response await client.post(url, jsonpayload) response.raise_for_status() return response.json() async def call_with_hedge( url_a: str, url_b: str, payload: dict, start_hedge_after: float 0.3, ): task_a asyncio.create_task(call_one(url_a, payload)) # 先給首選副本一點(diǎn)時(shí)間 done, _ await asyncio.wait({task_a}, timeoutstart_hedge_after) if task_a in done: # 首選副本已返回 return task_a.result() # 未返回則向備用副本發(fā)起第二個(gè)請(qǐng)求 task_b asyncio.create_task(call_one(url_b, payload)) done, pending await asyncio.wait( {task_a, task_b}, return_whenasyncio.FIRST_COMPLETED, ) for task in pending: task.cancel() for task in done: try: return task.result() except Exception: continue raise RuntimeError(all hedge requests failed)這段代碼是演示用生產(chǎn)環(huán)境還要處理幾個(gè)細(xì)節(jié)兩個(gè)請(qǐng)求最好打到不同副本否則無法解決副本級(jí)故障。LLM 請(qǐng)求不是天然冪等的同一個(gè)請(qǐng)求重復(fù)執(zhí)行會(huì)增加 token 消耗。建議只對(duì)“只讀類、關(guān)鍵鏈路、用戶可容忍一定成本”的請(qǐng)求開 hedge。如果數(shù)據(jù)有合規(guī)邊界不要把數(shù)據(jù)發(fā)到不同域名的外部服務(wù)。合理做法是在同一套內(nèi)部集群的不同副本之間做競(jìng)速避免擴(kuò)大數(shù)據(jù)暴露范圍。必須設(shè)置整體預(yù)算。不能讓 hedge 請(qǐng)求無限等下去要在 SLO 內(nèi)截?cái)唷_@個(gè)方案的優(yōu)點(diǎn)是邏輯簡(jiǎn)單不需要改推理服務(wù)內(nèi)部實(shí)現(xiàn)缺點(diǎn)是會(huì)帶來額外的 token 成本和算力消耗。所以它適合作為“兜底手段”而不是默認(rèn)策略。7. 簡(jiǎn)單修復(fù)三服務(wù)端排隊(duì)深度與優(yōu)先級(jí)隔離客戶端超時(shí)和 hedge requests 只是在入口做攔截服務(wù)端自身的排隊(duì)策略如果沒控制好請(qǐng)求還是會(huì)大量堆積。一個(gè)典型的壞現(xiàn)象是推理服務(wù)已經(jīng)處理不過來了網(wǎng)關(guān)仍然把請(qǐng)求全部塞進(jìn)來。任務(wù)隊(duì)列無界增長(zhǎng)每個(gè)請(qǐng)求都等到超時(shí)才走最后服務(wù)徹底不可用。正確做法是“有界隊(duì)列 快速失敗”。當(dāng)服務(wù)處于過載狀態(tài)時(shí)不再讓新請(qǐng)求進(jìn)入隊(duì)列而是直接返回 429 或 503讓上游去重試或降級(jí)。對(duì) LLM 推理這種高成本服務(wù)來說快速失敗比重試等待更健康。一個(gè)簡(jiǎn)單的容量門控可以用信號(hào)量實(shí)現(xiàn)import asyncio class CapacityGate: def __init__(self, max_inflight: int, max_waiting: int): self._semaphore asyncio.Semaphore(max_inflight) self._max_waiting max_waiting self._waiting 0 async def acquire(self, wait_timeout: float 1.0): if self._waiting self._max_waiting: raise OverloadedError(queue is full, please retry later) self._waiting 1 try: await asyncio.wait_for(self._semaphore.acquire(), timeoutwait_timeout) except asyncio.TimeoutError: raise OverloadedError(wait queue timeout) from None finally: self._waiting - 1 def release(self): self._semaphore.release()在業(yè)務(wù)服務(wù)里可以這樣使用gate CapacityGate(max_inflight16, max_waiting64) async def run_inference(payload): await gate.acquire() try: # 這里調(diào)用真正的 LLM 推理服務(wù) return await inference_client.generate(payload) finally: gate.release()這個(gè)方案的實(shí)質(zhì)是寧可讓請(qǐng)求快速失敗也不要讓請(qǐng)求在隊(duì)列里堆積成山。配合客戶端超時(shí)效果會(huì)非常明顯因?yàn)榉?wù)端不再消耗資源處理那些注定要超時(shí)的請(qǐng)求。除了容量控制還要考慮多租戶場(chǎng)景下的優(yōu)先級(jí)隔離。線上實(shí)時(shí)請(qǐng)求和離線批量任務(wù)如果共用同一個(gè)服務(wù)應(yīng)該分隊(duì)列或分實(shí)例。批量任務(wù)可以容忍慢線上請(qǐng)求不能。常見做法是線上實(shí)時(shí)請(qǐng)求高優(yōu)先級(jí)隊(duì)列小 batch嚴(yán)格 SLO。離線批任務(wù)低優(yōu)先級(jí)大 batch可以排隊(duì)等待。如果兩者必須混跑至少要做優(yōu)先級(jí)搶占或權(quán)重調(diào)度。這里的核心是“隔離”。不隔離的后果是離線任務(wù)一跑線上 P99 立刻變差。8. 再進(jìn)一步執(zhí)行層優(yōu)化前幾層修復(fù)做完客戶端不會(huì)無限等服務(wù)端隊(duì)列也不會(huì)無界增長(zhǎng)。但如果 GPU 執(zhí)行本身低效那么 p99 可能還是偏高只是用戶感知到的超時(shí)變少了。執(zhí)行層的問題通常要從推理框架入手。以下方向都需要根據(jù)實(shí)際負(fù)載驗(yàn)證而不是一概而論8.1 動(dòng)態(tài)批處理與 continuous batching讓推理框架在 batch 內(nèi)動(dòng)態(tài)加入新請(qǐng)求、動(dòng)態(tài)退出已完成請(qǐng)求而不是等整個(gè) batch 結(jié)束。這個(gè)能力能明顯提高吞吐但如果框架調(diào)度粒度粗長(zhǎng)請(qǐng)求仍然會(huì)拖慢同 batch 的其他請(qǐng)求。主流框架一般都實(shí)現(xiàn)了類似機(jī)制具體開關(guān)和參數(shù)以你所用的框架版本為準(zhǔn)。8.2 Chunked Prefill把 prefill 階段拆成小 chunk避免一個(gè)超長(zhǎng) prompt 的 prefill 長(zhǎng)時(shí)間阻塞其他請(qǐng)求的 decode。適合 prompt 長(zhǎng)度差異大的場(chǎng)景。啟用后首 token 延遲會(huì)改善但調(diào)度開銷可能上升需要壓測(cè)確認(rèn)。8.3 Prefill 與 Decode 分離部署把 prefill 階段和 decode 階段安排到不同實(shí)例上因?yàn)閮烧哂?jì)算特征不同分離后可以各自調(diào)優(yōu)。架構(gòu)更復(fù)雜適合大流量、長(zhǎng)上下文場(chǎng)景。如果只是一個(gè)內(nèi)部小服務(wù)沒有必要一開始就上。8.4 推測(cè)解碼用小模型/草稿模型推測(cè)多個(gè) token再由大模型驗(yàn)證能減少 decode 步數(shù)對(duì)延遲和吞吐都有幫助。但依賴模型類型、batch 大小和硬件是否有效需要實(shí)測(cè)。8.5 KV Cache 和顯存管理KV cache 會(huì)隨并發(fā)和上下文長(zhǎng)度快速增長(zhǎng)。顯存碎片、KV cache 淘汰策略、長(zhǎng)請(qǐng)求顯存占用過高都會(huì)影響調(diào)度。如果經(jīng)常出現(xiàn)顯存不足或反復(fù)重新調(diào)度優(yōu)先檢查 KV cache 管理。這些執(zhí)行層優(yōu)化沒有一個(gè)能保證在所有場(chǎng)景下都有效。正確流程是先做前面三層簡(jiǎn)單修復(fù)再通過壓測(cè)看瓶頸是否還在 GPU 執(zhí)行階段。如果 GPU 平均利用率已經(jīng)很高p99 仍然高才值得繼續(xù)深入執(zhí)行層。9. 驗(yàn)證閉環(huán)壓測(cè)與指標(biāo)對(duì)比做任何修復(fù)都要用同一套壓測(cè)腳本、同一份數(shù)據(jù)、同一批并發(fā)模型去對(duì)比修復(fù)前后。否則很容易把環(huán)境差異誤判成優(yōu)化效果。下面是一個(gè)簡(jiǎn)單的異步壓測(cè)腳本框架用來采集各個(gè)請(qǐng)求的延遲import asyncio import aiohttp import time ENDPOINT http://127.0.0.1:8000/v1/completions PAYLOAD { prompt: 用三句話總結(jié)這篇文章, max_tokens: 128, temperature: 0.7, } CONCURRENCY 50 TOTAL_REQUESTS 500 def percentile(sorted_data, p): if not sorted_data: return 0.0 k (len(sorted_data) - 1) * (p / 100.0) f int(k) c min(f 1, len(sorted_data) - 1) return sorted_data[f] (sorted_data[c] - sorted_data[f]) * (k - f) async def single_request(session): start time.perf_counter() async with session.post(ENDPOINT, jsonPAYLOAD) as response: status response.status return status, time.perf_counter() - start async def worker(session, results): status, elapsed await single_request(session) results.append((status, elapsed)) async def main(): results [] sem asyncio.Semaphore(CONCURRENCY) async def limited(): async with sem: await worker(session, results) async with aiohttp.ClientSession() as session: tasks [asyncio.create_task(limited()) for _ in range(TOTAL_REQUESTS)] await asyncio.gather(*tasks) latency [r[1] for r in results if r[0] 200] latency.sort() success_ratio len(latency) / len(results) print(fsuccess_ratio: {success_ratio:.2%}) if latency: print(fp50: {percentile(latency, 50) * 1000:.1f} ms) print(fp95: {percentile(latency, 95) * 1000:.1f} ms) print(fp99: {percentile(latency, 99) * 1000:.1f} ms) asyncio.run(main())這個(gè)腳本只是框架壓測(cè)時(shí)要注意幾點(diǎn)壓測(cè)時(shí)間不能太短至少持續(xù)數(shù)分鐘讓排隊(duì)效應(yīng)穩(wěn)定下來。數(shù)據(jù)集要覆蓋不同 prompt 長(zhǎng)度和 max_tokens不能全是短請(qǐng)求。對(duì)比修復(fù)前后保留同一份歷史請(qǐng)求分布。除了延遲還要統(tǒng)計(jì)成功率、超時(shí)率、token 吞吐和 GPU 利用率。判斷成功的標(biāo)準(zhǔn)要提前定好。例如p50 基本不變或略有上升但 p95 / p99 明顯下降。超時(shí)率下降且沒有拖垮吞吐。成功請(qǐng)求的總 token 數(shù)沒有因?yàn)橹卦嚩蠓黾?。看到這組指標(biāo)后再?zèng)Q定是否需要繼續(xù)做執(zhí)行層優(yōu)化還是當(dāng)前修復(fù)已經(jīng)夠用。10. 常見問題與排查方法下面是生產(chǎn)環(huán)境常見的尾延遲問題排查表可以對(duì)照檢查。問題現(xiàn)象可能原因排查方式解決方案p99 高但 GPU 平均利用率不高排隊(duì)不均或隊(duì)頭阻塞看隊(duì)列長(zhǎng)度、單請(qǐng)求耗時(shí)分布增加并發(fā)控制拆分長(zhǎng)短請(qǐng)求所有請(qǐng)求在同一時(shí)刻集中超時(shí)服務(wù)端過載或隊(duì)列被打滿看隊(duì)列深度和服務(wù)端日志返回限流狀態(tài)快速失敗限制并發(fā)客戶端超時(shí)后服務(wù)端仍在生成斷開連接后任務(wù)沒有被取消檢查框架是否支持 cancel在服務(wù)端實(shí)現(xiàn)請(qǐng)求取消或最大執(zhí)行時(shí)長(zhǎng)開啟重試后 token 消耗暴漲沒有限制重試預(yù)算統(tǒng)計(jì)重試率、重試導(dǎo)致的 token只對(duì)關(guān)鍵請(qǐng)求重試加退避和限量hedge requests 后延遲下降但不明顯兩個(gè)請(qǐng)求打到了同一副本或同一網(wǎng)絡(luò)路徑檢查路由策略確保請(qǐng)求發(fā)往不同副本使用內(nèi)部多副本p95 正常p99 異常高少數(shù)副本變慢或長(zhǎng)文本請(qǐng)求按副本、按 prompt 長(zhǎng)度分組統(tǒng)計(jì)解決副本冷熱不均限制超長(zhǎng)請(qǐng)求優(yōu)先級(jí)一個(gè)長(zhǎng)請(qǐng)求拖慢一批短請(qǐng)求batch 內(nèi)請(qǐng)求長(zhǎng)度差異大對(duì)比長(zhǎng)短請(qǐng)求的 TPOT啟用 chunked prefill 或拆分長(zhǎng)短請(qǐng)求隊(duì)列壓測(cè)結(jié)果波動(dòng)很大環(huán)境噪聲或壓測(cè)時(shí)間太短加長(zhǎng)壓測(cè)時(shí)長(zhǎng)固定請(qǐng)求分布多次壓測(cè)取穩(wěn)定結(jié)果這里強(qiáng)調(diào)一點(diǎn)不要一遇到 p99 高就立刻懷疑推理框架有問題。先看請(qǐng)求長(zhǎng)度分布和隊(duì)列。很多尾延遲的根源其實(shí)早就在進(jìn)入 GPU 之前就埋下了。11. 最佳實(shí)踐與落地順序結(jié)合上面的內(nèi)容推薦按下面的順序落地修復(fù)方案。11.1 先補(bǔ)觀測(cè)把所有請(qǐng)求加上 request_id記錄各階段時(shí)間戳。沒有觀測(cè)數(shù)據(jù)后續(xù)每一步優(yōu)化都沒有依據(jù)。11.2 客戶端超時(shí)先于一切所有調(diào)用 LLM 接口的地方都必須設(shè)置超時(shí)并明確超時(shí)后的降級(jí)行為。這是成本最低、收益最直接的一步。11.3 控制服務(wù)端入站流量通過有界隊(duì)列、信號(hào)量和最大并發(fā)數(shù)避免服務(wù)端進(jìn)入無界排隊(duì)狀態(tài)。高負(fù)載時(shí)寧可快速失敗也不要無限堆積。11.4 關(guān)鍵請(qǐng)求再用競(jìng)速方案默認(rèn)不雙發(fā)。只有當(dāng)歷史數(shù)據(jù)顯示某些請(qǐng)求會(huì)明顯慢于預(yù)期且業(yè)務(wù)允許更高的 token 消耗時(shí)才對(duì)這部分請(qǐng)求啟用 hedge requests。11.5 壓測(cè)對(duì)比不要憑感覺調(diào)參每做一步修改都跑同一套壓測(cè)流程對(duì)比 p50、p95、p99、成功率、超時(shí)率和 token 成本。用數(shù)據(jù)決定是否需要進(jìn)入執(zhí)行層優(yōu)化。11.6 涉及數(shù)據(jù)使用的合規(guī)提醒修復(fù)過程中涉及請(qǐng)求轉(zhuǎn)發(fā)、多副本調(diào)用時(shí)要確保數(shù)據(jù)只流向有授權(quán)的內(nèi)部服務(wù)。不要為了降低延遲把包含敏感信息的數(shù)據(jù)發(fā)送到未確認(rèn)合規(guī)的外部接口。人臉、聲音、隱私文本等數(shù)據(jù)都要先確認(rèn)授權(quán)邊界再進(jìn)入多副本鏈路。11.7 線上發(fā)布前做小流量驗(yàn)證不要一次性全量發(fā)布修復(fù)策略。先讓 5% 到 10% 流量走新邏輯觀察成功率、延遲和成本確認(rèn)穩(wěn)定后再擴(kuò)大。從長(zhǎng)期看尾延遲治理不是一次性項(xiàng)目。請(qǐng)求分布會(huì)變、流量模型會(huì)變、模型版本會(huì)換治理方案也需要持續(xù)調(diào)整。最穩(wěn)妥的起點(diǎn)永遠(yuǎn)是那三件成本最低的事情超時(shí)、有界隊(duì)列、先到先得的競(jìng)速兜底。它們不解決所有問題但能幫你把“不可控的慢”變成“可感知、可攔截、可降級(jí)的慢”這一步做完整個(gè)系統(tǒng)就已經(jīng)穩(wěn)了一大半。