SpringCloud自界说loadbalancer实现标签路由

铁佛  金牌会员 | 2025-2-14 20:51:21 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 877|帖子 877|积分 2631

一、配景

  近来前端反应开发环境有时间调接口会很慢,原因是有开发图方便将本地服务注册到开发环境,请求路由到开发本地导致,
为了解决该问题想到可以通过标签路由的方式克制该问题,实现前端联调和开发自测互不干扰。
  该方案除了用于本地调试,还可以用于用户灰度发布。
二、实现方案

  关于负载均衡,低版本的SpringCloud用的是Spring Cloud Ribbon,高版本用Spring Cloud LoadBalancer替换了,
Ribbon可以通过实现IRlue接口实现,这里只先容高版本的实现方案。
实现方案:

  • idea在环境变量中设置tag,本地服务启动时读取环境变量将tag注册到nacos的元数据
  • 重写网关的负载均衡算法,从请求头中获取到的request-tag和服务实例的元数据进行匹配,如果匹配到则返回对应的
    服务实例,否则提示服务未找到。
三、编码实现

3.1 order服务

新建一个SpringCloud服务order-service,注册元数据很简单,只需要清除掉NacosDiscoveryClientConfiguration,再写一个自己的NacosDiscoveryClientConfiguration设置类即可。
创建MyNacosDiscoveryClientConfiguration
  1. /**
  2. * @Author: Ship
  3. * @Description:
  4. * @Date: Created in 2025/2/12
  5. */
  6. @Configuration(
  7.         proxyBeanMethods = false
  8. )
  9. @ConditionalOnDiscoveryEnabled
  10. @ConditionalOnBlockingDiscoveryEnabled
  11. @ConditionalOnNacosDiscoveryEnabled
  12. @AutoConfigureBefore({SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class})
  13. @AutoConfigureAfter({NacosDiscoveryAutoConfiguration.class})
  14. public class MyNacosDiscoveryClientConfiguration {
  15.     @Bean
  16.     public DiscoveryClient nacosDiscoveryClient(NacosServiceDiscovery nacosServiceDiscovery) {
  17.         return new NacosDiscoveryClient(nacosServiceDiscovery);
  18.     }
  19.     @Bean
  20.     @ConditionalOnProperty(
  21.             value = {"spring.cloud.nacos.discovery.watch.enabled"},
  22.             matchIfMissing = true
  23.     )
  24.     public NacosWatch nacosWatch(NacosServiceManager nacosServiceManager, NacosDiscoveryProperties nacosDiscoveryProperties,
  25.                                  ObjectProvider<ThreadPoolTaskScheduler> taskExecutorObjectProvider, Environment environment) {
  26.         // 环境变量读取标签
  27.         String tag = environment.getProperty("tag");
  28.         nacosDiscoveryProperties.getMetadata().put("request-tag", tag);
  29.         return new NacosWatch(nacosServiceManager, nacosDiscoveryProperties, taskExecutorObjectProvider);
  30.     }
  31. }
复制代码
这里代码基本与NacosDiscoveryClientConfiguration一致,只是加上了设置元数据的逻辑。
  1. @SpringBootApplication(exclude = NacosDiscoveryClientConfiguration.class)
  2. public class OrderApplication {
  3.     public static void main(String[] args) {
  4.         SpringApplication.run(OrderApplication.class, args);
  5.     }
  6. }
复制代码
启动类上需要清除默认的NacosDiscoveryClientConfiguration,不然启动会报bean重复注册的错误,或者设置添加spring.main.allow-bean-definition-overriding=true允许重复注册也行。
写一个测试接口,方便背面测试
  1. /**
  2. * @Author: Ship
  3. * @Description:
  4. * @Date: Created in 2025/2/12
  5. */
  6. @RequestMapping("test")
  7. @RestController
  8. public class TestController {
  9.     @GetMapping("")
  10.     public String sayHello(){
  11.         return "hello";
  12.     }
  13. }
复制代码
3.2 gateway服务

