news 2026/10/15 14:01:55

Nacos点下线为何流量仍打到停机Pod?K8S场景全链路排查与优雅停机方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Nacos点下线为何流量仍打到停机Pod?K8S场景全链路排查与优雅停机方案

先说我遇到的实际场景。某个下午,某服务做滚动发布,K8S里旧Pod已经进入Terminating状态,我在Nacos控制台也手动点了下线,实例状态都变成灰的了,结果监控里还是很清楚看到那个IP在贡献错误率,调用方批量出现Connection refused。当时我整个人是懵的:注册中心都下线了,怎么流量还往死机器上打?而且这不是偶发,是每次发布几乎都会碰到,只是报错比例高低不同。

后来把整条链路捋了一遍才发现,原因根本不是某一个单一环节出错,而是Nacos下线动作、消费者本地缓存、K8S的Endpoint摘除、Pod终止顺序这几个环节叠加出来的结果。这篇文章就围绕“Nacos点了下线为什么流量还是打到停机Pod”这个问题,把那段时间踩过的坑和最终沉淀出的解决方案完整整理一遍,适合正在用K8S跑Spring Cloud Alibaba或Dubbo等微服务体系的同学参考。

1. 现象复盘:一场“下线”没生效的发布事故

1.1 事故现场还原

整个故障链路是这么走的:某后端服务做滚动更新,Deployment的strategy是默认的RollingUpdate,旧Pod被K8S标记为Terminating。发布前监听群里有人提醒“把旧实例从Nacos下线,免得流量打过去”,于是我在Nacos控制台找到对应服务,把那个旧IP点成了下线。

不到两分钟,调用方那边开始飘错误:

  • 大量Connection refused异常
  • 部分请求超时
  • 链路追踪里能看到目标IP就是刚下线的那个旧Pod地址
  • 错误率从0%猛增到8%~15%,持续了大概一两分钟才开始回落

更奇怪的是,监控看板显示旧Pod的IP在进入Terminating后的前30秒里,QPS不降反升,好像下线操作完全没有被调用方感知到。

1.2 初步排查动作

当时第一反应是查注册中心状态和K8S状态:

  1. Nacos控制台服务详情里,旧实例状态确实显示下线,健康状态为false
  2. 用kubectl get endpoints看Service后端,旧Pod IP已经从addresses中移除
  3. 到节点上看iptables规则,DNS解析出来的ClusterIP转发规则也已经没有旧Pod IP
  4. 但是调用方日志里的目标IP明确就是旧Pod IP

这就很有意思了:基础设施层面的路由已经切走,调用方的流量还在往旧地址打。说明问题出在“调用方的服务发现缓存”上,而不是K8S转发层。

我把调用方的一个测试实例打开调试日志,发现它本地缓存的服务实例列表中,旧IP仍然存在,并且被负载均衡算法选中。这时候基本定位了:Nacos服务端虽然下线了实例,但是调用方客户端本地缓存没有及时刷新,还在用旧的实例列表做负载均衡。

1.3 为什么这个故障让人头疼

这一类问题最麻烦的地方在于它“飘忽不定”:有时候下线后一两秒流量就切干净,有时候能持续报错三四十秒,完全取决于调用方数量、Nacos版本、客户端配置、K8S删除Pod的节奏。如果运维团队没有一套统一的下线流程,每次发版都会像开盲盒。

而且影响范围往往被低估。单个实例下线的瞬间,调用方如果还没刷新缓存,请求会持续打到Terminating Pod,直到连接被拒绝。像Feign、Dubbo这类框架都有连接池或者长连接复用,连接建立后不会每请求做一次服务发现,这种场景下流量打到垂死机器的概率会比想象中大得多。

2. 为什么“下线”失效:注册中心、客户端、K8S三方各有各的延迟

要解决这个问题,必须先把整条链路拆开。一个服务从“在Nacos点下线”到“调用方完全不往这个实例打流量”,中间至少经过三层:Nacos服务端、调用方客户端、K8S网络层。每一层都有自己的缓存和延迟机制,任何一层慢了,流量都会打到已停机的机器上。

2.1 Nacos的“下线”到底做了什么

先明确Nacos控制台点“下线”这个动作的本质。它调用的服务端接口会把实例的enabled字段置为false,同时实例进入不健康状态。

这一步完成了两件事:

  • 服务端不会再主动把该实例作为健康实例返回给新订阅的消费者
  • 服务端会向已经订阅该服务的客户端推送一条变更通知

