Spring Cloud LoadBalancer 实现自定义负载均衡策略(基于服务元数据筛选)

发布于:2025-08-08 ⋅ 阅读:(25) ⋅ 点赞:(0)

💡 Spring Cloud LoadBalancer 实现自定义负载均衡策略(基于服务元数据筛选)

在微服务架构中,我们常常希望对服务实例进行更精细的路由控制,例如:

  • 灰度发布:不同环境访问不同版本
  • 操作系统差异:Windows/Mac/Linux 客户端访问对应的服务
  • 多机房、多网段:按区域划分访问

本文将基于 Spring Cloud LoadBalancer 实现一个自定义策略,按服务实例 metadata 中的 env(环境)和 os(系统)字段进行智能路由。


🧩 配置目的和背景

我们假设服务实例在注册到 Nacos 时会带上 metadata 元信息:

# application.yml
spring:
  cloud:
    nacos:
      discovery:
        metadata:
          env: ${spring.profiles.active}
          os: ${os.name}

📌 上述配置的作用:

  • env: 当前应用运行环境(dev/test/prod)
  • os: 当前操作系统(如 macOS、Windows)

这样注册到 Nacos 的服务实例就带上了标签,可以作为路由依据。


⚙️ 自定义负载均衡策略核心代码

1️⃣ 负载均衡器实现类 CustomFilteredLoadBalancer.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * 自定义负载均衡器,实现 ReactorServiceInstanceLoadBalancer 接口。
 * 功能:
 * - 根据服务ID获取服务实例列表(由 ServiceInstanceListSupplier 提供)
 * - 过滤出满足当前环境(env)和操作系统(os)条件的实例
 * - 返回符合条件的实例封装成 Response,否则返回空响应
 */
@Slf4j
public class CustomFilteredLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    final AtomicInteger position;

    private final String serviceId;

    // 延迟获取 ServiceInstanceListSupplier,避免循环依赖
    private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    private final String currentEnv;  // 当前环境,如 "dev"
    private final String currentOs;   // 当前操作系统,如 "mac"

    public CustomFilteredLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
                                      String serviceId,
                                      String currentEnv,
                                      String currentOs) {
        this(serviceInstanceListSupplierProvider, serviceId, currentEnv, currentOs,new Random().nextInt(1000));
    }

    public CustomFilteredLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
                                      String serviceId,
                                      String currentEnv,
                                      String currentOs,
                                      int seedPosition) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
        this.currentEnv = currentEnv.toLowerCase();
        this.currentOs = currentOs.toLowerCase();
        this.position = new AtomicInteger(seedPosition);
    }

    /**
     * 选择符合条件的服务实例
     *
     * @param request 当前请求对象(通常不使用)
     * @return Mono<Response<ServiceInstance>> 响应封装选中的实例或空实例
     */
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map(this::getInstanceResponse);
    }

    /**
     * 根据 env 和 os 过滤服务实例列表
     *
     * @param serviceInstances 服务实例列表
     * @return Response<ServiceInstance> 包含选中实例或 EmptyResponse
     */
    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) {

        if (serviceInstances.isEmpty()) {
            log.warn("没有可供服务的服务器: " + this.serviceId);
            return new EmptyResponse();
        }

        log.info("原始实例数量:" + serviceInstances.size());

        // 自定义扩展过滤逻辑
        List<ServiceInstance> instances = serviceInstances.stream()
                .filter(instance -> {
                    String instanceEnv = instance.getMetadata().getOrDefault("env", "").toLowerCase();
                    boolean envMatch = instanceEnv.equals(currentEnv);
                    if (!envMatch) {
                        log.debug("过滤实例 [" + instance.getUri() + "],环境不匹配,实例env=" + instanceEnv + ", 当前env=" + currentEnv);
                    }
                    return envMatch;
                })
                .filter(instance -> {
                    String instanceOs = instance.getMetadata().getOrDefault("os", "").toLowerCase();
                    boolean osMatch;
                    if (currentOs.contains("mac")) {
                        osMatch = instanceOs.contains("mac");
                    } else if (currentOs.contains("windows")) {
                        osMatch = instanceOs.contains("windows");
                    } else {
                        osMatch = instanceOs.equals(currentOs);
                    }
                    if (!osMatch) {
                        log.debug("过滤实例 [" + instance.getUri() + "],系统不匹配,实例os=" + instanceOs + ", 当前os=" + currentOs);
                    }
                    return osMatch;
                })
                .collect(Collectors.toList());

        log.info("过滤后实例数量:" + instances.size());

        if (instances.isEmpty()) {
            log.warn("没有符合条件的实例,返回空实例");
            return new EmptyResponse();
        }

        // 如果只有一个实例,则直接返回
        if (instances.size() == 1) {
            return new DefaultResponse(instances.get(0));
        }

        // 如果有多个实例,可以采用随机或轮询
        int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;

        ServiceInstance instance = instances.get(pos % instances.size());

        log.info("选中实例:" + instance.getUri());

        return new DefaultResponse(instance);
    }


}