新建一个网关服务,pom文件如下:
  1. <properties>
  2.         <java.version>1.8</java.version>
  3.         <spring-cloud.version>2020.0.3</spring-cloud.version>
  4.         <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
  5.         <spring-boot.version>2.5.1</spring-boot.version>
  6.         <maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>
  7.     </properties>
  8.     <dependencies>
  9.         <dependency>
  10.             <groupId>org.springframework.boot</groupId>
  11.             <artifactId>spring-boot-starter-web</artifactId>
  12.         </dependency>
  13.         <dependency>
  14.             <groupId>org.springframework.cloud</groupId>
  15.             <artifactId>spring-cloud-starter-loadbalancer</artifactId>
  16.         </dependency>
  17.         <dependency>
  18.             <groupId>org.springframework.cloud</groupId>
  19.             <artifactId>spring-cloud-starter-openfeign</artifactId>
  20.         </dependency>
  21.         <dependency>
  22.             <groupId>org.springframework.boot</groupId>
  23.             <artifactId>spring-boot-starter-test</artifactId>
  24.             <version>${spring-boot.version}</version>
  25.             <scope>test</scope>
  26.         </dependency>
  27.         <dependency>
  28.             <groupId>org.springframework.boot</groupId>
  29.             <artifactId>spring-boot-starter</artifactId>
  30.             <version>${spring-boot.version}</version>
  31.         </dependency>
  32.         <dependency>
  33.             <groupId>com.alibaba.cloud</groupId>
  34.             <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  35.             <version>${spring-cloud-alibaba.version}</version>
  36.         </dependency>
  37.         <dependency>
  38.             <groupId>com.alibaba.cloud</groupId>
  39.             <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  40.             <version>${spring-cloud-alibaba.version}</version>
  41.         </dependency>
  42.     </dependencies>
  43.     <dependencyManagement>
  44.         <dependencies>
  45.             <dependency>
  46.                 <groupId>org.springframework.cloud</groupId>
  47.                 <artifactId>spring-cloud-dependencies</artifactId>
  48.                 <version>${spring-cloud.version}</version>
  49.                 <type>pom</type>
  50.                 <scope>import</scope>
  51.             </dependency>
  52.             <dependency>
  53.                 <groupId>org.springframework.boot</groupId>
  54.                 <artifactId>spring-boot-dependencies</artifactId>
  55.                 <version>${spring-boot.version}</version>
  56.                 <type>pom</type>
  57.                 <scope>import</scope>
  58.             </dependency>
  59.             <dependency>
  60.                 <groupId>org.springframework.cloud</groupId>
  61.                 <artifactId>spring-cloud-starter-bootstrap</artifactId>
  62.             </dependency>
  63.         </dependencies>
  64.     </dependencyManagement>
  65.     <build>
  66.         <plugins>
  67.             <plugin>
  68.                 <artifactId>maven-compiler-plugin</artifactId>
  69.                 <version>${maven-compiler-plugin.version}</version>
  70.                 <configuration>
  71.                     <source>${java.version}</source>
  72.                     <target>${java.version}</target>
  73.                 </configuration>
  74.             </plugin>
  75.             <plugin>
  76.                 <groupId>org.springframework.boot</groupId>
  77.                 <artifactId>spring-boot-maven-plugin</artifactId>
  78.             </plugin>
  79.         </plugins>
  80.     </build>
