灰度路由 : 自研一个 Spring Cloud starter 灰度路由 组件,实现动态灰度进阶

本文 的 原文 地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:

如果要你设计一个灰度发布组件,你会如何设计它的整体架构?

你们的灰度发布是如何设计的?发布的时候出现问题,如何 进行版本的回滚?

最近又有小伙伴在面试阿里、网易,都遇到了相关的面试题。

很多小伙伴回答了一些边边角角,但是回答不全面不体系,面试官不满意,面试挂了。

借着此文,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,展示一下雄厚的 “技术肌肉、技术实力”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提,offer自由”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V140版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取

自研一个通用Spring Cloud 灰度路由 组件,实现动态灰度发布 进阶应用

服务发现与负载均衡组件,是实现接口请求在众多服务实例间精准路由的关键。

然而,传统的负载均衡策略,如轮询(Round-Robin)或随机(Random),虽然简单高效,却也像一位“一视同仁”的舵手,无法应对日益复杂的生产环境需求。

有一个头疼的灰度场景:一个关键服务即将发布新版本,需要 先让一小部分真实流量进入新版本进行“实弹演习”,验证其稳定性 。而不是 “一刀切”式的流量调度方式。

传统的负载均衡,用的是一种静态、均等的策略,去管理一个动态、异构的服务集群。

当我们需要进行 A/B 测试、蓝绿部署、金丝雀发布(灰度发布)时,传统的负载均衡策略便显得力不从心。

我们需要一个自研 Spring Cloud LoadBalancer 灰度负载均衡组件,它能根据预设的规则,将请求流量按比例、按版本、按区域进行精妙的调度。

本文 将从剖析 Spring Cloud LoadBalancer 的核心机制出发,最终亲手实现一个企业级的、基于 Nacos 权重的动态灰度发布策略。

这不仅是一次负载均衡器的开发实践,更是一场关于微服务治理、风险控制与平滑发布的深度思考。

全文大纲

为了更好地规划这次流量探索之旅,我们先通过一张蓝图来了解将要经历的各个阶段。

第一章: 为啥 需要 一个自研 灰度负载均衡组件

在微服务实践的早期,我们最常使用的负载均衡策略是轮询。

它简单、公平,将请求依次分发给后端的每一个服务实例,如同一个尽职尽责的交通警察,让每条车道都承载相同的车流。然而,随着业务的快速迭代和微服务架构的深入,新的问题开始浮现。

想象一下,你的团队刚刚完成了一个核心交易接口的重大重构。

尽管经过了多轮测试,但谁也无法百分之百保证它在真实、复杂的生产流量下能完美运行。

此时,最稳妥的方式是进行金丝雀发布(或称灰度发布):先将 1% 的线上流量导入新版本的服务实例,观察其 CPU、内存、错误率等核心指标。如果一切正常,再逐步将流量比例提升到 10%、50%,直至 100%,最终完成新版本的平滑上线。

在这个场景下,轮询或随机策略 不灵了。

它们无法识别服务实例的版本差异,更无法按需分配流量比例。

这使得我们陷入了一个两难的境地:要么承担巨大风险进行全量发布,要么为了追求绝对安全而大大降低发布效率。
为了打破这一僵局,我们必须赋予负载均衡器“智能”,让它能够“看懂”服务实例的元数据(如版本、区域、环境等),并根据我们下发的“指令”(流量策略)进行决策。

第二章: 深入 Spring Cloud LoadBalancer 的内部世界

在构建我们自己的智能负载均衡策略之前,我们必须先理解 Spring Cloud LoadBalancer 的工作原理,因为它是我们所有自定义策略的基石。

自 Spring Cloud 2020.0.0 版本(代号 Ilford)开始,Netflix Ribbon 被正式移除,Spring Cloud LoadBalancer 成为了官方推荐的客户端负载均衡解决方案。

它以更简洁、更灵活、全面拥抱响应式编程(Project Reactor)的姿态,为我们提供了强大的扩展能力。

架构三大核心组件

Spring Cloud LoadBalancer 的架构可以被精炼地概括为三大核心组件的协作:

1、ReactorServiceInstanceLoadBalancer (负载均衡器):

这是负载均衡策略的直接体现者。

它的核心职责是接收一个服务实例列表,并根据内部实现的算法(如轮询、随机或我们即将自定义的灰度策略)从中选择一个最合适的实例。

2、ServiceInstanceListSupplier (服务实例列表供应商):

它的角色是 客户端 服务实例的“发现者”和“提供者”。

它负责与服务发现组件(如 Nacos, Eureka)交互,获取指定服务 ID(serviceId)下所有健康的服务实例列表,并将其提供给负载均衡器。

ServiceInstanceListSupplier 还内置了缓存机制,避免了对注册中心的频繁请求。

3、ServiceInstance (服务实例):

这是对一个具体服务节点的抽象,包含了该实例的 IP 地址、端口、元数据(metadata)等关键信息。

负载均衡器最终选择并返回的就是一个ServiceInstance对象。

核心接口:ReactorServiceInstanceLoadBalancer

负载均衡策略的核心接口是 ReactorServiceInstanceLoadBalancer


public interface ReactorServiceInstanceLoadBalancer extends ReactorLoadBalancer<ServiceInstance> {
    Mono<Response<ServiceInstance>> choose(Request request);
}

它的 choose 方法接收一个 Request 对象(包含了请求上下文),并返回一个 Mono<Response<ServiceInstance>>

这意味着整个负载均衡过程是异步的、非阻塞的。

choose 方法的内部实现,就是负载均衡算法的核心所在。

默认实现:RoundRobinLoadBalancer

Spring Cloud LoadBalancer 的默认实现是 RoundRobinLoadBalancer,它通过一个原子计数器,在所有可用的服务实例中循环选择,实现了轮询策略。

我们的目标,就是通过自定义配置,替换掉这个默认的 RoundRobinLoadBalancer,插入我们自己实现的、更智能的负载均衡器。

工作流程:一次完整的负载均衡之旅

当一个带有 @LoadBalanced 注解的 RestTemplateWebClient 发起请求时,一次完整的负载均衡流程便开始了:

这个时序图清晰展示了Spring Cloud 客户端负载均衡的完整流程,核心是将客户端对 “服务名” 的请求,通过负载均衡机制转换为对具体 “服务实例 IP + 端口” 的请求。

整个流程以 “客户端发起请求” 为起点,以 “客户端接收最终响应” 为终点,共分为 5 个关键阶段,涉及 5 个核心组件的协同工作。

1、发起请求与拦截触发

客户端(如使用 RestTemplate 或 WebClient)发起请求,请求 URL 中使用服务名而非具体 IP,

请求 地址 例如 http://ruoyi-system/user/profile/1

该请求会被负载均衡拦截器(LBInterceptor) 捕获,拦截器是实现客户端负载均衡的入口。

2、负载均衡器与实例获取

拦截器调用负载均衡器(Balancer,如 ReactorServiceInstanceLoadBalancer)choose(request) 方法,请求选择一个合适的服务实例。

负载均衡器向服务实例列表供应商(Supplier) 发送 get(request) 请求,获取目标服务的所有可用实例。

实例供应商向服务注册中心(Nacos) 发起查询,请求 “ruoyi-system” 微服务的所有健康实例(过滤掉下线或不健康的实例)。

3、实例筛选与算法执行

Nacos 返回 “ruoyi-system” 的健康实例列表,例如 [ins1, ins2, ins3]

实例供应商将实例列表传递给负载均衡器。

负载均衡器内部执行负载均衡算法(如轮询、随机、权重等),从实例列表中选定一个目标实例(例如 ins2)。

4、 URL 替换与实际请求

负载均衡器将选定的实例信息返回给拦截器。

拦截器执行关键操作:

将请求 URL 中的服务名替换为实例的 IP 和端口。

  • 替换前:http://ruoyi-system/user/profile/1
  • 替换后:http://192.168.1.102:9201/user/profile/1

拦截器使用替换后的真实 URL,向目标服务实例发起 HTTP 请求。

