灰度发布概念

— 摘自百度百科

​ 灰度发布(又名金丝雀发布)是指在黑与白之间,能够滑润过渡的一种发布办法。在其上能够进行A/B testing,即让一部分用户持续用产品特性A,一部分用户开始用产品特性B,假如用户对B没有什么反对定见,那么逐渐扩大范围,把所有用户都迁移到B上面来。灰度发布能够确保全体体系的稳定,在初始灰度的时候就能够发现、调整问题,以确保其影响度。

​ 比照灰度发布,还有蓝绿发布,翻滚发布,笔者在工作中用到最多的是翻滚发布,翻滚发布也并不能滑润的将一部分指定的流量切到新体系上,只能做到切部分流量这部分流量是无法指定的也是随机的,所以依据此笔者就着手做全链路灰度发布相关的研讨。蓝绿发布,翻滚发布的具体概念自行Google吧。

灰度发布架构

​ 这儿我画了一个图,简略描绘一下灰度发布,这儿有微服务A集群和微服务B集群,每个集群中蓝色彩的是出产实例,绿色就代表发布的灰度版别实例;能够看到网络流量是经过微服务网关为进口流入微服务集群,所以这儿微服务网关就担任过滤流量办理灰度规矩等,流量经过微服务网关后,能够看到灰度流量就指向了灰度版别,微服务A经过RPC调用微服务B时也经过loadbalancer也将灰度流量发到了微服务B的灰度实例上,这儿图中实际缺少了一个点,便是流量由微服务网关和微服务A集群中心也是有个loadbalancer的。

Spring Cloud Alibaba-全链路灰度设计

​ 经过上面的图和对于灰度发布架构的描绘,我这儿总结一下完成微服务全链路灰度发布的中心:

  1. 微服务过滤流量并办理灰度发布规矩;
  2. loadbalancer担任进行灰度流量的从头路由;

Spring Cloud Alibaba技能架构下的灰度发布完成

​ 完成灰度发布的技能方案有许多,比方Nginx + Lua脚本办法,Nginx即能够作为微服务网关又能够做负载均衡;再比方Service Mesh技能Istio + Envoy,Istio作为操控平台办理下发灰度规矩,Envoy作为网络调用组件,假如是Service Mesh技能架构能够彻底选用这种架构。假如是传统的微服务架构就有或许需求自己研发一套灰度发布的组件,所以经过上面对灰度发布架构的研讨,咱们大致知道了如何完成灰度发布体系,我这儿就依据Spring Cloud Alibaba传统微服务架构完成全链路灰度发布功能。

根底规划

完成灰度发布需求终端运用(客户端)和服务端做一些约好,这个约好就代表着是否是灰度发布的客户端的网络调用,当然假如不做约好也是能够完成灰度功能的,这就需求服务端的组件对流量做更详尽的过滤,比方从网络调用的报文中过滤出灰度发布运用的网络调用,这对服务端来说显然更加费事,也不利于维护,所以我这儿选用客户端和服务端约好的办法来规划。

HttpHeader规划

​ 客户端和服务端在Http恳求头中约好一个固定字段来标识,此标识能够代表“是否走灰度”,也能够设置成一个“用户ID”,“客户端IP”等,假如是“用户ID”,“客户端IP”那就在网关层有个装备,网关匹配到对应的参数就走灰度。

​ HttpHeader增加gray字段作为灰度符号

{
  "gray":"123"
}

Spring Cloud Gateway改造

​ Spring Cloud Gateway在架构中是微服务网关,在灰度发布的作用便是办理灰度发布规矩,设置灰度符号到HttpHeader而且传递下去。办理灰度发布规矩需求一个装备,我这儿挑选放到装备文件傍边,先完成一个Spring的自定义装备绑定,代码如下:

@Configuration
@RefreshScope
@ConfigurationProperties("spring.cloud.gateway.gray")
@Data
public class GrayProperties {
    /**
     * 灰度开关
     */
    private Boolean enabled;
    /**
     * 灰度匹配内容
     */
    private List<String> matches = new ArrayList<>();
}

对应的在Spring Cloud Gateway的application.yml中的装备示例如下:

spring:
  cloud:
    gateway:
      gray:
        enabled: true
        matches: 
          - 123
          - 456
          - 10.1.1.10

解释下装备文件,spring.cloud.gateway.gray.enabled操控敞开关闭灰度发布,spring.cloud.gateway.gray.matches装备的是灰度发布规矩匹配,该值是一个list,也便是说只需匹配到HttpHeaders中的gray的值就走灰度发布逻辑。

