经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring » 查看文章
Spring Cloud灰度部署
来源:cnblogs  作者:huan1993  时间:2023/6/21 10:52:26  对本文有异议

1、背景(灰度部署)

在我们系统发布生产环境时,有时为了确保新的服务逻辑没有问题,会让一小部分特定的用户来使用新的版本(比如客户端的内测版本),而其余的用户使用旧的版本,那么这个在Spring Cloud中该如何来实现呢?

负载均衡组件使用:Spring Cloud LoadBalancer

2、需求

需求

3、实现思路

Spring Cloud Loadbalancer
通过翻阅Spring Cloud的官方文档,我们知道,大概可以通过2种方式来达到我们的目的。

  1. 实现 ReactiveLoadBalancer接口,重写负载均衡算法。
  2. 实现ServiceInstanceListSupplier接口,重写get方法,返回自定义的服务列表

ServiceInstanceListSupplier: 可以实现如下功能,比如我们的 user-service在注册中心上存在5个,此处我可以只返回3个。

4、Spring Cloud中是否有我上方类似需求的例子

查阅Spring Cloud官方文档,发现org.springframework.cloud.loadbalancer.core.HintBasedServiceInstanceListSupplier 类可以实现类似的功能。

那可能有人会说,既然Spring Cloud已经提供了这个功能,为什么你还要重写一个? 此处只是为了一个记录,因为工作中的需求可能各种各样,万一后期有类似的需求,此处记录了,后期知道怎么实现。

5、核心代码实现

5.1 灰度核心代码

5.1.1 灰度服务实例选择器实现

  1. package com.huan.loadbalancer;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.cloud.client.ServiceInstance;
  4. import org.springframework.cloud.client.loadbalancer.Request;
  5. import org.springframework.cloud.client.loadbalancer.RequestDataContext;
  6. import org.springframework.cloud.loadbalancer.core.DelegatingServiceInstanceListSupplier;
  7. import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
  8. import org.springframework.http.HttpHeaders;
  9. import reactor.core.publisher.Flux;
  10. import java.util.List;
  11. import java.util.Objects;
  12. import java.util.stream.Collectors;
  13. /**
  14. * 自定义 根据服务名 获取服务实例 列表
  15. * <p>
  16. * 需求: 用户通过请求访问 网关<br />
  17. * 1、如果请求头中的 version 值和 下游服务元数据的 version 值一致,则选择该 服务。<br />
  18. * 2、如果请求头中的 version 值和 下游服务元数据的 version 值不一致,且 不存在 version 的值 为 default 则直接报错。<br />
  19. * 3、如果请求头中的 version 值和 下游服务元数据的 version 值不一致,且 存在 version 的值 为 default,则选择该服务。<br />
  20. * <p>
  21. * 参考: {@link org.springframework.cloud.loadbalancer.core.HintBasedServiceInstanceListSupplier} 实现
  22. *
  23. * @author huan.fu
  24. * @date 2023/6/19 - 21:14
  25. */
  26. @Slf4j
  27. public class VersionServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
  28. /**
  29. * 请求头的名字, 通过这个 version 字段和 服务中的元数据来version字段进行比较,
  30. * 得到最终的实例数据
  31. */
  32. private static final String VERSION_HEADER_NAME = "version";
  33. public VersionServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
  34. super(delegate);
  35. }
  36. @Override
  37. public Flux<List<ServiceInstance>> get() {
  38. return delegate.get();
  39. }
  40. @Override
  41. public Flux<List<ServiceInstance>> get(Request request) {
  42. return delegate.get(request).map(instances -> filteredByVersion(instances, getVersion(request.getContext())));
  43. }
  44. private String getVersion(Object requestContext) {
  45. if (requestContext == null) {
  46. return null;
  47. }
  48. String version = null;
  49. if (requestContext instanceof RequestDataContext) {
  50. version = getVersionFromHeader((RequestDataContext) requestContext);
  51. }
  52. log.info("获取到需要请求服务[{}]的version:[{}]", getServiceId(), version);
  53. return version;
  54. }
  55. /**
  56. * 从请求中获取version
  57. */
  58. private String getVersionFromHeader(RequestDataContext context) {
  59. if (context.getClientRequest() != null) {
  60. HttpHeaders headers = context.getClientRequest().getHeaders();
  61. if (headers != null) {
  62. return headers.getFirst(VERSION_HEADER_NAME);
  63. }
  64. }
  65. return null;
  66. }
  67. private List<ServiceInstance> filteredByVersion(List<ServiceInstance> instances, String version) {
  68. // 1、获取 请求头中的 version 和 ServiceInstance 中 元数据中 version 一致的服务
  69. List<ServiceInstance> selectServiceInstances = instances.stream()
  70. .filter(instance -> instance.getMetadata().get(VERSION_HEADER_NAME) != null
  71. && Objects.equals(version, instance.getMetadata().get(VERSION_HEADER_NAME)))
  72. .collect(Collectors.toList());
  73. if (!selectServiceInstances.isEmpty()) {
  74. log.info("返回请求服务:[{}]为version:[{}]的有:[{}]个", getServiceId(), version, selectServiceInstances.size());
  75. return selectServiceInstances;
  76. }
  77. // 2、返回 version=default 的实例
  78. selectServiceInstances = instances.stream()
  79. .filter(instance -> Objects.equals(instance.getMetadata().get(VERSION_HEADER_NAME), "default"))
  80. .collect(Collectors.toList());
  81. log.info("返回请求服务:[{}]为version:[{}]的有:[{}]个", getServiceId(), "default", selectServiceInstances.size());
  82. return selectServiceInstances;
  83. }
  84. }

