目录
1. 功能背景
灰度发布,是后端应用服务新、旧版本间平滑过渡的一种发布方式。根据特定的规则,挑选一部分用户访问灰度版本的服务,并逐步扩大范围,最终把所有用户访问迁移到新的版本。来实现灰度效果。本文是基于zuul网关以及eureka等组件实现的灰度发布功能,如果想通过gateway结合nacos实现灰度发布,其思想大致相同。只是代码实现细节有差异而已。
2. 产品功能优点
1.新版本风险高,按照灰度规则逐渐切入流量。不用一刀切的切换。显著降低新版本上线风险。
2.可以在白天上线新版本,逐步导入流量。不必等到深夜上线。
3.可以多个版本并行在线上提供服务。实现新老服务的并行兼容运行。
4.支持按照标签、流量百分比等不同灰度策略进行灰度发布。
5.新版本出现故障,通过删除标签或者配置流量占比,流量自动导回到老服务。
3. 原理图
注:无论是标签灰度还是权重灰度,其储存在网关本地内存的灰度配置下发策略都是一样的,可以在管理后台实现这一功能,结合zookeeper或者消息队列等组件实现配置的下发,最终同步到网关的本地内存中。
3.1 标签灰度
3.2 权重灰度
4. 代码讲解
4.1 接入方服务添加配置:
网关下游服务配置文件中添加eureka客户端配置,值依实际情况配置:
示例:
eureka.instance.metadata-map.serviceVersion= V1
(注:无论是eureka还是nacos,注册的服务都可以上报自己的元数据信息metadata)
4.2 网关路由部分核心代码
参考个人之前写的文章:zuul 1.x 源码解析 - 5.2.2.1 RibbonRoutingFilter 部分。
4.3 PredicateBasedRule
请求在经过路由过滤器RibbonRoutingFilter 的时候,会调用PredicateBasedRule的内部方法。PredicateBasedRule是ClientConfigEnabledRoundRobinRule的一个子类,它先通过内部定义的一个过滤器过滤出一部分服务实例清单,然后再采用线性轮询的方式从过滤出来的结果中选取一个服务实例。PredicateBasedRule实现了IRule接口,代码如下:
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
public abstract AbstractServerPredicate getPredicate();
@Override
public Server choose(Object key) {
//@1
ILoadBalancer lb = getLoadBalancer();
//@2
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
@1:获取负载均衡器
@2:getPredicate方法的目的是获取预测器,里面是个复合的AbstractServerPredicate,AbstractServerPredicate是Ribbon在进行Server过滤的一个重要基础组件。它的作用就是在众多Server的列表中,通过一定的过滤策略,剔除不合格的Server,留下来合格的Server列表,进而供以选择。
4.4 自定义MetaDataCanaryRule继承PredicateBasedRule
由于我们的灰度发布的核心功能是从所有的实例中筛选出符合版本要求的实例,所以我们需要自定义预测器继承自AbstractServerPredicate,并实现自定义的Server列表过滤策略。
自定义预测器:
public class MetaDataCanaryRule extends PredicateBasedRule {
private MetadataCanaryRuleHandler metadataCanaryRuleHandler;
public MetaDataCanaryRule() {
}
public MetaDataCanaryRule(MetadataCanaryRuleHandler metadataCanaryRuleHandler) {
this.metadataCanaryRuleHandler = metadataCanaryRuleHandler;
}
@Override
//自定义预测器
public AbstractServerPredicate getPredicate() {
return this.metadataCanaryRuleHandler;
}
}
4.5 自定义MetadataCanaryRuleHandler继承AbstractServerPredicate
前面4.3有提及到,在对所有实例进行筛选的时候,会调用AbstractServerPredicate的chooseRoundRobinAfterFiltering方法,代码如下:
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
//@1
List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
if (eligible.size() == 0) {
return Optional.absent();
}
return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
}
@1:getEligibleServers方法的目的是通过传入的所有servers列表,通过筛选,来获取合适的服务列表的。所以我们可以重写此方法,自定义我们的筛选过滤逻辑。
在MetadataCanaryRuleHandler类中自定义getEligibleServers方法:
@Override
public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
//一个不计算
if (servers.size() <= 1) {
return servers;
}
try {
RequestContext requestContext = RequestContext.getCurrentContext();
if (metadataEnabled) {
//开启过滤
AppRequest appRequest = null == requestContext.get(Constant.APP_ZUUL_REQUEST) ? new AppRequest() :
(AppRequest) requestContext.get(Constant.APP_ZUUL_REQUEST);
//用户标签
String tag = appRequest.getTag();
//转发参数
String action = appRequest.getAction();
String[] actionArr = action.split("\\.");
//服务id
String service = actionArr[0];
//本地缓存获取服务对应策略
Integer grayscalePubStrategy = cacheGrayscalePubSource.getGrayscalePubStrategy(service);
//没有配置,不过滤
if (grayscalePubStrategy == null) {
return servers;
}
//标签过滤
if (grayscalePubStrategy == B_Q) {
return BQStrategy(servers, service, tag);
//权重过滤
} else if (grayscalePubStrategy == Q_Z) {
return QZStrategy(servers, service);
}
}
} catch (Exception e) {
logger.error(">>>>>>> 灰度发布计算异常 ----> 当前轮询所有!", e);
}
return servers;
}
4.6 按标签过滤
public List<Server> BQStrategy(List<Server> servers, String service, String tag) {
//优先调用服务列表
List<Server> preResults = Lists.newArrayList();
//默认调用服务列表
List<Server> defaultLowerResults = Lists.newArrayList();
//默认调用服务列表
List<Server> defaultHightResults = Lists.newArrayList();
for (Server server : servers) {
DiscoveryEnabledServer discoveryEnabledServer = (DiscoveryEnabledServer) server;
//获取元数据信息
final Map<String, String> metadata = discoveryEnabledServer.getInstanceInfo().getMetadata();
String serviceVersion = metadata.get(SERVICE_VERSION);
if (StringUtils.isBlank(serviceVersion) || (serviceVersion != null && "defaultVersion".equals(serviceVersion))) {
defaultLowerResults.add(server);
} else {
//在本地缓存中做数据匹配
String servicePriority = cacheGrayscalePubSource.getGrayscalePubInfo(service, serviceVersion, tag);
if (Constant.ServicePriority.COMMON.name().equals(servicePriority)) {
preResults.add(server);
}
if (Constant.ServicePriority.DEFAULT.name().equals(servicePriority)) {
defaultHightResults.add(server);
}
if (Constant.ServicePriority.NULL.name().equals(servicePriority)) {
continue;
}
}
}
//preResults有数据返回preResults,如果没数据判断defaultResults,如果
//defaultResults有数据返回defaultResults,如果没数据,返回所有服务列表
if (CollectionUtils.isEmpty(preResults)) {
if (!CollectionUtils.isEmpty(defaultHightResults)) {
return defaultHightResults;
}
if (!CollectionUtils.isEmpty(defaultLowerResults)) {
return defaultLowerResults;
}
return servers;
}
return preResults;
}
4.7 按权重过滤
public List<Server> QZStrategy(List<Server> servers, String service) {
//默认调用服务列表
List<Server> defaultLowerResults = Lists.newArrayList();
//默认调用服务列表
List<Server> defaultHightResults = Lists.newArrayList();
//权重列表
List<Server> grayscaleWeightResults = Lists.newArrayList();
//如果权重则先计算
String randomVersion = "";
//本地缓存中获取服务的版本流量比例
TreeMap<String, Integer> randomMap = cacheGrayscalePubSource.getGrayscaleWeightMap(service);
if (randomMap == null) {
return servers;
}
//权重计算出版本
randomVersion = getrandomVersion(randomMap);
for (Server server : servers) {
DiscoveryEnabledServer discoveryEnabledServer = (DiscoveryEnabledServer) server;
final Map<String, String> metadata = discoveryEnabledServer.getInstanceInfo().getMetadata();
String serviceVersion = metadata.get(SERVICE_VERSION);
if (StringUtils.isBlank(serviceVersion) || (serviceVersion != null && "defaultVersion".equals(serviceVersion))) {
defaultLowerResults.add(server);
} else {
//兜底的版本
String defaultConfigVersion = cacheGrayscalePubSource.getDefaultServiceVersion(service);
if (randomVersion != null && randomVersion.equals(serviceVersion)) {
grayscaleWeightResults.add(server);
}
if (defaultConfigVersion != null && defaultConfigVersion.equals(serviceVersion)) {
defaultHightResults.add(server);
}
}
}
//选择返回 - 是否兜底 : 如果没有匹配的权重策略
if (CollectionUtils.isEmpty(grayscaleWeightResults)) {
if (!CollectionUtils.isEmpty(defaultHightResults)) {
return defaultHightResults;
}
if (!CollectionUtils.isEmpty(defaultLowerResults)) {
return defaultLowerResults;
}
return servers;
}
return grayscaleWeightResults;
}
public static String getrandomVersion(TreeMap<String, Integer> randomMap) {
Integer tmp = 0;
ArrayList<GrayscaleWeight> arr = new ArrayList<>();
for (Map.Entry<String, Integer> entry : randomMap.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
GrayscaleWeight gw = new GrayscaleWeight(key, tmp, tmp + value);
tmp += value;
arr.add(gw);
}
Random random = new Random();
int i = random.nextInt(tmp);
return binarySearch(arr, i);
}