按用户维度的灰度发布及灰度续传方案

399 阅读5分钟

gray-transmit

介绍

灰度发布组件:在网关层按用户维度进行流量分配,并将灰度标记续传到后续每一个节点

工作流程

灰度.png

  • 前端发起Http请求,header中携带用户信息
  • 网关gateway全局拦截器GlobalFilter获取用户信息,根据用户查询灰度用户配置
  • 如果当前用户是灰度用户,则在负载均衡时,选择灰度的实例,如果没有后续节点没有灰度节点,那就选择正式服务;如果当前用户是正式用户,走正式服务
  • 业务组件Mvc请求拦截器,如果header中的gray标记为true,则将灰度续传到下一个节点,LoadBalancer选择灰度实例。
具体实现
  • gray-common灰度发布组件实现:

    • 实现HandlerInterceptor接口,对Http请求进行灰度过滤,如果header中gray=true,将GrayRequestContextHolder中的灰度标记设置为ture,用ThreadLocal 保存gray状态,作为在后续负载均衡选择灰度服务的依据。

      public class GrayHandlerInterceptor implements HandlerInterceptor {
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              String gray = request.getHeader("gray");
              // 如果HttpHeader中灰度标记为true,则将灰度标记放到holder中,如果需要就传递下去
              if (gray != null && gray.equals("true")) {
                  GrayRequestContextHolder.setGrayTag(true);
              }
              return true;
          }
      
          @Override
          public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
              HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
          }
      
          @Override
          public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
              GrayRequestContextHolder.remove();
          }
      }
      
      public class GrayRequestContextHolder {
          private static final ThreadLocal<Boolean> GARY_TAG = new ThreadLocal<>();
      
          public static void setGrayTag(final Boolean tag) {
              GARY_TAG.set(tag);
          }
      
          public static Boolean getGrayTag() {
              return GARY_TAG.get();
          }
      
          public static void remove() {
              GARY_TAG.remove();
          }
      }
      

      将上述GrayHandlerInterceptor灰度拦截器,配置到MVC拦截器中,拦截请求

      @Configuration
      @ConditionalOnClass(value = WebMvcConfigurer.class)
      public class GrayMvcConfig {
      
          /**
           * Spring MVC 请求拦截器
           * @return WebMvcConfigurer
           */
          @Bean
          public WebMvcConfigurer webMvcConfigurer() {
              return new WebMvcConfigurer() {
                  @Override
                  public void addInterceptors(InterceptorRegistry registry) {
                      registry.addInterceptor(new GrayHandlerInterceptor());
                  }
              };
          }
      
      }
      
    • 自定义灰度负载均衡,实现springcloud的Loadbalancer

      @Slf4j
      public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
          private AtomicInteger position;
      
          private final String serviceId;
      
          private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
      
          public GrayLoadBalancer(String serviceId, ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
              this.position = new AtomicInteger(0);
              this.serviceId = serviceId;
              this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
          }
      
          @Override
          public Mono<Response<ServiceInstance>> choose(Request request) {
              ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
                      .getIfAvailable(NoopServiceInstanceListSupplier::new);
              return supplier.get(request).next()
                      .map(serviceInstances -> processInstanceResponse(supplier, serviceInstances, request));
          }
      
          private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
                                                                    List<ServiceInstance> serviceInstances,
                                                                    Request request) {
              Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances, request);
              if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
                  ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
              }
              return serviceInstanceResponse;
          }
      
          private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) {
              if (instances.isEmpty()) {
                  if (log.isWarnEnabled()) {
                      log.warn("No servers available for service: " + serviceId);
                  }
                  return new EmptyResponse();
              }
              // 获取ServiceInstance列表
              instances = getInstances(instances, request);
      
              if (instances.size() == 1) {
                  return new DefaultResponse(instances.get(0));
              }
      
              // 轮训策略
              int pos = this.position.incrementAndGet();
      
              ServiceInstance instance = instances.get(pos % instances.size());
      
              return new DefaultResponse(instance);
          }
      
          private List<ServiceInstance> getInstances(List<ServiceInstance> instances, Request request) {
              // 获取灰度标记
              String gray = GrayRequestContextHolder.getGrayTag().toString();
              // 灰度标记不为空并且标记为true, 筛选ServiceInstance
              if (StringUtils.isNotEmpty(gray) && StringUtils.equals("true", gray)) {
                  List<ServiceInstance> list = new ArrayList<>();
                  for (ServiceInstance instance : instances) {
                      if (StringUtils.isNotEmpty(instance.getMetadata().get("gray"))
                              && "true".equalsIgnoreCase(instance.getMetadata().get("gray"))) {
                          list.add(instance);
                      }
                  }
                  // 如果没有灰度实例,则转正式服务
                  return !list.isEmpty() ? list : instances;
              } else {
                  return instances;
              }
          }
      }
      

      getInstances方法中,取出本地线程变量中gray标记,根据标记过滤灰度服务实例;如果没有灰度实例,则走正式服务,保证业务正常使用,这里可根据具体业务场景进行改写

      • 将自定义负载均衡器实例化

        @Configuration
        public class LoadBalancerGrayAutoConfiguration {
        
            @Bean
            @ConditionalOnBean(LoadBalancerClientFactory.class)
            public ReactorLoadBalancer<ServiceInstance> grayReactorLoadBalancer(Environment environment,
                                                                                LoadBalancerClientFactory loadBalancerClientFactory) {
                String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
                return new GrayLoadBalancer(name, loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class));
            }
        }
        

      在开启灰度负载均衡,只需要在业务组件启动类上添加:

      @LoadBalancerClients(defaultConfiguration = {LoadBalancerGrayAutoConfiguration.class})

    • 改造客户端http请求拦截器,从ThreadLocal中取出灰度标记,并续传

      @Slf4j
      public class GrayRuleInterceptor implements ClientHttpRequestInterceptor {
          @Override
          public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
      
              HttpHeaders headers = request.getHeaders();
              log.info("灰度标记:{}", GrayRequestContextHolder.getGrayTag());
              headers.add("gray", GrayRequestContextHolder.getGrayTag().toString());
              return execution.execute(request, body);
      
          }
      }
      

      将客户端http请求拦截器放入RestTemplate,使RestTemplate实现灰度在服务间续传

      @Configuration
      public class RestConfig {
          @LoadBalanced
          @Bean
          public RestTemplate restTemplate() {
              RestTemplate restTemplate = new RestTemplate();
              List<ClientHttpRequestInterceptor> list = new ArrayList<>();
              list.add(new GrayRuleInterceptor());
              restTemplate.setInterceptors(list);
              return restTemplate;
          }
      
      }
      
  • Gateway灰度改造

    在gateway中实现GlobalFilter接口、Ordered接口。在filter方法中,根据请求头中的USER_ID,查询灰度配置表tb_gray_user,如果配置是Y,则将这条链路设置为灰度gray=true,保存在本地线程变量ThreadLocal中,为网关负载均衡获取灰度实例备用