5.1.2 灰度feign请求头传递拦截器

  1. package com.huan.loadbalancer;
  2. import feign.RequestInterceptor;
  3. import feign.RequestTemplate;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.springframework.stereotype.Component;
  6. import org.springframework.web.context.request.RequestContextHolder;
  7. import org.springframework.web.context.request.ServletRequestAttributes;
  8. /**
  9. * 将version请求头通过feign传递到下游
  10. *
  11. * @author huan.fu
  12. * @date 2023/6/20 - 08:27
  13. */
  14. @Component
  15. @Slf4j
  16. public class VersionRequestInterceptor implements RequestInterceptor {
  17. @Override
  18. public void apply(RequestTemplate requestTemplate) {
  19. String version = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()
  20. .getHeader("version");
  21. log.info("feign 中传递的 version 请求头的值为:[{}]", version);
  22. requestTemplate
  23. .header("version", version);
  24. }
  25. }

注意: 此处全局配置了,配置了一个feign的全局拦截器,进行请求头version的传递。

5.1.3 灰度服务实例选择器配置

  1. package com.huan.loadbalancer;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
  4. import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
  5. import org.springframework.cloud.client.discovery.DiscoveryClient;
  6. import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
  7. import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
  8. import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
  9. import org.springframework.context.ConfigurableApplicationContext;
  10. import org.springframework.context.annotation.Bean;
  11. import org.springframework.context.annotation.Configuration;
  12. /**
  13. * 此处选择全局配置
  14. *
  15. * @author huan.fu
  16. * @date 2023/6/19 - 22:16
  17. */
  18. @Configuration
  19. @Slf4j
  20. @LoadBalancerClients(defaultConfiguration = VersionServiceInstanceListSupplierConfiguration.class)
  21. public class VersionServiceInstanceListSupplierConfiguration {
  22. @Bean
  23. @ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet")
  24. public VersionServiceInstanceListSupplier versionServiceInstanceListSupplierV1(
  25. ConfigurableApplicationContext context) {
  26. log.error("===========> versionServiceInstanceListSupplierV1");
  27. ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder()
  28. .withBlockingDiscoveryClient()
  29. .withCaching()
  30. .build(context);
  31. return new VersionServiceInstanceListSupplier(delegate);
  32. }
  33. @Bean
  34. @ConditionalOnClass(name = "org.springframework.web.reactive.DispatcherHandler")
  35. public VersionServiceInstanceListSupplier versionServiceInstanceListSupplierV2(
  36. ConfigurableApplicationContext context) {
  37. log.error("===========> versionServiceInstanceListSupplierV2");
  38. ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder()
  39. .withDiscoveryClient()
  40. .withCaching()
  41. .build(context);
  42. return new VersionServiceInstanceListSupplier(delegate);
  43. }
  44. }

