阿里云MSE无损上下线原理
阿里云 MSE 无损上下线原理
从一条真实请求讲起:DNS · SLB · Nginx/Ingress · iot-fusion-gateway · Nacos/K8s Service · 业务 Pod
基于阿里云官方文档与 FAQ 整理 · 侧重机制细节与调用链路
2026-08-11 · v18 博客发布修订版
1.先把架构和“一条用户请求”讲清楚
1.1 整个系统里都有谁
为了把机制讲清楚,本文不用抽象的‘消费者/提供者代号’,而是采用已经确认的生产调用链。外部用户请求依次经过 DNS、SLB、Nginx/Ingress,固定进入 iot-fusion-gateway;网关按路由 URI 选择 lb:// Nacos 直连或 http:// Kubernetes Service。内部 Pod 调用同样是混合模式:目标以 LB:// 开头时,调用方从 Nacos 获取并缓存实例列表,在本地负载均衡后直连目标 Pod IP;cam-system 没有接入 Nacos,调用方通过 K8s DNS 和 Kubernetes Service 访问。
| 角色 | 本文中的说法 | 职责 |
| 用户(客户端) | 用户 | 真实的使用者(浏览器 / App / 外部系统),发出 HTTP 请求 |
| 入口层(南北向) | DNS / SLB / Nginx / Ingress |
接收集群外部流量,并按既定域名和路径把请求交给 iot-fusion-gateway。它们不从 Nacos 选择业务服务实例, 也不参与 MSE 的注册中心预热、响应打标或调用方摘除 |
| 入口应用网关 | iot-fusion-gateway |
所有外部用户请求的实际入口和转发方。网关自身已接入 MSE 微服务治理;路由上游配置为 lb://服务名时,通过 Nacos 适配器选择实例并直连 Pod IP; 配置为 http://服务名时,通过 K8s DNS 解析并访问同名 Kubernetes Service。它是本环境自研的网关应用,不是 MSE 云原生网关产品 |
| 注册中心 | Nacos | 注册发现控制面:保存并传播已注册服务的实例 IP、端口、元数据和权重。调用方查询或订阅实例列表;Nacos 不转发业务请求 |
| 微服务调用方 | 网关或业务 Pod | 遇到 lb:// / LB:// 目标时,从 Nacos 获得并缓存实例列表,在调用方进程内完成负载均衡,然后直接请求选中的目标 Pod IP |
| K8s Service 调用方 | 网关或业务 Pod | 遇到 http:// 服务名或 cam-system 时,使用 K8s DNS 解析 Service,由 EndpointSlice 和集群数据面选择后端 Pod |
| 微服务(提供者) | 订单服务 / 库存服务 / cam-system | 真正处理业务。接入 Nacos 的服务可同时承接 lb:// / LB:// 直连和 Kubernetes Service 流量;cam-system 当前只走 Kubernetes Service |
| 服务实例 | 实例 1 / 实例 2 / 实例 3 | 订单服务的一个具体运行副本,各有独立 IP。本文以“实例 1”为将要下线、以“实例 4”为将要上线的对象 |
| MSE Agent | 探针 / Agent | 注入到每个应用进程里的小程序,无损上下线就是靠它实现的(延迟注册、响应打标、主动通知、就绪接口) |
产品边界介绍:MSE 云原生网关和 MSE 微服务治理是两个不同产品。云原生网关是独立的数据面转发产品;本环境没有使用它。iot-fusion-gateway 承担实际入口网关和请求转发职责,功能角色与网关产品相近,但它是本环境自研的应用。MSE 微服务治理则通过注入应用进程的 Agent 增强注册、发现、延迟注册、注册状态检查和无损下线。它不替代 iot-fusion-gateway,也不集中转发业务请求。
1.2 谁是谁的“消费者”(以及 Nginx/Ingress 为什么不是)
‘消费者’= 发起后端调用,并负责选择目标地址的一方。在本环境架构中,必须按路由类型区分:
- iot-fusion-gateway 的上游地址配置为 lb://服务名时,网关通过 Nacos 适配器获取实例列表、在本地选择 Pod IP 并发起请求。此时网关就是该服务的 Nacos 消费者,也是 MSE 调用方治理实际生效的位置。
- iot-fusion-gateway 的上游地址配置为 http://服务名时,网关把服务名当作普通域名交给 K8s DNS,最终访问同名 Kubernetes Service。此时网关仍是业务调用方,但它不从 Nacos 实例列表中选 Pod,实际后端选择由 Service、EndpointSlice 和集群数据面完成。
- 普通业务 Pod 调用以 LB:// 开头的目标时,该业务 Pod就是 Nacos 消费者:它从 Nacos 获取并缓存对端实例列表,在本地选择一个 Pod IP 后直接发起业务请求。Nacos只提供名单,不是业务流量中转站。
- cam-system 没有接入 Nacos。调用方使用 cam-system 的 Kubernetes Service 名称,由 K8s DNS、Service、EndpointSlice 和集群数据面选择 Pod。
- Nginx / Ingress 负责把外部请求交给 iot-fusion-gateway,不直接选择后端业务 Pod,因此不是这些业务服务的 Nacos 消费者。
MSE 文档中的‘调用方需接入 MSE’主要针对受支持的微服务调用。本环境的 iot-fusion-gateway 和相关内部业务 Pod 已接入 MSE:响应打标可使兼容调用方临时拉黑待下线节点;主动通知按官方定义使消费者收到通知后不再调用该节点;Nacos 实例变化则继续传播并更新服务发现状态。http:// 路由以及 cam-system 的 K8s Service 路径不从 Nacos 本地列表选择 Pod,主要依赖 readiness、EndpointSlice、连接排空和应用优雅停机。
1.3 本环境中,用户请求实际会经过哪里
准确结论是:所有外部用户请求都先经过 DNS、SLB、Nginx/Ingress,再进入 iot-fusion-gateway。请求不会先到 MSE 微服务治理,因为 MSE 微服务治理不是网关。真正的分支发生在 iot-fusion-gateway 读取路由上游配置之后。
|
① 用户/小程序发出 HTTP 请求 ▼ ② DNS 解析域名,请求依次经过 SLB 和 Nginx/Ingress ▼ ③ Nginx/Ingress 把请求转发给实际入口 iot-fusion-gateway ▼ ④ iot-fusion-gateway 从 Redis 读取路由信息,并检查该路由配置的上游 URI ▼ ⑤ iot-fusion-gateway 根据上游 URI 进入下面两个互斥分支之一 ▼ |
| ⑤A:lb:// 服务名 | ⑤B:http:// 服务名 |
|
网关通过 Nacos 适配器获取并缓存实例列表, 在网关进程内负载均衡,然后直接请求选中的目标 Pod IP |
网关按普通域名发起请求, K8s DNS 解析到同名 Service,再由 EndpointSlice 和集群数据面选择目标 Pod |
⑤A 与⑤B 是由同一条路由配置决定的二选一分支,不是一笔请求先经过 Nacos 直连、再经过 Kubernetes Service。
|
▼ ⑥ 业务 Pod 处理请求;Pod 内的 MSE Agent 提供治理增强,但不转发这笔业务请求 ▼ ⑦ 响应沿原调用链返回 iot-fusion-gateway,再经 Nginx/Ingress、SLB 返回用户 |
lb:// 和 http:// 不是系统自动猜测出来的路径,而是本环境在 iot-fusion-gateway 的每条路由中自行决定的上游配置。需要使用 Nacos 实例发现的服务配置为 lb://服务名;
没有注册到 Nacos、或明确希望走 Kubernetes Service 的服务配置为 http://服务名。两种路径可以在同一个网关中同时存在。
iot-fusion-gateway 自身已经接入 MSE 微服务治理。对于 lb:// 路由,网关是使用 Nacos 实例列表做本地负载均衡的调用方,MSE 可以增强它的实例选址和上下线感知;对于 http:// 路由,后端 Pod 的选择由 Kubernetes Service 路径完成,不读取 Nacos 预热权重,也不依赖网关本地 Nacos 候选列表。
1.4 内部 Pod 调用的两条真实流程
内部调用不经过 iot-fusion-gateway,但也不能统一理解成 Kubernetes Service。根据已确认配置,调用方会按目标地址写法进入两条不同路径。
1.4.1 内部 LB://:Nacos 提供名单,调用方直连目标 Pod
|
① order-service 需要调用以 LB://inventory-service 配置的目标 ▼ ② order-service 的受支持客户端从 Nacos 查询或订阅 inventory-service 实例列表,并缓存在本地 ▼ ③ order-service 在自身进程内进行负载均衡,从本地候选中选择一个 inventory-service Pod IP ▼ ④ 业务请求从 order-service 直接发送到选中的 inventory-service Pod IP;请求不经过 Nacos ▼ ⑤ 下线时,调用方分别处理 Nacos 变化、响应标记和主动通知:前两者可更新实例列表或临时拉黑节点,主动通知按官方定义使消费者停止调用该节点 |
必须把控制面和数据面分开:控制面是‘调用方 ↔ Nacos,获取和更新实例名单’;业务数据面是‘调用方 → 已选中的目标 Pod IP’。不能画成‘调用方 → Nacos → 目标 Pod’,因为 Nacos 不转发业务请求。
1.4.2 cam-system:K8s DNS 与 Kubernetes Service
|
① 业务 Pod 需要调用 cam-system;cam-system 当前没有注册到 Nacos ▼ ② 调用方使用 cam-system 的 Service 名称发起请求,K8s DNS 解析到 ClusterIP ▼ ③ Kubernetes Service 根据 EndpointSlice 和集群数据面规则选择一个可用的 cam-system Pod ▼ ④ 请求到达 cam-system Pod;这条路径不读取 Nacos 实例列表,也不执行 Nacos 调用方本地候选摘除 |
cam-system 不是‘没有上下线保护’,而是保护机制不同:它依赖准确的 readiness、EndpointSlice 和数据面收敛、连接排空、应用 graceful shutdown
以及足够的 terminationGracePeriodSeconds。Kubernetes提供这些基础机制,但仅凭 Service 和滚动更新不能保证绝对零损。
1.5 无损上下线到底在解决什么问题
直接说结论:注册中心状态、调用方本地实例列表和实例真实可用状态不是一个原子事务,三者之间存在传播与处理时间窗口。
- 注册中心侧有变化窗口:实例注册、注销、心跳/连接状态变化和服务端处理都需要时间。
- 调用方侧有缓存窗口:调用方接收变更、刷新本地候选列表和连接状态也需要时间。
- 如果实例已不可用、调用方却仍按旧状态选中它,请求就可能连接失败或超时。
无损上线要解决:新实例还没就绪时,别让 lb:// / LB:// 的 Nacos 调用方或 Kubernetes Service 过早把请求送给它。前者靠延迟注册和调用方本地选址控制,后者靠 readiness 和 EndpointSlice 控制。
无损下线要解决:实例准备退出时,让所有 lb:// / LB:// 的网关与内部调用方,以及 http:// 和 cam-system 使用的 Kubernetes 路由层,都在实例真正停止前停止发送新请求,并给在途请求留出完成时间。
本环境同时存在两类排流机制:外部网关 lb:// 和内部 Pod LB:// 由 MSE/Nacos 调用方选址机制保护;网关 http:// 和 cam-system 由 Kubernetes Service、EndpointSlice、readiness、连接排空和应用优雅停机保护。MSE 注入的 /offline + 30 秒等待为状态传播和在途请求处理提供时间,但不能用 Nacos 列表变化代替 K8s Service 排流,也不能把 K8s Endpoint 更新当成 Nacos 直连调用方的摘除机制。
2.无损上线:让新实例“准备好了再露脸”
2.1 场景设定
假设要给订单服务扩容:新起一个实例 4(新 Pod)。如果没有延迟注册和注册状态门槛,新实例可能在业务异步初始化、缓存、连接池或外部依赖尚未稳定时进入可调用状态,引发报错、超时或 RT 升高。本环境通过延迟注册和 55199/health 注册状态检查降低这一风险;小流量预热属于产品可选能力,但本环境未启用。
2.2 第一步:延迟注册(实例 4 先不注册)
实例 4 里的 MSE Agent 会‘扣住’注册动作:实例 4 启动后先不注册到 Nacos。于是所有基于 Nacos 发现它的调用方,本地列表里暂时都没有实例 4,自然不会通过这条发现链路给它发请求。
- 为什么能扣住:注册时机(Spring 上下文刷新完)通常早于应用真正就绪(比如还要从 OSS 拉几百兆数据、建连接池)。
- 配置项:延迟注册时长 delayTime(默认 0 秒),设 30 秒就是等初始化完再注册。
效果:在设定的延迟时间内,实例 4 对基于 Nacos 发现的调用方不可见。需要注意,delayTime 是配置的等待时长,不会自动判断业务缓存、数据库连接或外部依赖是否已经准备完成,应根据启动观测设置,并与业务 readiness 配合。
2.3 第二步:让 K8s 等到“已经注册成功”再把新 Pod 算作可用
先看它解决的具体问题:第 2.2 步里,实例 4 已经启动,但 MSE Agent 还在延迟它注册到 Nacos。这个时候 Java 进程可能已经能响应普通健康检查,K8s 却不知道‘实例 4 还没有出现在 Nacos 列表里’。如果 K8s 过早把实例 4 判定为 Ready,滚动发布就可能继续减少旧实例。结果是:旧实例正在变少,新实例 4 又暂时不能被基于 Nacos 的实际调用方发现。
解决办法很直接:让 K8s 不只检查‘进程活着没有’,还向实例 4 里的 MSE Agent 询问‘服务注册成功没有’。本环境 Pod 使用 55199/health,并已启用注册状态检查。现行产品中,Agent 4.1.10 及以上推荐 /readiness,4.1.10 以下使用 /health;本文描述本环境时以实际生效的 55199/health 为准。
- 返回 500:Agent 还没有观察到受支持的微服务注册完成。K8s 将实例 4 保持为 NotReady,不把它当成已经准备好的新副本。
- 返回 200:Agent 已观察到注册完成,实例 4 的‘注册门槛’通过,K8s 才可以把它判定为 Ready。
- 本环境已经开启无损上线和注册状态检查,本环境 Pod 探针请求 55199/health。发布时应持续确认该接口返回码与实际 Nacos 注册事件一致。
用 3 个旧实例加 1 个新实例来理解:实例 4 尚未注册时接口返回 500,K8s 继续把它看作未就绪,并按 Deployment 策略保留足够的旧实例;实例 4 注册到 Nacos 后接口变成 200,K8s 才允许滚动发布进入下一阶段。这里是 K8s 在主动询问 Agent,不是网关通过这个接口转发业务请求。
这个 readinessProbe 会产生两类效果:第一,对匹配 Kubernetes Service 的常规流量,NotReady Pod 通常不会作为正常 Endpoint 接流;第二,对 Deployment,它会影响 Ready/Available 统计以及何时继续替换旧 Pod。具体还要和 maxUnavailable、maxSurge、minReadySeconds 一起看。例如不能接受可用副本下降时,通常应将 maxUnavailable 设为 0,并根据实际发布策略验证。
本环境没有启用小流量预热,因此 minReadySeconds 不需要承担‘等待预热完成’的职责,仍应按滚动发布稳定性设置。若以后开启预热,MSE FAQ 建议 minReadySeconds 大于预热时长;同时要记住预热从第一笔未被忽略的外部请求开始,不能只用注册时间推算结束时间。
本环境的 55199/health 回答的是‘MSE 是否观察到受支持的微服务注册完成’,不等于数据库、缓存、消息订阅等业务初始化也全部完成。业务 readiness 仍需覆盖这些依赖,发布门槛应同时满足业务就绪和注册完成。
2.4 产品能力介绍:小流量预热(本环境未启用)
小流量预热是 MSE 的可选无损上线能力,本环境没有启用。本节保留它是为了说明产品机制和未来启用时的影响:实例注册到 Nacos 后,lb:// 路径的调用方本地列表会出现新实例;若立即按正常比例分配流量,冷缓存和 JIT 等因素可能使 RT 升高。预热通过调用方侧逐步提高新实例选址权重来降低这一风险。
当前产品行为:实例注册后并不是立即按注册时刻开始倒计时。当前 MSE FAQ 说明,预热由第一笔未被忽略的外部请求触发;如果一直没有外部流量,预热不会开始。触发后,受支持且已接入 MSE 的调用方根据实例启动时间等信息调整选址权重。
- 预热刚开始:新实例权重处于较低水平。文中的“比如 5%”只能作为帮助理解的示意,不是所有版本、框架和配置都固定从 5% 起步的产品契约。
- 随时间推移:新实例权重逐步提高,收到的流量逐渐增多。当前 FAQ 将权重范围描述为 0%~100%,预热结束后达到 100%。
- 达到配置的预热时长(默认 120 秒):实例恢复正常选址权重;具体曲线、起点和步进应以当前 Agent、框架和控制台观测为准。
权重调整和实例选择发生在使用 Nacos 本地列表的调用方侧。本环境的 iot-fusion-gateway 和内部 LB:// 调用方都已接入 MSE,因此未来开启预热后,这两类 Nacos 发现路径可以执行逐步放量逻辑;http:// 路由和 cam-system 的 Kubernetes Service 路径不读取 Nacos 预热权重。由于首请求才触发预热,也不能仅用‘注册时间 + warmupTime’推算预热结束时间。
本环境配置结论:已启用延迟注册和注册状态检查,未启用小流量预热。本文后续描述当前发布时序时,不把预热作为已经执行的步骤;本节仅作为 MSE 产品能力介绍。
2.5 无损上线完整时序(用户视角)
|
实例 4 启动 → Agent 按 delayTime 扣住注册,Nacos 列表暂时没有实例 4 ▼ 本环境探针 55199/health 返回 500 → K8s 判定实例 4 未就绪 ▼ 在 maxUnavailable=0 或其他已验证的滚动策略下,实例 4 未就绪期间保留足够的旧实例 ▼ 延迟结束且实例 4 注册到 Nacos → 55199/health 返回 200;业务依赖也应满足就绪条件 ▼ 实例 4 变为 Ready:K8s Service 路径可把它作为正常 Endpoint;外部 lb:// 和内部 LB:// 调用方也能从 Nacos 列表看到它 ▼ 当前未启用小流量预热,因此 Nacos 本地负载均衡路径不会执行逐步加权;上线效果应通过注册事件、Ready 状态和各路径 QPS 验证 |
2.6 Kubernetes Service 原生是否支持无损上线
准确结论是:Kubernetes原生提供了平滑上线所需的基础机制,但不能仅凭 Service 和滚动更新就宣称绝对无损。新 Pod 只有在 readinessProbe 成功后,才通常进入 Service 的正常 Ready Endpoint;Deployment 再结合 maxSurge、maxUnavailable 和 minReadySeconds 控制新旧副本替换节奏。
|
新 Pod 启动 → readinessProbe 未通过 → Pod 保持 NotReady,不进入 Service 的正常可用 Endpoint ▼ 业务依赖和探针条件满足 → readinessProbe 成功 → EndpointSlice 出现 Ready Endpoint ▼ Service 数据面更新后开始向新 Pod 分配流量 → Deployment 按滚动策略继续替换旧 Pod |
- K8s 能阻止 NotReady Pod 承接常规 Service 流量,但前提是 readiness 准确代表‘业务已经可服务’,不能只检查进程或端口活着。
- 本环境的 55199/health 主要表示 MSE 已观察到受支持的注册动作完成,不代表数据库、缓存、消息订阅和外部依赖都已完成,因此业务 readiness 仍需覆盖业务依赖。
- 如果 maxUnavailable 允许可用副本下降,或者 readiness 过早成功,发布期间仍可能出现容量不足或错误;K8s 提供机制,最终效果取决于探针和滚动策略配置。
对 cam-system 这类纯 Kubernetes Service 路径,上线是否平滑主要看业务 readiness、EndpointSlice 生效速度和 Deployment 策略;它不使用 MSE 的 Nacos 延迟注册、Nacos 权重预热或调用方本地实例列表。
3.无损下线:让调用方在实例停止前停止选择它
3.1 场景设定:滚动发布下掉实例 1
回到本文的例子。假设订单服务要做滚动发布,旧实例 1 要被新实例 4 替换下线。此刻的现状是:
- 基于 Nacos 的调用方本地缓存列表:实例 1、实例 2、实例 3(实例 1 还在里面,因为它确实还在运行)。
- Nacos 列表:实例 1、实例 2、实例 3(实例 1 还未被剔除)。
现在的问题是:如果不做任何处理,直接把实例 1 杀掉,会发生什么?
3.2 不做无损下线会发生什么
直接把实例 1 停掉:
- 实例 1 进程退出 → 端口关闭。
- Nacos 与客户端需要通过心跳、连接状态或注销事件感知实例变化;具体机制和时延取决于注册类型、Nacos/客户端版本及网络状态。
- 调用方收到变化后还要更新本地候选列表和相关连接状态。
- 在状态尚未收敛的窗口内,调用方可能仍按旧列表选中实例 1 → 请求发到已停止的进程 → 连接失败或超时。
- 同时,已经到达实例 1 的在途请求也可能随进程退出而中断。
这就是流量风险。无损下线的目标不是宣称分布式传播延迟消失,而是让调用方更早停止选择实例,并给在途请求和各层状态收敛留出可验证的时间。
3.3 机制一(默认):响应打标——请求的“回执”里夹带下线消息
这是 MSE 默认启用的机制。在本环境架构中,它适用于外部 iot-fusion-gateway 的 lb:// 路由和内部 Pod 的 LB:// 调用:提供方实例 1 在‘还没停止’的等待阶段,通过正常业务响应携带下线标记,使已接入且兼容的实际调用方尽快刷新并停止选择该实例。http:// 网关路由和 cam-system 的 Kubernetes Service 调用不依赖这套响应标记来选择 Pod。
完整过程如下(注意:此时实例 1 仍在运行):
|
第 1 步:实例 1 的 /offline 被调用,进入预下线等待阶段;Agent 同时启动下线协同流程 ▼ 第 2 步:尚未感知变化的网关 lb:// 或内部 LB:// 调用方,仍可能按本地旧列表把请求发送给实例 1 ▼ 第 3 步:实例 1 处理请求,并在响应中携带 MSE 下线标记 ▼ 第 4 步:调用方进程内已接入且兼容的 Agent/客户端识别标记,立即拉黑实例 1,并可主动拉取注册中心最新列表 ▼ 第 5 步:该网关或业务 Pod 将实例 1 从本地候选中移除或拉黑,后续本地负载均衡不再选择它 ▼ 第 6 步:等待窗口给调用方收敛和在途请求处理留出时间,之后容器进入停止阶段 |
所以‘调用方感知后不再转发新请求’不是 Nacos 直接控制每一笔请求,而是每个网关或业务 Pod 内的 Agent/客户端改变自己的 lb:// / LB:// 本地选址状态:识别响应标记后先把实例 1 移出候选或拉黑,并可主动拉取注册中心最新列表。每个调用方实例各自维护本地候选,是否全部收敛必须通过选址日志和 QPS 观测验证。
这个机制的前提:必须有请求在实例 1 的等待阶段到达它,调用方才有机会从响应中识别标记;调用方本身还必须成功接入 MSE 并处于支持范围内。
3.3.1 响应打标的盲区
响应打标需要等待窗口内仍有请求命中实例 1,调用方才有机会从响应中看到标记。低流量路径中,某个网关 Pod 或内部调用方 Pod 可能一次都没有命中该实例。本环境已经开启主动通知,因此提供者还会主动向受支持的消费者发起额外网络请求,缩小只依赖业务响应造成的感知盲区。
主动通知仍要求提供者和消费者都接入受支持的 MSE 微服务治理,并受网络可达性、框架版本和通知处理结果影响。因此,本环境仍同时依靠 Nacos 状态传播、响应打标、主动通知、约 30 秒等待和最终观测,而不是只依赖其中一个信号。
3.4 机制二(当前已开启):MSE 主动通知调用方
先说结论:主动通知不是‘通知 Nacos 刷新状态’。阿里云官方 FAQ 明确说明:在 MSE 增强下,Spring Cloud 服务提供者下线时,提供者会主动向服务消费者发起一次网络请求,通知消费者该节点已经下线;消费者收到通知后不再调用该提供者节点。提供方向 Nacos 注销或改变注册状态,是同时进行的另一条注册中心链路。
本环境已通过 mse.lossless.notice=true 开启主动通知。它属于 MSE 微服务治理/应用治理,不是 Nacos 自带功能,也不需要业务代码自行实现这笔通知请求。
3.4.1 官方确认了什么,哪些内部细节没有公开
|
前提:Spring Cloud 服务提供者和服务消费者都接入受支持的 MSE 微服务治理 ▼ 提供者进入下线阶段后,MSE 主动通知能力使提供者向消费者发起一笔额外的网络请求 ▼ 这笔请求告诉消费者:指定的提供者节点已经下线,不要再调用它 ▼ 消费者收到通知后,停止把后续请求发送到该提供者节点 |
以上流程有阿里云官方 FAQ 直接支持。它能够说明‘谁通知谁’和‘收到通知后做什么’,但不能反推出 MSE 内部怎样获得每个消费者实例的地址。Nacos负责保存服务实例列表,不提供‘哪些消费者调用过这个提供者’的查询结果。
阿里云公开文档没有完整披露主动通知目标的采集、存储和路由算法。因此,本文不再把‘根据此前识别的调用关系找到调用方’写成已确认事实,也不猜测它只依赖请求源 IP、
调用链 Header、MSE 服务拓扑或其中某一种实现。可以确认的产品边界是:提供者和消费者都要接入受支持的 MSE 治理,通知由 MSE 无损下线能力完成。
3.4.2 主动通知的完整下线流程
|
第 1 步:K8s preStop 调用 inventory-service 实例 1 的 127.0.0.1:54199/offline ▼ 第 2 步:实例 1 进入预下线状态,同时启动两条可以交叠执行的独立链路 ▼ |
| 注册中心链路(独立推进) | 主动通知链路(独立推进) |
| 实例 1 从 Nacos 注销或推动注册状态变化;Nacos 开始传播新的实例列表 |
MSE 在产品内部确定通知目标(算法未公开); 实例 1 主动向受支持的消费者发起额外网络请求;消费者收到后停止调用实例 1 |
上面两列是并列分支,不表示 Nacos 注销一定先于主动通知,也不表示主动通知一定先于 Nacos 注销。阿里云公开文档没有规定二者的严格先后顺序。
|
▼ 第 3 步:Nacos 列表传播、消费者停止调用和 /offline 等待可以同时推进,为状态收敛和在途请求留出时间 ▼ 第 4 步:等待阶段结束后,实例 1 再收到停止信号并执行应用优雅退出 |
主动通知的固定定义是‘提供者主动向消费者发起网络请求,使消费者停止调用该节点’,不是‘让消费者通知 Nacos 刷新’。
不同 Agent 版本是否还会触发注册中心列表拉取属于实现细节,本文不把它写成主动通知的固定步骤。
3.4.3 三条下线信号不要混在一起
| 机制 | 信号发给谁 | 具体作用 |
| Nacos 注销/状态变化 | 提供方 → Nacos | 告诉注册中心该实例下线;Nacos 正常传播新的实例列表 |
| 响应打标 | 提供方的正常业务响应 → 当前调用方 | 响应携带特殊标记;兼容调用方识别后停止选择该节点,并可能刷新注册中心列表 |
| MSE 主动通知 | MSE 增强的提供者 → MSE 增强的消费者 | 不等待下一笔业务请求命中,提供者主动发起额外网络请求,消费者收到后停止调用该节点 |
三条链路可以同时发生:Nacos 注销负责注册中心状态传播;响应打标借助仍然到达的业务响应;主动通知则由提供者额外发起网络请求,不必等待消费者下一次碰巧调用待下线节点。三者共同缩短调用方继续选择旧实例的时间窗口,但含义和传输路径不同。
3.5 两条机制对比
| 对比项 | 响应打标(默认开启) | 主动通知(可选能力,当前已开启) |
| 信号怎么传 | 实例 1 在响应里加标记,实际调用方从响应中发现 | 实例 1 主动发起额外网络请求通知受支持的消费者 |
| 需要请求碰巧到达实例 1 吗 | 需要(否则感知不到) | 不需要 |
| 适合什么流量 | 流量密集、高频调用 | 流量稀疏、Spring Cloud 应用 |
| 额外开销 | 低:复用业务响应携带标记;调用方识别后可能主动刷新一次注册中心实例列表 | 需发送额外的通知请求,并处理可达性和结果 |
| 默认状态 | 开启 | 关闭 |
3.6 为什么不能把无损只寄托于 Nacos 列表变更
/offline 触发预下线后,MSE 会在等待阶段内向注册中心通知实例下线或状态变化;这一步发生在容器收到停止信号之前,是注册中心调用路径中的关键动作。但注册中心状态变化仍不能单独当作“已经无损”的保证:
- Nacos 接收实例变化并向调用方传播需要时间,具体时延取决于 Nacos 服务端、客户端版本和网络状态。
- 调用方收到变化后还要更新本地实例列表;响应标记场景下,兼容调用方还可能主动刷新一次注册中心列表。连接池、重试和已有连接也有自己的状态。
- 即使新请求已经不再选择该实例,已经到达实例的在途请求仍需由应用优雅处理。
MSE 的注册中心状态变化、响应打标和当前已开启的主动通知共同用于缩短调用方停止选择实例的窗口,preStop 中的等待用于给调用方收敛和在途请求处理留出时间。停止信号到达后,应用 shutdown hook 或框架还可能再次执行反注册;实际顺序和重复操作的幂等性应按所用框架、客户端版本和日志验证。
3.7 MSE 何时、怎样向 K8s Pod 注入无损下线能力
MSE 的注入不是应用启动后再动态修改容器,而是在 Kubernetes 创建新 Pod 时完成。Deployment/ReplicaSet 发起 Pod 创建请求后,请求进入 API Server;当该 Pod 符合已启用的 MSE 服务治理接入规则时,MSE/ack-onepilot 的 mutating admission webhook 在 admission 阶段修改 Pod Spec,然后修改后的 Pod 才被持久化、调度并启动。
- 注入时机:发生在新 Pod 创建、容器启动之前。之后才会看到 Agent 参数、挂载、lifecycle.preStop 或 gracefulshutdown sidecar 等运行时配置。
- 已存在的旧 Pod:不会因为之后开启 MSE 治理就被原地补上 hook。通常需要通过重新发布、滚动重建或其他 Pod 重建动作,让新的 Pod 创建请求再次经过 admission。
- 业务容器没有自定义 preStop:MSE 可直接向业务容器注入 lifecycle.preStop。该 hook 调用 127.0.0.1:54199/offline,并等待约 30 秒。Kubernetes 保证的是同一业务容器内:preStop 完成后才向该容器发送停止信号(通常为 SIGTERM)。
- 业务容器已经有自定义 preStop:MSE 不覆盖业务钩子,而是增加名为 gracefulshutdown 的 sidecar,并把 MSE hook 放入该 sidecar。sidecar 与业务容器共享 Pod 网络,因此可访问业务 Pod 内的 127.0.0.1:54199。
- 重要边界:普通多容器 Pod 的各容器终止请求是异步的,Kubernetes 不保证普通 sidecar 的 preStop 一定先于业务容器停止信号完成。MSE sidecar 的 hook 不能被描述成天然的 Pod 级跨容器屏障。业务自定义 preStop 应按官方要求预留至少约 30 秒,并通过终止日志和压测验证 MSE 下线流程确实先获得足够时间。
- terminationGracePeriodSeconds 是整个 Pod 终止流程的共同预算,从终止流程开始计时,包含 preStop 和应用 shutdown。MSE 官方建议设置为 90 秒,避免约 30 秒的 hook 耗尽默认 30 秒宽限期。停止信号通常为 SIGTERM,但镜像 STOPSIGNAL 或受支持的 lifecycle.stopSignal 可以改变实际信号。
注入是否真正发生,必须查看 admission 后的本环境 Pod,而不能只看 Git/云效里的源 Deployment YAML。该方案只覆盖会执行正常终止流程的滚动升级、缩容和重启;OOMKill、节点断电、强制删除、网络分区等异常场景不能靠 preStop 保证无损。
3.8 源 YAML、本环境 Pod、K8s Service 与 Nacos 不是二选一
判断无损下线不能只看源 app.yaml,也不能看到一个 ClusterIP Service 就断定“服务只走 K8s Service、不需要 MSE”。同一个 Pod 可以同时接收两条不同来源的流量:
| 流量路径 | 谁做实例选择 | Pod 下线时的保护机制 |
| 外部网关 lb://:用户请求到 iot-fusion-gateway 后直连业务 Pod |
网关从 Nacos 获取并缓存实例列表,在网关进程内负载均衡后直接请求选中的 Pod IP; Nacos不转发业务请求 |
MSE /offline、Nacos 状态传播和响应打标共同推动列表更新或临时拉黑; 主动通知按官方定义使消费者停止调用待下线节点;约 30 秒等待和应用优雅停机处理在途请求 |
| 外部网关 http://:iot-fusion-gateway → K8s Service → Pod | K8s DNS、Service、EndpointSlice 与 kube-proxy/eBPF 等集群数据面 |
readiness、EndpointSlice 状态传播、连接排空、preStop 等待和应用优雅停机; Nacos 本地列表不负责选择 Pod |
| 内部 Pod LB://:服务 A → 目标服务 Pod IP |
服务 A 从 Nacos 获取并缓存服务 B 的实例列表, 在自身进程内负载均衡后直连目标 Pod IP |
与网关 lb:// 相同:Nacos 变化更新实例列表,响应标记可临时拉黑节点, 主动通知按官方定义使消费者停止调用待下线节点; 业务请求本身不经过 Nacos |
| 内部 cam-system:服务 A → cam-system Service → Pod | cam-system 未接入 Nacos;K8s DNS、Service、EndpointSlice 和集群数据面选择 Pod |
依赖 K8s readiness、EndpointSlice/数据面收敛、连接排空、preStop 与应用 graceful shutdown; 不使用 Nacos 调用方摘除 |
| 同一已注册提供方同时承接 Nacos 直连和 Service 流量 |
lb:// / LB:// 可直连 Pod;http:// 或 Service 名称可经 Kubernetes Service 到达同一组 Pod |
必须同时处理 MSE/Nacos 调用方摘除和 Kubernetes Endpoint/连接排空,不能只保护其中一层 |
本环境不能按‘外部走 Nacos、内部走 Service’简单二分。外部由 iot-fusion-gateway 的 lb:// 或 http:// 路由决定;内部由目标是否以 LB:// 配置决定,LB:// 使用 Nacos 本地列表并直连 Pod,cam-system 则固定走 Kubernetes Service。
MSE 微服务治理不是隐藏在调用方与提供方之间的转发代理,它在实际应用进程中增强注册发现和上下线协同。
K8s 的 EndpointSlice 更新、kube-proxy/Ingress/负载均衡规则刷新都是分布式异步过程;EndpointSlice 状态也不会自动关闭已有连接。通常终止中的 endpoint 为 terminating=true、ready=false,并通过 serving 表达其是否仍能服务;publishNotReadyAddresses=true 会改变 ready 语义。Kubernetes 文档还说明,当所有可用 endpoint 都在终止时,代理可能为保证连通性继续选择 serving=true 且 terminating=true 的 endpoint。因此“Pod 进入 Terminating = 所有新请求立即归零”不是可保证结论,必须结合 readiness、连接排空、应用优雅停机和实际数据面观测。
3.8.1 本环境配置给出的准确结论
源 app.yaml 里确实没有 lifecycle.preStop,但本环境 Pod 出现了下面两类配置(敏感值已省略):
ONE_AGENT_PROPERTY='-Dmse.enable=true ... -Dprofiler.micro.service.mse.version=pro' lifecycle.preStop: wget http://127.0.0.1:54199/offline; sleep 30; exit 0
这两行能够确定三件事:
- 应用已经启用 MSE Java Agent(-Dmse.enable=true)。
- 本环境 Pod 中,MSE 无损下线钩子已经被自动注入业务容器;它不是在源 app.yaml 中手动配置的。
- 本环境 Pod 的正常停止流程会先在同一业务容器内调用 MSE Agent 的 /offline,再等待 30 秒,然后该容器才接收停止信号(通常为 SIGTERM)并退出。这个结论来自本环境 Pod 的直接 hook 形态,不能外推到所有采用 gracefulshutdown sidecar 的 Pod。
同时,源 app.yaml 存在 ClusterIP Service 并不代表所有调用都会经过它。已确认的内部 LB:// 和网关 lb:// 调用通过 Nacos 发现并直连 Pod;网关 http:// 路由和 cam-system 则通过 Kubernetes Service。同一个已注册 Pod 还可以同时承接 Nacos 直连和 Service 转发两类流量,必须按调用方实际配置判断。
本环境配置已经确认:readinessProbe 请求 55199/health;无损上线启用了延迟注册和注册状态检查,未启用小流量预热;无损下线已经启用响应打标和主动通知。
正常下线同时依靠 Nacos 状态传播、MSE 主动通知与调用方本地摘除、/offline 等待,以及 Kubernetes Service 路径的 EndpointSlice 和连接排空。
3.8.2 为什么源 YAML 看不到,本环境 Pod 却有
|
① 源 Deployment YAML 提交到 API Server:业务容器模板中没有 lifecycle.preStop ▼ ② Deployment/ReplicaSet 创建新 Pod;Pod 创建请求进入 admission 链路 ▼ ③ 符合 MSE 接入规则时,MSE/ack-onepilot mutating webhook 在容器启动前修改 Pod Spec ▼ ④ 无自定义 hook 时向业务容器注入 preStop;已有自定义 hook 时改为增加 gracefulshutdown sidecar ▼ ⑤ admission 后的 Pod 对象被持久化并启动,因此源模板、ReplicaSet 模板与本环境 Pod 可能不同 ▼ ⑥ 已运行旧 Pod 不会事后动态改变;开启治理后通常需重建 Pod,判断结果必须查看本环境 Pod |
3.8.3 正确的检查方法
- 查看本环境 Pod(最权威):kubectl get pod <pod-name> -o yaml,搜索 ONE_AGENT_PROPERTY、mse.enable、AliyunJavaAgent/ArmsAgent、lifecycle.preStop、gracefulshutdown。
- 查看宽限期:kubectl get pod <pod-name> -o jsonpath='{.spec.terminationGracePeriodSeconds}'。如果仍是 30,而 preStop 本身 sleep 30,应按 MSE 官方建议评估调到 90。
- 查看 Nacos:同时核对 iot-fusion-gateway lb:// 路由和内部 LB:// 调用所使用的服务实例、IP 与下线变化;不要把 Nacos 实例列表用于解释网关 http:// 或 cam-system 的 Kubernetes Service 选址。
- 查看应用优雅停机:先确认 Spring Boot 版本。Spring Boot 3.4+ 默认启用 graceful shutdown,除非显式设置 server.shutdown=immediate;3.3 及更早版本通常需要显式 server.shutdown=graceful。再核对 spring.lifecycle.timeout-per-shutdown-phase 和实际关闭日志。该 timeout 是每个 SmartLifecycle phase 的等待上限,不是整个进程唯一的总上限。
- 查看 MSE 控制台:当前延迟注册、注册状态检查和主动通知已启用,小流量预热未启用;发布观测重点检查注册事件、55199/health、响应打标、主动通知结果、下线事件和各路径 QPS 归零曲线。
3.9 本环境 Pod 的完整无损下线时序
下面结合本环境的业务容器内直接注入的 /offline + sleep 30、MSE 现行产品机制和 Kubernetes 终止语义给出正常时序。K8s Endpoint 更新与 Kubelet 执行 preStop 属于不同控制链路,不能假设某一条会在另一条之前瞬间完成。若其他工作负载采用 gracefulshutdown sidecar,则不能照搬这里“同一业务容器内 preStop 先于停止信号”的顺序保证,应按 3.7 的多容器边界验证。
|
① 滚动发布/缩容触发 Pod 删除:Pod 进入终止流程;K8s 控制面开始更新 EndpointSlice/Endpoints ▼ ② Kubelet 执行当前业务容器内的 MSE preStop:调用 127.0.0.1:54199/offline ▼ ③ 在停止信号到达前的等待阶段,MSE 推动 Nacos 实例状态变化、开始响应打标;已开启的主动通知还会使提供者向受支持的消费者发起额外网络请求 ▼ ④ iot-fusion-gateway 的 lb:// 路由和内部 Pod 的 LB:// 调用方分别处理三类信号:Nacos 变化用于更新实例列表;响应标记可临时拉黑节点;主动通知按官方定义使消费者停止调用实例 1 ▼ ⑤ 同时,网关 http:// 和 cam-system 的 K8s Service 路径传播 ready/serving/terminating 等 EndpointSlice 状态,逐步停止给实例 1 分配新连接 ▼ ⑥ preStop 继续完成约 30 秒等待,给调用方收敛、路由传播和在途请求处理留出时间 ▼ ⑦ 当前业务容器的 preStop 结束后收到停止信号(通常为 SIGTERM);应用 shutdown hook/框架执行优雅停机,并可能再次反注册 ▼ ⑧ 超过 terminationGracePeriodSeconds 仍未退出则会被强制终止;重复反注册的幂等性和实际顺序按客户端与日志验证 |
3.10 内部 LB:// 服务发版时的无损上下线
以 order-service 通过 LB://inventory-service 调用 inventory-service 为例。调用不经过入口网关:order-service 从 Nacos 获取并缓存 inventory-service 实例列表,在自身进程内负载均衡后,直接请求选中的 inventory-service Pod IP。Nacos只参与注册发现控制面,不承载这笔业务请求。
3.10.1 inventory-service 实例 1 无损下线
|
① K8s 滚动发布删除 inventory-service 实例 1,Kubelet 执行 MSE 注入的 preStop 并调用 /offline ▼ ② 提供方 MSE Agent 进入预下线阶段,推动 Nacos 实例状态变化,并开始在正常业务响应中携带下线标记 ▼ ③ 已开启的 MSE 主动通知使 inventory-service 实例 1 主动向受支持的 order-service 消费者发起额外网络请求;这一步是 MSE 功能,不是 Nacos 功能 ▼ ④ order-service 分别处理三类信号:Nacos 变化用于更新实例列表;响应标记可临时拉黑实例 1;主动通知按官方定义使 order-service 停止调用实例 1 ▼ ⑤ order-service 后续本地负载均衡不再选择实例 1,直接改选其他 inventory-service Pod IP ▼ ⑥ preStop 继续等待约 30 秒,为各调用方收敛和在途请求完成留出时间;随后应用收到停止信号并优雅退出 |
主动通知用于加快调用方感知,但不会替代 Nacos 列表传播、响应打标、应用 graceful shutdown 和最终观测。
每个 order-service Pod 都有自己的本地列表,必须确认各调用方实例均已停止选择下线 Pod。
3.10.2 inventory-service 新实例 4 无损上线
|
① inventory-service 实例 4 启动,MSE Agent 按本环境配置延迟向 Nacos 注册 ▼ ② 注册尚未完成时 55199/health 返回 500,Pod 保持 NotReady,Deployment 不把它当作可用新副本 ▼ ③ 延迟结束并成功注册 Nacos,55199/health 返回 200;业务 readiness 也通过后,Pod 变为 Ready ▼ ④ order-service 的 Nacos 订阅/刷新得到实例 4,并把它加入 LB://inventory-service 的本地候选列表 ▼ ⑤ 当前未启用小流量预热,因此实例 4 不执行 MSE 逐步加权;需通过调用方选址日志和 QPS 验证上线状态 |
3.11 cam-system 走 K8s Service:原生是否支持无损下线
准确结论是:Kubernetes原生提供平滑下线的基础机制,但不保证只要使用 Service 和滚动更新就一定零错误。本环境的 cam-system 没有接入 Nacos,调用方经 K8s DNS、cam-system Service 和 EndpointSlice 到达 Pod,不按 Nacos 本地实例列表选择具体 Pod。因此,本文不把 MSE/Nacos 调用方列表摘除、响应打标或主动通知计入这条路径已经确认的保护机制。
删除 cam-system Pod 时,Kubernetes 控制面更新 EndpointSlice;终止中的 endpoint 通常会呈现 terminating=true、ready=false,并通过 serving 表达是否仍能服务。与此同时,Kubelet 执行 preStop 并向容器发送停止信号。这两条控制链路并非一个不可分割的原子操作。
3.11.1 cam-system 正常下线时序
|
① Deployment 滚动删除 cam-system 旧 Pod,Pod 进入 Terminating;EndpointSlice 开始更新 ▼ ② 各节点的 kube-proxy、IPVS、eBPF 或其他集群数据面逐步接收新 Endpoint 状态,并停止为新 Service 连接选择旧 Pod ▼ ③ 同期执行的 preStop/drain 让应用停止接受新工作,并为路由传播和在途请求处理预留时间 ▼ ④ 已建立的 HTTP keep-alive、HTTP/2、gRPC、WebSocket 等连接不会因为 EndpointSlice 更新自动迁移或消失,需要协议和应用层排空 ▼ ⑤ 应用收到 SIGTERM 后执行 graceful shutdown,等待正在处理的请求完成,再关闭端口和资源 ▼ ⑥ 整个流程必须在 terminationGracePeriodSeconds 内完成,否则 K8s 会强制终止容器 |
3.11.2 为什么 K8s 不能单独保证绝对无损
- EndpointSlice、节点数据面和连接状态的传播是异步的,Pod 进入 Terminating 不等于所有节点在同一瞬间停止选择它。
- Endpoint 移除主要影响后续选址,不会自动关闭或迁移已建立的长连接,也不会自动等待应用中的在途任务完成。
- 如果应用收到 SIGTERM 后立即退出,或者总宽限期不足,仍在执行的请求可能被重置、超时或返回 5xx。
- OOMKill、强制删除、节点故障、断电和网络分区不执行完整的正常优雅终止流程,不能依赖 preStop 保证。
因此,K8s Service 是 cam-system 平滑上下线的基础,不是零损承诺。要尽量接近无损,必须同时具备准确 readiness、合理 maxUnavailable/maxSurge、下线 drain、协议级连接排空、应用 graceful shutdown、足够宽限期,以及调用方重试和业务幂等。
3.11.3 四类实际路径分别由什么机制保护
| 实际路径 | 实例选择方式 | 上线/下线保护重点 |
| 网关 lb:// 路由 | iot-fusion-gateway 从 Nacos 获取实例,在网关进程内选址并直连 Pod IP | 延迟注册、55199/health、Nacos 变化、响应打标、已开启的主动通知、/offline 等待;当前未启用预热 |
| 网关 http:// 路由 | K8s DNS → Kubernetes Service → EndpointSlice → Pod | 业务 readiness、EndpointSlice/数据面收敛、连接排空、preStop 和应用 graceful shutdown |
| 内部 Pod LB:// | 调用方从 Nacos 获取对端实例,在业务 Pod 进程内选址并直连目标 Pod IP |
与网关 lb:// 相同;响应标记可使兼容调用方临时拉黑节点, 主动通知按官方定义使消费者停止调用待下线节点,Nacos 列表传播负责最终状态收敛 |
| 内部 cam-system | 调用方 → K8s DNS → cam-system Service → EndpointSlice → Pod | 完全按 K8s Service 路径排流,不使用 Nacos 本地列表摘除;重点验证 readiness、长连接、优雅停机和宽限期 |
3.12 长连接场景:连接最终会断,怎样实现业务级无损
普通 HTTP 请求通常在几百毫秒或几秒内完成。Pod 进入下线阶段后,只要及时停止接收新请求,并给正在处理的请求留出足够时间,大多数在途请求都可以在 Pod 停止前完成。
但 WebSocket、gRPC Stream、HTTP/2 长时间流式请求、长轮询和自定义 TCP 长连接可能持续数分钟、数小时,甚至更久。滚动发布不可能无限等待这些连接自然结束,因此长连接场景通常无法保证‘连接始终不断’。
准确结论:长连接服务很难保证连接层绝对无损,但可以通过连接排空、客户端自动重连、会话恢复、消息确认、补偿和幂等机制,尽量做到业务数据不永久丢失。
本节讨论的是正常滚动发布、缩容和重启。OOMKill、强制删除、节点故障、断电和网络分区不执行完整的优雅终止流程,不能依赖 preStop 或宽限期保证。
3.12.1 为什么已有长连接不能自动迁移
无论调用方通过 Nacos 直连 Pod,还是通过 Kubernetes Service 访问 Pod,负载均衡通常发生在建立连接或发起新调用时。连接一旦建立,后续数据会继续沿原来的 TCP 连接发送到原 Pod。
|
客户端或调用方 → Service / Nacos 服务发现 → 选择 Pod 1 → 与 Pod 1 建立长连接 ▼ 连接建立后:客户端 ═════ 已建立的连接 ═════ Pod 1 |
此时,即使 Pod 1 已从 Nacos 注销、在 EndpointSlice 中进入终止状态、触发 MSE 响应打标或主动通知,这些变化也主要用于阻止后续新调用继续选择 Pod 1,不能把已建立的 TCP、WebSocket 或 gRPC 连接自动迁移到 Pod 2。
已建立连接与客户端地址、服务端 Pod IP、端口以及两个进程中的 Socket 状态绑定。Pod 2 没有 Pod 1 内存中的连接状态,也不能直接接管 Pod 1 的现有连接。正常过程是旧连接结束后,由客户端重新建立新连接。
|
旧连接结束或被服务端正常关闭 ▼ 客户端重新执行 DNS / Service 访问或服务发现 ▼ 负载均衡重新选择 Pod 2 ▼ 客户端与 Pod 2 建立一条新连接 |
3.12.2 MSE、Nacos 和 Kubernetes 能解决什么
MSE、Nacos 和 Kubernetes 在长连接下线中仍然有价值,但它们主要负责减少新流量继续进入待下线实例,并为已有连接和在途请求留出排空时间。
| 机制 | 能够完成的动作 | 不能自动完成的动作 |
| Nacos 注销/列表传播 | 使基于 Nacos 实例列表的调用方逐步不再为新调用选择待下线实例 | 不能迁移已经建立的连接 |
| MSE 响应打标 |
待下线提供者在正常响应中加入特殊标记;兼容调用方识别后可临时拉黑节点, 并可能刷新注册中心列表 |
不能接管或恢复现有 WebSocket、gRPC Stream 和 TCP 连接 |
| MSE 主动通知 |
MSE 增强的提供者主动向受支持的消费者发起额外网络请求; 消费者收到后不再调用该节点 |
不能自动迁移连接、恢复业务会话或补发断线消息 |
| Kubernetes EndpointSlice | 使 Service 数据面逐步停止为新连接选择终止中的 Pod | 不会自动关闭、迁移或恢复已经建立的连接 |
| preStop 与终止宽限期 | 为注册发现传播、连接排空、在途请求和应用优雅退出预留时间 | 不能保证持续数小时或数天的连接自然结束 |
MSE 主动通知与 Nacos 注销是不同链路。阿里云公开文档没有规定两者的严格先后顺序,也没有披露主动通知目标的采集、存储和路由算法。
3.12.3 90 秒不是连接一定能正常结束的保证
MSE 官方建议将 Kubernetes 的 terminationGracePeriodSeconds 设置为 90 秒,但 90 秒不是固定值,也不是长连接永远不会被强制断开的保证。
terminationGracePeriodSeconds: 90
这 90 秒是整个 Pod 终止流程共享的总预算,通常需要同时覆盖:
- MSE preStop 调用 /offline 及其等待阶段;
- Nacos、MSE 调用方和 Kubernetes 路由状态传播;
- 已有连接排空和在途请求处理;
- 应用 graceful shutdown、线程池关闭和资源释放。
|
Pod 开始终止:终止宽限期开始计时 ▼ preStop 执行 /offline,并完成配置的等待阶段 ▼ 应用使用剩余预算排空连接并执行 graceful shutdown ▼ 总宽限期到期仍未退出 → Kubernetes 强制终止容器,剩余连接立即断开 |
preStop 执行和等待也会占用这一总预算,因此应用后续用于关闭连接和退出的时间不是额外的 90 秒,而是共享预算中的剩余时间。
也不能仅靠无限增加宽限期解决问题。长连接可能持续几小时甚至几天;如果必须等所有连接自然结束,旧 Pod 可能长期无法退出,滚动发布也无法完成。
3.12.4 长连接场景中的‘无损’应该怎样定义
连接层无损:同一条连接从建立到业务结束始终保持,不发生任何中断。旧 Pod 最终必须退出时,这个目标通常无法绝对保证。
业务层无损:允许连接在发布期间短暂断开,但客户端能够重连,会话和订阅能够恢复,断线数据可以补偿,重复消息不会产生重复业务副作用。
- 客户端能够自动重连并重新鉴权;
- 订阅、会话和消费位置可以恢复;
- 未确认消息能够重试,断线期间的数据能够补拉;
- 消息具有唯一标识,消费端可以去重;
- 扣款、下单、设备控制等业务操作具备幂等能力;
- 关键会话状态不只保存在待下线 Pod 的本地内存中。
长连接场景更现实的目标是:连接允许短暂中断,但业务消息不能永久丢失,会话能够恢复,重复请求不会造成错误业务结果。
3.12.5 服务端怎样排空长连接
长连接服务收到下线信号后,不应立即退出,而应进入明确的 draining 状态。
|
第 1 步:Pod 进入预下线,从服务发现和新连接入口中逐步摘除 ▼ 第 2 步:停止接受新的长连接、新流或新的长时间任务 ▼ 第 3 步:通知已有客户端当前连接即将关闭 ▼ 第 4 步:等待正在处理的消息完成,并保存必要的会话、游标和确认位置 ▼ 第 5 步:由服务端主动、正常地关闭剩余连接 ▼ 第 6 步:应用完成 graceful shutdown,并在宽限期内退出 |
- WebSocket:可以先发送业务下线通知,再发送 Close Frame;
- HTTP/2 与 gRPC:使用协议和框架支持的优雅关闭能力,停止接受新流或新 RPC;
- 长轮询:停止建立新的轮询,让当前请求完成后返回;
- 自定义 TCP:设计明确的‘服务即将重启,请重新连接’消息。
应尽量由服务端主动、正常地关闭连接,让客户端能够识别计划内下线,而不是等 Kubernetes 强制终止容器后才发现异常断开。
3.12.6 客户端必须具备自动重连能力
连接迁移实际由客户端通过重新连接完成。客户端至少需要实现以下流程:
检测连接关闭或超时 ▼ 按退避策略重新连接,并重新执行 DNS / Service 访问或服务发现 ▼ 负载均衡选择新的 Pod ▼ 重新鉴权,恢复订阅、会话和最后确认位置 ▼ 补拉或重放断线期间尚未确认的数据 |
重连不应采用无间隔的无限循环,否则大量客户端同时断开时可能形成重连风暴,瞬间压垮剩余 Pod。通常应使用带随机抖动的指数退避。
示意:约 1 秒 → 2 秒 → 4 秒 → 8 秒;每次增加随机抖动,并设置上限
具体重连间隔、最大重试次数和熔断策略应按客户端规模、剩余副本容量和业务允许的恢复时间进行压测。
3.12.7 会话状态不能只保存在旧 Pod 内存中
如果连接对应的关键状态只保存在 Pod 本地内存中,连接重新建立到其他 Pod 后,新 Pod 无法恢复原会话。
Pod 1 本地状态:用户登录状态、订阅列表、最后消息序号、未确认消息
Pod 1 停止后,这些状态可能同时消失。连接断开后仍然不能丢失的关键状态,应根据业务需要存放在共享或可恢复的存储中,例如:
- Redis 或专门的会话存储;
- 数据库;
- 消息队列;
- 可重放的消息日志。
客户端重新连接到新 Pod 后,可以通过用户身份、会话 ID、恢复令牌或消息序号重新加载必要状态。并不是所有临时连接状态都必须持久化,但凡是连接断开后仍不能丢失的业务状态,就不能只依赖单个 Pod 的本地内存。
3.12.8 消息不丢失还需要确认、补偿和幂等
自动重连只能重新建立连接,不能自动证明业务消息没有丢失。服务端发送消息后连接立即断开时,服务端可能无法确定客户端是否收到、是否处理成功。
|
服务端已经发送消息 ▼ 连接断开:客户端是否收到、是否处理成功均不确定 ▼ 不重发可能丢消息;直接重发又可能产生重复消息 |
重要消息通常需要以下机制:
- 唯一消息 ID 和单调递增的消息序号;
- 客户端确认机制;
- 服务端保存最后确认位置;
- 重连后的断点续传或消息补拉;
- 消费端去重和业务操作幂等。
客户端最后确认:lastAck=1050
重新连接后携带 lastAck=1050,服务端从 1051 开始补发
如果同一消息可能被重复发送,消费端应根据消息 ID 判断是否已经处理,避免重复扣款、重复创建订单或重复执行设备控制。
3.12.9 长连接下线的完整建议时序
|
① Kubernetes 开始终止旧 Pod,preStop 调用 MSE /offline ▼ ② Nacos 状态传播、响应打标、主动通知和 EndpointSlice 更新分别推进 ▼ ③ 新调用和新连接逐步停止进入旧 Pod,旧 Pod 进入 draining 状态 ▼ ④ 服务端通知已有客户端即将关闭,并完成正在处理的消息 ▼ ⑤ 客户端按退避策略重新连接,负载均衡选择其他 Pod ▼ ⑥ 客户端重新鉴权、恢复订阅,并携带最后确认位置 ▼ ⑦ 新 Pod 恢复会话,补发或补拉断线期间的数据 ▼ ⑧ 旧 Pod 正常关闭剩余连接,执行 graceful shutdown 并退出 |
如果上述过程未能在 terminationGracePeriodSeconds 内完成,Kubernetes 最终仍会强制结束旧容器。因此必须在受控发布中观测真实连接排空时间、重连成功率、消息恢复结果和剩余 Pod 容量。
3.12.10 MSE 能解决什么,不能解决什么
| 能力 | 能否由 MSE/Kubernetes 直接完成 | 说明 |
| 提供者提前进入预下线 | 可以 | MSE /offline 启动预下线流程 |
| 减少新调用继续选择旧实例 | 可以加快实现 | 依靠注册中心变化、响应打标和主动通知 |
| Service 数据面停止选择终止 Pod | 可以逐步实现 | 依靠 EndpointSlice 和集群数据面收敛 |
| 自动迁移已有 WebSocket/TCP 连接 | 不可以 | 已有连接绑定原 Pod 和原进程 |
| 自动恢复 gRPC Stream | 不可以 | 需要客户端和业务协议重新建立流 |
| 保存业务会话 | 不可以 | 需要共享会话存储或业务状态持久化 |
| 补发断线期间消息 | 不可以 | 需要消息序号、日志或消息队列 |
| 防止重复业务操作 | 不可以 | 需要消息去重和业务幂等 |
| 客户端自动重连 | 不可以 | 需要客户端 SDK 或业务代码实现 |
| 避免重连风暴 | 不可以 | 需要指数退避、随机抖动和容量保护 |
3.12.11 本节结论
长连接服务在滚动发布时,旧 Pod 最终必须退出,绑定在旧 Pod 上的连接也最终必须结束。因此,不能把‘绝对无损’理解为‘连接永远不断’。
|
停止新连接进入旧 Pod ▼ 尽量排空已有连接 ▼ 客户端自动重连并恢复会话 ▼ 断线数据能够补偿,消息能够去重,业务操作保持幂等 |
一句话总结:MSE、Nacos 和 Kubernetes 可以帮助新流量停止进入待下线实例,但不能迁移已经建立的长连接。长连接场景真正的无损,不是保证连接永远不断,而是保证连接断开后能够重连、状态能够恢复、消息不会永久丢失、重复处理不会产生错误业务结果。
3.13 ECS(非 K8s)场景怎么配
如果应用部署在 ECS 上(没有 K8s 的 preStop),需要在应用的停机脚本里、靠前的位置加一行:
curl http://127.0.0.1:54199/offline 2>/tmp/null; sleep 30;
其中 /offline 是实例上 MSE Agent 暴露的下线接口,sleep 30 是留给在途请求处理完的时间。
4.适用范围与注意事项
4.1 无损上线适用范围
- 延迟注册和小流量预热面向 Nacos 等受支持的注册发现路径;K8s Service 不读取 Nacos 实例权重。本环境把 55199/health 用作 readinessProbe,因此‘注册状态检查’还会间接控制 Pod 何时进入 Service 的 Ready Endpoint,但这不等于 K8s Service 获得了 Nacos 预热能力。
- Spring Cloud 应用当前仅支持 Nacos、ZooKeeper、Eureka 三种注册中心的服务预热。
- 服务预热基于默认负载均衡器(ZoneAware / RoundRobin / Random),若修改了该配置会导致预热失效。
- 小流量预热要求“调用方(网关/微服务)也接入 MSE”;如果调用方是 Nginx/Ingress 这类“只转发、不做服务发现”的入口,它本身不做微服务负载均衡,谈不上参与预热。
4.2 无损下线适用范围
- 应用接入 MSE 即默认开启无损下线;主动通知需手动配置。
- 暂不支持非 Java 应用体系的无损下线。
- 当前文档范围内,MSE 无损下线面向受支持的 Java Web 应用(Spring MVC 或 WebFlux);其他语言、框架和协议应以 MSE 最新支持矩阵为准。
- 暂不支持“调用方不是微服务”(如纯外部系统直接调服务)的下游提供者的无损下线。
- 本环境已确认的 lb:// / LB:// 路径由 Spring Cloud 调用方按 Nacos 实例列表做本地选址;要获得响应打标和主动通知能力,实际调用方与提供方都需要接入受支持的 MSE 治理。本环境包括 iot-fusion-gateway 和相关内部业务 Pod。http:// 和 cam-system 的 K8s Service 路径不按 Nacos 本地候选选择具体 Pod,本文将其已确认的保护重点写为 readiness、EndpointSlice、连接排空、preStop 和应用优雅停机。
4.3 关键配置速查
| YAML 字段 | 含义 | 默认值 |
| mse.lossless.enable | 是否开启无损上线规则 | false |
| mse.lossless.warmupTime | 预热时长(秒) | 120 |
| mse.lossless.delayTime | 延迟注册时间(秒) | 0 |
| mse.lossless.notice | 是否开启提供者下线时主动通知调用方 | false |
本环境:mse.lossless.enable = true 本环境:延迟注册 = 已启用;注册状态检查 = 55199/health 本环境:小流量预热 = 未启用 本环境:mse.lossless.notice = true(主动通知已启用)
| 其他关键参数 | 默认 / 建议 | 说明 |
| 注册状态接口(55199) | 按 Agent 版本配置 | Agent 4.1.10+ 使用 /readiness,旧版使用 /health;未注册返回 500、已注册返回 200;未开启无损上线时接口不开放 |
| minReadySeconds | 当前 FAQ 建议 > 预热时长 | 它影响 Deployment Available/替换节奏,不是业务 readiness;预热由首笔外部请求触发,需结合事件验证 |
| MSE 自动注入 preStop | 查看本环境 Pod | 新 Pod 创建的 admission 阶段注入;旧 Pod 不会事后动态改变;源 YAML 可以没有 |
| terminationGracePeriodSeconds | MSE 官方建议 90 | 总预算包含 preStop 和应用 shutdown;停止信号通常为 SIGTERM,但可被镜像或 stopSignal 改变 |
| 业务自定义 preStop | 按官方要求至少预留约 30 秒 | MSE 改用 gracefulshutdown sidecar;普通多容器 Pod 无跨容器顺序保证,必须实测 |
| Spring Boot 优雅停机 | 按版本检查 | 3.4+ 默认 graceful(除非 immediate);3.3 及更早通常需显式开启;phase timeout 不是单一总上限 |
本环境的四类流量路径已经确认同时存在:外部网关 lb:// 与内部 Pod LB:// 使用 Nacos 实例列表并直连 Pod;外部网关 http:// 与内部 cam-system 使用 K8s Service。MSE/Nacos 与 Service/EndpointSlice 各保护自己负责的路径,发布时必须分别观测。
5.可观测性:怎么确认上下线真的“无损”了
在 MSE 治理中心控制台 > 应用治理 > 目标应用详情 > 流量治理 > 无损上下线页签中:
- 上线观测:当前重点查看延迟注册、服务注册和 55199/health 从 500 变为 200 的事件,并核对 Pod Ready 与 Service Endpoint 变化。当前未启用小流量预热,因此不应期待‘开始预热/预热结束’事件或逐步加权曲线。
- 下线观测:分别查看提供方 /offline、Nacos 实例变化、响应打标、主动通知发送与调用方处理结果,以及 K8s EndpointSlice 变化;目标是在实例停机前,各实际入口 QPS 已快速降为 0。
- 如果 MSE 显示无损下线成功但实例 QPS 没有快速降为 0,应区分剩余流量来源:可能有未纳入 MSE 的调用方、主动通知未覆盖/处理失败、框架版本不支持、本地直连,或者来自 K8s Service 与已有长连接。不能只根据一个控制台事件认定所有路径都已排空。
MSE 注册状态探针一直不通过的可能原因:① 未开启无损上线(未开启时 55199 接口不开放);② 探针路径与 Agent 版本不匹配(4.1.10+ 为 /readiness,旧版为 /health);③ 应用未成功接入服务治理;④ 启动/存活探针失败导致应用反复重启。
6.脱离 MSE:自建无损上下线
前面五章讲的都是“在 MSE 的体系内”。这一章回答两个实际工程里一定会遇到的问题:① 无损上下线到底是谁做的?② 不用 MSE、只用自建组件,还能不能做到无损?
6.1 无损上下线到底是“谁”做的:Nacos 还是 MSE
先给结论:无损上下线不是注册中心 Nacos 的功能,而是 MSE 探针(Agent)在应用进程里实打实做出来的。Nacos 只是“底座”:存实例列表、存实例元数据、推送变更。
看一张分工表就很清楚:
| 环节 | Nacos(注册中心) | MSE 探针 + MSE 治理(无损机制) |
| 延迟注册 | 只被动接收注册 | 探针“扣住”注册到 Nacos 的动作,延迟执行 |
| 小流量预热 | 只存了元数据 | 提供者探针在元数据带启动时间;消费者探针按启动时间算权重(0%~100%) |
| 就绪检查 | 不参与 | 探针在 55199 提供注册状态接口:Agent 4.1.10+ 为 /readiness,旧版为 /health |
| 响应打标 | 标记本身不参与;提供实例列表供调用方刷新 | 提供者响应携带标记;兼容调用方识别后可能主动刷新注册中心列表,再移除或拉黑实例 |
| 主动通知 | 通知本身不参与;注册中心状态变化仍会传播 | MSE 增强的提供者主动向受支持的消费者发起网络请求;消费者收到后停止调用该节点;通知目标的内部采集和路由算法未公开 |
用一个类比:在本文的 Nacos 架构里,Nacos 是“门牌登记处”(谁住哪、几号房),MSE 探针相当于给受治理应用增加“对讲机 + 搬离前的排流流程”。
- 没有 MSE 探针、也没有自行实现反注册、调用方收敛和优雅停机:实例直接退出后,注册中心传播和调用方缓存更新存在时间窗口,调用方可能继续访问已经不可用的地址。
- 接入 MSE 探针:实例停止前可通过主动通知或响应打标让受治理调用方尽快停止选择它,并在等待阶段处理在途请求。
一句话:在 Nacos 调用路径中,Nacos 提供注册发现底座,MSE 提供无侵入的预下线与调用方协同能力。移除 MSE 并不等于技术上无法做到无损,但如果只保留默认注册发现、又没有自行补齐排流和优雅停机,就不能把 Nacos 列表变更直接当作无损保证。
6.2 使用自建 Nacos 时,MSE 无损上下线还能用吗
可以,但结论有明确边界:MSE 治理的是接入探针的应用调用链,注册中心按官方支持的类型和兼容版本接入;Nacos 是自建还是托管,并不改变“提供者与调用方都需要接入治理”这一要求。
- MSE 无损上线文档按注册中心类型描述支持范围:Spring Cloud 场景列出 Nacos、ZooKeeper 和 Eureka,并明确纯 K8s Service 发现路径不适用该能力。
- MSE 的开源 Kubernetes 接入文档说明,非阿里云托管集群中的 Spring Cloud 和 Dubbo 应用也可以接入微服务治理;因此部署环境本身不是限制条件。
延迟注册、预热元数据、55199 注册状态接口、响应打标和主动通知主要由应用侧 Agent 实现。注册状态路径需按 Agent 版本选择:4.1.10+ 为 /readiness,旧版为 /health。使用自建 Nacos 时仍需核对 Nacos 服务端/客户端版本、认证方式、网络连通性和 MSE 最新兼容矩阵,并用注册、预热和下线事件做实际验证,不能只凭“都是 Nacos”推断兼容。
典型链路:本环境实际链路是:用户 → DNS → SLB → Nginx/Ingress → iot-fusion-gateway。网关上游配置为 lb://服务名时,通过 Nacos 适配器发现实例并直连 Pod IP;配置为 http://服务名时,通过 K8s DNS 访问同名 Kubernetes Service。Nacos 改为自建后,这两种网关路由的角色分工不变,但 lb:// 路径仍必须满足 MSE 对 Nacos 服务端、客户端和适配器版本的兼容要求。
这里的前提针对 MSE 的注册中心治理链路:① 开通 MSE 微服务治理;② 提供者和需要识别下线/预热信号的调用方都成功接入 MSE;③ 调用通过受支持的注册中心发现。纯 K8s Service 流量不参与 MSE 的注册中心预热和调用方摘除机制,但它可以与 Nacos 流量同时存在,需另行做好 K8s 路由排流。
6.3 完全不用 MSE:自建无损上线怎么做
如果连 MSE 都不用,本质就是自己把 MSE 探针的活儿重做一遍。核心思路一句话:把“注册、摘除、就绪、在途请求”这几个时间点自己控制好。无损上线对应三件事:
6.3.1 延迟注册:先关掉自动注册,初始化完成再手动注册
spring.cloud.nacos.discovery.register-enabled: false # 先不自动注册
应用初始化完成后(数据拉取、连接池就绪),在代码里手动注册:
namingService.registerInstance(serviceName, ip, port); // 手动注册
6.3.2 小流量预热:使用注册中心权重时必须验证调用方是否生效
Nacos 实例支持 weight 属性,可以先设置较低权重,再逐步提升到正常权重。下面只表示控制思路;具体 OpenAPI 路径、认证参数、权重范围和更新语义应以本环境部署的 Nacos 版本文档为准:
registerInstance(..., weight=<低权重>);
按时间阶梯更新:<低权重> → ... → <正常权重>
是否真正预热取决于调用方的实际负载均衡实现。必须验证当前 Spring Cloud Alibaba、Dubbo 或自研客户端版本确实读取 Nacos 权重,并通过选址日志/QPS 观察权重变化;K8s Service 路径不会读取 Nacos 权重。
6.3.3 就绪检查:让 Readiness 与真实注册状态绑定
@Component("microserviceRegistration") public class MicroserviceRegistrationHealthIndicator implements HealthIndicator { private volatile boolean registered = false; public void markRegistered() { this.registered = true; } @Override public Health health() { return registered ? Health.up().build() : Health.down().build(); } } management.endpoint.health.group.readiness.include: readinessState,microserviceRegistration
再让 K8s readinessProbe 请求 /actuator/health/readiness。这样做的目标与 MSE 的 55199 注册状态接口类似:只有真实注册成功后才返回就绪;但它是自建实现,并不等同于 MSE 接口,也不受 MSE Agent 4.1.10 的 /readiness 与旧版 /health 路径规则约束。Spring Boot 不同版本的 Health Group 配置可能不同,必须确认该 Indicator 确实被纳入 readiness 组,并测试注册失败时返回非 2xx。
6.4 完全不用 MSE:自建无损下线怎么做
完全自建时,先盘点所有真实流量路径,再设计一个统一的“开始排流 → 停止接收新工作 → 等待在途请求 → 退出”协议。不能因为存在 K8s Service 就忽略 Nacos,也不能只做 Nacos 反注册而忽略 Ingress、长连接和 K8s 路由传播。
6.4.1 注册中心调用路径:先反注册,再等待消费者收敛
namingService.deregisterInstance(serviceName, ip, port);
实例先从 Nacos 注销;消费者收到实例列表变更并刷新本地缓存后,才会停止选择该实例。具体推送协议和收敛时间取决于 Nacos/客户端版本,不能固定假设为 UDP 或“必然秒级”。需要通过日志与压测测出实际 p99 收敛时间。
6.4.2 K8s Service 调用路径:等待 Endpoint 和数据面收敛
Pod 删除时 EndpointSlice 通常进入 terminating=true、ready=false,并通过 serving 表达是否仍在服务;但 publishNotReadyAddresses=true 会强制 ready=true。当所有可用 endpoint 都在终止时,代理还可能继续选择 serving=true 且 terminating=true 的 endpoint。与此同时,EndpointSlice 控制器、kube-proxy/eBPF、Ingress 和云负载均衡的传播是异步的,状态变化也不会自动关闭已有 keep-alive、HTTP/2、gRPC 或 WebSocket 连接。稳健做法是进入 draining 状态、停止接受新业务,并给路由传播和协议级连接排空留出经过实测的时间。
preStop 不是唯一方案,但常用于调用应用 drain/offline 接口并等待:
lifecycle.preStop.exec.command: ["sh", "-c", "curl http://127.0.0.1:8080/offline; sleep <实测排流时间>"]
6.4.3 混合流量路径:两层都要处理
本环境已确认四类实际流量:① iot-fusion-gateway 的 lb:// 路由通过 Nacos 发现并直连 Pod;② iot-fusion-gateway 的 http:// 路由通过 Kubernetes Service;③ 内部 Pod 的 LB:// 调用通过 Nacos 获取对端列表并直连目标 Pod;④ cam-system 没有接入 Nacos,走 Kubernetes Service。完全自建 drain/offline 时必须同时覆盖:Nacos 反注册与所有网关/内部调用方本地候选摘除、K8s readiness/EndpointSlice 状态切换、已有连接排空、停止接受新业务和等待在途请求。不能只处理其中一条路径。
6.4.4 应用优雅停机与总宽限期
server.shutdown: graceful # Spring Boot 3.3 及更早通常需显式配置;3.4+ 默认开启,除非设为 immediate spring.lifecycle.timeout-per-shutdown-phase: 30s terminationGracePeriodSeconds: <preStop实耗 + 各生命周期阶段实测最坏关闭时间 + 连接排空 + 安全余量>
K8s 的宽限期从终止流程开始计时,preStop 和应用 shutdown 共用同一个预算,不是 preStop 结束后重新计时。停止信号通常为 SIGTERM,但镜像 STOPSIGNAL 或受支持的 lifecycle.stopSignal 可以改变它。Spring Boot 3.4+ 默认启用 graceful shutdown(除非显式 immediate),3.3 及更早通常需显式开启。spring.lifecycle.timeout-per-shutdown-phase 是每个 SmartLifecycle phase 的上限,不是整个进程关闭的唯一总上限,因此总预算应以实际 phase 数量、协议排空和最坏关闭观测为准。
没有任何静态 YAML 能证明“绝对零损”。还要用足够副本、合理的滚动更新策略、重试与幂等、连接排空,以及一次受控下线观测来验证。OOM、强制删除、节点故障等异常退出不属于优雅下线保证范围。
6.5 自建方案能覆盖到什么程度
| MSE 机制 | 自建难度 | 自建时必须验证的内容 |
| 延迟注册 | 低 | 初始化完成后才注册;失败时不能提前暴露实例 |
| 注册状态就绪 | 中 | Readiness 必须与真实注册成功绑定,不能只是进程存活 |
| 小流量预热 | 中~高 | 消费者负载均衡认权重;老实例不能在预热完成前全部退出 |
| 路由/注册中心排流 | 高 | Nacos、EndpointSlice、Ingress/LB、长连接各自的收敛时间 |
| 优雅停机 / 在途请求 | 中~高 | Spring Boot/协议支持、停止信号传递、各 phase 超时预算、强制终止 |
| 响应打标 | 很高 | 提供者加标记,所有受治理且兼容的调用方识别并停止选择实例 |
| 主动通知 | 极高 | 自行实现通知目标发现、额外网络请求、消费者停止调用和失败处理;具体方案必须自行设计并验证 |
自建可以实现高质量的优雅上下线,但不能用固定的“90%+”描述,也不能仅凭某个配置推断无损。MSE 的价值是把注册中心摘除、消费者感知、等待阶段、事件观测等能力无侵入地落到两端;自建则必须逐条实现并通过受控发布验证。
6.5.1 可以利用的框架能力(仍需验证)
- Dubbo、Spring Boot、网关和 Service Mesh 通常提供不同程度的优雅停机/连接排空能力,但必须核对实际版本、开关、协议和超时,不能只凭框架名称认定已无损。
- 纯 K8s Service 发现路径不适用 MSE 服务治理的小流量预热;应使用 readiness、EndpointSlice/负载均衡排流、应用优雅停机和足够宽限期,并通过实测决定是否需要 preStop 延时。
- Nacos 调用路径即使能及时推送实例变更,也仍要验证消费者缓存刷新、长连接、重试和在途请求;不能把“有推送”直接等同于零错误。
7.总结
请求路径:外部请求固定经过 DNS → SLB → Nginx/Ingress → iot-fusion-gateway。网关按路由 URI 分流:lb://服务名通过 Nacos 获得实例列表,在网关本地负载均衡后直连 Pod IP;http://服务名通过 K8s DNS 访问同名 Kubernetes Service。内部调用同样是混合模式:LB:// 目标由调用方从 Nacos 获取实例列表并直连对端 Pod;cam-system 没有接入 Nacos,走 Kubernetes Service。Nacos只提供注册发现控制面,不转发业务请求。
本环境现状:源 app.yaml 没写 preStop,但本环境 Pod 的 ONE_AGENT_PROPERTY 包含 -Dmse.enable=true,并存在调用 127.0.0.1:54199/offline、sleep 30 的 lifecycle.preStop。这能够确认 MSE Agent 与无损下线钩子已被运行时自动注入。
MSE 无损下线:正常终止时,/offline 会在停止信号之前启动预下线。对网关 lb:// 和内部 LB:// 路径,注册中心下线与 MSE 通知协同是可以交叠执行的独立链路:提供方向 Nacos 注销或改变注册状态,Nacos 继续传播实例列表;响应打标可使兼容调用方临时拉黑节点;当前已开启的主动通知则由 MSE 增强的提供者主动向消费者发起额外网络请求,消费者收到后不再调用该节点。公开文档没有规定 Nacos 注销与主动通知的严格先后顺序,也没有披露通知目标采集和消费者内部处理算法。对网关 http:// 和 cam-system,Kubernetes 通过 readiness、EndpointSlice 和集群数据面收敛停止分配新流量,并由连接排空和应用 graceful shutdown 处理已有请求。本环境 Pod 的 hook 直接位于业务容器,因此同一容器内 preStop 完成后才收到停止信号(通常为 SIGTERM)。
MSE 无损上线:本环境已启用延迟注册和注册状态检查,本环境 Pod 使用 55199/health;未启用小流量预热。新实例先延迟向 Nacos 注册,注册状态与业务 readiness 通过后才进入 Ready。网关 lb:// 和内部 LB:// 调用方随后可从 Nacos 看到实例;Kubernetes Service 路径则在 EndpointSlice 出现 Ready Endpoint、数据面更新后开始转发。
K8s 原生能力:Kubernetes通过 readiness、Deployment滚动策略、EndpointSlice、SIGTERM 和终止宽限期提供平滑上下线的基础,但不天然承诺零损。路由传播异步、已有长连接不会自动排空、应用可能提前退出,因此 cam-system 仍需准确业务 readiness、drain、协议级连接排空、应用 graceful shutdown、充足宽限期,以及调用方重试与幂等。
边界:EndpointSlice/负载均衡传播、keep-alive/HTTP2/gRPC/长连接、应用 shutdown 和注册中心缓存都是独立环节;任何静态 YAML 都不能证明绝对零损。最终必须用足够的 terminationGracePeriodSeconds、应用优雅停机、重试/幂等和受控下线观测来验证。异常停机(OOM、强制删除、节点故障)不属于 preStop 的保证范围。
7.1 白皮书与现行产品文档的证据边界
阿里云官方《微服务治理技术白皮书》发布于 2022-04-13。它适合证明 MSE 无损上下线的架构背景、设计思想和当时的产品实践,但不能单独作为 2026 年所有产品细节的现行契约。
- 白皮书能够支持:注册中心列表传播存在时间窗口、延迟注册、服务预热、注册状态与 readiness 联动、响应标记、主动注销/通知、在途请求等待,以及历史实践中的 /offline + 约 30 秒等待流程。
- 白皮书中的特定 Demo 显示开启无损下线的版本为 0 错误、对照版本有错误;这只能说明该实验条件下的结果,不能外推为任意框架、协议、网络和发布方式下的绝对零错误 SLA。
- 白皮书不能证明 2026 年版本敏感细节,例如 Agent 4.1.10 前后的 /health 与 /readiness 路径、当前默认值和支持矩阵、首笔外部请求触发预热、Kubernetes 多容器终止语义、Spring Boot 3.4 默认 graceful shutdown。
- 本文采用的优先级是:现行 MSE 产品文档/FAQ与配置说明 → 本环境 Pod、控制台和日志证据 → 白皮书的架构与历史机制 → Kubernetes、Spring Boot、Nacos 等各自官方文档。若资料冲突,以更贴近当前版本和本环境的证据为准。
附:参考文档
- 阿里云官方《微服务治理技术白皮书》(2022-04-13):developer.aliyun.com/ebook/7565
- 阿里云 MSE 无损上下线总览:help.aliyun.com/zh/mse/user-guide/lossless-upper-and-lower-lines22
- 阿里云 MSE 无损上线:help.aliyun.com/zh/mse/user-guide/graceful-start
- 阿里云 MSE 无损下线:help.aliyun.com/zh/mse/user-guide/graceful-shutdown
- 阿里云 MSE 无损上下线 FAQ:help.aliyun.com/zh/mse/support/lossless-start-and-shutdown-faq
- 阿里云 MSE YAML 配置:help.aliyun.com/zh/mse/user-guide/use-yaml-to-configure-graceful-start-and-shutdown
- 阿里云 MSE ACK/Nacos 无损上下线实践:www.alibabacloud.com/help/en/mse/use-cases/implement-graceful-start-and-shutdown-of-microservice-applications-by-using-mse
- Kubernetes Pod 生命周期与终止:kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/
- Kubernetes EndpointSlice:kubernetes.io/docs/concepts/services-networking/endpoint-slices/
- Kubernetes 容器生命周期钩子:kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/
- Spring Boot 优雅停机:docs.spring.io/spring-boot/reference/web/graceful-shutdown.html

浙公网安备 33010602011771号