public class GrayPublishFilter implements GlobalFilter, Ordered {

    private IGrayService grayService;

    public GrayPublishFilter(IGrayService grayService) {
        this.grayService = grayService;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        GrayRequestContextHolder.setGrayTag(false);

        // 从header中获取用户信息
        HttpHeaders headers = exchange.getRequest().getHeaders();
        String userId = Objects.requireNonNull(headers.get("USER_ID")).get(0);
        // 查询用户的灰度信息
        GrayUser user = GrayUser.builder().userId(userId).build();
        GrayUser grayUser = grayService.queryUserById(user);
        // Y:需要走灰度服务
        if ("Y".equalsIgnoreCase(grayUser.getGray())) {
            GrayRequestContextHolder.setGrayTag(true);

            ServerHttpRequest newRequest = exchange.getRequest().mutate()
                    .header("gray", GrayRequestContextHolder.getGrayTag().toString())
                    .build();
            ServerWebExchange newExchange = exchange.mutate()
                    .request(newRequest)
                    .build();
            return chain.filter(newExchange);
        }
        return chain.filter(exchange);

    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

将GrayPublishFilter灰度发布过滤器生效

@Configuration
public class GrayConfig {

    @Bean
    public GlobalFilter globalFilter(IGrayService grayService) {
        return new GrayPublishFilter(grayService);
   
   }

}
  • 灰度用户配置的增删改查接口封装在gray-common组件,在网关层对外暴露

灰度用户查询.png

数据库配置:

create table tb_gray_user
(
    id      int auto_increment comment '唯一id'
        primary key,
    user_id varchar(11)            not null comment '用户id',
    gray    varchar(1) default 'N' not null comment '是否灰度用户'
)
    comment '灰度用户配置表';
使用说明
  • 在gateway组件引入gray-common组件包

    <dependency>
        <groupId>com.sky</groupId>
        <artifactId>gray-common</artifactId>
        <version>1.0.1</version>
    </dependency>
    
    • 启动类上@LoadBalancerClients(defaultConfiguration = {LoadBalancerGrayAutoConfiguration.class})

      @SpringBootApplication
      @Configurable
      @EnableEurekaClient
      @LoadBalancerClients(defaultConfiguration = {LoadBalancerGrayAutoConfiguration.class})
      public class GatewayApplication {
      
          public static void main(String[] args) {
              SpringApplication.run(GatewayApplication.class, args);
          }
      }
      
    • gateway组件需要配置好数据库信息,以更新灰度用户

  • 业务组件引入gray-common包

    <dependency>
        <groupId>com.sky</groupId>
        <artifactId>gray-common</artifactId>
        <version>1.0.1</version>
    </dependency>
    
    • 启动类上@LoadBalancerClients(defaultConfiguration = {LoadBalancerGrayAutoConfiguration.class})

    • 如果是灰度实例,需要在yml中配置:

      instance: metadata-map: { gray: true }

      eureka:
        client:
          service-url:
            defaultZone: http://127.0.0.1:8081/eureka/
        instance:
          metadata-map: { gray: true }
      
验证

order-service(8086):正式服务

order-service(8087):灰度服务

user-service(8088):正式服务

user-service(8089):灰度服务

网关服务:gateway(8030)

当前用户:10001 配置为灰度用户

调用关系:前端 -> gateway -> order-service -> user-service

在灰度服务中,打印日志

log.info("【灰度服务】订单服务查询订单:{}", orderId);
log.info("【灰度服务】查询User,by id:{}", id);
  • 验证灰度发布及续传结果:

    访问 GET: localhost:8030/api/v1/order/query_order/202502190001

hd.png

验证调用.png

order-gray.png

user-gray.png

  • 验证gateway网关查询灰度用户:

    GET: localhost:8030/api/gray/query_users

gray-user.png

完整代码: gitee.com/dmgyz/gray-…