前两天面试问了一个非常日常的问题❝你们线上发版的时候怎么保证旧服务下线时用户的请求不报错他挺自信地回答这很简单啊去 Nacos 控制台找到那个实例点一下下线按钮。或者在发布脚本里直接kill -15杀进程Nacos 收不到心跳自然就把节点剔除了网关就不会往这台机器发流量了。额在真实的生产环境下如果发布流水线真的只是这么干那每次发版的一两分钟里你们的网关日志里一定会刷出一堆Connection Refused或者Read Timeout的报错。如果刚好赶上晚高峰用户的直观体验就是点了一下按钮界面弹出一个醒目的网络异常。1. 流量为什么停不住很多同学对注册中心有一个误解觉得它是一个绝对实时的系统只要 Nacos 知道某个节点下线了所有依赖的微服务就瞬间都知道了。但在分布式系统里信息的传递是需要时间的。服务调用方比如 Gateway 网关在发起 HTTP 请求前需要知道目标服务的 IP。但为了性能网关不可能每来一个请求都去 Nacos 查一次网络。会用像 Ribbon 或者 SpringCloud LoadBalancer 这样的客户端负载均衡组件会在自己的内存里维护一个ServerList 服务 IP 列表缓存。坑就踩在这个缓存的更新机制上调用方默认会启动一个后台定时任务每隔一段时间去 Nacos 拉取最新的实例列表。虽然 Nacos 服务端也有 UDP 实时推送变更的机制但在复杂的生产网络里UDP 推送是有概率丢失的。这就导致了一个长达几十秒的时间差。在这几十秒里虽然 Nacos 服务端已经把那个节点标记为下线了但网关本地的缓存还没刷新网关手里拿着的依然是旧名单。现在回想一下你在控制台点下线后的操作你调用了下线紧接着就触发了重启进程被杀掉。但此时网关还没更新缓存依然把用户的请求往这台已经死掉的机器上送。操作系统一看端口都没人监听了直接回一个 RST 包网关马上就报Connection Refused了。 欢迎加入小哈的星球你将获得:专属的项目实战多个项目 / 1v1 提问 /Java 学习路线 /学习打卡 / 每月赠书 / 社群讨论新项目《Spring AI 项目实战》正在更新中..., 基于 Spring AI Spring Boot 3.x JDK 21;《从零手撸仿小红书微服务架构》 已完结基于 Spring Cloud Alibaba Spring Boot 3.x JDK 17..., 点击查看项目介绍演示地址http://116.62.199.48:7070/《从零手撸前后端分离博客项目全栈开发》2期已完结,演示链接http://116.62.199.48/;专栏阅读地址https://www.quanxiaoha.com/column截止目前累计输出 100w 字讲解图 4013 张还在持续爆肝中..后续还会上新更多项目目标是将 Java 领域典型的项目都整一波如秒杀系统, 在线商城, IM 即时通讯Spring Cloud Alibaba 等等戳我加入学习解锁全部项目已有4500小伙伴加入2. 中小团队的解法知道了原因解决思路也就有了。很多中小团队的做法是在 CI/CD 发布流水线里加一行sleep 40。流程变成了这样主动摘流,脚本先调 Nacos 的 OpenAPI把机器标记为下线。脚本强行sleep 40秒。在这 40 秒里机器还在正常运行依然在处理请求只是我们在这 40 秒里等待全网所有的网关更新完缓存。优雅停机40 秒一过没人往这发请求了再发送kill -15杀进程。这套方案确实管用。但如果你拿着这个方案去大厂面试依然是不及格的。原因很简单太慢了且不具备普适性。如果一个核心服务有 500 个节点滚动发布时每个节点都要干等 40 秒发一次版要好几个小时。而且单纯靠等根本无法应对物理机突然宕机、网络抖动等突发情况因为机器意外宕机时你根本没机会去 sleep。3. 大厂的无损下线方案第一步编排层摘流正规一点的公司下线动作不该由 Jenkins 脚本控制而应该与 K8s 的 Pod 生命周期深度绑定。当 K8s 决定缩容或更新一个 Pod 时它不会立刻发送kill -15而是会先触发一个生命周期前置钩子PreStop Hook。在 K8s 的 yaml 里这么写lifecycle: preStop: exec: command: - /bin/sh - -c - | # 1. 立即向 Nacos 发送下线请求主动摘除自己 curl -X PUT http://nacos-server:8848/nacos/v1/ns/instance?serviceNamemy-serviceip${POD_IP}port8080enabledfalse # 2. 象征性地短暂停顿 3~5 秒给 Nacos 的 UDP 主动推送一点时间 sleep 5这一步把主动下线的逻辑封装在了容器内部只要 Pod 要死临死前第一件事就是去注册中心销户。第二步客户端重试即使有 PreStop依然无法 100% 避免在那几秒钟里有残余的请求顺着网关的旧缓存打过来。当这台机器停止接收新请求时网关打过来的请求会立刻收到操作系统的Connection Refused。注意这个极其关键的细节ConnectException发生在 TCP 三次握手阶段此时 HTTP 业务数据根本还没有发出去既然业务没执行那这个请求就是绝对安全、绝对幂等的。所以我们在网关或上游微服务里必须开启底层的重试机制。以 SpringCloud 为例只要加上spring: cloud: loadbalancer: retry: enabled: true # 开启客户端重试此时发生的奇迹是网关拿着旧 IP 发请求 - 碰壁报错Connection Refused-网关底层的 LoadBalancer 捕获到连接异常默默把它掐掉然后自动从缓存里换另外一个健康的 IP 重新发起请求。整个过程在几毫秒内完成最终用户在手机上看到的就是一个正常的 200 OK用户是无感知的。第三步应用的优雅停机有了前面两步进入这台机器的新请求已经被完美挡住并重试了。最后是处理那些已经接进来了正在工作的老请求。在 Spring Boot 项目的application.yml里加上这两行配置server: shutdown: graceful # 开启优雅停机 spring: lifecycle: timeout-per-shutdown-phase: 30s # 最多等老请求 30 秒配合 K8s 的停机机制Java 进程在等待一段时间后然后彻底释放资源。4. 无损下线全流程为了让你看得更直观我画了这张涵盖了K8s Nacos 客户端重试的联动时序图。总结其实微服务里很多看似复杂的高可用问题最后落到实处拼的都是对细节的把控。 欢迎加入小哈的星球你将获得:专属的项目实战多个项目 / 1v1 提问 /Java 学习路线 /学习打卡 / 每月赠书 / 社群讨论新项目《Spring AI 项目实战》正在更新中..., 基于 Spring AI Spring Boot 3.x JDK 21;《从零手撸仿小红书微服务架构》 已完结基于 Spring Cloud Alibaba Spring Boot 3.x JDK 17..., 点击查看项目介绍演示地址http://116.62.199.48:7070/《从零手撸前后端分离博客项目全栈开发》2期已完结,演示链接http://116.62.199.48/;专栏阅读地址https://www.quanxiaoha.com/column截止目前累计输出 100w 字讲解图 4013 张还在持续爆肝中..后续还会上新更多项目目标是将 Java 领域典型的项目都整一波如秒杀系统, 在线商城, IM 即时通讯Spring Cloud Alibaba 等等戳我加入学习解锁全部项目已有4500小伙伴加入1. 我的私密学习小圈子从0到1手撸企业实战项目~ 2. 如何画出一张优秀的架构图老鸟必备 3. 面试官说说动态线程池实现原理 4. 实战Arthas 定位 接口的超时问题直接起飞最近面试BAT整理一份面试资料《Java面试BATJ通关手册》覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。 获取方式点“在看”关注公众号并回复 Java 领取更多内容陆续奉上。PS因公众号平台更改了推送规则如果不想错过内容记得读完点一下“在看”加个“星标”这样每次新文章推送才会第一时间出现在你的订阅列表里。 点“在看”支持小哈呀谢谢啦
Nacos 点了下线,为什么流量还是打到了停机的机器上?
前两天面试问了一个非常日常的问题❝你们线上发版的时候怎么保证旧服务下线时用户的请求不报错他挺自信地回答这很简单啊去 Nacos 控制台找到那个实例点一下下线按钮。或者在发布脚本里直接kill -15杀进程Nacos 收不到心跳自然就把节点剔除了网关就不会往这台机器发流量了。额在真实的生产环境下如果发布流水线真的只是这么干那每次发版的一两分钟里你们的网关日志里一定会刷出一堆Connection Refused或者Read Timeout的报错。如果刚好赶上晚高峰用户的直观体验就是点了一下按钮界面弹出一个醒目的网络异常。1. 流量为什么停不住很多同学对注册中心有一个误解觉得它是一个绝对实时的系统只要 Nacos 知道某个节点下线了所有依赖的微服务就瞬间都知道了。但在分布式系统里信息的传递是需要时间的。服务调用方比如 Gateway 网关在发起 HTTP 请求前需要知道目标服务的 IP。但为了性能网关不可能每来一个请求都去 Nacos 查一次网络。会用像 Ribbon 或者 SpringCloud LoadBalancer 这样的客户端负载均衡组件会在自己的内存里维护一个ServerList 服务 IP 列表缓存。坑就踩在这个缓存的更新机制上调用方默认会启动一个后台定时任务每隔一段时间去 Nacos 拉取最新的实例列表。虽然 Nacos 服务端也有 UDP 实时推送变更的机制但在复杂的生产网络里UDP 推送是有概率丢失的。这就导致了一个长达几十秒的时间差。在这几十秒里虽然 Nacos 服务端已经把那个节点标记为下线了但网关本地的缓存还没刷新网关手里拿着的依然是旧名单。现在回想一下你在控制台点下线后的操作你调用了下线紧接着就触发了重启进程被杀掉。但此时网关还没更新缓存依然把用户的请求往这台已经死掉的机器上送。操作系统一看端口都没人监听了直接回一个 RST 包网关马上就报Connection Refused了。 欢迎加入小哈的星球你将获得:专属的项目实战多个项目 / 1v1 提问 /Java 学习路线 /学习打卡 / 每月赠书 / 社群讨论新项目《Spring AI 项目实战》正在更新中..., 基于 Spring AI Spring Boot 3.x JDK 21;《从零手撸仿小红书微服务架构》 已完结基于 Spring Cloud Alibaba Spring Boot 3.x JDK 17..., 点击查看项目介绍演示地址http://116.62.199.48:7070/《从零手撸前后端分离博客项目全栈开发》2期已完结,演示链接http://116.62.199.48/;专栏阅读地址https://www.quanxiaoha.com/column截止目前累计输出 100w 字讲解图 4013 张还在持续爆肝中..后续还会上新更多项目目标是将 Java 领域典型的项目都整一波如秒杀系统, 在线商城, IM 即时通讯Spring Cloud Alibaba 等等戳我加入学习解锁全部项目已有4500小伙伴加入2. 中小团队的解法知道了原因解决思路也就有了。很多中小团队的做法是在 CI/CD 发布流水线里加一行sleep 40。流程变成了这样主动摘流,脚本先调 Nacos 的 OpenAPI把机器标记为下线。脚本强行sleep 40秒。在这 40 秒里机器还在正常运行依然在处理请求只是我们在这 40 秒里等待全网所有的网关更新完缓存。优雅停机40 秒一过没人往这发请求了再发送kill -15杀进程。这套方案确实管用。但如果你拿着这个方案去大厂面试依然是不及格的。原因很简单太慢了且不具备普适性。如果一个核心服务有 500 个节点滚动发布时每个节点都要干等 40 秒发一次版要好几个小时。而且单纯靠等根本无法应对物理机突然宕机、网络抖动等突发情况因为机器意外宕机时你根本没机会去 sleep。3. 大厂的无损下线方案第一步编排层摘流正规一点的公司下线动作不该由 Jenkins 脚本控制而应该与 K8s 的 Pod 生命周期深度绑定。当 K8s 决定缩容或更新一个 Pod 时它不会立刻发送kill -15而是会先触发一个生命周期前置钩子PreStop Hook。在 K8s 的 yaml 里这么写lifecycle: preStop: exec: command: - /bin/sh - -c - | # 1. 立即向 Nacos 发送下线请求主动摘除自己 curl -X PUT http://nacos-server:8848/nacos/v1/ns/instance?serviceNamemy-serviceip${POD_IP}port8080enabledfalse # 2. 象征性地短暂停顿 3~5 秒给 Nacos 的 UDP 主动推送一点时间 sleep 5这一步把主动下线的逻辑封装在了容器内部只要 Pod 要死临死前第一件事就是去注册中心销户。第二步客户端重试即使有 PreStop依然无法 100% 避免在那几秒钟里有残余的请求顺着网关的旧缓存打过来。当这台机器停止接收新请求时网关打过来的请求会立刻收到操作系统的Connection Refused。注意这个极其关键的细节ConnectException发生在 TCP 三次握手阶段此时 HTTP 业务数据根本还没有发出去既然业务没执行那这个请求就是绝对安全、绝对幂等的。所以我们在网关或上游微服务里必须开启底层的重试机制。以 SpringCloud 为例只要加上spring: cloud: loadbalancer: retry: enabled: true # 开启客户端重试此时发生的奇迹是网关拿着旧 IP 发请求 - 碰壁报错Connection Refused-网关底层的 LoadBalancer 捕获到连接异常默默把它掐掉然后自动从缓存里换另外一个健康的 IP 重新发起请求。整个过程在几毫秒内完成最终用户在手机上看到的就是一个正常的 200 OK用户是无感知的。第三步应用的优雅停机有了前面两步进入这台机器的新请求已经被完美挡住并重试了。最后是处理那些已经接进来了正在工作的老请求。在 Spring Boot 项目的application.yml里加上这两行配置server: shutdown: graceful # 开启优雅停机 spring: lifecycle: timeout-per-shutdown-phase: 30s # 最多等老请求 30 秒配合 K8s 的停机机制Java 进程在等待一段时间后然后彻底释放资源。4. 无损下线全流程为了让你看得更直观我画了这张涵盖了K8s Nacos 客户端重试的联动时序图。总结其实微服务里很多看似复杂的高可用问题最后落到实处拼的都是对细节的把控。 欢迎加入小哈的星球你将获得:专属的项目实战多个项目 / 1v1 提问 /Java 学习路线 /学习打卡 / 每月赠书 / 社群讨论新项目《Spring AI 项目实战》正在更新中..., 基于 Spring AI Spring Boot 3.x JDK 21;《从零手撸仿小红书微服务架构》 已完结基于 Spring Cloud Alibaba Spring Boot 3.x JDK 17..., 点击查看项目介绍演示地址http://116.62.199.48:7070/《从零手撸前后端分离博客项目全栈开发》2期已完结,演示链接http://116.62.199.48/;专栏阅读地址https://www.quanxiaoha.com/column截止目前累计输出 100w 字讲解图 4013 张还在持续爆肝中..后续还会上新更多项目目标是将 Java 领域典型的项目都整一波如秒杀系统, 在线商城, IM 即时通讯Spring Cloud Alibaba 等等戳我加入学习解锁全部项目已有4500小伙伴加入1. 我的私密学习小圈子从0到1手撸企业实战项目~ 2. 如何画出一张优秀的架构图老鸟必备 3. 面试官说说动态线程池实现原理 4. 实战Arthas 定位 接口的超时问题直接起飞最近面试BAT整理一份面试资料《Java面试BATJ通关手册》覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。 获取方式点“在看”关注公众号并回复 Java 领取更多内容陆续奉上。PS因公众号平台更改了推送规则如果不想错过内容记得读完点一下“在看”加个“星标”这样每次新文章推送才会第一时间出现在你的订阅列表里。 点“在看”支持小哈呀谢谢啦