1、布景(灰度布置)

在咱们系统发布出产环境时,有时为了确保新的服务逻辑没有问题,会让一小部分特定的用户来运用新的版别(比方客户端的内测版别),而其他的用户运用旧的版别,那么这个在Spring Cloud中该如何来完成呢?

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

2、需求

Spring Cloud灰度部署

3、完成思路

Spring Cloud灰度部署
经过翻阅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
 */
@Slf4j
public 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
@Slf4j
public 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
 */
@RestController
public 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:8848
server:
  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:8848
server:
  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:8848
server:
  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
 */
@RestController
public 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:8848
server:
  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:8848
server:
  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:8848
server:
  port: 8003

留意 metadata中version的值

6、测试

Spring Cloud灰度部署

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"}%
➜  ~

Spring Cloud灰度部署

能够看到,顾客回来的端口是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、完整代码

gitee.com/huan1993/sp…

8、参阅文档

1、docs.spring.io/spring-clou…

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。