在前面实现的功能中,动态路由其实只是拓展了获取路由数据的途径,那么如何去控制路由规则的转发呢?
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;
}
}
上面的类,比较简单,那么要注意以下几点。
- 该过滤器执行顺序需在RibbonRoutingFilter之前
首先由于zuul处理请求的核心是以链式的过滤器去执行的,所以需要控制好顺序。先生成需要的缓存信息,那么在后续RibbonRoutingFilter执行的时候,才可以通过LoadBalancer中的rule(rule需要通过缓存信息去用于判断)去控制。
- 该过滤器执行顺序需在RibbonRoutingFilter之前
- 拦截什么类型的请求
路由规则中只有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的访问
至此,简单的灰度发布就完成了,从上面的结果来看,可以通过请求头从而控制最终的路由。