但这里就有第一个坑:Nacos服务端推送通知的可靠性,跟你用的Nacos版本强相关。

Nacos 1.x时代的核心推送是基于UDP的。服务端变更后往客户端注册时的监听端口发一个UDP数据包,告诉它“服务列表变了,快来拉取”。容器化环境里UDP丢包是家常便饭,比如K8S Pod经过NAT转发、多网卡环境、防火墙策略不一致,UDP通知经常悄无声息就丢了。客户端一旦没收到通知,只能等兜底的轮询逻辑,而轮询周期默认在30秒左右。

Nacos 2.x版本改成了gRPC长连接,服务端主动推送的可靠性大幅提升,变更通知能秒级到达客户端。但即使升级到2.x,下面的客户端缓存问题依旧存在。

2.2 消费者本地的“过期服务列表”

这是整个问题里贡献最大的一个点。调用方在启动时会从Nacos拉取目标服务的全量实例列表,缓存在本地。之后每次发起远程调用,负载均衡器都是从本地缓存里挑选实例,而不是每次都去Nacos查。

这个本地缓存其实有两层:

  • Nacos客户端SDK本身维护了一份服务实例缓存
  • Spring Cloud LoadBalancer为了性能还会再包一层缓存,默认TTL在几十秒量级

两份缓存的刷新周期叠加起来,最坏情况下,一个实例从Nacos下线之后,调用方在几十秒内依然会把它当作健康实例来路由。

我自己测试过一个比较典型的组合:

  • Nacos 1.4.x
  • Spring Cloud Alibaba 2.2.x
  • Spring Cloud LoadBalancer默认配置

在Nacos控制台把实例下线后,调用方本地缓存里的旧实例过了大约30秒左右才消失。这30秒内如果发请求,有概率命中下线实例。如果刚好赶上那个Pod已经被K8S强杀,表现就是Connection refused。

更隐蔽的是,很多调用方没有配置重试,或者负载均衡策略是“同实例优先”,一旦选中了死实例,这次请求直接失败。即使配置了重试,重试间隔通常几百毫秒,如果缓存一直不刷新,多次重试也都是在同一个死实例上撞墙。

2.3 K8S的Endpoint和转发规则更新慢半拍

Nacos层处理完,K8S层也有自己的节奏。当你删除一个Pod时,流程是这样的:

  1. Pod进入Terminating状态
  2. Endpoint Controller异步将该Pod IP从对应的Service Endpoints中移除
  3. 每个节点上的kube-proxy watch到Endpoints变化,更新本机的iptables或ipvs转发规则

这三步在绝大多数集群里都是秒级完成的,但不是瞬间完成。在那个时间窗口内,Service ClusterIP的转发规则仍然包含旧Pod IP,请求依然会被DNAT到停机中的Pod。

还有一个特别容易忽略的点:conntrack连接跟踪表。即使iptables规则已经更新,如果调用方和Service之间已经建立了长连接,这条连接在conntrack表项的过期时间之前,依然会继续被转发到旧的Pod IP上。也就是说,网络层的“残留连接”会持续一小段时间。

这就是为什么有时候你看到kubectl get endpoints里旧IP已经没了,但线上仍然有请求打到旧IP,需要再等一段时间才能彻底干净。K8S本身没办法做到“规则更新瞬间掐断存量连接”,这是内核连接跟踪机制决定的。

2.4 Pod“猝死”:优雅终止没做好

还有一个经常被忽视的问题:Pod收到SIGTERM后怎么退出。

默认情况下,K8S向容器主进程发送SIGTERM信号后,会等一个terminationGracePeriodSeconds时间(默认30秒),超时后直接SIGKILL强杀。很多Java应用压根没有优雅停机配置,收到SIGTERM立刻退出,存量请求还没来得及处理完就断了。

即使你想让应用处理SIGTERM,如果没有preStop钩子配合,应用进程退出瞬间,Nacos里的实例也还没来得及彻底摘除,调用方的本地缓存可能还记录着这个实例,新请求照样打过来。这时候Pod已经死了,流量自然直接报错。

所以在故障复盘时,不要把锅都甩给Nacos。K8S的Pod生命周期管理、应用自身的优雅停机能力、调用方的缓存刷新策略,这三者任何一个不配合,都可能导致“下线后流量继续打过来”。

3. 解决实操:从快速止血到全链路根治

定位清楚了,接下来就是怎么把问题解决掉。我按“先止血、再优化、后固化”的顺序来写,每一步都有可以直接抄走的配置和命令。

3.1 快速止血:重启调用方清缓存

