无损上下线

  1. 为了提高系统的效率响应速度,减少任务切换时的等待时间,同时提高系统的可用性稳定性
  2. 本文介绍微服务治理中的流量治理模块的无损上下线功能

无损上线

无损上线能提供相应的保护能力,包括服务延迟注册服务小流量预热服务就绪检查三个功能

无损下线

  1. 高并发下,服务提供端应用实例的直接下线,会导致服务消费端应用实例无法实时感知下游提供者实例的实时状态,因而出现继续将请求转发已下线的实例,从而导致请求报错,流量有损
  2. 为了解决这类问题,MSE提供了无损下线功能,帮助您更好的应对一个线上应用,在服务更新部署等过程中的问题

使用YAML配置无损上下线

您不仅可以在MSE控制台上配置无损上下线功能,也可以通过在容器服务ACK中配置YAML无损上下线参数方式来开启功能

无损上线

  1. 对于任何一个线上应用来说,发布扩容重启等操作不可避免
  2. 应用启动各阶段,无损上线能提供相应的保护能力,包括服务延迟注册服务小流量预热服务就绪检查三个功能
  3. 本文介绍MSE提供的无损上线功能

无损上线功能概述

延迟注册

  1. 微服务提供者实例会在应用启动过程中,进行服务的注册
  2. 一旦完成服务注册,就可以被外部服务消费者应用订阅调用
  3. 对于基于Spring框架开发的Java应用而言,注册过程一般发生在Spring上下文刷新完毕之后
  4. 如果应用有一些异步初始化逻辑还没有执行完毕,就直接进行服务注册,此时消费者前来调用时很可能会产生请求报错
    • 比如大数据计算服务需要提前从OSS拉取几百兆数据,待数据拉取完成后才能对外提供服务
  5. 因此如果应用启动直接注册服务,会导致流量资源未就绪而报错
  6. 有了延迟注册功能之后,我们可以通过设置一定的延迟时间,将原本的服务注册动作往后推迟
  7. 让应用在充分初始化后再注册到注册中心对外提供服务
  8. 避免了微服务提供者未准备完毕,就被外部调用,产生调用出错的情况

小流量预热

  1. 很多时候,刚启动的新实例会处于一种“冷机”状态
  2. 冷机状态下,需要进行比如连接池的懒加载缓存预热热点代码生成等操作,因此这些新实例对请求的处理能力远远小于运行很久的实例
  3. 如果开发与运维人员不对这个过程进行干涉,可能会造成新实例上线这段时间内,系统整体平均RT时间变高
  4. 更坏的情况下,还可能会造成服务“夯”住,导致大量请求调用超时报错的情况
  5. 以下是需要进行资源加载的实例,在尚未完成资源加载的情况下,与资源加载完毕后的两次请求调用时长对比
  6. 资源加载过程中,如果有大量请求到达该实例,可能都会发生阻塞
1
2
3
4
5
6
7
8
9
10
11
[arthas@37035]$ trace com.alibaba.mse.consumer.TestController eurekaRest  -n 5 \
--skipJDKMethod false
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 105 ms, listenerId: 1
`---ts=2022-02-14 21:28:02;thread_name=http-nio-18099-exec-1;id=39;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@60e5272
`---[464.275852ms] com.alibaba.mse.consumer.TestController:eurekaRest()
`---[464.018509ms] org.springframework.web.client.RestTemplate:getForObject() #50

`---ts=2022-02-14 21:28:08;thread_name=http-nio-18099-exec-3;id=3b;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@60e5272
`---[8.46028ms] com.alibaba.mse.consumer.TestController:eurekaRest()
`---[8.402525ms] org.springframework.web.client.RestTemplate:getForObject() #50
  1. 小流量预热功能的思路是,在新服务实例刚上线的这段时间里,控制消费者应用对其调用的流量大小,来避免Java应用冷启动时请求处理能力差,系统整体RT变高的问题,并且保护新启动的服务实例不会被大流量击垮
  2. 进入该实例的流量会按一定规则随时间不断加大,当达到设置的预热时长时,小流量预热过程结束,实例正常接收流量
  3. 小流量预热使用的是在线的消费者的流量,需要该服务实例的消费者应用也接入MSE服务治理

服务注册状态检查

  1. K8s提供了就绪检查机制(Readiness Probe),在进行服务发布时,新实例就绪检测通过后,就会下线旧的实例(具体情况取决于设置的发布策略)
  2. 然而K8s无法感知微服务何时就绪,当端口启动时,K8s会认为应用已经就绪
  3. 这可能会导致刚启动的服务还未注册到注册中心,就被K8s判定为已经就绪,进而继续推进服务发布的动作,下线正在运行的旧实例
  4. 从而引发消费端调用出错,并在调用过程中出现service no provider/instance等异常
  5. 无损上线的服务就绪检查功能,通过Agent的无侵入方式为应用提供一个检测其是否完成注册的HTTP接口,如果未完成注册,则返回500状态码;当应用注册完成后,会返回200状态码
  6. 用户将应用的就绪检测配置成该接口后,可以帮助K8s判定应用是否就绪,保障K8s场景下服务发布上线过程中,服务消费者一直有可用的提供者,不会产生无提供者的报错

使用无损上线

注意事项

  1. 目前只支持通过微服务注册中心(如 Nacos)进行注册发现体系下实例的无损上线,不支持 K8s Service 体系下微服务实例的无损上线
  2. 对于Spring Cloud应用,当前仅支持利用Nacos、ZooKeeper以及Eureka这三种类型的注册中心构建的应用进行服务预热
  3. Spring Cloud服务预热功能是基于Spring Cloud框架默认的ZoneAwareLoadBalancerRoundRobinLoadBalancerRandomLoadBalancer负载均衡器实现的,如果应用本身修改了该配置,会导致服务预热功能失效
  4. 服务预热需要提供者消费者都接入MSE服务治理才能生效
  5. 比如网关应用,是通过直接对外暴露 API的方式接收外部流量,因此MSE当前的小流量预热功能对此类应用不生效

使用方式

步骤一:开启无损上线

  1. 登录MSE治理中心控制台,并在顶部菜单栏选择地域
  2. 在左侧导航栏,选择治理中心 > 应用治理,然后单击目标应用的资源量卡片
  3. 在目标应用详情页面的左侧导航栏,单击流量治理,然后选择无损上下线页签
  4. 在配置信息模块,单击修改,打开无损上线按钮,单击下方确定

步骤二:配置k8s服务就绪检查

该操作会直接引起应用的重启,如果是生产环境,建议您挑选发布窗口执行该操作!

  1. 登录容器服务管理控制台,在左侧导航栏选择集群列表
  2. 在集群列表页面单击目标集群,在左侧导航栏选择工作负载 > 无状态,单击部署的应用操作列下的编辑,在健康检查栏处,单击就绪检查右侧的开启,并配置如下参数。完成后单击更新
    • 路径:**/readiness**
      • 如果您的应用所使用的探针版本低于4.1.10,路径需要配置成/health。查看探针版本方式:MSE 控制台 > 治理中心 > 应用治理 > 单击对应的应用 > 节点详情,右侧可以看到探针版本
    • 端口:55199
    • 延迟探测时间(秒):推荐该值的配置大于应用启动所需时间 + 无损上线功能模块中配置的延迟注册时间(默认0秒)二者之,如果您不按照该建议进行配置,不会影响功能正常使用
    • 其他参数,请参见创建无状态工作负载Deployment,应用重启后,通过就绪检查前完成服务注册即可生效

配置延迟注册时长 - 可选

设置完毕后,在下一次应用启动时,延迟注册时长才会生效