此处过滤后的服务实例选择逻辑:

  • 如果只有一个实例,则直接返回
  • 如果有多个实例,可以采用随机或轮询 ,这里我采用了org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer官方的轮循负载均衡器里面的源码

这个自定义的负载均衡策略过滤逻辑可以用一句话概括:

根据当前环境变量(env)和操作系统类型(os),从服务实例中筛选出最匹配的实例来使用。

👇 简单白话解释:

当你访问某个服务时,这个策略会先从注册中心获取到所有可用的服务实例,然后一步步筛选:

  1. 先看环境变量(env)是否匹配
    比如你本地运行的是 dev 环境,那就只选出那些注册时标记了 env=dev 的实例。

  2. 再看操作系统(os)是否匹配
    继续在第一步筛出来的基础上,再选出和你当前电脑或服务器系统一致的实例。

    • 你用的是 Mac,就只选出标记为 os=mac 的实例;

    • Windows,就选 os=windows

    • Linux,就严格匹配 os=linux

  3. 最终选中一个实例返回
    如果匹配到多个符合的实例,就默认选第一个(你也可以改成随机、轮询等方式);
    如果一个都没有匹配上,就返回空结果,意味着没有服务可用。

📌 这个策略适合什么场景?
  • 多套部署环境并存(如 dev/test/prod);

  • 某些服务只适用于特定操作系统(比如调用原生接口或依赖特定底层库);

  • 希望本地测试时访问本地服务实例,线上访问线上服务实例。


2️⃣ 配置类 CustomLoadBalancerConfiguration.java

/**
 * 自定义负载均衡器配置类,用于将自定义实现注册为 Spring Bean。
 * 系统会通过该配置在启动时注入我们的自定义负载均衡逻辑。
 */
public class CustomLoadBalancerConfiguration {

    @Bean
    public ReactorLoadBalancer<?> reactorServiceInstanceLoadBalancer(Environment environment,
                                                                      LoadBalancerClientFactory loadBalancerClientFactory) {
        // 从环境中获取当前调用的服务名(被调用方)
        String serviceId = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);

        // 获取当前环境配置(如 dev、test、prod)
        String currentEnv = SpringUtils.getActiveProfile();

        // 获取当前操作系统名(如 Windows、Mac OS X)
        String currentOs = System.getProperty("os.name", "unknown");

        return new CustomFilteredLoadBalancer(
                loadBalancerClientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
                serviceId,
                currentEnv,
                currentOs);
    }
}

3️⃣ 启动类中启用配置 Application.java

/**
 * 启动类
 * 使用 @LoadBalancerClients 注册我们的自定义负载均衡策略配置
 */
@EnableFeignClients
@SpringBootApplication
@LoadBalancerClients(defaultConfiguration = {CustomLoadBalancerConfiguration.class})
public class GsApplication {
    public static void main(String[] args) {
        SpringApplication.run(GsApplication.class, args);
    }
}

📋 核心逻辑解析

步骤 动作 说明
1 注入元数据 在服务注册时注入 envos 字段
2 实现 LoadBalancer 自定义 choose 方法,按需过滤实例
3 注册 Bean 使用 ReactorLoadBalancer 接口注册负载均衡器
4 全局启用 启动类中用 @LoadBalancerClients 激活策略

✅ 优点

  • ✔️ 支持灰度环境隔离(按环境路由)
  • ✔️ 支持客户端系统识别(按 OS 分流)
  • ✔️ 可扩展为版本控制、机房标签等智能路由

❗ 注意事项

  • 💡 元数据必须在注册服务时注入
  • 💡 所有调用方都必须配置正确的 spring.profiles.activeos.name
  • 💡 默认只取第一个匹配的实例,如需其他策略请自行修改

📝 小结与建议

该方案通过 Spring Cloud LoadBalancer 提供的 SPI 接口扩展机制,灵活实现了按元数据(环境+系统)进行服务调用的策略。非常适用于以下场景:

  • 多环境隔离(dev/test/prod)
  • 灰度发布和分阶段上线
  • 跨平台客户端(Windows/Mac/Linux)路由控制

网站公告

今日签到

点亮在社区的每一天
去签到