如果线上已经在报错,最快的动作不是改配置,而是让调用方实例重启,把本地缓存的过期实例列表清掉。这个操作听起来很粗暴,但确实是最有效的临时方案。

重启之后,调用方会从Nacos重新拉取服务列表,已经下线的实例不会出现在列表里,流量立刻恢复正常。缺点是影响面大,尤其在调用方很多、无法批量操作的时候,这个方案不现实。

另一个止血手段是调用Nacos OpenAPI把实例直接删除,而不仅仅是标记下线:

curl -X DELETE "http://nacos-server:8848/nacos/v1/ns/instance?serviceName=my-service&ip=192.168.1.100&port=8080&ephemeral=true"

注意这里的ephemeral参数,如果是临时实例(默认),填true;如果是持久实例,填false。这个接口比控制台点下线更彻底,直接从Nacos服务端把实例摘掉,不再返回给任何消费者。

还要检查一件事:这个服务在Nacos里是否配置了保护阈值。如果保护阈值大于0,服务端健康实例比例低于阈值时,会把不健康实例也返回给消费者,防止服务雪崩。这种配置下,即使实例下线了,调用方依然可能拿到它的地址。正常情况下保护阈值建议保持为0。

# 查询服务详情,确认保护阈值 curl -X GET "http://nacos-server:8848/nacos/v1/ns/service?serviceName=my-service"

3.2 给Pod配置preStop钩子,走优雅下线流程

止血完成,一定要把优雅下线做成标准流程。第一步就是在K8S的Deployment里配置preStop生命周期钩子。

preStop的核心作用:让Pod在收到SIGTERM之前,先执行一段脚本,把当前实例从Nacos下线,并等待一段时间,让调用方的本地缓存过期、K8S Endpoints摘除、conntrack连接自然老化,然后再让应用进程退出。

具体Deployment配置参考如下:

spec: terminationGracePeriodSeconds: 60 containers: - name: my-service image: my-service:latest ports: - containerPort: 8080 env: - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP lifecycle: preStop: exec: command: - /bin/sh - -c - | echo "preStop started, pod IP: ${POD_IP}" # 调用Nacos OpenAPI将当前实例标记为下线 curl -s -X PUT "http://nacos-server:8848/nacos/v1/ns/instance/update?serviceName=my-service&ip=${POD_IP}&port=8080&enabled=false&ephemeral=true" || true # 等待Nacos通知传播、LoadBalancer缓存过期、Endpoint摘除 sleep 15 echo "preStop finished" exit 0

几点实际操作经验:

  • curl命令在镜像里不一定存在。很多精简的Java运行镜像里没有curl,连wget都没有。如果preStop脚本执行失败,K8S会直接发SIGTERM,等于没有优雅下线。所以要么在镜像里装上curl,要么改成用/dev/tcp或者应用自带的健康检查接口来触发下线逻辑。实测下来,在基础镜像里装一个静态编译的curl最省心。
  • enabled=false只是标记下线,不是删除实例。如果你希望实例彻底从服务列表消失,可以改成DELETE请求。但标记下线已经能阻止Nacos把实例返回给消费者,删除更彻底。我倾向于用DELETE,彻底摘干净。
  • sleep 15这个时间不是拍脑袋定的,需要根据调用方的LoadBalancer缓存TTL来算。如果把消费者侧的缓存TTL调整到5秒,Nacos推送按秒级算,K8S Endpoints摘除按秒级算,15秒基本够。如果调用的下游特别多,或者网络环境复杂,可以再往上加到20秒甚至30秒。
  • terminationGracePeriodSeconds必须大于preStop执行时间加上应用本身优雅停机时间。preStop花了15秒,Spring Boot优雅停机配置了20秒,那Pod级别的这个参数至少要50秒以上,留点余量。否则preStop还在sleep,Grace Period就到了,Pod直接SIGKILL,前面的努力全白费。

3.3 消费者侧优化:缩短缓存TTL与主动订阅刷新

光改服务提供方一侧还不够,调用方也要配合。最直接的办法是把Spring Cloud LoadBalancer的缓存TTL调小:

spring: cloud: loadbalancer: cache: enabled: true ttl: 5s

TTL设置为5秒,意味着调用方在最坏情况下5秒内就会重新拉取服务列表,比默认的几十秒好很多。TTL再往小调意义不大,因为会增加每次请求的缓存穿透,反而影响性能。

如果希望做到“变更秒级感知”,可以在调用方订阅Nacos服务变更事件,收到事件后手动清空LoadBalancer缓存。核心思路是这样的:

@Component public class NacosServiceChangeListener { @Resource private NacosNamingService nacosNamingService; @Resource private LoadBalancerCacheManager loadBalancerCacheManager; @PostConstruct public void init() throws NacosException { nacosNamingService.subscribe("my-service", new EventListener() { @Override public void onEvent(Event event) { if (event instanceof NamingEvent) { // 服务列表变化,清空该服务的负载均衡缓存 loadBalancerCacheManager.evictCache("my-service"); } } }); } }

这段代码思路是通用的,具体API根据你使用的Spring Cloud Alibaba版本和Nacos客户端版本会有差异。重点是让调用方在服务列表变化时主动淘汰本地缓存,而不是被动等TTL过期。

Nacos 1.x用户如果想彻底摆脱UDP推送丢包问题,最推荐的做法就是升级到Nacos 2.x或更高版本。2.x的gRPC长连接推送机制可靠得多,服务端变更能秒级通知到所有客户端。如果暂时不能升级,就通过上面的主动订阅和缩短TTL兜底。

3.4 应用侧优雅停机与K8S就绪探针配合

preStop负责“延迟”,应用侧还要负责“排空存量请求”。Spring Boot应用建议开启优雅停机:

server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 20s

这样配置后,应用收到SIGTERM不会立即退出,会等待存量请求处理完毕,最多等20秒,超时再强制退出。整个过程和preStop是衔接的:preStop期间应用还在正常运行并处理存量请求,等preStop结束、SIGTERM发出后,应用进入优雅停机,把最后一批请求处理完再退出。

K8S这一侧,还有一个容易被忽略的配合项:就绪探针。很多团队的就绪探针只是启动时检查一次,运行期间基本不干活。实际上,就绪探针的作用是持续告诉K8S“这个Pod能不能接流量”,如果探针失败,K8S会自动把Pod IP从Endpoints摘除,比任何preStop里的sleep都更快生效。

建议把就绪探针配置得积极一点:

readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 2

当应用准备退出时,可以在SIGTERM处理逻辑里主动把就绪状态切换为不通过,K8S会在1~2个探针周期内把Pod摘除,这个速度比等待Endpoint Controller处理要快。摘除之后,外部新流量不会再进到这个Pod,存量流量则通过优雅停机继续处理完。

4. 常见问题排查与避坑实录

4.1 同类问题怎么快速定位

再遇到“点了下线流量还打过来”的问题,我建议按下面这个表格逐层排查,不要一上来就怀疑Nacos。

排查层命令/操作预期结果
Nacos服务端实例状态控制台看实例列表,或OpenAPI查询下线实例enabled=false,健康状态false,没有被心跳重新拉回
Nacos服务端保护阈值查询服务详情里的protectThreshold阈值必须为0,否则不健康实例也会被返回给消费者
调用方本地缓存查看调用方日志或抓包,确认目标IP候选列表缓存中不应再包含已下线实例的IP
K8S Endpointskubectl get endpoints -n {ns} {svc} -o yaml已下线Pod IP不在subsets.addresses中
K8S转发规则到节点执行iptables-save或ipvsadm -Ln集群IP的DNAT规则里已没有旧Pod IP
残留连接到节点执行 `conntrack -L -ngrep {podIP}`

顺带说一个经验:排这类问题最好的手段不是看监控,而是看调用方实际的负载均衡器日志。把调用方日志级别调到DEBUG,能看到它每次选中的目标IP列表,如果列表里还有旧IP,问题一定出在本地缓存的刷新上。

排查命令建议这样用:

# 检查端点 kubectl get endpoints -n my-ns my-service -o yaml # 检查Pod状态 kubectl get pod -n my-ns -o wide | grep my-service # 到被下线Pod所在节点上,观察iptables规则 iptables-save | grep {clusterIP} # 查看残留连接 conntrack -L -n | grep {podIP}

4.2 最容易踩的几个坑

第一个坑:临时实例控制台点下线会自动“复活”。Nacos里SDK注册的临时实例,健康状态是靠客户端心跳维持的。控制台点了下线,实例的enabled被置为false,但只要提供者进程还在运行,心跳还在续约,过一会儿服务端可能又把实例置为健康状态。你会在控制台上看到实例刚点下线又变绿了。

这是很多团队绕不过去的坑。临时实例真正可靠的下线方式是:停止进程,让心跳停止,服务端自动剔除;或者在preStop里调用OpenAPI DELETE把实例删掉。控制台点下线只适合临时摘流量,不适合作为发布流程的依赖。

第二个坑:镜像里没有curl,preStop静默失败。我在一个内部项目里遇到过,preStop脚本写了调用Nacos下线,但因为基础镜像里没有curl,脚本执行直接报错,K8S没有反馈任何告警,Pod照常进入SIGTERM流程。等到问题复盘才发现preStop从来没成功执行过。建议在镜像里装好curl,或者在应用启动时先诊断一下preStop会用到的命令是否存在。

第三个坑:保护阈值配置了不为0。某团队为了“防止健康实例过少导致雪崩”,把保护阈值设成了0.5。结果发布时新实例还没完全起来,健康比例低于阈值,Nacos就把已经下线的不健康实例也发给了消费者,流量大量打到死机器上。保护阈值这个参数是双刃剑,不是所有服务都适合开。

第四个坑:conntrack残留连接导致的“幽灵流量”。即使Nacos、K8S Endpoints、iptables规则全部更新完毕,已经建立的连接在conntrack表中短暂残留,仍然会把流量导入旧Pod。这个窗口通常几秒到几十秒,取决于连接状态和超时时间。解决办法是接受这个窗口的存在,在preStop里多sleep几秒,或者在消费者侧配置连接失败重试。

4.3 我现在沉淀下来的发布流程

经历这几轮排查和整改之后,我这边现在的新服务发布标准配置是三层同时落地:

  • 提供方:preStop里调用Nacos OpenAPI删除实例,sleep 15~30秒,terminationGracePeriodSeconds设为60秒,应用侧开启优雅停机
  • 调用方:LoadBalancer缓存TTL调至5秒,关键服务订阅Nacos变更事件主动清缓存
  • 基础设施:就绪探针设置成主动探测,让K8S快速摘除不健康Pod

这套组合下来,滚动发布时再也没出现过“点了下线流量还打过来”的情况。偶尔有一两个连接因为conntrack残留正好落在旧Pod上,也会因为消费者侧配置了重试,在下一个实例上成功返回,对业务无感知。

最后分享一个小技巧:preStop脚本里一定要打日志。很多人以为preStop执行了就是成功了,实际上K8S不会把preStop的stdout/stderr展示在Pod事件里,需要到容器日志里看。我在脚本里echo了开始和结束日志,排查问题时能快速确认preStop是否真的跑完,这个习惯帮我节省了不少定位时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 14:01:38

聚类算法与分类算法的本质区别及工程选型指南

最近做几个项目时发现,很多同学包括一些工作了一两年的开发,经常把“聚类算法”和“分类算法”混在一起聊,甚至有人觉得“聚类就是分类的一种”。实际上这两者虽然长相接近,但骨子里是完全不同的两套逻辑。如果你刚接触机器学习&a…

作者头像 李华
网站建设 2026/10/15 14:01:38

Android悬浮窗技术原理与AutoJS实战应用

1. 悬浮窗技术解析与应用场景在移动端自动化领域,悬浮窗(Floaty)是一个极具实用价值的功能组件。它允许我们在设备屏幕上创建一个始终置顶的透明窗口,这个窗口可以叠加在其他应用界面上显示自定义内容或操作按钮。不同于传统的Act…

作者头像 李华
网站建设 2026/10/15 14:01:34

.NET Framework核心架构与开发实战解析

1. .NET Framework 概述2002年微软推出的.NET Framework,至今仍是Windows平台应用开发的中流砥柱。这个运行时环境和类库集合最初的设计目标,是解决当时COM组件模型的复杂性问题和"DLL地狱"困境。我在企业级应用开发中深度使用.NET Framework已…

作者头像 李华
网站建设 2026/10/15 14:01:21

SMT工厂核心技术解析与生产优化实践

1. SMT工厂的行业定位与技术价值在电子制造业的产业链中,SMT(Surface Mount Technology)工厂扮演着精密装配核心环节的角色。我十年前第一次走进SMT车间时,就被产线上贴片机"闪电手"般的操作震撼——每分钟可完成数万次…

作者头像 李华
网站建设 2026/10/15 14:00:55

GoldenGate禁用DDL时表结构变更的标准化协同流程

1. 项目概述:GoldenGate在DDL禁用模式下的表结构变更应对逻辑GoldenGate不开启支持DDL时,对新建表、增加字段的处理——这个标题看似只讲一个配置开关,实则直击数据同步场景中最容易被低估、却最常引发生产事故的核心矛盾。我接触过的二十多个…

作者头像 李华