(十二)实现灰度发布

在前面实现的功能中,动态路由其实只是拓展了获取路由数据的途径,那么如何去控制路由规则的转发呢?
zuul的ZoneAwareLoadBalancer的实现流程虽然看上去比较复杂,其实链路也是比较简单的。
下面简单梳理一下
1.先通过LoadBalancerStats的upServerListZoneMap找出对应分区的所有服务列表
2.随后通过LoadBalancerStats中的serverStatsCache,将服务列表的统计数据聚合
3.生成分区的统计数据快照ZoneSnapshot
4.通过规则挑选出合适的分区
5.通过分区的负载均衡器,结合rule挑选出合适的服务。
所以如果要控制路由的转发,其实通过Rule来控制即可。

实现灰度发布

那么下面简单实现一下灰度发布。
先说一下实现的原理,我的方案比较简单,就是通过请求头的一些信息(如应用名称,以及版本号),来确定最后转发到哪个服务中。

1.自定义请求头缓存过滤器

这个过滤器主要是将请求中的请求头信息进行缓存。

@Component
public class CacheFilter extends com.netflix.zuul.ZuulFilter{

    @Override
    public boolean shouldFilter() {
        //只拦截通过ribbon进行路由的---此处与ribbon源码一致
        RequestContext ctx = RequestContext.getCurrentContext();
        return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
                && ctx.sendZuulResponse());
    }

    @Override
    public Object run() throws ZuulException {
        将请求头信息进行缓存放入到请求上下文中。
        其中的app以及version都是自定义的请求头。用于模拟版本号以及应用名称
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String app = request.getHeader("app");
        String version = request.getHeader("version");
        ctx.put("app", app);
        ctx.put("version", version);
        return null;
    }

    @Override
    public String filterType() {
        return FilterConstants.ROUTE_TYPE;
    }
    
    @Override
    public int filterOrder() {
        return 0;
    }
}

上面的类,比较简单,那么要注意以下几点。

    1. 该过滤器执行顺序需在RibbonRoutingFilter之前
      首先由于zuul处理请求的核心是以链式的过滤器去执行的,所以需要控制好顺序。先生成需要的缓存信息,那么在后续RibbonRoutingFilter执行的时候,才可以通过LoadBalancer中的rule(rule需要通过缓存信息去用于判断)去控制。
    1. 拦截什么类型的请求
      路由规则中只有serverId的,这样子底层是通过serverId去路由(服务id与真实ip地址的映射最终是通过注册中心的客户端拉取的注册表中获取)。

2.如何通过rule去控制路由

  • 2.1首先先定义一个Predicate
public class GrayPredicate extends AbstractServerPredicate{

    @Override
    public boolean apply(PredicateKey input) {
        DiscoveryEnabledServer server = (DiscoveryEnabledServer) input.getServer();
        Map<String, String> metaDataMap = server.getInstanceInfo().getMetadata();
        String app = RequestContext.getCurrentContext().get("app").toString();
        String version = RequestContext.getCurrentContext().get("version").toString();
        return metaDataMap.get("app").equals(app) && metaDataMap.get("version").equals(version);
    }
}

这个判断的标准,其实就是根据服务的元数据,与请求头对比,看看服务的元数据与请求头中的应用以及版本是否匹配,决定该服务是否符合标准。

  • 2.2 如何利用Predicate
    由于Rule本身支持多个Predicate的使用。另外要兼顾此前的一些判断标准。
    所以此处新增一个Rule用于支持多种类型Predicate。
public class GrayRule extends ZoneAvoidanceRule{
    
    private CompositePredicate compositePredicate;

    public GrayRule() {
        用于判断该服务的分区是否是一个坏的分区,需要避免使用该分区
        ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this, null);
        灰度,用于判断该服务是否适用于该请求
        GrayPredicate grayPredicate = new GrayPredicate();
        用于判断该服务的压力是否过大
        AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this, null);
        compositePredicate = CompositePredicate.withPredicates(zonePredicate, availabilityPredicate, grayPredicate)
                .build();
    }

    覆写该方法,否则会返回父类的Predicate
    @Override
    public AbstractServerPredicate getPredicate() {
        return compositePredicate;
    }
}
  • 2.3 如何让负载均衡器使用该rule?
    简单进行注入即可
@Configuration
public class GrayConfiguration {
    
    @Bean
    GrayRule grayRule() {
        GrayRule gr = new GrayRule();
        return gr;
    }
}

需要注意的点,该Rule需要比起默认的Rule(ZoneAvoidanceRule )先被扫描到。否则将不会被注入。
源码如下

@Import({ HttpClientConfiguration.class, OkHttpRibbonConfiguration.class,
        RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class })
public class RibbonClientConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public IRule ribbonRule(IClientConfig config) {
        if (this.propertiesFactory.isSet(IRule.class, name)) {
            return this.propertiesFactory.get(IRule.class, config, name);
        }
        ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
        rule.initWithNiwsConfig(config);
        return rule;
    }
}

3.简单测试

  • 3.1环境准备
    准备2个服务端对应不同的版本,对应的注册信息是如下
    服务A-one:对应版本beta-one
spring:
  application:
    name: client
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    registry-fetch-interval-seconds: 10
    heartbeat-executor-exponential-back-off-bound: 1
    cache-refresh-executor-exponential-back-off-bound: 1
    service-url:
      default-zone: http://localhost:8761/eureka/
  instance:
    lease-renewal-interval-in-seconds: 10
    metadata-map:
      app: clientA
      version:  beta-one
      zone: a

服务A-two:对应版本beta-two

spring:
  application:
    name: client
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    registry-fetch-interval-seconds: 10
    heartbeat-executor-exponential-back-off-bound: 1
    cache-refresh-executor-exponential-back-off-bound: 1
    service-url:
      default-zone: http://localhost:8761/eureka/
  instance:
    lease-renewal-interval-in-seconds: 10
    metadata-map:
      app: clientA
      version:  beta-two
      zone: b

分别启动对应的服务。


对于服务A-1的访问

对于服务A-2的访问

至此,简单的灰度发布就完成了,从上面的结果来看,可以通过请求头从而控制最终的路由。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容