微服务灰度发布的底层实现?
文章目录
微服务中的灰度发布(又称为金丝雀发布)是一种持续部署策略,它允许在正式环境的小部分用户群体上先部署新版本的应用程序或服务,而不是一次性对所有用户同时发布全新的版本。
这种方式有助于在生产环境中逐步验证新版本的稳定性和兼容性,同时最小化潜在风险,不影响大部分用户的正常使用。
1.灰度发布关键步骤
在 Spring Cloud 微服务架构中,实现灰度发布通常涉及到以下几个方面:
-
流量分割
- 根据一定的策略(如用户 ID、请求头信息、IP 地址等)将流入的请求分配给不同版本的服务实例。
- 使用 Spring Cloud Gateway、Zuul 等 API 网关组件实现路由规则,将部分请求定向至新版本的服务节点。
-
版本标识
- 新版本服务启动时会注册带有特定版本标签的服务实例到服务注册中心(如 Eureka 或 Nacos)。
- 请求在路由时可以根据版本标签选择相应版本的服务实例。
-
监控与评估
- 在灰度发布的阶段,运维团队会对新版本服务的性能、稳定性以及用户体验等方面进行实时监控和评估。
- 如果新版本表现良好,则可以逐渐扩大灰度范围直至全面替换旧版本。
-
故障恢复与回滚:若新版本出现问题,可通过快速撤销灰度发布策略,使所有流量恢复到旧版本服务,实现快速回滚,确保服务整体可用性。
通过 Spring Cloud 的扩展组件和自定义路由策略,开发人员可以轻松实现灰度发布功能,确保在微服务架构中安全、平滑地进行版本迭代升级。
2.实现思路
灰色发布的常见实现思路有以下几种:
- 根据用户划分:根据用户标识或用户组进行划分,在整个用户群体中只选择一小部分用户获得新功能。
- 根据地域划分:在不同地区或不同节点上进行划分,在其中的一小部分地区或节点进行新功能的发布。
- 根据流量划分:根据流量的百分比或请求次数进行划分,只将一部分请求流量引导到新功能上。
而在生产环境中,比较常用的是根据用户标识来实现灰色发布,也就是说先让一小部分用户体验新功能,以发现新服务中可能存在的某种缺陷或不足。
3.底层实现
Spring Cloud 全链路灰色发布的关键实现思路如下图所示:
灰度发布的具体实现步骤如下:
- 前端程序在灰度测试的用户 Header 头中打上标签,例如在 Header 中添加“gray-tag: true”,其表示要进行灰常测试(访问灰度服务),而其他则为访问正式服务。
- 在负载均衡器 Spring Cloud LoadBalancer 中,拿到 Header 中的“gray-tag”进行判断,如果此标签不为空,并等于“true”的话,表示要访问灰度发布的服务,否则只访问正式的服务。
- 在网关 Spring Cloud Gateway 中,将 Header 标签“gray-tag: true”继续往下一个调用服务中传递。
- 在后续的调用服务中,需要实现以下两个关键功能:
- 在负载均衡器 Spring Cloud LoadBalancer 中,判断灰度发布标签,将请求分发到对应服务。
- 将灰度发布标签(如果存在),继续传递给下一个调用的服务。
经过第四步的反复传递之后,整个 Spring Cloud 全链路的灰度发布就完成了。
4.具体实现
4.1 版本标识
在灰度发布的执行流程中,有一个核心的问题,如果在 Spring Cloud LoadBalancer 进行服务调用时,区分正式服务和灰度服务呢?
这个问题的解决方案是:在灰度服务既注册中心的 MetaData(元数据)中标识自己为灰度服务即可,而元数据中没有标识(灰度服务)的则为正式服务,以 Nacos 为例,它的设置如下:
yaml
代码解读
复制代码spring:
application:
name: gray-user-service
cloud:
nacos:
discovery:
username: nacos
password: nacos
server-addr: localhost:8848
namespace: public
register-enabled: true
metadata: {
"gray-tag":"true" } # 标识自己为灰度服务
4.2 负载均衡调用灰度服务
Spring Cloud LoadBalancer 判断并调用灰度服务的关键实现代码如下:
java
代码解读
复制代码private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances,
Request request) {
// 实例为空
if (instances.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + this.serviceId);
}
return new EmptyResponse();
} else {