复制代码
Spring-Cloud-loadBalancer默认使用轮询的算法,即org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer类实现,因此可以参考RoundRobinLoadBalancer实现一个TagLoadBalancer,代码如下:
  1. /**
  2. * @Author: Ship
  3. * @Description:
  4. * @Date: Created in 2025/2/12
  5. */
  6. public class TagLoadBalancer implements ReactorServiceInstanceLoadBalancer {
  7.     private static final String TAG_HEADER = "request-tag";
  8.     private static final Log log = LogFactory.getLog(TagLoadBalancer.class);
  9.     final AtomicInteger position;
  10.     final String serviceId;
  11.     ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
  12.     public TagLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
  13.         this(serviceInstanceListSupplierProvider, serviceId, (new Random()).nextInt(1000));
  14.     }
  15.     public TagLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId, int seedPosition) {
  16.         this.serviceId = serviceId;
  17.         this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
  18.         this.position = new AtomicInteger(seedPosition);
  19.     }
  20.     @Override
  21.     public Mono<Response<ServiceInstance>> choose(Request request) {
  22.         ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier) this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
  23.         return supplier.get(request).next().map((serviceInstances) -> {
  24.             return this.processInstanceResponse(supplier, serviceInstances, request);
  25.         });
  26.     }
  27.     private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances, Request request) {
  28.         Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances, request);
  29.         if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
  30.             ((SelectedInstanceCallback) supplier).selectedServiceInstance((ServiceInstance) serviceInstanceResponse.getServer());
  31.         }
  32.         return serviceInstanceResponse;
  33.     }
  34.     private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) {
  35.         if (instances.isEmpty()) {
  36.             if (log.isWarnEnabled()) {
  37.                 log.warn("No servers available for service: " + this.serviceId);
  38.             }
  39.             return new EmptyResponse();
  40.         }
  41.         if (request instanceof DefaultRequest) {
  42.             DefaultRequest<RequestDataContext> defaultRequest = (DefaultRequest) request;
  43.             // 上下文获取请求头
  44.             HttpHeaders headers = defaultRequest.getContext().getClientRequest().getHeaders();
  45.             List<String> list = headers.get(TAG_HEADER);
  46.             if (!CollectionUtils.isEmpty(list)) {
  47.                 String requestTag = list.get(0);
  48.                 for (ServiceInstance instance : instances) {
  49.                     String str = instance.getMetadata().getOrDefault(TAG_HEADER, "");
  50.                     if (requestTag.equals(str)) {
  51.                         return new DefaultResponse(instance);
  52.                     }
  53.                 }
  54.                 log.error(String.format("No servers available for service:%s,tag:%s ", this.serviceId, requestTag));
  55.                 return new EmptyResponse();
  56.             }
  57.         }
  58.         int pos = Math.abs(this.position.incrementAndGet());
  59.         ServiceInstance instance = instances.get(pos % instances.size());
  60.         return new DefaultResponse(instance);
  61.     }
  62. }
复制代码
这里需要实现ReactorServiceInstanceLoadBalancer接口,如果请求头带有标签则根据标签路由,否则使用默认的轮询算法。
还要把TagLoadBalancer用起来,所以需要界说一个设置类TagLoadBalancerConfig,并通过@LoadBalancerClients注解添加默认设置,代码如下:
  1. /**
  2. * @Author: Ship
  3. * @Description:
  4. * @Date: Created in 2025/2/12
  5. */
  6. public class TagLoadBalancerConfig {
  7.     @Bean
  8.     public ReactorLoadBalancer reactorTagLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
  9.         String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
  10.         return new TagLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
  11.     }
  12. }
  13. @LoadBalancerClients(defaultConfiguration = {TagLoadBalancerConfig.class})
  14. @SpringBootApplication
  15. public class GatewayApplication {
  16.     public static void main(String[] args) {
  17.         SpringApplication.run(GatewayApplication.class, args);
  18.     }
  19. }
复制代码
最后在application.yml文件添加网关路由设置
  1. spring:
  2.   application:
  3.     name: gateway
  4.   cloud:
  5.     nacos:
  6.       config:
  7.         server-addr: 127.0.0.1:8848
  8.         namespace: dev
  9.         group: DEFAULT_GROUP
  10.       discovery:
  11.         server-addr: 127.0.0.1:8848
  12.         namespace: dev
  13.     gateway:
  14.       routes:
  15.         - id: order-service
  16.           uri: lb://order-service
  17.           predicates:
  18.             - Path=/order/**
  19.           filters:
  20.             - StripPrefix=1
  21. server:
  22.   port: 9000
复制代码
3.3 代码测试


  • 本地启动nacos后启动order(注意需要在idea设置环境变量tag=ship)和gateway服务,可以看到order服务已经成功注册了元数据

  • 然后用Postman请求网关http://localhost:9000/order/test

可以看到请求成功路由到了order服务,说明根据tag路由成功了。

  • 去掉环境变量tag后重新启动Order服务,再次请求相应报文如下:
    1. {
    2.     "timestamp": "2025-02-14T12:10:44.294+00:00",
    3.     "path": "/order/test",
    4.     "status": 503,
    5.     "error": "Service Unavailable",
    6.     "requestId": "41651188-4"
    7. }
    复制代码
说明根据requst-tag找不到对应的服务实例,代码逻辑生效了。
四、总结


  聪明的人已经发现了,本文只实现了网关路由到下游服务这部分的标签路由,下游服务A调服务B的标签路由并未实现,实在现方案也不难,只需要通过上下文通报+feign拦截器就可以做到全链路的标签路由,有兴趣的可以自己试试。

  本文代码已上传github,趁便推广下前段时间写的idea插件CodeFaster(快速生成常用流操作的代码,Marketplace搜索下载即可体验)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

铁佛

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表