在前面實(shí)現(xiàn)的功能中,動(dòng)態(tài)路由其實(shí)只是拓展了獲取路由數(shù)據(jù)的途徑,那么如何去控制路由規(guī)則的轉(zhuǎn)發(fā)呢?
zuul的ZoneAwareLoadBalancer的實(shí)現(xiàn)流程雖然看上去比較復(fù)雜,其實(shí)鏈路也是比較簡(jiǎn)單的。
下面簡(jiǎn)單梳理一下
1.先通過(guò)LoadBalancerStats的upServerListZoneMap找出對(duì)應(yīng)分區(qū)的所有服務(wù)列表
2.隨后通過(guò)LoadBalancerStats中的serverStatsCache,將服務(wù)列表的統(tǒng)計(jì)數(shù)據(jù)聚合
3.生成分區(qū)的統(tǒng)計(jì)數(shù)據(jù)快照Z(yǔ)oneSnapshot
4.通過(guò)規(guī)則挑選出合適的分區(qū)
5.通過(guò)分區(qū)的負(fù)載均衡器,結(jié)合rule挑選出合適的服務(wù)。
所以如果要控制路由的轉(zhuǎn)發(fā),其實(shí)通過(guò)Rule來(lái)控制即可。
實(shí)現(xiàn)灰度發(fā)布
那么下面簡(jiǎn)單實(shí)現(xiàn)一下灰度發(fā)布。
先說(shuō)一下實(shí)現(xiàn)的原理,我的方案比較簡(jiǎn)單,就是通過(guò)請(qǐng)求頭的一些信息(如應(yīng)用名稱(chēng),以及版本號(hào)),來(lái)確定最后轉(zhuǎn)發(fā)到哪個(gè)服務(wù)中。
1.自定義請(qǐng)求頭緩存過(guò)濾器
這個(gè)過(guò)濾器主要是將請(qǐng)求中的請(qǐng)求頭信息進(jìn)行緩存。
@Component
public class CacheFilter extends com.netflix.zuul.ZuulFilter{
@Override
public boolean shouldFilter() {
//只攔截通過(guò)ribbon進(jìn)行路由的---此處與ribbon源碼一致
RequestContext ctx = RequestContext.getCurrentContext();
return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
&& ctx.sendZuulResponse());
}
@Override
public Object run() throws ZuulException {
將請(qǐng)求頭信息進(jìn)行緩存放入到請(qǐng)求上下文中。
其中的app以及version都是自定義的請(qǐng)求頭。用于模擬版本號(hào)以及應(yīng)用名稱(chēng)
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;
}
}
上面的類(lèi),比較簡(jiǎn)單,那么要注意以下幾點(diǎn)。
- 該過(guò)濾器執(zhí)行順序需在RibbonRoutingFilter之前
首先由于zuul處理請(qǐng)求的核心是以鏈?zhǔn)降倪^(guò)濾器去執(zhí)行的,所以需要控制好順序。先生成需要的緩存信息,那么在后續(xù)RibbonRoutingFilter執(zhí)行的時(shí)候,才可以通過(guò)LoadBalancer中的rule(rule需要通過(guò)緩存信息去用于判斷)去控制。
- 該過(guò)濾器執(zhí)行順序需在RibbonRoutingFilter之前
- 攔截什么類(lèi)型的請(qǐng)求
路由規(guī)則中只有serverId的,這樣子底層是通過(guò)serverId去路由(服務(wù)id與真實(shí)ip地址的映射最終是通過(guò)注冊(cè)中心的客戶(hù)端拉取的注冊(cè)表中獲取)。
- 攔截什么類(lèi)型的請(qǐng)求
2.如何通過(guò)rule去控制路由
- 2.1首先先定義一個(gè)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);
}
}
這個(gè)判斷的標(biāo)準(zhǔn),其實(shí)就是根據(jù)服務(wù)的元數(shù)據(jù),與請(qǐng)求頭對(duì)比,看看服務(wù)的元數(shù)據(jù)與請(qǐng)求頭中的應(yīng)用以及版本是否匹配,決定該服務(wù)是否符合標(biāo)準(zhǔn)。
- 2.2 如何利用Predicate
由于Rule本身支持多個(gè)Predicate的使用。另外要兼顧此前的一些判斷標(biāo)準(zhǔn)。
所以此處新增一個(gè)Rule用于支持多種類(lèi)型Predicate。
public class GrayRule extends ZoneAvoidanceRule{
private CompositePredicate compositePredicate;
public GrayRule() {
用于判斷該服務(wù)的分區(qū)是否是一個(gè)壞的分區(qū),需要避免使用該分區(qū)
ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this, null);
灰度,用于判斷該服務(wù)是否適用于該請(qǐng)求
GrayPredicate grayPredicate = new GrayPredicate();
用于判斷該服務(wù)的壓力是否過(guò)大
AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this, null);
compositePredicate = CompositePredicate.withPredicates(zonePredicate, availabilityPredicate, grayPredicate)
.build();
}
覆寫(xiě)該方法,否則會(huì)返回父類(lèi)的Predicate
@Override
public AbstractServerPredicate getPredicate() {
return compositePredicate;
}
}
- 2.3 如何讓負(fù)載均衡器使用該rule?
簡(jiǎn)單進(jìn)行注入即可
@Configuration
public class GrayConfiguration {
@Bean
GrayRule grayRule() {
GrayRule gr = new GrayRule();
return gr;
}
}
需要注意的點(diǎn),該Rule需要比起默認(rèn)的Rule(ZoneAvoidanceRule )先被掃描到。否則將不會(huì)被注入。
源碼如下
@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.簡(jiǎn)單測(cè)試
- 3.1環(huán)境準(zhǔn)備
準(zhǔn)備2個(gè)服務(wù)端對(duì)應(yīng)不同的版本,對(duì)應(yīng)的注冊(cè)信息是如下
服務(wù)A-one:對(duì)應(yīng)版本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
服務(wù)A-two:對(duì)應(yīng)版本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
分別啟動(dòng)對(duì)應(yīng)的服務(wù)。


至此,簡(jiǎn)單的灰度發(fā)布就完成了,從上面的結(jié)果來(lái)看,可以通過(guò)請(qǐng)求頭從而控制最終的路由。