大模型私有化部署在各行各业落地过程中,绝大多数架构师在传统微服务中积累的限流经验都会彻底失效。传统的 API 网关限流通常基于固定的 QPS 或并发连接数,比如给某个接口配置 200 QPS 的令牌桶。但这套逻辑放在大模型推理集群上几乎等于自杀。
在 LLM 推理场景中,一个包含 32K 上下文的长文档摘要请求,其显存占用与计算开销可能是一个 50 字客服问答请求的上百倍。如果网关仍按固定请求频次放行,一旦突发涌入数十个长上下文请求,GPU 算力集群会在瞬间陷入“热崩溃”(Thermal Runaway / GPU Thrashing)状态。
推理集群的热崩溃机理
当前主流的私有化推理引擎(如 vLLM、Triton + TensorRT-LLM)均采用类似 PagedAttention 的分页显存管理机制。推理服务在启动阶段就会把绝大部分可用显存(通常设置为 90%)预分配作为 KV Cache 物理块池。
很多刚接触 GPU 运维的工程师会犯一个致命错误:用nvidia-smi输出的显存占用率去配告警或做限流。由于预分配机制的存在,nvidia-smi看到的显存占用永远是横盘在 90% 左右,没有任何动态参考价值。真正决定集群生死的是引擎内部的 KV Cache 逻辑使用率(kv_cache_usage_percent)以及调度队列中的等待请求数(num_requests_waiting)。
当网关放行的请求量超过推理卡池的显存承载能力时,灾难会按照以下骨牌效应爆发:
- 显存耗尽与抢占:新的解码步(Decode Phase)无法分配到可用的 KV Cache 块,引擎被迫触发抢占机制(Preemption)。引擎通常会选择将某些低优先级请求的 KV Cache 换出(Swap Out)到主机 CPU 内存,或者直接中断这些请求并在后续从头重新 Prefill 计算。
- 延迟断崖式暴跌:发生 Swap 和重计算后,单 Token 生成延迟会从正常的 20-30 毫秒突增到数百毫秒甚至数秒。
- 客户端超时与重试风暴:前端或调用方微服务设置的读超时(例如 30 秒)密集触发,客户端断开连接并自动发起重试。
- 僵尸请求霸占算力:更致命的是,如果网关和推理引擎没有打通级联取消机制,推理引擎底层的 GPU 依然在耗费昂贵算力为已经断开的连接计算后续的 Token,导致新请求根本无法进入队列,整个 GPU 集群算力彻底归零,系统进入吞吐量接近为零、延迟无线接近超时的假死状态。
基于实时背压的反向自适应门控设计
为了防止集群陷入死锁,必须将限流控制从“静态单向配额”重构成“基于引擎真实水位的反向背压限流(Reverse Backpressure Rate Limiting)”。
网关与后端 GPU 节点建立长连接或高频旁路监控(50ms - 100ms 周期),持续获取每个推理实例的关键指标:
KVCacheUsage:KV Cache 块占用比例。WaitingRequests:队列中排队等待 Prefill 的请求数。AvgIterationLatency:单步迭代延迟波动。
限流网关依据采集到的数据流,采用类似 PID 调节的自适应算法动态调整网关侧的最大允许在途请求数(Max In-Flight Requests)和准入概率。
package gateway import ( "context" "errors" "math" "sync" "sync/atomic" "time" ) var ( ErrClusterSaturated = errors.New("gpu cluster saturated: dynamic backpressure triggered") ErrRequestTimeout = errors.New("request canceled by client") ) // GPUNodeMetrics 推理节点实时指标 type GPUNodeMetrics struct { NodeID string KVCacheUsage float64 // 0.0 ~ 1.0 WaitingRequests int64 IterationLatencyMs float64 LastReportTime time.Time } // AdaptiveAdmissionController 自适应反向限流控制器 type AdaptiveAdmissionController struct { mu sync.RWMutex nodeMetrics map[string]*GPUNodeMetrics currentInFlight atomic.Int64 targetMaxTokens atomic.Int64 rejectionRate atomic.Uint32 // 存储百分比 0 ~ 100 // 水位阈值 highWatermark float64 // 触发背压阈值,如 0.80 panicWatermark float64 // 紧急熔断阈值,如 0.92 } func NewAdaptiveController(highWatermark, panicWatermark float64) *AdaptiveAdmissionController { c := &AdaptiveAdmissionController{ nodeMetrics: make(map[string]*GPUNodeMetrics), highWatermark: highWatermark, panicWatermark: panicWatermark, } // 启动后台调节循环,每 50 毫秒根据聚合指标重新校准准入策略 go c.runFeedbackLoop() return c } // UpdateNodeMetrics 接收推理节点推送的高频心跳 func (c *AdaptiveAdmissionController) UpdateNodeMetrics(metrics GPUNodeMetrics) { c.mu.Lock() defer c.mu.Unlock() metrics.LastReportTime = time.Now() c.nodeMetrics[metrics.NodeID] = &metrics } // Acquire 尝试通过准入控制 func (c *AdaptiveAdmissionController) Acquire(ctx context.Context, estimatedTokens int64) (func(), error) { // 1. 检查整体拒绝率(基于伪随机的平滑拒绝) rejRate := c.rejectionRate.Load() if rejRate > 0 { // 取当前纳秒作为轻量随机种子,决定是否命中拒绝 if uint32(time.Now().UnixNano()%100) < rejRate { return nil, ErrClusterSaturated } } // 2. 检查客户端上下文是否已取消,避免将已超时请求送入集群 select { case <-ctx.Done(): return nil, ErrRequestTimeout default: } c.currentInFlight.Add(1) // 返回清理句柄 releaseFunc := func() { c.currentInFlight.Add(-1) } return releaseFunc, nil } // runFeedbackLoop 基于控制理论的反向反馈调节 func (c *AdaptiveAdmissionController) runFeedbackLoop() { ticker := time.NewTicker(50 * time.Millisecond) defer ticker.Stop() for range ticker.C { c.mu.RLock() var ( totalKVCacheUsage float64 validNodes int totalWaiting int64 ) now := time.Now() for _, node := range c.nodeMetrics { // 剔除超过 1 秒未汇报的下线节点 if now.Sub(node.LastReportTime) < 1*time.Second { totalKVCacheUsage += node.KVCacheUsage totalWaiting += node.WaitingRequests validNodes++ } } c.mu.RUnlock() if validNodes == 0 { // 集群失联,保守阻断 50% 流量防止击穿 c.rejectionRate.Store(50) continue } avgKVCacheUsage := totalKVCacheUsage / float64(validNodes) // 动态调节准入拒绝率 if avgKVCacheUsage >= c.panicWatermark || totalWaiting > int64(validNodes*20) { // 水位处于濒危区间,全量快速失败,保护存量推理 c.rejectionRate.Store(100) } else if avgKVCacheUsage > c.highWatermark { // 水位处于高负载区间,按超限比例平滑提高丢弃率 // 超出比例从 0% 到 100% 映射 ratio := (avgKVCacheUsage - c.highWatermark) / (c.panicWatermark - c.highWatermark) computedRejection := uint32(math.Min(100, math.Max(10, ratio*100))) c.rejectionRate.Store(computedRejection) } else { // 水位健康,放行全部请求 c.rejectionRate.Store(0) } } }级联取消与工程避坑实践
实施反向限流时,必须保证全链路的“取消传播”(Cancellation Propagation)。在 HTTP/2 或 gRPC 架构下,当终端用户在手机端点击了“停止生成”,或者客户端网络超时断开连接时,网关的连接层会收到一个 RST_STREAM 帧。
网关必须立即捕获这个取消事件,并向后端推理集群派发主动 Abort 请求(例如调用 vLLM 的/abortAPI 或向 gRPC 上下文发送 Cancel)。后端推理引擎一旦收到 Abort,会立刻在下一个迭代周期将该请求对应的 Sequence 从运行队列中移除,并将其占用的所有 KV Cache 物理块标记为可回收。
如果缺少这一步,即使网关成功拦截了后续的新增流量,卡池里积压的数十个已死请求依然会继续消耗整整几分钟的算力去计算无意义的输出,反向限流的效果将大打折扣。
在拒绝策略的返回上,当触发自适应拒绝时,网关切忌直接返回通用的 HTTP 500 状态码,这会诱导客户端的重试拦截器立即进行指数退避重试,导致流量峰值被原地放大。网关应当返回明确的 HTTP 429 Too Many Requests,并在 Header 中附带计算后的动态Retry-After: 3(并加入 20% 的抖动 Jitter),明确告知调用方当前算力池处于排队收敛阶段,强制客户端退避,从而给 GPU 显存回收争取宝贵的缓冲时间。