Spring Cloud Gateway完成灰度发布过滤器

这个过滤器是灰度发布流量过滤的一个中心,大致逻辑是经过处理HttpHeaders中的gray值决定是够要走灰度发布,假如走灰度发布将HttpHeaders中的gray值设置为true即可,这儿也能够和客户端约好两个字段,一个是匹配规矩,一个灰度操控,我这儿就把它放在一个字段中了。

spring.cloud.gateway.gray.enabled为false是不会走到这个过滤器,还有一点要注意,必须要完成Ordered接口,而且设置其次序为Ordered.HIGHEST_PRECEDENCE,由于灰度发布过滤这归于一个最高等级的过滤器,要先执行。

​ 为了确保线程隔离,经过GrayRequestContextHolder存取灰度符号。

@Component
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.gray.enabled", havingValue = "true")
@AllArgsConstructor
public class GrayscalePublishFilter implements GlobalFilter, Ordered {
    private final GrayProperties grayProperties;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        try {
            GrayRequestContextHolder.setGrayTag(false);
            if (grayProperties.getEnabled()) {
                var headers = exchange.getRequest().getHeaders();
                if (headers.containsKey("gray")) {
                    List<String> grayValues = headers.get("gray");
                    if (!Objects.isNull(grayValues) && grayValues.size() > 0) {
                        // 灰度符号为true,直接走灰度
                        String grayValue = grayValues.get(0);
                        // 装备中的值匹配到header中的灰度值,走灰度(但是用户ID,IP,APP版别号等等,只需匹配到就走灰度)
                        if (grayProperties.getMatches().stream().anyMatch(grayValue::equals)) {
                            GrayRequestContextHolder.setGrayTag(true);
                        }
                    }
                }
                var newRequest = exchange.getRequest().mutate()
                        .header("gray", GrayRequestContextHolder.getGrayTag().toString())
                        .build();
                var newExchange = exchange.mutate()
                        .request(newRequest)
                        .build();
                return chain.filter(newExchange);
            }
            return chain.filter(exchange);
        } finally {
            GrayRequestContextHolder.remove();
        }
    }
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
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();
    }
}

后端服务自定义Spring MVC恳求拦截器

​ 自定义Spring MVC拦截器的意图是下流服务收到恳求,经过拦截器检查HttpHeader中是否有灰度符号,假如有灰度符号那么就将灰度符号保存到Holder中,之后假如有后续的RPC调用同样的将灰度符号传递下去

@SuppressWarnings("all")
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 {
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        GrayRequestContextHolder.remove();
    }
}

​ 装备拦截器:

@Configuration
@ConditionalOnClass(value = WebMvcConfigurer.class)
public class GrayWebMvcAutoConfiguration {
    /**
     * Spring MVC 恳求拦截器
     * @return WebMvcConfigurer
     */
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(new GrayHandlerInterceptor());
            }
        };
    }
}

OpenFeign改造

​ RPC结构OpenFeign改造,完成OpenFeign拦截器,支持从Holder中取出灰度符号,而且放到调用下流服务的恳求头中,将灰度符号传递下去。

public class FeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        // 假如灰度符号为true,将灰度符号经过HttpHeader传递下去
        if (GrayRequestContextHolder.getGrayTag()) {
            template.header(GrayConstant.HEADER_GRAY_TAG, Collections.singleton(GrayConstant.HEADER_GRAY_TAG_VALUE));
        }
    }
}

​ 装备OpenFeign拦截器

@Configuration
@ConditionalOnClass(value = RequestInterceptor.class)
public class GrayFeignInterceptorAutoConfiguration {
    /**
     * Feign拦截器
     * @return FeignRequestInterceptor
     */
    @Bean
    public FeignRequestInterceptor feignRequestInterceptor() {
        return new FeignRequestInterceptor();
    }
}

自定义Loadbalancer

​ 这儿根底的LoadBalancer结构运用的是Spring Cloud LoadBalancer,所以需求引入LoadBalancer,代码如下:

				<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>

​ 我这儿的是将负载均衡作为一个独自模块开发的,假如有需求灰度发布的微服务引用该模块而且装备自定义Loadbalancer即可,Loadbalancer的中心逻辑是依据HttpHeaders中的灰度发布符号,从服务发现的服务列表中筛选出灰度发布的机器实例,然后再经过loadbalancer算法就行负载均衡返回一个服务实例,RPC调用不必管,我这儿运用的是OpenFeign作为RPC结构。

