K8s 滚动发布、Nacos 主动下线实例,自以为做到了平滑无感升级,结果每次发版都批量报 RPC 调用异常。
我们做了完整优雅下线逻辑:新 Pod 就绪再停旧 Pod、进程销毁主动调用 Nacos 接口摘除自身,理论上调用方应当实时感知实例下线。排查 3 年无人定位根因,直到开发灰度路由才揪出元凶:
spring:cloud.loadbalancer.cache.enabled=true 默认开启的本地缓存。这套缓存是为老旧 Eureka 设计的冗余产物,适配 Nacos2.x 长连接实时推送完全多余,本文拆解历史设计缺陷、线上故障根源、一键根治配置。
每轮滚动发布,都会持续数十秒出现大量 RPC 连接失败、超时报错;
Nacos 控制台已经看不到下线 Pod 实例,但调用方依旧持续路由到已销毁节点,报错峰值持续 30s+。
我们反复核对下线代码、Nacos 推送日志、K8s 发布流程,全部无异常,故障持续近 3 年无人定位。
yaml
spring:
cloud:
loadbalancer:
cache:
enabled: true # 默认开启,全公司项目无一人主动关闭Spring Cloud LoadBalancer 默认包装一层 CachingServiceInstanceListSupplier,逻辑流程:
就算 Nacos 客户端秒级收到下线推送,消费端 LoadBalancer 依旧拿着 35 秒前的旧实例列表分发流量,这就是发布期批量报错的直接原因。
从 Spring Cloud LoadBalancer 源码可以直接看到,缓存 TTL 被硬编码为 35 秒,这就是我们线上 30 + 秒报错窗口的直接来源:

// 源码片段:LoadBalancerCacheProperties.java
private Duration ttl = Duration.ofSeconds(35);注释明确标注:Time To Live - time counted from writing of the record, after which cache entries are expired. (TTL:从记录写入开始计算,缓存条目过期的时间,默认 35 秒)
早期 Spring Cloud 主流注册中心是 Eureka,Eureka Client 采用HTTP 定时轮询拉取服务列表(默认 30s 一次),每次拉取都要建立 HTTP 请求,网络开销大。
为了减少注册中心压力、降低 HTTP 请求耗时,Spring Cloud 官方设计了 LoadBalancer 本地缓存:
Nacos 2.x 重构底层通信模型,抛弃 HTTP 轮询,改用gRPC 长连接双向推送:
Spring Cloud Commons 要统一抽象层,兼容 Eureka、Consul、Zookeeper、Nacos 全注册中心,不能为 Nacos 单独移除缓存逻辑。为了 API 一致性,只能保留缓存开关,默认开启,导致所有使用 Nacos 的团队,都被动吃下 35 秒流量延迟的大坑。
我们实现了进程主动摘除 Nacos 实例,但只能修改 Nacos 服务端数据,无法穿透清除消费端 LoadBalancer 缓存:
这套为平滑发布设计的优雅下线机制,被一层默认开启的缓存彻底废掉,近 3 年无人发现该配置。
yaml
spring:
cloud:
loadbalancer:
cache:
enabled: false很多人担心关闭缓存会频繁查询 Nacos,压测验证结论:
Nacos Client 自身已经维护实时实例内存缓存,调用getInstances是纯内存操作,无网络 IO,关闭 LoadBalancer 缓存不会增加注册中心压力,无性能损耗。
cache.enabled=false;绝大多数线上长期疑难故障,都不是业务代码 Bug,而是框架为兼容旧组件留下的历史遗留设计陷阱。
如果只会复制粘贴通用配置、不深究底层通信原理,就算做了完整的平滑发布方案,依旧会持续忍受发布期批量报错。
吃透 Spring Cloud LoadBalancer 缓存的历史成因、Nacos 长连接推送机制,才能真正实现零故障无感滚动发布。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。