🚨 命中率看起来更高,为什么尾延迟反而先炸了
很多团队一做会话级KV复用,就顺手把同一用户或同一session的请求固定打到同一台推理节点。📉 离线压测里,prefix cache hit rate的确会上升,运维面板也能看到重复前缀被更多复用,可线上很快又会冒出另一组更难看的数字:少数GPU长时间满载,P99拉长,池子里其余机器却吃不满。🚨
这类故障最迷惑人的地方,在于“局部最优”非常显眼。🧠 聊天、Agent 和长文问答都会天然偏好多轮连续会话,于是调度器把 locality 直接等同于永远粘住。📌 结果缓存命中了,负载均衡却被破坏;一旦某个大租户、长上下文或慢解码会话落到单分片上,整个池子的吞吐就会被一个热点队列牵着走。⚠️
🔍 真正失衡的,不只是流量,而是租户偏斜、KV 足迹和解码队列耦合
真正把Session Affinity推成热点分片的,通常有三层叠加。🔍 第一层是租户偏斜,企业大客户或高活跃 Agent 会把大量连续请求推向同一 shard;第二层是KV足迹放大,长上下文会话让显存长时间被占住,迁移成本越来越高;第三层是解码队列耦合,节点一旦进入慢批次,后续同会话请求还会继续追着同一队列排队。🧩
一组客服对话流量灰度里,硬粘性路由让全局缓存命中率从41%升到58%,看起来很漂亮,但最热节点的GPU利用率冲到96%,冷节点只有33%,P99从1.4 s拉到2.9 s。✅ 当系统改成“预算门禁 + 软粘性 + 超阈值移交”后,缓存命中仍有54%,P99却回落到1.8 s。📊 这说明真正该优化的不是单次命中,而是命中收益能否在整池范围内稳定兑现。
| 方案 | 全局缓存命中率 | 最热节点利用率 | P99延迟 | 典型问题 |
|---|---|---|---|---|
| 随机均衡 | 41% | 71% | 1.4 s | 缓存复用偏低 |
| 硬粘性路由 | 58% | 96% | 2.9 s | 热点分片和冷节点并存 |
| 软粘性 + 门禁移交 | 54% | 82% | 1.8 s | 更稳,适合池化推理 |
🛠️ 更稳的工程做法,是把 locality 从硬约束改成软提示
更稳的做法,是先按队列压力、显存水位和租户预算筛出可运行节点,再在候选集合里优先选择已有前缀缓存的 shard。🛠️ 也就是说,locality 应该是加分项,而不是一票否决项。🔒 当decode queue、kv_watermark或同租户并发超过阈值时,调度器要允许会话在相邻 shard 之间移交,而不是继续把后续 turn 塞进已经过热的节点。
真正关键的一步,是把session id和缓存命名空间拆开。🔁 节点移交时同步的是前缀摘要、模型版本指纹和可复用片段元数据,而不是粗暴复制整段KV。💡 这样既能保住大部分缓存收益,又能给continuous batching留出重新均衡的空间;再配合租户权重、分片TTL和 admission control,热点会话就不会长期霸住单节点。🙂
defroute_request(req,pool):candidates=[nodefornodeinpool.by_model(req.model)ifnode.kv_watermark<0.78andnode.queue_wait_p99_ms<220]preferred=sorted(candidates,key=lambdanode:(-node.prefix_cache.score(req.prefix_digest),-node.tenant_budget.remaining(req.tenant_id),-node.free_decode_slots,),)node=preferred[0]ifnode.is_hot_for(req.session_id):node=pool.pick_handoff(req,exclude=node.id)returnnode📈 接下来 3 到 6 个月,推理调度的分水岭会转向“缓存感知但不失公平”
接下来3到6个月,推理服务的竞争点不会只是“谁的缓存命中率更高”,而是谁能把缓存复用、租户公平和尾延迟一起纳入同一套调度合同。📈 团队至少要持续盯住hot_shard_ratio、cache_hit_by_tenant、handoff_success_rate和queue_wait_p99,否则很容易在全局均值漂亮的情况下,悄悄把热点留给少数节点。🚦
笔者认为,成熟的推理集群最终更像一套缓存感知的资源编排器,而不是只会做粘性路由的四层负载均衡。💡 如果你们线上已经开启会话级缓存,最值得复盘的问题不是“命中涨了多少”,而是“最热 shard 是否比昨天更热了”。欢迎交流你们遇到的热点分片形态。