请根据业务场景需要决定是否配置,操作步骤如下:

  1. 根据步骤一、二进入无损上线功能页面,开启无损上线功能,并配置K8s服务就绪检查
  2. 修改无损上下线配置信息,单击无损上线模块左侧箭头展开配置选项,在延迟注册时长(秒)处设置延迟注册时长,然后单击下方确定按钮

调整小流量预热时长

开启无损上线后,该功能会自动开启。默认预热时长120秒。可以根据业务场景需要进行调整:

  1. 根据步骤一、二进入无损上线功能页面,开启无损上线功能,并配置K8s服务就绪检查
  2. 修改无损上下线配置信息,单击无损上线模块左侧箭头展开配置选项,单击高级选项,在小流量预热时长(秒)处设置小流量预热时长,然后单击下方确定按钮
  3. 如果您希望开启小流量预热的服务,其调用方是 MSE 云原生网关,那么这里配置的小流量预热无法生效
    • 对应的解决方案是在 MSE 云原生网关配置该服务的预热
    • 在云原生网关控制台中,单击目标网关实例,在左侧导航栏的路由管理 > 服务页签中,找到该服务,单击目标服务操作列下的更多 > 策略配置,在策略配置页签下的流量管理 > 负载均衡配置的右侧,单击编辑,调整配置项预热时间即可
    • 注意,云原生网关默认预热 QPS 曲线图形一次曲线,和 MSE 服务治理提供的二次曲线略有差异,实际效果上差别不大
    • 一次曲线 / 二次曲线指预热窗口内新实例分到的流量权重随时间的爬升形状,即数学里的一次函数 / 二次函数曲线(不是圆锥曲线那个”二次曲线”)
      • 一次曲线(线性):流量占比 = t/T,随时间匀速直线上升
      • 二次曲线(抛物线):流量占比 ≈ (t/T)²先慢后快,前半程压得更低、临近预热结束才加速放行
      • 选二次曲线的直觉:实例最”冷”的阶段处理能力最差,且 JIT 热点编译缓存填充等能力恢复本身就是先慢后快,抛物线正好把最少的流量分给最冷的时段,比线性更保守;文档说”实际效果差别不大”,是因为预热过半后实例通常已足够热
      • 该注意事项只在调用方是 MSE 云原生网关时才有意义(网关自带预热配置,不走治理中心);自建网关 + Spring Cloud 消费方场景下,预热由 MSE 治理的二次曲线接管,一次曲线这条碰不到

两条曲线的流量占比对比(同一预热进度下新实例的放行比例,最终都收敛到 100% 全量):

预热进度 t/T 一次曲线(线性 t/T) 二次曲线(抛物线 (t/T)²)
25% 25% ~6%
50% 50% 25%
75% 75% ~56%
100% 100% 100%

说明

  1. 设置完毕后,在下一次应用启动时,调整后的小流量预热时长才会生效
  2. 小流量预热方法通过在服务消费端根据各个服务提供者实例的启动时间计算权重,结合负载均衡算法,控制刚启动应用流量启动时间逐渐递增到正常水平的过程,帮助刚启动运行的服务进行预热
    • 同时这也要求了服务消费者也接MSE服务治理
  3. 建议您在首次使用无损上线的小流量预热功能时,使用默认的预热时长即可
    • 如果在使用默认值预热服务的过程中发现预热效果不明显,出现流量损失,再通过调节该参数进行优化

无损上线观测

经过上述配置后,当您的应用再次启动时,可以在无损上下线功能页面,看到应用实例上线下线具体时间,以及同时期该实例的QPS曲线

  1. 登录MSE治理中心控制台,并在顶部菜单栏选择地域。
  2. 在左侧导航栏,选择治理中心 > 应用治理,然后单击目标应用的资源量卡片
  3. 在目标应用详情页面的左侧导航栏,单击流量治理,然后选择无损上下线页签
  4. 上下线概览中,单击左侧的上下线实例,在右侧可以看到该实例在上线阶段QPS变化以及对应发生的事件

您可以看到,应用先后会出现服务注册开始预热预热结束这几个事件,并且K8s Readines检查通过的事件也发生在服务注册事件之后。QPS曲线是在预热时长内(默认 120s)逐步上升到最大值,而不是直接陡升上去。如果您的应用在上线时,事件顺序、QPS曲线形状不符合预期,可以参考FAQ来解决。

image-20260908145023157

以一个典型配置为例,应用的K8s就绪检测配置成55199/readiness,并且将最小准备时间minReadySeconds)配置成120秒,和默认的预热时长一致

无损下线

概述

  1. 对于任何一个线上应用,在服务更新部署等过程中,需要尽量保证客户端无感知,即从应用停止重启恢复服务这个阶段不能影响正常的业务请求
  2. 在应用执行部署停止回滚缩容重置时,需要通过无损下线配置来保证应用正常关闭
  3. 本文介绍使用无损下线的注意事项、使用优势以及如何在控制台使用无损下线功能

为什么需要无损下线

由于微服务应用自身调用特点,在高并发下,服务提供端应用实例的直接下线,会导致服务消费端应用实例无法实时感知下游提供者实例的实时状态,因而出现继续将请求转发到已下线的实例,从而出现请求报错,流量有损

下线感知的传播链:Eureka 拉取 vs Nacos 推送

一次“提供者下线”要走到“消费者不再调用它”,感知速度取决于注册中心的通知模型——Eureka 的纯拉取与 Nacos 原生的订阅推送是两条完全不同的链路:

逐环节对比