5、 响应接收与返回

目标服务实例处理请求后,将响应返回给客户端。

拦截器将收到的响应透传给最初发起请求的客户端,整个负载均衡流程结束。

第三章: 一个简单而实用的自定义负载均衡策略

在深入探讨复杂的灰度发布之前,我们先来分析 一个简单而实用的自定义负载均衡策略——本地优先。

场景:通过调用本地服务提升性能

在一个大型微服务项目中,很多的服务部署在 同一个节点。

假设一个 “订单服务”,而它依赖于“用户服务”和“商品服务”。

在 rpc 时, 希望“订单服务”调用的 本地启动的“用户服务”, 而不是 其他节点的 “用户服务”和“商品服务”,以方便本地调试并发。

默认的轮询或随机策略无法满足这个需求,它们可能会将请求发送到开发环境的任何一个“用户服务”实例上,导致你的本地调试过程变得极其困难和不可控。

实现:CustomSpringCloudLoadBalancer

为了解决这个问题,一个定制的 本地优先的 `CustomSpringCloudLoadBalancer ,它的核心逻辑非常清晰:


// org.dromara.common.loadbalance.core.CustomSpringCloudLoadBalancer.java
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
    if (instances.isEmpty()) {
        // ...
        return new EmptyResponse();
    }
    // 1. 遍历所有实例
    for (ServiceInstance instance : instances) {
        // 2. 判断实例的IP是否是本机IP之一
        if (NetUtil.localIpv4s().contains(instance.getHost())) {
            // 3. 如果是, 立即返回该实例
            return new DefaultResponse(instance);
        }
    }
    // 4. 如果没有找到本机实例, 则退化为随机策略
    return new DefaultResponse(instances.get(ThreadLocalRandom.current().nextInt(instances.size())));
}

这个实现非常巧妙:

(1) 它首先获取本地服务器的所有 IPv4 地址。

(2) 然后遍历注册中心返回的服务实例列表。

(3) 如果发现某个实例的 host(IP 地址)是本机 IP,就“插队”,直接选择这个实例。

(4) 如果遍历完所有实例都没有找到本机 IP,说明本地没有启动该服务,此时策略自动“降级”,采用随机算法选择一个远程实例。

通过 CustomLoadBalanceAutoConfiguration 将这个自定义负载均衡器设置为默认策略,RuoYi-Cloud-Plus` 完美地解决了多团队协同开发时的服务调用问题,极大地提升了开发效率。

第四章: 自定义 一个 动态灰度发布策略

现在,我们将基于 上面的本地优先负载均衡 代码,一步步构建支持动态权重调整的灰度发布策略。

设计理念:Nacos 权重 + 版本元数据

我们的核心设计思想是:利用 Nacos 作为“指挥中心”

(1) 服务版本定义:

在服务的 `application.yml` 中定义版本号,并在注册到 Nacos 时,将其作为实例的元数据(metadata)。


```yaml

spring:
    cloud:
        nacos:
            discovery:
                metadata:
                    version: v1.1.0

```

(2) 灰度规则定义:

在 Nacos 配置中心,为需要进行灰度发布的服务定义流量规则。


```yaml

gray:
    loadbalancer:
        enabled: true
        rules:
            # 为 ruoyi-system 服务配置灰度规则
            ruoyi-system:
                version: v1.1.0 # 灰度版本的版本号
                weight: 20 # 分配给灰度版本的流量权重 (0-100)

```

(3) 智能负载均衡器:

创建一个新的负载均衡器 `GrayLoadBalancer`,它会同时读取 Nacos 注册中心的服务实例列表(及其元数据)和配置中心的灰度规则,然后按权重动态地将流量分配给“灰度版本实例”和“主版本实例”。

核心实现:GrayLoadBalancer

我们创建的 GrayLoadBalancer 是整个灰度策略的核心。

getInstanceResponse 方法的执行逻辑如下:

源码剖析



