Skip to content

灰度组件

通常我们所说的灰度发布指的是一种软件发布方式(以下引自ChatGPT):

灰度发布

灰度发布(Gray Release),也被称为渐进式发布或金丝雀发布,是一种软件发布的策略。在灰度发布中,新的软件版本会逐步地在一小部分用户中进行测试和部署,而不是立即在所有用户中推出。 灰度发布的目的是减少潜在的风险和影响范围。通过先在有限的用户群体中测试新版本,开发人员可以及早发现和解决可能存在的问题,以确保软件稳定性和可靠性。这种渐进式的发布方式还可以帮助开发团队收集用户反馈,并根据反馈进行改进和优化。 灰度发布通常包括以下步骤:

  1. 选择一小部分用户或服务器作为测试群体。
  2. 在测试群体中部署新版本,并观察其表现和性能。
  3. 针对测试结果进行评估和修复,以解决潜在的问题。
  4. 逐步扩大发布范围,将新版本推出给更多的用户或服务器。

通过使用灰度发布,开发团队可以降低发布新功能或更新的风险,并确保用户体验的稳定性和提升。

从上面的描述可以看到,灰度发布常用于微服务多实例部署的场景加,是为了降低更新部署的风险、解决潜在问题而产生的一种部署发布方式。

大部分项目使用单体架构即可,即便使用了微服务,服务数量也控制在5个以内,基本不会遇到大规模部署的情况,那么,为什么还要开发灰度组件呢?

功能说明

在进行微服务开发,一般会由多人共同维护同一个服务。在本地开发的过程中,如果多人启动同一个服务,那么网关会将请求随机转发至某一个服务上,导致前后端无法正常联调。

之前我们通过修改服务名的方式,在前后端代码或配置文件中硬编码属于自己的服务名,来确保前端能够准确的请求到相应的后端服务。但这样做会给后续的部署工作造成极大的困扰。

为解决以上问题,平台提供了灰度组件,借助灰度的思想,在仅修改后端服务配置的前提下,达到“前端能够准确的请求到相应的后端服务”的目标。

如需获取灰度能力,请确保依赖了以下组件:

xml
<dependency>
    <groupId>com.ikingtech.framework</groupId>
    <artifactId>sdk-gray</artifactId>
</dependency>
  • 实现方式

灰度组件通过在Nacos元信息中添加自定义标识,重写ReactorServiceInstanceLoadBalancer来实现灰度功能。

在每次请求时,从请求头中获取灰度标识,使用改标识从注册中心的服务实例列表中获取带有该标识的服务实例(如有多个则随机选择一个),将请求转发至该服务实例。

灰度登录流程.png

  • 重写ReactorServiceInstanceLoadBalancer
java
@RequiredArgsConstructor
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        HttpHeaders headers = (HttpHeaders) request.getContext();
        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
        .getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get().next().map(x -> this.getInstanceResponse(x, headers));
    }



    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, HttpHeaders headers) {
        if (instances.isEmpty()) {
            return new EmptyResponse();
        } else {
            String versionNo = Optional.ofNullable(headers.getFirst(HEADER_VERSION)).orElse("default");
            List<ServiceInstance> matchedInstances = new ArrayList<>();
            for (ServiceInstance instance : instances) {
                Map<String,String> metadata = instance.getMetadata();
                if(metadata.containsValue(versionNo)){
                    matchedInstances.add(instance);
                    break;
                }
            }

            if(matchedInstances.isEmpty()){
                return new DefaultResponse(instances.get(ThreadLocalRandom.current().nextInt(instances.size())));
            }
            return new DefaultResponse(matchedInstances.get(ThreadLocalRandom.current().nextInt(matchedInstances.size())));
        }
    }
}
  • 灰度过滤器
java
@Slf4j
@RequiredArgsConstructor
public class GrayLoadBalancerFilter implements GlobalFilter, Ordered {
    private final LoadBalancerClientFactory clientFactory;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
        if (url == null) {
            return chain.filter(exchange);
        }
        addOriginalRequestUrl(exchange, url);

        URI requestUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        if (null == requestUri) {
            return chain.filter(exchange);
        }
        return choose(exchange).doOnNext(response -> {
                    if (!response.hasServer()) {
                        throw NotFoundException.create(true, "Unable to find instance for " + url.getHost());
                    }

                    ServiceInstance retrievedInstance = response.getServer();

                    URI uri = exchange.getRequest().getURI();

                    // if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
                    // if the loadbalancer doesn't provide one.
                    String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";
                    if (schemePrefix != null) {
                        overrideScheme = url.getScheme();
                    }

                    DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(retrievedInstance,
                            overrideScheme);

                    URI requestUrl = LoadBalancerUriTools.reconstructURI(serviceInstance, uri);

                    if (log.isTraceEnabled()) {
                        log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
                    }
                    exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
                    exchange.getAttributes().put(GATEWAY_LOADBALANCER_RESPONSE_ATTR, response);
                }).then(chain.filter(exchange));
    }

    private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
        URI uri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        if (null == uri) {
            throw NotFoundException.create(true, "Unable to find " + ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR + " from exchange!");
        }
        GrayLoadBalancer loadBalancer = new GrayLoadBalancer(clientFactory.getLazyProvider(uri.getHost(), ServiceInstanceListSupplier.class));
        return loadBalancer.choose(this.createRequest(exchange));
    }


    private Request<HttpHeaders> createRequest(ServerWebExchange exchange) {
        HttpHeaders headers = exchange.getRequest().getHeaders();
        return new DefaultRequest<>(headers);
    }


    @Override
    public int getOrder() {
        return LOAD_BALANCER_CLIENT_FILTER_ORDER - 1;
    }

配置说明


nginx配置

在nginx中添加以下配置,让nginx转发请求时自动添加灰度请求头。

nginx
server {
  listen 8001;
  location / {
    proxy_pass http://192.168.2.207:8000/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $http_host;
    proxy_set_header X-Forwarded-Port $server_port;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-VERSION gray_tag;
  }
}

服务配置

在服务配置文件中的Nacos配置中,将自定义标识添加到Nacos元信息的version字段中。

yaml
spring:
  config:
    activate:
      on-profile: alibaba
  cloud:
    nacos:
      discovery:
        server-addr: ${DISCOVERY_SRV_ADDR:192.168.2.169:8850}
        ip: ${server.address}
        namespace: ${ENV:dev}
        username: nacos
        password: nacos
        group: ${SERVICE_GROUP:DEFAULT_GROUP}
        metadata:
          version: gray_tag # 自定义标识

服务注册后在注册中心可看到设置的自定义标识。
image.png