延迟感知过载保护
一、它是什么:不是路由策略,而是"过载标记器"
在 SMG 的策略矩阵里,这个模块容易被人误读。先澄清一点:它并不参与"选哪个 worker"的决策——选 worker 是缓存感知、分桶、轮询这些路由策略的活。它做的是更上游的一件事:
持续监控每个 worker 的延迟指标,给它打上一个"过载"标记,让 HTTP 路由器在选 worker 之前把过载者从候选集里剔除。
它被设计成采集器上的一个"指标钩子"。采集器每隔几秒抓一次各 worker 的 TTFT/TPOT 分位数,算完之后回调这个钩子,由判定逻辑依据阈值决定该 worker 当前是否过载。路由器随后读这个标记做预过滤。
这个分层是有意为之:采集、判定、过滤、路由四件事解耦。latency_aware 关掉时钩子变成空操作,不打标、不过滤,但原始指标照常流动,不会留下脏状态。希望每个策略都能被独立开关、独立演进,而不是搅在一起。
二、核心机制:两级阈值 + 滞回 + 主动探测
2.1 状态机:进入容易、退出难
判定逻辑是一个三态状态机:
- 未过载 → 看是否该标过载:当 TTFT p90 或 TPOT p90 超过"过载阈值"时,标记为过载并启动探测;
- 已过载 → 看能否恢复:只有当延迟回落到"降级阈值"以下、且探测也认为恢复了,才清除过载标记。
默认配置下的阈值是非对称的:
| 维度 | 进入过载 | 退出过载 |
|---|---|---|
| TTFT p90 | ≥ 30.0s | < 16.0s |
| TPOT p90 | ≥ 0.3s (300ms) | < 0.08s (80ms) |
这是典型的**滞回(hysteresis)**设计:进入门槛 30s,退出门槛 16s。中间 16–30s 的灰色地带,worker 会被"锁"在过载状态,直到延迟真正回落到降级线以下。
这样设计是吃过反复进出(thrashing)的亏——如果进出阈值一样,延迟在边缘抖一下就进、抖一下就出,路由选路跟着反复跳。而反复跳对缓存是灾难(后文会专门讲)。非对称阈值就是为了把 worker "锁"住,逼它彻底缓过来再放回。
退出判定要求指标严格低于降级阈值,并且探测也确认恢复。这是"双重确认":单看采样分位数不够稳,必须叠加一个独立信号才放行。宁慢勿错。
2.2 主动探测:独立于用户流量的心跳
光看采集来的 p90 不够——它有采样滞后,而且过载时请求可能根本进不来、样本不足导致分位数失真。所以给每个被标记过载的 worker 额外配了一个独立的探测任务:以固定节拍向该 worker 发一个极小的探测请求,把响应情况作为"它还能不能接活"的探针,用连续成功/失败的计数来判定恢复——连续若干次成功才认为恢复,连续若干次失败则重新标记过载。
这个探测的价值在于:它给了一个与用户流量解耦的活性信号。过载时用户请求可能被队列阻塞、超时、打不进,p90 早已失真;而固定节拍的探测以稳定频率"敲"worker,能捕获到"它缓过来了"的那一刻。配合降级阈值,恢复判定更稳健。
2.3 热更新与解耦
尽量让一切都可热更:
- 阈值热更:阈值配置可无锁热替换,探测任务在下一轮读取新值;整套配置可经 ConfigMap 热下发,不重启。
- active 热开关:运行时关闭后钩子变空操作,路由器随之跳过预过滤,便于独立启停。
- 无编译期耦合:采集器持有的是钩子接口,判定逻辑只是它的一个实现,二者可独立演进。
这是网关类服务的底线要求——零停机运维,所有策略都得能在线调整。
三、这套设计的优点
职责单一、分层干净。判定(延迟感知)与路由(缓存感知等)分离,可独立演进、独立开关。这是最满意的地方。
滞回 + 双重确认,抗抖动。进入 30s / 退出 16s 的非对称阈值,加上"指标 + 探测"双确认,几乎杜绝了在阈值边缘反复横跳导致的路由震荡。
探测与流量解耦。过载时仍能拿到稳定节拍的活性信号,不依赖可能失真的分位数样本,探测成本也压到了最低。
全链路热更新。阈值、探测节拍、启停开关都可运行时切换,贴合零停机运维。
优雅降级。关闭时不打标、不过滤,原始指标照常流动,不会因关策略而留下脏状态。
四、也知道的缺点
设计没有银弹,这套东西有几个心知肚明的短板:
探测信号与真实业务负载存在偏差。探测是一个极简的统一请求,未必能反映 worker 上真实热点模型的排队情况。若 worker 按模型分片、某个热门模型正在排队而探测命中的路径空闲,就可能误判为"健康"。这是判定精度上的固有隐患。
探测可能加剧过载。worker 已经过载(TTFT 30s 级),探测仍以固定节拍持续敲门,虽小但是真实开销,在极端高负载下会与用户请求争抢调度。这是个"必要的痛",可以接受,但极端场景得留意。
检测存在固有滞后。判定依赖周期性采集与时间窗口内的分位数,过载检测并非实时,从延迟劣化到标记有数秒延迟;恢复同理。瞬态尖刺可能漏判或迟判,这是周期采样的天然代价。
退出过严可能"锁死"容量。要求延迟低于降级阈值且连续探测成功,三者全满足才放回。若集群整体水位偏高、降级线难触及,worker 会被长时间挡在候选集外,整体可用容量缩水。这点需要结合实际 SLO 校准降级阈值——锁得稳,但也可能锁过头。
五、对缓存命中率的影响(这才是想讲的重点)
前面那些都是这个模块本身的设计。但当它和缓存感知路由叠加在一起时,会出现一些不太显眼、却很值得讲的连带影响。这才是写这篇复盘的主要目的。
5.1 链路:预过滤先于亲和路由
路由器的处理顺序是先过滤、后路由:当延迟感知策略开启时,在把候选集交给路由策略之前,先把被标记过载的 worker 剔除;随后缓存感知策略在缩水后的候选集里选 worker。
这里有两套"过载"概念,容易混,特意区分一下:
- 延迟型过载(本策略打的,基于延迟分位数)→ 路由器层预过滤用;
- 负载型过载(基于并发负载与上限的比较)→ 缓存感知策略在选 worker 内部用。
延迟感知影响的是第一层。
5.2 矛盾:过载 worker 往往正是"该命中"的 worker
缓存感知路由的命中机制是"相同前缀 → 记录在案的 worker":当某前缀匹配率超过阈值,策略会锁定那个持有该前缀 KV cache 的 worker。
而正是这个 worker,可能因为处理这个热点前缀的流量、TTFT 飙到 30s 被延迟感知标了过载、被路由器踢出了候选集。
于是那个"本该命中"的 worker 不在候选集里了。策略找不到亲和目标,退回到最小负载兜底,把请求送到另一个没有该前缀 KV cache 的 worker,后端必须重新 prefill → 物理缓存未命中。
这是这套设计最本质的矛盾:保护延迟,和保护命中率,在这个点上撞车了。
5.3 "逻辑命中"与"物理命中"脱钩:指标会骗人
这里有个格外想提醒的细节。网关侧的缓存命中率指标,统计的是"前缀是否匹配到某个已知 worker 条目",而不是"请求是否真的落在了持有该 KV cache 的 worker 上"。
于是当匹配到的 worker 被过载过滤剔除、请求改投他处时,指标仍可能记一次"命中",但后端实际经历的是一次未命中。运维若只看命中率曲线,会误以为"过载过滤没伤缓存"。
这是最大的监控盲区:延迟感知一旦踢掉持有热点前缀的 worker,命中率指标仍显示命中,但 TTFT、prefill 成本在偷偷上升。说实话,这个盲区当初设计时没完全想清楚,是后面复盘才意识到的。
5.4 持续性未命中:陈旧亲和 + 延迟清理
更麻烦的是,worker 被剔除后,它在路由树里留下的"前缀 → 该 worker"的亲和记录并不会立即清除,而是要靠后续的 LRU 淘汰慢慢回收;兜底路径也不会主动更新这条映射。
所以这条陈旧记录会一直留着。后续相同前缀的请求会反复命中它、反复找不到目标 worker、反复退回兜底、反复物理未命中——直到下列任一情况结束:
- 过载 worker 恢复并重新进入候选集。滞回设计在此反而发挥正向作用:它等延迟真正降到降级线以下才放回,避免了"刚放回又过载"的抖动,前缀命中得以稳健恢复;
- LRU 淘汰把这条陈旧记录清掉,下次该前缀才会重新建立到新 worker 的亲和。
在 worker 过载持续期间,该热点前缀的命中率会被持续压低,压低时长 ≈ 过载时长。这是这套设计为"保延迟"向"保命中率"支付的代价——知道它在,但判断这个代价值得付。
5.5 净效应:用命中率换延迟稳定性
综合来看,这套东西对缓存命中率是有方向的负面扰动,但这个扰动是有意为之且被控制的:
- 它牺牲的是"把请求继续砸向延迟劣化 worker"这一类命中——而那类命中本就会带来 30s 级 TTFT,用户体验极差,且 worker 的 KV cache 在高压下也未必稳;
- 滞回与双重确认把扰动限制在"确实过载"的窗口内,且恢复稳健,抑制了反复抖动带来的命中率震荡;
- 真正的代价藏在监控盲区里:网关侧命中率虚高,需结合"被过滤 worker 数量"、"prefill 开销"、"后端实际 KV 命中"一起看,才能度量真实影响。
六、如果重新设计,会改什么
复盘下来,有几条后续想做的事:
监控补盲:增加"逻辑命中但亲和目标被过滤"的计数,让"虚命中"显形;并关注被延迟感知过滤的 worker 数与后端 prefill 增量之间的相关性,把被掩盖的真实命中率暴露出来。这是最该先补的。
降级阈值与 SLO 对齐:退出过严会锁死容量。若业务 SLO 的 TTFT 目标本身就在 20s 量级,降级线设 16s 就偏紧,应结合集群稳态水位评估,避免健康 worker 被长期挡在门外。
过载时主动迁移亲和(可选增强):当持有热点前缀的 worker 过载、请求退回兜底到新 worker 时,可考虑显式在新 worker 上重建该前缀的亲和并广播,让后续请求更快在新 worker 建立命中,缩短未命中窗口——而非被动等待 LRU 自然淘汰陈旧记录。这是下一步最想试的方向。
考虑前缀粒度的过载:当前过载是 worker 粒度的全量过滤。若过载只源于某几个热点前缀,全量踢出会误伤该 worker 上其他冷前缀的命中。更细粒度的"热点前缀限流"可减少误伤,但复杂度显著上升,需要权衡收益。
最后说一句:这套延迟感知保护,职责清晰、抗抖动设计到位。但当它和缓存感知路由叠加时,本质是在"保延迟"和"保命中率"之间做了一次有意识的权衡——滞回与探测让这个权衡稳健,但代价集中在"过载期间热点前缀的持续物理未命中"上,而且被网关侧命中率指标部分掩盖了。用好它的关键,不在于关掉它,而在于理解这条取舍,并把那条被掩盖的真实命中率暴露到监控里。