private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
    // ... 省略非空判断 ...

    // 1. 从配置属性中获取当前服务的灰度规则
    GrayRule grayRule = grayLoadBalancerProperties.getRules().get(serviceId);
    String grayVersion = grayRule.getVersion();
    Integer grayWeight = grayRule.getWeight();

    // 2. 根据版本号元数据, 将实例分为灰度组和主版本组
    List<ServiceInstance> grayInstances = instances.stream()
        .filter(instance -> grayVersion.equals(instance.getMetadata().get("version")))
        .collect(Collectors.toList());
    List<ServiceInstance> normalInstances = instances.stream()
        .filter(instance -> !grayVersion.equals(instance.getMetadata().get("version")))
        .collect(Collectors.toList());

    // ... 省略其中一组为空的降级逻辑 ...

    // 3. 按权重进行流量分配
    int random = new Random().nextInt(100);
    if (random < grayWeight) {
        // 流量走向灰度实例
        // 在选定的灰度实例组中再进行一次随机, 避免所有灰度流量打到同一台机器
        return new DefaultResponse(grayInstances.get(new Random().nextInt(grayInstances.size())));
    } else {
        // 流量走向主版本实例
        // 同样, 在主版本实例组中进行随机, 保证流量均匀
        return new DefaultResponse(normalInstances.get(new Random().nextInt(normalInstances.size())));
    }
}

自动装配:让策略 生效(并且动态生效)

为了让 GrayLoadBalancer 能够被 Spring Cloud 框架发现并使用,我们创建了 GrayLoadBalancerAutoConfiguration 。



@Configuration
// 1. 启用我们定义的灰度配置属性类
@EnableConfigurationProperties(GrayLoadBalancerProperties.class)
// 2. 只有在配置文件中 gray.loadbalancer.enabled = true 时, 该配置才会生效
@ConditionalOnProperty(value = "gray.loadbalancer.enabled", havingValue = "true")
public class GrayLoadBalancerAutoConfiguration {

    @Bean
    public ReactorLoadBalancer<ServiceInstance> grayLoadBalancer(
        Environment environment,
        LoadBalancerClientFactory loadBalancerClientFactory,
        GrayLoadBalancerProperties grayLoadBalancerProperties) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        // 3. 将我们的 GrayLoadBalancer 注入到 Spring 容器, 替换掉默认的负载均衡器
        return new GrayLoadBalancer(
            loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
            name,
            grayLoadBalancerProperties);
    }
}

通过 @ConditionalOnProperty 注解,我们实现了一个“开关”。

只有当用户显式地在配置中开启灰度功能时,GrayLoadBalancer 才会覆盖掉默认的 CustomSpringCloudLoadBalancerRoundRobinLoadBalancer,从而实现了对框架的无侵入式扩展。

第五章:实现动态灰度的 实战演练

理论的价值在于指导实践。

现在,让我们一步步完成一次完整的灰度发布流程。

步骤一:准备灰度版本

假设我们正在对 ruoyi-system 服务进行升级。

1、 修改代码:完成新功能的开发或 Bug 修复。

2、 修改版本号: 在 ruoyi-system 模块的 application.yml 中,将元数据版本号修改为新版本,例如 v1.1.0

```yaml

spring:
  cloud:
    nacos:
      discovery:
        metadata:
          version: v1.1.0 # 新版本

```

3、 打包: 单独打包 ruoyi-system 模块,生成 ruoyi-system.jar

步骤二:配置 Nacos 灰度规则

登录 Nacos 控制台,找到 ruoyi-system-dev.yml(或对应环境的配置),添加或修改灰度发布配置:


# 灰度发布配置
gray:
    loadbalancer:
        # 开启灰度功能
        enabled: true
        # 定义具体服务的规则
        rules:
            # 为 ruoyi-system 服务配置灰度规则
            ruoyi-system:
                # 灰度版本的版本号, 必须与jar包中定义的元数据一致
                version: v1.1.0
                # 分配给灰度版本的流量权重 (0-100), 这里设置为20%
                weight: 20

点击“发布”,Nacos 配置中心的这条规则将动态地被我们的 GrayLoadBalancer 感知到。

步骤三:启动并验证流量

…由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值