4

Spring Cloud灰度部署 - huan1993

 1 year ago
source link: https://www.cnblogs.com/huan1993/p/17494527.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1、背景(灰度部署)

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

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

需求

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 灰度服务实例选择器实现

package com.huan.loadbalancer; import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.client.ServiceInstance;import org.springframework.cloud.client.loadbalancer.Request;import org.springframework.cloud.client.loadbalancer.RequestDataContext;import org.springframework.cloud.loadbalancer.core.DelegatingServiceInstanceListSupplier;import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;import org.springframework.http.HttpHeaders;import reactor.core.publisher.Flux; import java.util.List;import java.util.Objects;import java.util.stream.Collectors; /** * 自定义 根据服务名 获取服务实例 列表 * <p> * 需求: 用户通过请求访问 网关<br /> * 1、如果请求头中的 version 值和 下游服务元数据的 version 值一致,则选择该 服务。<br /> * 2、如果请求头中的 version 值和 下游服务元数据的 version 值不一致,且 不存在 version 的值 为 default 则直接报错。<br /> * 3、如果请求头中的 version 值和 下游服务元数据的 version 值不一致,且 存在 version 的值 为 default,则选择该服务。<br /> * <p> * 参考: {@link org.springframework.cloud.loadbalancer.core.HintBasedServiceInstanceListSupplier} 实现 * * @author huan.fu * @date 2023/6/19 - 21:14 */@Slf4jpublic class VersionServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier { /** * 请求头的名字, 通过这个 version 字段和 服务中的元数据来version字段进行比较, * 得到最终的实例数据 */ private static final String VERSION_HEADER_NAME = "version"; public VersionServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) { super(delegate); } @Override public Flux<List<ServiceInstance>> get() { return delegate.get(); } @Override public Flux<List<ServiceInstance>> get(Request request) { return delegate.get(request).map(instances -> filteredByVersion(instances, getVersion(request.getContext()))); } private String getVersion(Object requestContext) { if (requestContext == null) { return null; } String version = null; if (requestContext instanceof RequestDataContext) { version = getVersionFromHeader((RequestDataContext) requestContext); } log.info("获取到需要请求服务[{}]的version:[{}]", getServiceId(), version); return version; } /** * 从请求中获取version */ private String getVersionFromHeader(RequestDataContext context) { if (context.getClientRequest() != null) { HttpHeaders headers = context.getClientRequest().getHeaders(); if (headers != null) { return headers.getFirst(VERSION_HEADER_NAME); } } return null; } private List<ServiceInstance> filteredByVersion(List<ServiceInstance> instances, String version) { // 1、获取 请求头中的 version 和 ServiceInstance 中 元数据中 version 一致的服务 List<ServiceInstance> selectServiceInstances = instances.stream() .filter(instance -> instance.getMetadata().get(VERSION_HEADER_NAME) != null && Objects.equals(version, instance.getMetadata().get(VERSION_HEADER_NAME))) .collect(Collectors.toList()); if (!selectServiceInstances.isEmpty()) { log.info("返回请求服务:[{}]为version:[{}]的有:[{}]个", getServiceId(), version, selectServiceInstances.size()); return selectServiceInstances; } // 2、返回 version=default 的实例 selectServiceInstances = instances.stream() .filter(instance -> Objects.equals(instance.getMetadata().get(VERSION_HEADER_NAME), "default")) .collect(Collectors.toList()); log.info("返回请求服务:[{}]为version:[{}]的有:[{}]个", getServiceId(), "default", selectServiceInstances.size()); return selectServiceInstances; }}

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

package com.huan.loadbalancer; import feign.RequestInterceptor;import feign.RequestTemplate;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes; /** * 将version请求头通过feign传递到下游 * * @author huan.fu * @date 2023/6/20 - 08:27 */@Component@Slf4jpublic class VersionRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { String version = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest() .getHeader("version"); log.info("feign 中传递的 version 请求头的值为:[{}]", version); requestTemplate .header("version", version); }}

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

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