​ Spring Cloud LoadBalancer自定义Loadbalancer完成ReactorServiceInstanceLoadBalancer接口,代码如下:

getInstances办法包含了筛选服务列表逻辑,假如从HttpHeaders中获取到gary字段而且该字段值是true就走灰度发布;至于负载均衡逻辑彻底拷贝了spring cloud gatewayRoundRobinLoadBalancer的负载均衡逻辑。

@Slf4j
public class GrayscaleLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    final AtomicInteger position;
    final String serviceId;
    private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
    public GrayscaleLoadBalancer(String serviceId, ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        this(new Random().nextInt(1000), serviceId, serviceInstanceListSupplierProvider);
    }
    public GrayscaleLoadBalancer(int seedPosition, String serviceId, ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        this.position = new AtomicInteger(seedPosition);
        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);
        // Do not move position when there is only 1 instance, especially some suppliers
        // have already filtered instances
        if (instances.size() == 1) {
            return new DefaultResponse(instances.get(0));
        }
        // Ignore the sign bit, this allows pos to loop sequentially from 0 to
        // Integer.MAX_VALUE
        int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
        ServiceInstance instance = instances.get(pos % instances.size());
        return new DefaultResponse(instance);
    }
    private List<ServiceInstance> getInstances(List<ServiceInstance> instances, Request request) {
        DefaultRequest<RequestDataContext> defaultRequest = Convert
                .convert(new TypeReference<DefaultRequest<RequestDataContext>>() {
                }, request);
        RequestDataContext dataContext = defaultRequest.getContext();
        RequestData requestData = dataContext.getClientRequest();
        HttpHeaders headers = requestData.getHeaders();
        // 获取灰度符号
        String gray = CollectionUtil.get(headers.get("gray"), 0);
        // 灰度符号不为空而且符号为true, 筛选ServiceInstance
        if (StringUtils.isNotBlank(gray) && StringUtils.equals("true", gray)) {
            return instances.stream()
                    .filter(instance -> StringUtils.isNotBlank(instance.getMetadata().get(GrayConstant.HEADER_GRAY_TAG))
                            && gray.equals(instance.getMetadata().get(GrayConstant.HEADER_GRAY_TAG)))
                    .collect(Collectors.toList());
        } else {
            return instances;
        }
    }
}

装备自定义LoadBalancer,代码如下:

@Configuration
public class LoadBalancerGrayAutoConfiguration {
    @Bean
    @ConditionalOnProperty(value = "spring.cloud.loadbalancer.gray.enabled", havingValue = "true", matchIfMissing = true)
    @ConditionalOnBean(LoadBalancerClientFactory.class)
    public ReactorLoadBalancer<ServiceInstance> grayReactorLoadBalancer(Environment environment,
                                                                        LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new GrayscaleLoadBalancer(name, loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class));
    }
}

测验

​ 三个微服务ruuby-gateway,order-svc,account-svc,调用的关系式经过ruuby-gateway调用order-svc,order-svc内部经过OpenFeign调用

微服务注册元信息修正

微服务注册增加灰度服务符号装备,微服务注册到服务注册中心(Nacos)时经过附加元数据的办法来符号该服务是一个灰度发布的微服务

spring:
  cloud:
    nacos:
      discovery:
        metadata:
          gray: true

自定义LoadBalancer运用

经过如下装备敞开自定义LoadBalancer。

spring:
  cloud:
    loadbalancer:
      gray:
        enabled: true

代码中装备LoadBalancer,在微服务发动类上经过注解敞开运用自定义LoadBalancer。

@SpringBootApplication
@EnableDiscoveryClient
@LoadBalancerClients(defaultConfiguration = {LoadBalancerGrayAutoConfiguration.class})
public class GateWayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GateWayApplication.class, args);
    }
}

account-svc,现在order-svc,account-svc服务都是灰度版别,测验自定义LoadBalancer作用,下面是服务元数据中的灰度符号

Spring Cloud Alibaba-全链路灰度设计

咱们在Postman中设置HttpHeaders的灰度符号gray,设置其值为123,由于咱们在网关中装备的matches中有123。

Spring Cloud Alibaba-全链路灰度设计

源码

源码参阅GitHub,假如对您有帮助费事点个赞支持下,谢谢!