此处偷懒全局配置了
@Configuration @Slf4j @LoadBalancerClients(defaultConfiguration = VersionServiceInstanceListSupplierConfiguration.class)

5.2 网关核心代码

5.2.1 网关配置文件

  1. spring:
  2. application:
  3. name: lobalancer-gateway-8001
  4. cloud:
  5. nacos:
  6. discovery:
  7. # 配置 nacos 的服务地址
  8. server-addr: localhost:8848
  9. group: DEFAULT_GROUP
  10. config:
  11. server-addr: localhost:8848
  12. gateway:
  13. discovery:
  14. locator:
  15. enabled: true
  16. server:
  17. port: 8001
  18. logging:
  19. level:
  20. root: info

5.3 服务提供者核心代码

5.3.1 向外提供一个方法

  1. package com.huan.loadbalancer.controller;
  2. import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RestController;
  5. import javax.annotation.Resource;
  6. /**
  7. * 提供者控制器
  8. *
  9. * @author huan.fu
  10. * @date 2023/3/6 - 21:58
  11. */
  12. @RestController
  13. public class ProviderController {
  14. @Resource
  15. private NacosDiscoveryProperties nacosDiscoveryProperties;
  16. /**
  17. * 获取服务信息
  18. *
  19. * @return ip:port
  20. */
  21. @GetMapping("serverInfo")
  22. public String serverInfo() {
  23. return nacosDiscoveryProperties.getIp() + ":" + nacosDiscoveryProperties.getPort();
  24. }
  25. }

5.3.2 提供者端口8005配置信息

  1. spring:
  2. application:
  3. name: provider
  4. cloud:
  5. nacos:
  6. discovery:
  7. # 配置 nacos 的服务地址
  8. server-addr: localhost:8848
  9. # 配置元数据
  10. metadata:
  11. version: v1
  12. config:
  13. server-addr: localhost:8848
  14. server:
  15. port: 8005

注意 metadata中version的值

5.3.2 提供者端口8006配置信息

  1. spring:
  2. application:
  3. name: provider
  4. cloud:
  5. nacos:
  6. discovery:
  7. # 配置 nacos 的服务地址
  8. server-addr: localhost:8848
  9. # 配置元数据
  10. metadata:
  11. version: v1
  12. config:
  13. server-addr: localhost:8848
  14. server:
  15. port: 8006

注意 metadata中version的值

5.3.3 提供者端口8007配置信息

  1. spring:
  2. application:
  3. name: provider
  4. cloud:
  5. nacos:
  6. discovery:
  7. # 配置 nacos 的服务地址
  8. server-addr: localhost:8848
  9. # 配置元数据
  10. metadata:
  11. version: default
  12. config:
  13. server-addr: localhost:8848
  14. server:
  15. port: 8007

注意 metadata中version的值

5.4 服务消费者代码

5.4.1 通过 feign 调用提供者方法

  1. /**
  2. * @author huan.fu
  3. * @date 2023/6/19 - 22:21
  4. */
  5. @FeignClient(value = "provider")
  6. public interface FeignProvider {
  7. /**
  8. * 获取服务信息
  9. *
  10. * @return ip:port
  11. */
  12. @GetMapping("serverInfo")
  13. String fetchServerInfo();
  14. }