环节 Eureka 协议(8761 兼容端点) Nacos 原生(gRPC 长连接)
服务端感知 显式注销走 readOnlyCache ~30s 同步;硬杀等 90s 租约驱逐 显式注销立即;硬杀 = 长连接关闭秒级感知(机器失联才等 keepalive,~20s 量级)
通知消费者 纯拉取:定时拉注册表(默认 30s / 自研调优 10s) 订阅推送:实例列表变更沿长连接推给订阅方,亚秒~秒级
消费端 LB Ribbon ServerList 缓存再刷一层(KickOut 监听器绕开) 同样存在——需订阅直连进 LB(自研 NacosSubscriptionServerListUpdater,SDK ≥ 1.14.6
端到端窗口 优雅下线最坏 ~40s(自研调优后)/ ~90s(社区默认) 秒级

关键结论

  1. “实时感知”由客户端协议决定,不由服务端版本决定——服务端是 Nacos 3.x 也救不了 Eureka 客户端:兼容层翻译的是协议,拉取模型原样保留(“双配置、单注册”的应用正是此形态:Nacos 原生地址躺在环境变量里,走的却是慢车道)
  2. Eureka 的延迟是设计取舍而非缺陷:AP 优先 + TTL 租约 + 多级缓存扛读压力,用传播延迟换可用性(自我保护机制同源)
  3. gRPC 长连接是 Nacos 2.0 引入的:1.x 客户端走 HTTP + UDP 推送(有丢包,靠定时拉取兜底),还是半个拉取模型——客户端版本由 Spring Cloud Alibaba / 自研 SDK 决定
  4. 推送只到客户端内存,最后一公里在 LB:Ribbon 的 ServerList 有自己的刷新周期,需要订阅直连;实测 logan-admin-svr 的 forge: 1.14.3 恰好差 1.14.6 这一档——就算切到 Nacos 原生通道,不升 SDK 也拿不到即时刷新
  5. 治理选型推论:主动通知在 Nacos 路径增量小(功能重叠),在 Eureka 路径是唯一秒级手段(MSE 补位)——Eureka→Nacos 客户端收尾迁移 + SDK 升级后,下线感知问题被架构消解;在那之前,主动通知是过渡期的桥
  6. 无损上线不受此影响:预热加权、延迟注册是上线侧能力,推送再快也给不了,Agent 价值独立存在

无损下线实现方案

  1. 提供者A下线时,虽然会主动通知注册中心,但此时消费者B可能无法实时感知到,导致消费者B继续调用已下线的提供者A
  2. 为了避免这种情况的发生,在提供者A接到下线命令即将下线前,对于在等待下线阶段收到的请求,在其返回值中都增加上特殊标记,让消费者B接收到返回值并识别到相关标志主动拉取一次注册中心服务实例,从而实时感知提供者A最新状态,从而达到提供者A的下线状态能够被消费者B实时感知
  3. 此外,提供者A在下线时,还会等待一定的时间,以保证下线时已经收到的在途请求被执行完毕,避免这些请求未被执行完毕,提供者A就已经停机

被动通知(标记+按需拉取)能打掉几级延迟

“标记 + 主动拉取”这套机制(被动通知)打不穿 Eureka 服务端的只读缓存——文档说“拉一次就能实时感知”,对默认配置的 Eureka 是不严谨的。把下线感知的延迟拆成四级,看它逐级打掉谁:

延迟层 默认耗时 被动通知(标记+拉取)
① 服务端 readOnlyCacheMap 同步 ~30s 打不掉
② 消费端定时拉取周期 30s(自研调优 10s) ✅ 标记触发按需拉取,绕开周期
③ Ribbon ServerList 刷新 ~30s ✅ Agent 见标记直接改本地列表(KickOut 同思路)
④ 在途请求 RT 量级 ✅ 等待窗口内执行完

机制细节与推论

  1. Eureka 的显式注销/状态变更会立即失效 readWriteCacheMap(Guava invalidate)——所以“拉一次”拿到的是否新鲜,取决于这次读走不走 readOnly:默认 use-read-only-response-cache=true,读由 readOnly 承接,最坏残留 ~30s;社区经典调优就是关掉它,让读直击 readWrite,显式操作后立即新鲜
  2. 被动通知的真实语义:把感知窗口从周期链路(社区默认 ~90s / 自研调优后 ~40s)压到只剩 ① 的 readOnly 残留(≤30s),剩下交给“等待下线时间”盖住——等待窗口必须 ≥ ① 的残留 + 余量。消费方里有 Eureka 的按 30s+ 设计;Nacos 原生路径注册中心读本来就新鲜,秒级即可。这就是“优雅停机窗口按消费方注册中心类型取最坏值”的理论依据
  3. 标记本身是带内信号:从濒死实例的响应里直接来,就是“这个实例要下线”的第一手信息,比任何注册中心读都新鲜;按需拉取的真正价值是同步其余拓扑(比如顶上来的新实例)——对被标记的这个实例,合理实现是见标记即本地踢除,不依赖拉取结果是否新鲜
  4. 主动通知(Agent 广播)完全旁路注册中心,连 ① 都绕开——Dubbo READONLY_EVENT 就是连接级带内信号,连注册中心都不经过;对 Eureka 路径这才是秒级手段
  5. 环境补记:公司“Eureka 服务端”实为 MSE Nacos 兼容层(8761),读路径是否照抄社区 readOnly 语义是开放问题(可直接问阿里云);生产主动通知=关,走的就是被动路径——生产无损下线的等待时长未实勘记录,需补记对账(是否盖住 30s 残留)
  6. 补证(主动通知 FAQ,2026-09-08):标记 = 响应中的特殊 header,消费者识别后即“拉黑”该节点;被动通知的隐含前提是下线窗口内(~30s)有请求流过——消费者流量稀疏(窗口内一笔请求都没有)时被动机制失效,这才是必须开主动通知的判据:低频被调用的服务是主动通知的第一批候选,高频服务被动方案已足够

当前应用只需要接入MSE,就会默认开启无损下线功能,但无损下线主动通知功能需要手动配置。同时MSE无损上下线功能提供可观测能力,帮助您判断应用是否无损下线成功。

p847390

  1. K8s 环境中接入 MSE 服务治理无损下线时,无损下线相关的操作会以lifecycle.preStop的形式注入到 Pod 中,并在 Pod 停机前得以执行,因此不建议您配置自定义的preStop
  2. 如果您的preStop中的行为是进行微服务注册中心下线,那么完全可以将其删除,并且使用无损下线自动注入的preStop来实现这一需求
  3. 如果您确实有一些业务上的优雅停机操作,并且已经为业务容器配置上了您自定义的preStop,那么无损下线preStop不再进行注入到您的业务容器中
    • 相应地,无损下线会注入一个名字为gracefulshutdownsidecar容器,并为该容器注入无损下线preStop,通过sidecar默认与业务容器共享网络命名空间的机制,在 Pod 停止前执行sidecarpreStop也能实现业务容器的无损下线
  4. 为了确保sidecar的preStop能够顺利执行完毕,您在业务容器自定义的preStop中,至少有 30 秒的 sleep 时间,保证在途请求被执行完毕
  5. sidecar 容器的资源消耗极低,目前配置为:CPU: 50m,Memory: 50Mi
  6. 由于无损下线K8s 场景下是基于 preStop 来实现的,目前只能支持 pod 正常停机场景的无损下线(如:缩容重启滚动升级等),不支持异常停机场景的无损下线(如:OOM kill

如何开启无损下线

  1. 如果您的应用部署在阿里云容器服务 ACK 环境下,在接入 MSE 服务治理后,无需额外执行开启无损下线的操作,无损下线功能默认自动开启
  2. 如果您的应用部署在阿里云 ECS 环境下,那么需要您在应用的停机脚本,加入下面的内容,并在脚本运行阶段靠前优先执行
1
curl http://127.0.0.1:54199/offline 2>/tmp/null; sleep 30;

无损下线观测

应用接入无损下线之后,在应用实例下线时,可以在应用治理界面观测到,下线实例的流量很短时间内被清空,QPS数据很快降为0

  1. 登录MSE治理中心控制台,并在顶部菜单栏选择地域
  2. 在左侧导航栏,选择治理中心 > 应用治理,然后单击目标应用的资源量卡片
  3. 在目标应用详情页面的左侧导航栏,单击流量治理,然后选择无损上下线页签

上下线概览处,可以看到最近一段时间内,该应用上下线过程中的发生的事件。在左侧上下线实例中找到想要查看的下线实例,单击该实例,可以在右侧看到,该实例在下线时执行了“无损下线”流程,并且在执行完该流程后,流量会被快速清空,一直到实例“停机”之前,已经不再有流量进入

如果观测到无损下线成功事件产生之后,QPS数据没有快速降为0,可以先确认是否存在本地调用等非来自于微服务请求调用的流量

当探针版本在 4.2.0 后才会上报“应用停机”事件,如果您未观测到该事件,可以考虑升级您的探针版本

如何开启主动通知

什么是主动通知

  1. 主动通知是无损下线功能模块提供的一个进阶功能,默认关闭
  2. 一般情况下,MSE无损上下线提供的下线方案已经可以解决大部分场景下的问题
  3. 而当,待下线应用使用Spring Cloud框架时,在应用下线时发现有消费者调用出错的情况,可以尝试开启该功能来解决
  4. 开启后,提供者实例在下线阶段将会主动通知服务消费者,通知后,服务消费者将不再请求该提供者实例

注意事项

MSE治理中心暂不支持如下应用无损下线:

  1. 暂不支持非Java应用体系的无损下线
  2. 暂不支持非WebFlux或SpringMVC应用的无损下线
  3. 暂不支持消费端非微服务应用的下游提供者应用的无损下线
  4. 需要消费者端提供端应用都接入MSE微服务治理,才能实现应用无损下线

由于无损下线流程中需要等待一定的时间来保证下线示例在途中的请求都能执行完毕,K8s容器中的terminationGracePeriodSeconds 参数值(默认值 30)需要大于30,建议配置为90

如果您使用默认的30,可能会存在应用shutdownhook无法正常执行完毕的情况,从而导致应用停机时一些资源无法正常关闭

开启无损下线主动通知

  1. 登录MSE治理中心控制台,并在顶部菜单栏选择地域
  2. 在左侧导航栏,选择治理中心 > 应用治理,然后单击目标应用的资源量卡片
  3. 在目标应用详情页面的左侧导航栏,单击流量治理,然后选择无损上下线页签
  4. 在配置信息模块,单击修改,然后单击无损下线折叠块,打开主动通知按钮,然后单击确定

基于自建Spring Cloud Gateway或Zuul网关实现全链路灰度

概述

  1. Spring Cloud GatewayZuul是两种常用的微服务架构中的API网关,它们均能实现路由转发过滤器处理等功能
  2. 通过配置路由规则,可以将请求路由到灰度环境中,对灰度版本进行验证和测试
  3. 借助MSE提供的全链路灰度能力,您无需修改业务代码,即可实现端到端全链路流量控制
  4. 本文介绍如何通过配置Spring Cloud Gateway或者Zuul网关实现全链路灰度

背景信息

  1. 本文通过模拟真实的调用链路为您演示MSE全链路灰度功能
  2. 无需修改任何业务代码,只需要给入口应用设置流量规则,该流量的标签会通过链路透传下一个灰度版本
  3. 每个应用调用过程中,符合金丝雀条件的流量会优先调用对应的版本,如果没有对应版本则会自动切换回基线版本(即稳定版本)
  4. 部署spring-cloud-gateway、spring-cloud-a、spring-cloud-b、spring-cloud-c这四个业务应用,以及注册中心Nacos Server
  5. 调用链路为:spring-cloud-gateway->A->B->C
  6. 应用之间的调用既包含了Spring Cloud服务调用,也包含了Dubbo服务调用

CAEQOhiBgIDZ4rSN6xgiIDFhZWE3Mzc4OTY0MzRiNzk4ZjM2MDE3MDcwNmRhMWYy4199635_20240201142154.554

全链路灰度提供了给流量染色,并让灰度流量优先调用灰度节点的能力,帮助您进行可控的灰度验证,保障稳定性。全链路灰度验证通常采用以下策略:

  1. 直接调用现有线上流量的一小部分进行测试,通常按照百分比控制
  2. 按照特定规则筛选线上流量进行验证,如使用指定的HeaderCookie

本文将分别介绍上述两种策略配置方式,以便适应微服务架构中不同的灰度发布需求

步骤一:将应用接入MSE微服务治理

ACK微服务应用接入MSE治理中心,您可以选择您需要的方式实现应用接入 - 为单个应用开启MSE微服务治理

  1. 进入集群工作负载-无状态应用页面,切换到应用的命名空间下
  2. 找到所接入的应用,点击「查看Yaml」
  3. 按以下格式编辑Labels,完成后点击「更新」
1
2
3
4
5
6
7
8
9
10
spec:
template:
metadata:
labels:
# 填写“on”表示开启接入,需加上双引号
msePilotAutoEnable: "on"
# 填写接入到的治理命名空间,值不存在可自动新建
mseNamespace: default
# 填写接入MSE的实际应用名称,需加上双引号
msePilotCreateAppName: "your-deployment-name"

步骤二:部署应用(模拟线上场景)

  1. 登录容器服务管理控制台,在左侧导航栏选择集群列表
  2. 在集群列表页面,单击目标集群名称,然后在左侧导航栏,选择工作负载 > 无状态
  3. 在无状态页面选择命名空间,然后单击使用YAML创建资源
  4. 本文示例中部署一个注册中心Nacos Server,然后部署spring-cloud-gateway、spring-cloud-a、spring-cloud-b、spring-cloud-c这四个业务应用。您也可以直接在Demo中获取对应的源码

注册中心 Nacos Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
apiVersion: apps/v1
kind: Deployment
metadata:
name: nacos-server
spec:
replicas: 1
selector:
matchLabels:
app: nacos-server
template:
metadata:
labels:
msePilotAutoEnable: "off"
app: nacos-server
spec:
containers:
- name: nacos-server
image: 'registry.cn-hangzhou.aliyuncs.com/mse-governance-demo/nacos-server:v2.1.2'
env:
- name: MODE
value: standalone
- name: JVM_XMS
value: 512M
- name: JVM_XMX
value: 512M
- name: JVM_XMN
value: 256M
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
initialDelaySeconds: 15
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 8848
timeoutSeconds: 3
readinessProbe:
failureThreshold: 5
initialDelaySeconds: 15
periodSeconds: 15
successThreshold: 1
tcpSocket:
port: 8848
timeoutSeconds: 3
resources:
requests:
cpu: '1'
memory: 2Gi
dnsPolicy: ClusterFirst
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: nacos-server
spec:
type: ClusterIP
ports:
- name: nacos-server-8848-8848
port: 8848
protocol: TCP
targetPort: 8848
- name: nacos-server-9848-9848
port: 9848
protocol: TCP
targetPort: 9848
selector:
app: nacos-server

spring-cloud-c 应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# Source: mse-simple-demo/templates/spring-cloud-c-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-c
spec:
replicas: 1
selector:
matchLabels:
app: spring-cloud-c
template:
metadata:
labels:
msePilotAutoEnable: "on"
msePilotCreateAppName: "spring-cloud-c"
app: "spring-cloud-c"
spec:
containers:
- name: spring-cloud-c
image: "registry.cn-hangzhou.aliyuncs.com/mse-governance-demo/spring-cloud-c:3.0.1"
imagePullPolicy: Always
env:
- name: nacos.host
value: "nacos-server"
- name: nacos.namespace
value: "public"
resources:
requests:
cpu: 1
memory: 2Gi
limits:
cpu: 1
memory: 2Gi
ports:
- name: http-port
containerPort: 20003
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
topologyKey: failure-domain.beta.kubernetes.io/zone
weight: 50
- podAffinityTerm:
topologyKey: kubernetes.io/hostname
weight: 100

spring-cloud-b 应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# Source: mse-simple-demo/templates/spring-cloud-b-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-b
spec:
replicas: 1
selector:
matchLabels:
app: spring-cloud-b
template:
metadata:
labels:
msePilotAutoEnable: "on"
msePilotCreateAppName: "spring-cloud-b"
app: "spring-cloud-b"
spec:
containers:
- name: spring-cloud-b
image: "registry.cn-hangzhou.aliyuncs.com/mse-governance-demo/spring-cloud-b:3.0.1"
imagePullPolicy: Always
env:
- name: nacos.host
value: "nacos-server"
- name: nacos.namespace
value: "public"
resources:
requests:
cpu: 1
memory: 2Gi
limits:
cpu: 1
memory: 2Gi
ports:
- name: http-port
containerPort: 20002
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
topologyKey: failure-domain.beta.kubernetes.io/zone
weight: 50
- podAffinityTerm:
topologyKey: kubernetes.io/hostname
weight: 100

spring-cloud-a 应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# Source: mse-simple-demo/templates/spring-cloud-a-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-a
spec:
replicas: 1
selector:
matchLabels:
app: spring-cloud-a
template:
metadata:
labels:
msePilotAutoEnable: "on"
msePilotCreateAppName: "spring-cloud-a"
app: "spring-cloud-a"
spec:
containers:
- name: spring-cloud-a
image: "registry.cn-hangzhou.aliyuncs.com/mse-governance-demo/spring-cloud-a:3.0.1"
imagePullPolicy: Always
env:
- name: nacos.host
value: "nacos-server"
- name: nacos.namespace
value: "public"
resources:
requests:
cpu: 1
memory: 2Gi
limits:
cpu: 1
memory: 2Gi
ports:
- name: http-port
containerPort: 20001
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
topologyKey: failure-domain.beta.kubernetes.io/zone
weight: 50
- podAffinityTerm:
topologyKey: kubernetes.io/hostname
weight: 100

spring-cloud-gateway 应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# Source: mse-simple-demo/templates/spring-cloud-gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-gateway
spec:
replicas: 1
selector:
matchLabels:
app: spring-cloud-gateway
template:
metadata:
labels:
msePilotAutoEnable: "on"
msePilotCreateAppName: "spring-cloud-gateway"
app: spring-cloud-gateway
spec:
containers:
- image: "registry.cn-hangzhou.aliyuncs.com/mse-governance-demo/spring-cloud-gateway:3.0.1"
imagePullPolicy: Always
env:
- name: nacos.host
value: "nacos-server"
- name: nacos.namespace
value: "public"
- name: enable.auto
value: "true"
- name: enable.rpc.invoke
value: "true"
- name: enable.sql
value: "false"
- name: enable.sentinel.demo.flow
value: "true"
name: "spring-cloud-gateway"
resources:
requests:
cpu: 1
memory: 2Gi
limits:
cpu: 1
memory: 2Gi
ports:
- containerPort: 20000
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
topologyKey: failure-domain.beta.kubernetes.io/zone
weight: 50
- podAffinityTerm:
topologyKey: kubernetes.io/hostname
weight: 100
---
# Source: mse-simple-demo/templates/spring-cloud-gateway-service.yaml
apiVersion: v1
kind: Service
metadata:
annotations:
service.beta.kubernetes.io/alibaba-cloud-loadbalancer-spec: slb.s1.small
service.beta.kubernetes.io/alicloud-loadbalancer-address-type: internet
name: spring-cloud-gateway-slb
spec:
ports:
- port: 80
protocol: TCP
targetPort: 20000
selector:
app: spring-cloud-gateway
type: LoadBalancer

执行以下命令查看部署结果:

1
kubectl get svc,deploy

预期输出:

1
2
3
4
5
6
7
8
9
10
NAME                               TYPE           CLUSTER-IP   EXTERNAL-IP  PORT(S)             AGE
service/kubernetes ClusterIP 172.16.x.x <none> 443/TCP 23h
service/nacos-server ClusterIP 172.16.x.x <none> 8848/TCP,9848/TCP 94s
service/spring-cloud-gateway-slb LoadBalancer 172.16.x.x 8.130.x.x 80:32641/TCP 57s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nacos-server 1/1 1 1 94s
deployment.apps/spring-cloud-a 1/1 1 1 66s
deployment.apps/spring-cloud-b 1/1 1 1 74s
deployment.apps/spring-cloud-c 1/1 1 1 83s
deployment.apps/spring-cloud-gateway 1/1 1 1 57s

步骤三:部署spring-cloud-c、spring-cloud-a应用的灰度版本

登录容器服务管理控制台。使用如下YAML部署spring-cloud-c应用的灰度版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Source: mse-simple-demo/templates/spring-cloud-c-gray-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-c-gray
spec:
replicas: 1
selector:
matchLabels:
app: spring-cloud-c-gray
version: gray
template:
metadata:
labels:
msePilotAutoEnable: "on"
msePilotCreateAppName: "spring-cloud-c"
alicloud.service.tag: gray
app: "spring-cloud-c-gray"
version: gray
spec:
containers:
- name: spring-cloud-c
image: "registry.cn-hangzhou.aliyuncs.com/mse-governance-demo/spring-cloud-c:3.0.1"
imagePullPolicy: Always
env:
- name: nacos.host
value: "nacos-server"
- name: nacos.namespace
value: "public"
resources:
requests:
cpu: 1
memory: 2Gi
limits:
cpu: 1
memory: 2Gi
ports:
- name: http-port
containerPort: 20002
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
topologyKey: failure-domain.beta.kubernetes.io/zone
weight: 50
- podAffinityTerm:
topologyKey: kubernetes.io/hostname
weight: 100

使用如下YAML部署spring-cloud-a应用的灰度版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Source: mse-simple-demo/templates/spring-cloud-a-gray-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-a-gray
spec:
replicas: 1
selector:
matchLabels:
app: spring-cloud-a-gray
version: gray
template:
metadata:
labels:
msePilotAutoEnable: "on"
msePilotCreateAppName: "spring-cloud-a"
alicloud.service.tag: gray
app: "spring-cloud-a-gray"
version: gray
spec:
containers:
- name: spring-cloud-a
image: "registry.cn-hangzhou.aliyuncs.com/mse-governance-demo/spring-cloud-a:3.0.1"
imagePullPolicy: Always
env:
- name: nacos.host
value: "nacos-server"
- name: nacos.namespace
value: "public"
resources:
requests:
cpu: 1
memory: 2Gi
limits:
cpu: 1
memory: 2Gi
ports:
- name: http-port
containerPort: 20001
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
topologyKey: failure-domain.beta.kubernetes.io/zone
weight: 50
- podAffinityTerm:
topologyKey: kubernetes.io/hostname
weight: 100

步骤四:创建灰度环境泳道组

  1. 登录MSE治理中心控制台,并在顶部菜单栏选择地域
  2. 在左侧导航栏,选择
  3. 全链路灰度页面顶部,选择微服务命名空间
    • 您选择的微服务命名空间内如果没有泳道组,请单击创建泳道组及泳道;如果已存在泳道组,则请单击+创建泳道组
  4. 在创建泳道组面板,单击+ 创建泳道组,在创建泳道组页面,设置如下相关配置,然后单击确定
配置项 说明
泳道组名称 自定义泳道组的名称
入口类型 选择Java服务网关
入口应用 选择spring-cloud-gateway
泳道组涉及应用 选择spring-cloud-aspring-cloud-bspring-cloud-c

步骤五:创建灰度环境泳道

说明

  1. 使用全链路灰度功能时,需要您给灰度应用添加一个特殊的tag标记,以便将这些节点和其他节点区分开来
    • 容器环境下需要您在spec.template.metadata.labels下增加alicloud.service.tag: ${tag}信息
    • ECS 环境下需要添加Java启动参数-Dalicloud.service.tag=${tag}
  2. Java网关作为全链路灰度入口时,MSE支持两种泳道模式
    • 请求内容灰度
      • 当请求内容可以用于灰度识别时,建议采用此种灰度模式
      • 如果不可以,也强烈建议您通过系统改造增加此类灰度标识,以便获得更好的灰度效果
      • 比如,可以保持灰度请求前后一致性
    • 比例路由灰度
      • 当请求内容无法用于灰度识别且遗留系统也无法进行改造时,可以采用此种退化的灰度模式
      • 该模式有一定弊端,可能导致同一来源的请求进入不同泳道,而造成灰度请求前后行为的不一致
  3. 泳道路由模式在一个泳道组中需要保持一致
    • 您只有在创建泳道组中第一条泳道时可以调整网关路由规则Path泳道路由模式

步骤

  1. 全链路灰度页面底部,单击点击创建第一个分流泳道
  2. 如果您选择的微服务空间内已经创建过泳道,则单击创建泳道
  3. 创建泳道面板,设置流控泳道相关配置,然后单击确定
配置项 说明
配置节点标签 需要您手工给您的灰度应用节点打上标签,用来和正常节点做区分
填写泳道信息 泳道标签:该泳道内的匹配的流量去往的目标标签
确认匹配关系:检查您配置了该标签的应用节点数是否符合预期
配置路由和灰度规则 设置流量进入该泳道的规则
1. 输入Path:若为则代表匹配所有路径
2. 灰度模式:选择按比例灰度
3. 流量比例:30%

您还可以为每条网关path分别设置不同的流量比例,开启该功能时,您需要注意该每条path当前在所有泳道组中配置的流量比例总和不应超过100%

创建按请求内容路由的泳道

  1. 配置节点标签
    • 需要您给灰度应用节点打上标签,用来和正常节点做区分
  2. 填写泳道信息
    • 泳道标签:该泳道内所匹配的流量去往的目标标签。本示例将泳道目标标签设置为gray
    • 确认匹配关系:检查您配置了该标签应用节点数是否符合预期。
  3. 配置路由灰度规则
    • 输入Path。若为则代表匹配所有路径
    • 灰度模式:选择按内容灰度
    • 灰度条件:新增规则条件,选择以下条件同时满足
      • 本示例设置灰度条件为:请求Parameter名为name,值为xiaoming。配置如下:
      • 参数类型:Parameter
      • 参数:name
      • 条件:==
      • :xiaoming

创建按比例路由的泳道

请确保MSE Java Agent的版本为3.2.3及以上,否则会影响百分比灰度能力

  1. 配置节点标签
    • 需要您给灰度应用节点打上标签,用来和正常节点做区分
  2. 填写泳道信息
    • 泳道标签:该泳道内所匹配的流量去往的目标标签。本示例将泳道目标标签设置为gray
    • 确认匹配关系:检查您配置了该标签应用节点数是否符合预期。
  3. 配置路由灰度规则
    • 输入Path。若为则代表匹配所有路径
    • 灰度模式:选择按比例灰度
    • 流量比例:30%。

完成泳道创建后,在全链路灰度流量分配区域,可以查看泳道详情,还可以进行如下操作

  1. 操作列,选择开启,创建的泳道将会生效,即流量会按照泳道方式进行流转
  2. 满足规则的流量优先流向标记有当前泳道对应标签应用版本,如果没有对应标签的应用版本,则流向未打标的应用版本
  3. 操作列,选择关闭,关闭创建的泳道,即该应用往后的流量会流向未打标的应用版本。

image-20260908174236217

步骤六:测试基线及灰度版本流量

测试按请求内容路由的泳道

使用curl命令测试基线流量:命令中的8.130.x.x是Spring Cloug Gateway暴露出的公网IP地址。

1
2
curl 8.130.x.x/A/a
A[192.168.x.x][config=base] -> B[192.168.x.x] -> C[192.168.x.x]

使用curl命令测试灰度流量:

1
2
curl 8.130.x.x/A/a?name=xiaoming
Agray[192.168.x.x][config=base] -> B[192.168.x.x] -> Cgray[192.168.x.x]
  1. 当参数带上name=xiaoming时,会命中灰度标签,并向后透传
  2. 比如灰度请求到A应用和C应用时,会请求到AgrayCgray节点。
  3. 请求到B应用时,由于不存在Bgray节点,仍然会请求B的基线节点

测试按比例路由的泳道

通过如下Python3脚本测试按比例路由的分流情况(需要安装requests包)。注意将x.x.x.x替换为spring-cloud-gateway网关的入口SLB地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# pip3 install requests
# python3 traffic.py
import requests
TOTAL_REQUEST = 100
ENTRY_URL = 'http://x.x.x.x/A/a'
def parse_tag(text:str):
'''
A[10.0.23.64][config=base] -> B[10.0.23.65] -> C[10.0.23.61]
Agray[10.0.23.64][config=base] -> B[10.0.23.65] -> Cgray[10.0.23.61]
Ablue[10.0.23.64][config=base] -> B[10.0.23.65] -> Cblue[10.0.23.61]
'''
print(text)
app_parts = text.split(' -> ')
# tag_app: C[10.0.23.61] / Cgray[10.0.23.61]
tag_app = app_parts[-1]
splits = tag_app.split('[')
# tag_part: C / Cgray
tag_part = splits[0]
tag = tag_part[1:]
return tag if len(tag) > 0 else 'base'
def get_tag(url:str):
resp = requests.get(url)
resp.encoding = resp.apparent_encoding
return parse_tag(resp.text)
def cal_tag_count(url:str, total_request:int):
count_map = {}
for i in range(total_request):
tag = get_tag(url)
if tag not in count_map:
count_map[tag] = 1
else:
count_map[tag] += 1
print()
print('Total Request:', total_request)
print('Traffic Distribution:', count_map)
if __name__ == '__main__':
cal_tag_count(ENTRY_URL, TOTAL_REQUEST)

运行以上脚本后,约有30%的流量会去往灰度环境。

步骤七:可观测

若应用出现异常,您可以通过MSE提供的可观测能力查看异常数据,帮助您快速定位问题。

微服务治理可观测

在MSE微服务治理的全链路灰度页面,单击目标应用,在应用 QPS 监控区域,可查看对应泳道基线版本灰度版本的流量情况

p855116

  • 总QPS:该应用总的QPS。
  • 异常QPS:该应用出错的请求数。
  • GrayQPS:该应用的灰度版本的QPS。

无损上下线常见问题

小流量预热的原理是什么?为什么小流量预热需要提供者、消费者都接入服务治理?

  1. 消费者对某个服务进行调用时,会对该服务的提供者进行选择
  2. 提供者应用开启小流量预热的情况下,服务治理对这里“选择提供者”的过程进行了增强
  3. 消费者选择提供者时,会计算每个提供者的权重大小(0%~100%),权重越大的提供者节点,被选择和调用的概率就会越高
  4. 开启小流量预热的提供者在刚启动时,消费者对其计算的权重很低,因此预热的节点被调用的概率也会变低
  5. 随着时间增加,这个权重计算结果会不断增大,最终达到100%,达到100%时,预热流程结束了,开始和正常节点一样接收流量
  6. 提供者会在服务注册元数据中,带上自身启动的时间,以供消费者进行权重计算
  7. 这个过程需要提供者消费者双方都开启服务治理才能实现

重要

  1. 小流量预热的“开始”会在应用收到第一笔请求后触发,当预热过程已经达到配置的预热时长后(默认120秒),小流量预热结束
    • 如果应用一直没有收到外部流量,则不会触发预热的开始
  2. 小流量预热触发前提是有外部流量进来,这就要求服务已经完成了注册
    • 如果您发现应用还未进行服务注册,却已经开始了小流量预热(即您在控制台观察到预热开始事件出现在服务注册事件之前)

为什么我的预热曲线不符合预期?该如何解决?

正常情况下小流量预热时,应用的QPS曲线图如下:

p847207

但是有些时候由于不合理不支持的使用场景,进行小流量预热的应用,其QPS曲线图形状会不太符合预期(缓慢上升),下面是两种常见的不符合预期的情况

QPS 曲线中途出现陡升现象

p847211

  1. 这种现象一般发生在服务发布的场景,如果在服务发布时,新节点的预热还没有达到指定时长老节点就被下线了,那么消费者端在选择提供者时,就无法实现控制新节点被“低概率”调用到了
  2. 所以会在 QPS 曲线图中看到,某个新节点的 QPS 曲线在前半段时缓慢上线的态势,达到某个时间节点后,老节点被全部下线,QPS 曲线就出现陡升现象了

QPS 曲线未呈现缓升趋势

p847214

  1. 出现这种现象,建议检查对该应用发起请求的消费者应用已经接入服务治理,如果消费者端没接入,则考虑将消费者应用都接入服务治理,就可以解决该问题
  2. 如果您需要预热的应用,其流量来自外部(比如Java网关),这种场景下消费者是没有接入服务治理的,小流量预热也无法支持这种场景

小流量预热的最佳实践是什么?

滚动发布的情况下,经常会出现预热不充分的问题。您可以参考以下实践,来保证服务预热达到预期效果

  1. 配置最小准备时间(推荐)
    • 您可以为工作负载配置.spec.minReadySeconds来控制pod就绪后达到可用状态时的时间间隔,并且设置该参数的值大于pod的小流量预热时长,以使得K8s等待 pod 预热完毕后继续滚动发布
    • 如果您使用的是ACK,您可以直接在容器平台上找到您的应用,在 更多 > 升级策略 > 滚动升级 > 最小准备时间(minReadySeconds) 中直接设置。
    • 设置 minReadySeconds 可以在发布时,让新启动的pod状态达到ready并维持固定时长之后,才会继续发布
  2. 使用分批发布(推荐):您可以考虑使用OpenKruise等方式,实现工作负载分批发布,并且控制每个批次发布时的时间间隔大于小流量预热时长,保证批次内的新节点预热充分后,再继续发布一下次批次的节点

此外,延长Readiness就绪检测的初始探测时间(不推荐),也是一种方式,您可以增加工作负载Readiness首次探测延迟时长initialDelaySeconds),并且大于小流量预热时长延迟注册时长应用启动时间三者之。注意,应用启动时间一般需要观测实际日志输出来得出,并且随着业务发展,应用的启动时间也会随之变化;此外,延迟 Readiness 通过的时间,也会导致新启动的节点迟迟无法被加入到 K8s Service的Endpoint中,因此我们不推荐您使用这种方式来保证预热效果达到最佳。

如果您按照最佳实践进行操作后,发现预热QPS曲线仍然不符合预期,可以考虑应用接收的流量,是否都来自于已经接入服务治理的消费者应用,如果有消费者没有接入服务治理,或者存在来自于外部负载均衡的调用流量,那么应用预热时的QPS曲线图也会不符合预期。

三种实践对比与决策图

测试环境实勘(2026-09-08,xp_hd1_ack_test 全部 2898 个 Deployment):设置 spec.minReadySeconds
的仅 1 个——kube-system/ack-koord-manager(阿里云组件自带,3s,与预热无关),业务侧 0/2897
同时未安装 Kruise/OpenKruise(无相关 CRD),分批发布路径也缺位;logan ns 滚动策略普遍
surge=25% / unavail=0~25%minReady=0。即两条推荐实践都没在用:滚动节奏完全由 Ready 推进,
MSE 预热 120s 期间老节点照常成批退出——上文 FAQ 说的”QPS 曲线中途陡升”是公司滚动发布的默认形态
低成本补法(零代码零组件):ACK 控制台 升级策略>滚动升级>最小准备时间设 150s(> 预热 120s),
代价仅是每批 ready 后多等 150s、发布总时长拉长。生产集群无 kubeconfig 未勘,ACK 控制台只读确认即可。

55199/readiness是做什么的?为什么不配置55199/readiness会有流量跌0的风险?

  1. 55199/readiness是MSE微服务治理提供的一套内置的、HTTP类型就绪检查端口,当应用的K8s就绪检查配置成55199/readiness时,在新节点上线阶段,如果该节点尚未完成服务注册,则就绪检查返回500;如果该节点已经完成服务注册,则就绪检查返回 200
  2. 按照 K8s 默认的发布策略,新节点没有就绪,老节点就不会下线
    • 当就绪检查配置了55199/readiness之后,新节点完成服务注册之后,才会进入就绪状态,即只有在新节点完成服务注册的情况下,老节点才会下线
    • 这样就会保证注册中心上该服务一直会有可用的节点
    • 如果您不配置55199/readiness,可能会在服务发布时,新节点尚未注册,老节点就被下线,进而导致注册中心上该服务没有可用节点,进而导致该服务的所有消费者在调用时因为没有提供者而发生报错,从而产生该服务的流量跌0
    • 因此我们强烈建议您开启无损上线,并为应用配置55199/readiness就绪检测
  3. 如果您的应用所使用的探针版本低于4.1.10,就绪检测的路径需要配置成/health,而非 /readiness。查看探针版本方式:MSE 控制台 > 治理中心 > 应用治理 > 单击对应的应用 > 节点详情,右侧可以看到探针版本。

公司现状实勘与统一编排的落地路径(2026-09-08)

测试环境实勘(logan ns 83 个 Deployment)55199/readiness 使用数 0;统一编排模板口径 =
readinessProbe GET /health:8080(49/83 此形态,另见 5678/9999 等变体、5 个无探针)+ initialDelay 60s。
/health(业务口 actuator)语义 = Spring 上下文健康,≠ 已注册——web 服务先于注册收尾起来,
/health 可能提前 200;消费者看得到新节点还要再等 Eureka 传播(readOnly ~30s + 拉取 10s)。
initialDelay 只盖启动,不盖这段空窗

组合风险(比裸跑更危险的形态):无损上线开启(延迟注册 20s,生产 xp-biz-boot 实测值)+ readiness
仍是业务口 /health——K8s 在 /health 200 就推进滚动下老节点,而新节点注册被刻意推迟 20s + 传播 40s
≈ 60s,老节点 preStop sleep 只有 40s,理论空窗 ~20s。“开了延迟注册却不换门”= 把窗口拉大而门没换。
现有兜底:`unavail=0
1% 先扩后缩(Endpoint 容量不掉)+ preStop DOWN + sleep40 + KickOut——自研体系的 合理性正在于此;55199/readiness` 是把门从“上下文健康”换成“注册完成”。

统一编排下的落地(不是做不到,是改模板)——preStop 脚本进基础镜像、grace=60、/health 探针统一
都是模板下发,同一条路再走一次:

动作 内容 备注
① 无损规则进模板 Pod labels:mse.lossless.enable / delayTime / warmupTime / notice使用YAML配置无损上下线 YAML 优先级高于控制台(重启即覆盖控制台改动)→ 配置主权留在模板,天然防控制台散改,对统一编排是完美属性;配置变更会触发应用重启
② 换门 readinessProbe → GET /readiness:55199 Agent ≥4.1.10 才用 /readiness 路径(沙箱 4.4.0 / 生产 4.7.0 均 ✅);探针变更触发滚动重启,选发布窗口
③ 更优组合 startupProbe=业务 /health(管“起来没”)+ readiness=55199(管“注册没”) 顺带消灭 initialDelay 60/120 魔数——冷启动时长漂移的痛点(沙箱实测 114s)
④ 联动修正 业务口探针请求会触发“预热开始”事件(第一笔外部请求语义,见下节 FAQ) 换 55199 后探针打 Agent 端口,预期不再计入业务请求触发预热(待实验验证);或按 FAQ 用 env profile_micro_service_record_warmup_ignored_path 忽略 /health

沙箱 lab 是手写 YAML,不受编排约束——A‘ 对照实验可做:lab-provider readiness /b:20002(现状)
vs 55199/readiness,滚动重启对比控制台事件顺序(服务注册 ↔ Readiness 通过)与 K8s Ready 时间戳;
lab 的无损配置也可直接用 mse.lossless.* labels 声明式下发,比控制台点选更贴近公司模板形态。

为什么应用先出现预热事件后出现服务注册事件?如何解决?

  1. 在当前版本下,服务收到了第一笔外部请求时,就会开始预热流程,并且上报预热开始事件
  2. 而有些时候,应用收到的第一笔请求,未必是微服务调用请求,因此这种请求也不会触发业务逻辑的预热
  3. 比如应用的工作负载配置了K8s的Liveness探针,在新节点上线时,即便还没有进行服务注册操作,只要K8s对Liveness进行了探测,就会判定预热已经开始
  4. 为了避免这种情况, 您可以在提供者应用工作负载的环境变量中,配置如下参数,来忽略这些请求对预热逻辑的触发:
1
2
# 忽略路径为 /xxx、/yyy/zz 的请求对预热流程的触发
profile_micro_service_record_warmup_ignored_path="/xxx,/yyy/zz"

主动通知是做什么的?什么时候需要开启主动通知?

  1. 主动通知是无损下线功能模块提供的一种进阶能力,该功能可以在SpringCloud提供者下线时,让提供者主动发起一次网络请求到服务消费者,告知其自身已经下线
    • 消费者收到通知后,不会再对该节点进行调用
    • 一般情况下,当提供者消费者都使用SpringCloud框架时,消费者本地缓存提供者节点列表
    • 在某些场景下,即便消费者收到注册中心的通知,也可能没有及时刷新本地缓存,进而导致消费者对下线的节点仍然发起调用
    • 主动通知则很好地解决了这个问题
  2. 主动通知功能默认关闭,因为开启服务治理之后,默认的无损下线方案中,下线阶段的提供者收到请求时,会在响应中加入一个特殊的header,消费者收到响应时会识别该header,并且不再调用该提供者节点
    • 因此,只要在提供者下线时,消费者有流量到达下线的提供者,就会感知到该提供者已经下线,并且会自动将其“拉黑
    • 而如果在提供者下线的这段时间内(一般30秒左右),消费者没有请求到达正在下线的提供者,消费者就有可能未感知到该提供者节点已经下线
      • 有可能会在提供者刚好走完下线流程、即将停机时,消费者请求正好过来,此时就会出现请求报错的问题
      • 这个时候,就需要开启主动通知。换句话说,如果消费者的流量非常“稀疏”,就建议您为提供者开启主动通知的功能。

为什么已经看到无损下线事件后,流量还是没有快速降为0?

一般情况下,看到无损下线事件后,流量会在短时间内快速降为0。如果没有降为0,可能的原因和解决方案如下:

  1. 该应用收到了非微服务方式的调用,比如收到来自外部负载均衡器的流量,或者存在本地脚本定时任务等调用方式产生了流量
    • 目前无损上下线只支持治理内部微服务调用的流量,上述场景并不在无损上下线功能支持的范围之内
    • 建议您根据这些基础设施、框架提供的优雅下线特性来定制相应的解决方案
  2. 该应用需要开启主动通知,但是没有开启
    • 建议您开启主动通知后再次观测下线曲线是否符合预期
  3. 该应用使用的框架版本不在支持的范围之内,服务治理无损上下线支持的框架可以参考:微服务治理支持的Java框架
    • Spring MVC、Spring Boot、Spring Cloud、Feign
    • 如果您发现应用使用的框架版本不再支持版本之中,可以考虑对应用的框架版本进行升级。

应用接入无损上下线后,现在发版时间非常长,怎么办?

您可以通过如下步骤检查应用是否开启过【通过就绪检查前完成服务预热】。

  1. 登录MSE治理中心控制台,并在顶部菜单栏选择地域。
  2. 在左侧导航栏,选择治理中心 > 应用治理。
  3. 在应用列表页面,单击目标应用,选择流量治理 > 无损上下线页签。
  4. 在无损上下线页签中,使用 F12 按键打开网页调试面板。在Network一栏中,搜索请求 GetLosslessRuleByApp (如果没看到可以刷新一下页面),在 Response 中,可以看到 Data 中 Related 字段值是否为 true。如果为 true 的话说明应用很久之前开启过【通过就绪检查前完成服务预热】这一功能(该功能目前已经不再提供),这个功能在开启的情况下可能会导致发版时间变长,建议您提交工单来关闭该功能。

MSE 服务治理提供的 55199/readiness 是做什么的?为什么有时候 mse readiness 一直不通过?

K8s 提供了三种可选的探针检查,分别是启动探针,存活探针,就绪探针:

  • 启动探针:用于检测应用是否启动成功。仅在 Pod 启动阶段进行探测,如果在 Pod 启动阶段,多次探测失败并达到配置的失败阈值时,会触发 Pod 重启
  • 存活探针:用于检测应用当前是否存活。会在启动探测成功后开始探测,探测会伴随 Pod 整个生命周期。多次探测失败并达到配置的失败阈值时,会触发 Pod 重启
  • 就绪探针:用于检测应用当前状态是否就绪。会在启动探测成功后开始探测,探测会伴随 Pod 整个生命周期。多次探测失败并达到配置的失败阈值时,K8s 会将 Pod 的状态置为 not ready,但不会触发 Pod 的重启
    • 在应用发布时,就绪探针也可以用来控制发布的节奏,按照默认的发布策略,如果新启动的 Pod 一直没有达到 ready 状态,K8s 会暂停发布流程,等待新 Pod 状态变为 ready。

在您的服务接入 MSE 服务治理后,您可以使用 MSE 服务治理内置的 55199/readiness 来提供服务就绪检测配置。

配置后K8s 的就绪探测会在应用完成服务注册后才会通过,这样可以保证发布过程中,新拉起的 Pod 都已经完成了微服务注册中心的注册,老的 Pod 才会被 K8s 下线并且进行注册中心注销,让服务的调用方一直有可用的节点去发起调用,避免出现无可用服务的异常。关于为什么需要配置 mse readiness,可参见服务注册状态检查。

如果您 mse readiness 一直没有通过,一般有三种可能的原因:

  1. 当前服务是否未开启无损上线。未开启无损上线的情况下,mse 55199/readiness 接口不会开放,所以 readiness 检查不会通过。
  2. 当前应用没有接入服务治理。可以检查服务治理探针目录下是否存在探针日志。K8s 环境下探针目录默认为 /home/admin/.opt/AliyunJavaAgent/home/admin/.opt/ArmsAgent目录。如果目录下没有 logs 目录,说明应用服务治理接入失败,请提交工单联系我们。
  3. 当前应用因为启动探针存活探针失败达到阈值,一直在重启应用。因为应用没有启动完毕,所以 mse readiness 也不会通过。可以后台检查一下 Pod 的 K8s 事件,是否存在启动探针或存活探针失败相关的事件。

自建Eureka注册中心迁移到MSE Nacos

使用限制

  1. 迁移工具宕机会导致同步服务中断,因此建议最少部署2个节点。迁移流程启动后,请您尽快完成迁移操作
  2. 确保自建Eureka、迁移工具和MSE Nacos三者之间的网络互相联通

迁移步骤

迁移的部署结构如下所示。

p500789

Eureka

Eureka原生服务类型。

此同步实例类型需要将Nacos注册的服务名小写。因为Eureka默认注册的服务名大写,但通过同步工具MSE Sync将服务同步到Nacos时,默认会将服务名转换成小写。如果原服务名中有大写字母,同步到Nacos的服务可能不互通。

例如,服务Service-1注册到Eureka的服务名是SERVICE-1,通过MSE Sync同步到Nacos的服务名是service-1,如果客户端使用nacosSDK之后注册到Nacos的服务名是Service-1,那么service-1Service-1在Nacos中其实是两个服务,即服务中的实例信息不互通,Nacos注册的实例无法发现注册到MSE Sync同步到Nacos上的实例。若将所有的服务名改成小写,MSE Sync会将从Eureka同步的服务名转化成小写,这样两侧服务就能够互通了。