前段时间接手一个老服务的故障复盘,问题表象很迷惑:小部分用户时不时报请求超时,大部分用户访问正常,流量监控看整体 QPS、CPU、内存都稳稳的,看不出明显异常。
查了半天链路才锁定,是负载均衡上开启了会话保持,也就是大家常说的粘性会话。
当初开这个配置,是因为早期服务没有做状态剥离,会话信息存在单机内存里,用户请求必须落到同一台实例才能读到登录态。后来项目迭代,早就把会话迁到了 Redis,所有节点都是无状态的,但是负载均衡这边的配置没人关掉,就一直遗留下来了。
无状态服务还开粘性会话,隐患藏得很深。
首先是流量天然不均。如果一部分客户端固定绑死某一台实例,一旦这部分用户量慢慢涨起来,这台机器的负载会持续高于其他节点。别的节点 CPU 还在 30%,它已经跑到 80%,超时就集中在这一批用户身上。扩容的时候更尴尬,新增的实例很难分到流量,扩容基本等于白扩,负载均衡没法把流量均匀打散。
还有实例上下线的问题。滚动更新的时候,一部分用户绑定的旧实例被销毁,会话绑定关系失效,请求会重新调度。这个过程本身可以接受,但如果是批量重启,大量连接同时解绑、重新分配,短时间会出现一波集中报错。更麻烦的是健康检查剔除节点的时候,已经绑定到故障节点的连接,只要连接没断,LB 还会继续往故障实例转发请求。
很多人有个误区,觉得粘性会话只是一个兼容选项,开着也没坏处,大不了流量不均一点。实际上它直接破坏了无状态服务最核心的优势:弹性、容错、流量均分。
当然也不是说粘性会话一无是处,它有自己适合的场景。比如一些需要长连接、服务端内存缓存临时计算结果的业务,或者老旧的、暂时没法改造会话存储的单体应用,短期过渡可以启用。但只要业务已经改成无状态,优先关掉,用一致性哈希或者轮询,比依赖会话绑定稳妥得多。
我们当时的处理也简单,分灰度关掉会话保持,观察一两天错误曲线,超时报错直接消失,各个实例的负载也拉平了。顺便在 wiki 上加了一条规范:新服务上线默认关闭粘性会话,开启必须写清楚理由和下线时间,避免以后再出现这种配置遗留。
很多负载均衡层面的故障,都不是复杂算法问题,而是早年为了兼容写的配置,业务改了,配置没人同步清理,慢慢埋成定时炸弹。