Zuul 网关层的灰度发布 使用及手动实现

    技术2022-07-10  138

    Zuul 网关层的灰度发布

    引言简单使用自己实现(原理)Ribbon 中 ILoadBalancer 如何获取元数据ILoadBalancer::chooseServer(key) 自己实现(代码篇)写一个Predicate写自定义Rule将规则配置进Zuul,写个配置类测试

    引言

    什么是灰度发布? 灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。 灰度期:灰度发布开始到结束期间的这一段时间,称为灰度期。【百度百科】

    目前有2种思路

    一种是请求头上带上flag,也就是说http header中带上如version.权重路由,这和nginx的权重类似。不需要多解释。

    本文写的是第一种。

    网关层只能路由一次,如果想实现全链路,看完本篇文章后看这里: openfeign 转发header 实现全链路灰度发布

    简单使用

    ribbon-discovery-filter-spring-cloud-starter

    引入 https://github.com/jmnarloch/ribbon-discovery-filter-spring-cloud-starter 的依赖

    pom.xml

    <dependency> <groupId>io.jmnarloch</groupId> <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId> <version>2.1.0</version> </dependency> 写个filter,将需要匹配的信息(Http header)写入RibbonFilterContextHolder,这个方式很简单

    github上有例子这里就不多说了。

    自己实现(原理)

    只要搞明白原理,实现起来就不难,这样无论你使用gateway还是zuul,区别都不大。

    Ribbon 中 ILoadBalancer 如何获取元数据

    可以在任意一个服务中引入下面的代码

    @Autowired private SpringClientFactory factory; @GetMapping("/service_list") private List<Server> serviceList(){ //获取LoadBalancer ILoadBalancer lb = factory.getLoadBalancer("服务名"); //获取所有服务列表 List<Server> allServers = lb.getAllServers(); //获取正常的服务列表 List<Server> upServers = lb.getReachableServers(); //打印 System.out.println(allServers); System.out.println(upServers); //注意 想要获得元数据(不是metainfo,是metadata) //跟踪源码你可以看到必须强转成DiscoveryEnabledServer //获取metadata中的version upServers.forEach(s->{ System.out.println("version:" + ((DiscoveryEnabledServer)s).getInstanceInfo().getMetadata().get("version")); }); return upServers; }

    ILoadBalancer::chooseServer(key)

    其实返回的是IRule::choose(key); 所以如果要实现灰度发布,其实我们只需要自定义Rule即可。

    自己实现(代码篇)

    这里我尽量将代码写的直观一些,虽然为了方便的移植到gateway写一个ContextHolder更好,但是为了代码直观,省略掉。 因为我们省略掉了ContextHolder,所以连Filter都不用写了。

    写一个Predicate

    Predicate大家都懂得,我们继承AbstractServerPredicate,重写apply即可。 原因是为了后面写规则,规则需要继承PredicateBasedRule,其中的choose方法内,会调用到getPredicate。

    import com.google.common.base.Optional; import com.netflix.loadbalancer.AbstractServerPredicate; import com.netflix.loadbalancer.PredicateKey; import com.netflix.loadbalancer.Server; import com.netflix.niws.loadbalancer.DiscoveryEnabledServer; import org.checkerframework.checker.nullness.compatqual.NullableDecl; import org.springframework.util.StringUtils; import java.util.List; public class MyPredicate extends AbstractServerPredicate { @Override public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) { if (loadBalancerKey == null) { return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate())); } else { List<Server> results = Lists.newArrayList(); for (Server server: servers) { //这里是下面的apply方法 if (this.apply(new PredicateKey(loadBalancerKey, server))) { results.add(server); } } //这里我们需要重写 //如果results.size为0,说明所有服务都没有写metadata,或者和headers不匹配,所以这时候我们就得忽略这个规则,否则servers返回一个空集,就会找不到服务报错。 if (results.size()>0) return results; else return servers; } } @Override public boolean apply(@NullableDecl PredicateKey predicateKey) { Server server = predicateKey.getServer(); //Object loadBalancerKey = predicateKey.getLoadBalancerKey(); if (server instanceof DiscoveryEnabledServer){ HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); //从http头获取version final String contextVersion = request.getHeader("version"); //获取服务注册进注册中心的metadata final String metaVersion = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata().get("version"); return StringUtils.isEmpty(contextVersion) || contextVersion.equals(metaVersion); } return true; } @Override public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) { //有需要的话连这也可以重写掉,具体下面写规则的时候会说 return super.chooseRoundRobinAfterFiltering(servers, loadBalancerKey); } }

    写自定义Rule

    自定义规则直接继承PredicateBasedRule即可。 还记得上面说到lb在chooseServer时会调用Rule的choose方法吧? PredicateBasedRule中choose方法关键部分在这: Optional server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key); 我们写自定义规则事实上就是让getPredicate时,返回我们上面写好的Predicate。 然后会调用chooseRoundRobinAfterFiltering 而 chooseRoundRobinAfterFiltering 中,会获取所有的server List<Server> eligible = getEligibleServers(servers, loadBalancerKey);。 我们只是改写了this.predicate。

    public class MyPredicateRule extends PredicateBasedRule { private final MyPredicate predicate; public MyPredicateRule() { this.predicate = new MyPredicate(); } public MyPredicateRule(MyPredicate predicate) { this.predicate = predicate; } @Override public AbstractServerPredicate getPredicate() { return this.predicate; } }

    将规则配置进Zuul,写个配置类

    看一下autoconfiguration抄一份

    @Configuration @AutoConfigureBefore(RibbonClientConfiguration.class) @ConditionalOnProperty(value = "ribbon.filter.metadata.enabled", matchIfMissing = true) public class RuleConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnClass(DiscoveryEnabledNIWSServerList.class) @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) //@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public MyPredicateRule metaDataAwareRule() { return new MyPredicateRule(); } }

    测试

    随便搭2个相同的服务,yml中metadata的version写不同版本

    eureka: instance: prefer-ip-address: true metadata-map: version: 1 # 2个服务 版本不要相同 client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:8761/eureka/

    在zuul中定义routes,然后测试,这里我就不写了,直接看图 如果请求header没有包含version或version为空,又或者没有任何一个服务的metadata中的version与之对应,就会回到轮询的规则。

    Processed: 0.014, SQL: 9