5.4.2 向外提供一个方法

  1. package com.huan.loadbalancer.controller;
  2. import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
  3. import com.huan.loadbalancer.feign.FeignProvider;
  4. import org.springframework.web.bind.annotation.GetMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import javax.annotation.Resource;
  7. import java.util.HashMap;
  8. import java.util.Map;
  9. /**
  10. * 消费者控制器
  11. *
  12. * @author huan.fu
  13. * @date 2023/6/19 - 22:21
  14. */
  15. @RestController
  16. public class ConsumerController {
  17. @Resource
  18. private FeignProvider feignProvider;
  19. @Resource
  20. private NacosDiscoveryProperties nacosDiscoveryProperties;
  21. @GetMapping("fetchProviderServerInfo")
  22. public Map<String, String> fetchProviderServerInfo() {
  23. Map<String, String> ret = new HashMap<>(4);
  24. ret.put("consumer信息", nacosDiscoveryProperties.getIp() + ":" + nacosDiscoveryProperties.getPort());
  25. ret.put("provider信息", feignProvider.fetchServerInfo());
  26. return ret;
  27. }
  28. }

消费者端口 8002 配置信息

  1. spring:
  2. application:
  3. name: consumer
  4. cloud:
  5. nacos:
  6. discovery:
  7. # 配置 nacos 的服务地址
  8. server-addr: localhost:8848
  9. register-enabled: true
  10. service: nacos-feign-consumer
  11. group: DEFAULT_GROUP
  12. metadata:
  13. version: v1
  14. config:
  15. server-addr: localhost:8848
  16. server:
  17. port: 8002

注意 metadata中version的值

消费者端口 8003 配置信息

  1. spring:
  2. application:
  3. name: consumer
  4. cloud:
  5. nacos:
  6. discovery:
  7. # 配置 nacos 的服务地址
  8. server-addr: localhost:8848
  9. register-enabled: true
  10. service: nacos-feign-consumer
  11. group: DEFAULT_GROUP
  12. metadata:
  13. version: v2
  14. config:
  15. server-addr: localhost:8848
  16. server:
  17. port: 8003

注意 metadata中version的值

消费者端口 8004 配置信息

  1. spring:
  2. application:
  3. name: consumer
  4. cloud:
  5. nacos:
  6. discovery:
  7. # 配置 nacos 的服务地址
  8. server-addr: localhost:8848
  9. register-enabled: true
  10. service: nacos-feign-consumer
  11. group: DEFAULT_GROUP
  12. metadata:
  13. version: default
  14. config:
  15. server-addr: localhost:8848
  16. server:
  17. port: 8003

注意 metadata中version的值

6、测试

代码与图的对应关系

6.1 请求头中携带 version=v1

从上图中可以看到,当version=v1时,服务消费者为consumer-8002, 提供者为provider-8005provider-8006

  1. ? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' --header 'version: v1'
  2. {"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8005"}%
  3. ? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' --header 'version: v1'
  4. {"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8006"}%
  5. ? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' --header 'version: v1'
  6. {"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8005"}%
  7. ? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' --header 'version: v1'
  8. {"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8006"}%
  9. ? ~

请求头中携带 version=v1

可以看到,消费者返回的端口是8002,提供者返回的端口是8005|8006是符合预期的。

6.2 不传递version

从上图中可以看到,当不携带时,服务消费者为consumer-8004, 提供者为provider-8007

  1. ? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo'
  2. {"consumer信息":"192.168.8.168:8004","provider信息":"192.168.8.168:8007"}%
  3. ? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo'
  4. {"consumer信息":"192.168.8.168:8004","provider信息":"192.168.8.168:8007"}%
  5. ? ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo'
  6. {"consumer信息":"192.168.8.168:8004","provider信息":"192.168.8.168:8007"}%
  7. ? ~

可以看到,消费者返回的端口是8004,提供者返回的端口是8007是符合预期的。

7、完整代码

https://gitee.com/huan1993/spring-cloud-alibaba-parent/tree/master/loadbalancer-supply-service-instance

8、参考文档

1、https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer

原文链接:https://www.cnblogs.com/huan1993/p/17494527.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号