模型服务多实例负载均衡与请求亲和性调度落地

日期:2026-08-24

一、项目背景 我们把推理服务从单实例扩到多实例,本来是想扛住更多并发,结果上线后发现一个怪现象:延迟不降反升。查了一圈才明白,网关对多实例是简单轮询,同一个会话的连续请求被均匀打散到不同实例,每个实例的 KV 缓存都接不住前一个请求的上下文,长一点的对话每次都要从头算。相当于我们花钱加了机器,却把最值钱的缓存复用给打散了。我们当时还以为是模型或者显卡的问题,调了一周参数没用,最后在网关日志里看到请求分布才反应过来,根子在路由策略。

实例扩容我们接了弹性伸缩,但亲和路由要求扩出来的新实例先进入候补池,等被会话认领后再进主力池,避免新实例一上来承接陌生会话把缓存打空。

这个项目让我们重新认识了缓存的价值,之前以为加机器就能解决延迟,其实是路由把缓存红利吃掉了,想清楚这一点之后,后面几个推理服务的架构都按亲和思路重画了一遍。

二、落地场景 第一块是实例健康与容量探测,网关定期探每个实例的显存、队列长度和存活状态,不健康的不派流。第二块是会话级亲和路由,给每个会话绑一个首选实例,同一会话的后续请求优先回到那个实例,KV 缓存能接着用。第三块是热点实例隔离,某个实例被长请求占满时,新会话自动避开,避免雪崩。第四块是故障实例摘除,实例掉线后其上会话平滑迁到健康实例,迁移窗口里在途请求不丢。最后是队列积压预警,实例排队超过阈值就告警并触发弹性扩容。

还有一个隐蔽问题是会话标识漂移,前端偶尔换 token 导致同一用户被当成新会话,我们让网关按用户加设备做稳定标识,亲和的命中率又提了一截。

三、关键技术挑战与解决思路 亲和太硬会出事,这是我们踩的第一个坑。最初我们把会话死绑实例,一个实例挂了,绑在上面的所有会话一起掉,恢复要重连重算。后来改成软亲和:优先回首选实例,首选不可用就漂到别的实例,下次再漂回来。这样既保住大部分缓存复用,又不至于一挂挂一片。第二个难点是容量探测的粒度,按显存看不够,还要看实际队列深度,我们用队列长度加权做路由权重。第三个难点是故障迁移的在途请求,我们让网关在摘除前先 drain 在途请求再切,避免半截请求丢给客户端报错。

稳定性上我们也补了压测,模拟单实例掉线时整批会话的重连表现,确认软亲和加 drain 在真实故障里不会把错误暴露给客户端,才敢动生产路由。

还有一个细节是权重更新频率,队列长度加权如果每秒都重算,路由表抖动会让会话频繁漂移反而伤缓存,我们改成滑动窗口每十秒更新一次,既跟得上容量变化又不过敏,这个节奏是压测时对着缓存命中率曲线一点点拧出来的,没有现成公式可抄。

四、效果数据 软亲和上线后,KV 缓存命中率从约四成提升到约七成,长上下文场景的 P99 延迟降了约四成。实例利用率更均衡,热点实例的队列长度峰值降了一半多,因为流量会被自动避开。故障切换时长从分钟级压到秒级,一次实例掉线不再连累整批会话。整体吞吐提升约两成,等于同样的机器多扛了五分之一的量。

上线后我们还做了一次混沌演练,随机杀掉一个实例,观察会话漂移和队列变化,结果符合预期,这才把软亲和从试验特性转成了默认配置写进规范。

五、可复用经验总结 多实例路由别无脑轮询,会话亲和是把缓存价值兑现的关键,但亲和必须做软的,死绑等于把故障域也绑死了。容量探测不能只看显存,队列深度才是真实水位。故障迁移要先 drain 再切,在途请求比看起来娇贵。我们那次调了一周模型参数纯属走偏,根子在网络层,后来我养成习惯,延迟异常先查路由分布再查模型。

多实例这套,监控得跟上,我们给每个实例加了缓存命中率和跨实例率的实时曲线,哪天命中率掉下去能立刻看出是路由还是模型的问题,定位快了不止一倍。

现在新上推理服务默认走这套路由,新同事不用再踩一遍我们踩过的坑,规范的价值就在这里。

结语 现在这套软亲和加队列加权成了新推理服务的默认路由,近期在接跨可用区的亲和,让就近实例优先。

案例片段(已脱敏): 软亲和路由片段(伪码):inst := preferMap[sessionID]            // 会话首选实例 if inst != nil && inst.Healthy && inst.Queue < hi {    return inst } inst = pickByWeight(instances)          // 软漂移:按队列加权 preferMap[sessionID] = inst             // 下次回漂 return inst上线前我们看了一天请求分布,同一会话跨实例率高达九成,缓存几乎没复用;软亲和后跨实例率降到约三成,剩下的多是故障漂移,属正常开销。