package com.huan.loadbalancer; import lombok.extern.slf4j.Slf4j;import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;import org.springframework.cloud.client.discovery.DiscoveryClient;import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;import org.springframework.context.ConfigurableApplicationContext;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration; /** * 此处选择全局配置 * * @author huan.fu * @date 2023/6/19 - 22:16 */@Configuration@Slf4j@LoadBalancerClients(defaultConfiguration = VersionServiceInstanceListSupplierConfiguration.class)public class VersionServiceInstanceListSupplierConfiguration { @Bean @ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet") public VersionServiceInstanceListSupplier versionServiceInstanceListSupplierV1( ConfigurableApplicationContext context) { log.error("===========> versionServiceInstanceListSupplierV1"); ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder() .withBlockingDiscoveryClient() .withCaching() .build(context); return new VersionServiceInstanceListSupplier(delegate); } @Bean @ConditionalOnClass(name = "org.springframework.web.reactive.DispatcherHandler") public VersionServiceInstanceListSupplier versionServiceInstanceListSupplierV2( ConfigurableApplicationContext context) { log.error("===========> versionServiceInstanceListSupplierV2"); ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder() .withDiscoveryClient() .withCaching() .build(context); return new VersionServiceInstanceListSupplier(delegate); }}

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

5.2 网关核心代码

5.2.1 网关配置文件

spring: application: name: lobalancer-gateway-8001 cloud: nacos: discovery: # 配置 nacos 的服务地址 server-addr: localhost:8848 group: DEFAULT_GROUP config: server-addr: localhost:8848 gateway: discovery: locator: enabled: true server: port: 8001 logging: level: root: info

5.3 服务提供者核心代码

5.3.1 向外提供一个方法

package com.huan.loadbalancer.controller; import com.alibaba.cloud.nacos.NacosDiscoveryProperties;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * 提供者控制器 * * @author huan.fu * @date 2023/3/6 - 21:58 */@RestControllerpublic class ProviderController { @Resource private NacosDiscoveryProperties nacosDiscoveryProperties; /** * 获取服务信息 * * @return ip:port */ @GetMapping("serverInfo") public String serverInfo() { return nacosDiscoveryProperties.getIp() + ":" + nacosDiscoveryProperties.getPort(); }}

5.3.2 提供者端口8005配置信息

spring: application: name: provider cloud: nacos: discovery: # 配置 nacos 的服务地址 server-addr: localhost:8848 # 配置元数据 metadata: version: v1 config: server-addr: localhost:8848server: port: 8005

注意 metadata中version的值

5.3.2 提供者端口8006配置信息

spring: application: name: provider cloud: nacos: discovery: # 配置 nacos 的服务地址 server-addr: localhost:8848 # 配置元数据 metadata: version: v1 config: server-addr: localhost:8848server: port: 8006

注意 metadata中version的值

5.3.3 提供者端口8007配置信息

spring: application: name: provider cloud: nacos: discovery: # 配置 nacos 的服务地址 server-addr: localhost:8848 # 配置元数据 metadata: version: default config: server-addr: localhost:8848server: port: 8007

注意 metadata中version的值

5.4 服务消费者代码

5.4.1 通过 feign 调用提供者方法

/** * @author huan.fu * @date 2023/6/19 - 22:21 */@FeignClient(value = "provider")public interface FeignProvider { /** * 获取服务信息 * * @return ip:port */ @GetMapping("serverInfo") String fetchServerInfo(); }

5.4.2 向外提供一个方法

package com.huan.loadbalancer.controller; import com.alibaba.cloud.nacos.NacosDiscoveryProperties;import com.huan.loadbalancer.feign.FeignProvider;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource;import java.util.HashMap;import java.util.Map; /** * 消费者控制器 * * @author huan.fu * @date 2023/6/19 - 22:21 */@RestControllerpublic class ConsumerController { @Resource private FeignProvider feignProvider; @Resource private NacosDiscoveryProperties nacosDiscoveryProperties; @GetMapping("fetchProviderServerInfo") public Map<String, String> fetchProviderServerInfo() { Map<String, String> ret = new HashMap<>(4); ret.put("consumer信息", nacosDiscoveryProperties.getIp() + ":" + nacosDiscoveryProperties.getPort()); ret.put("provider信息", feignProvider.fetchServerInfo()); return ret; }}

消费者端口 8002 配置信息

spring: application: name: consumer cloud: nacos: discovery: # 配置 nacos 的服务地址 server-addr: localhost:8848 register-enabled: true service: nacos-feign-consumer group: DEFAULT_GROUP metadata: version: v1 config: server-addr: localhost:8848server: port: 8002

注意 metadata中version的值

消费者端口 8003 配置信息

spring: application: name: consumer cloud: nacos: discovery: # 配置 nacos 的服务地址 server-addr: localhost:8848 register-enabled: true service: nacos-feign-consumer group: DEFAULT_GROUP metadata: version: v2 config: server-addr: localhost:8848server: port: 8003

注意 metadata中version的值

消费者端口 8004 配置信息

spring: application: name: consumer cloud: nacos: discovery: # 配置 nacos 的服务地址 server-addr: localhost:8848 register-enabled: true service: nacos-feign-consumer group: DEFAULT_GROUP metadata: version: default config: server-addr: localhost:8848server: port: 8003

注意 metadata中version的值

代码与图的对应关系

6.1 请求头中携带 version=v1

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

➜ ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' \--header 'version: v1'{"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8005"}%➜ ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' \--header 'version: v1'{"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8006"}%➜ ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' \--header 'version: v1'{"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8005"}%➜ ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo' \--header 'version: v1'{"consumer信息":"192.168.8.168:8002","provider信息":"192.168.8.168:8006"}%➜ ~
请求头中携带 version=v1

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

6.2 不传递version

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

➜ ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo'{"consumer信息":"192.168.8.168:8004","provider信息":"192.168.8.168:8007"}%➜ ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo'{"consumer信息":"192.168.8.168:8004","provider信息":"192.168.8.168:8007"}%➜ ~ curl --location --request GET 'http://localhost:8001/nacos-feign-consumer/fetchProviderServerInfo'{"consumer信息":"192.168.8.168:8004","provider信息":"192.168.8.168:8007"}%➜ ~

可以看到,消费者返回的端口是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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK