投资网站哪个好,网站网站游戏怎么做,seo网络推广技术员招聘,软文推广有哪些平台作者#xff1a;vivo 互联网中间件团队- Wang Xiaochuang 本文主要介绍在vivo内部针对Dubbo路由模块及负载均衡的一些优化手段#xff0c;主要是异步化缓存#xff0c;可减少在RPC调用过程中路由及负载均衡的CPU消耗#xff0c;极大提升调用效率。
一、概要
vivo内部Java… 作者vivo 互联网中间件团队- Wang Xiaochuang 本文主要介绍在vivo内部针对Dubbo路由模块及负载均衡的一些优化手段主要是异步化缓存可减少在RPC调用过程中路由及负载均衡的CPU消耗极大提升调用效率。
一、概要
vivo内部Java技术栈业务使用的是Apache Dubbo框架基于开源社区2.7.x版本定制化开发。在海量微服务集群的业务实践中我们发现Dubbo有一些性能瓶颈的问题会极大影响业务逻辑的执行效率尤其是在集群规模数量较大时(提供方数量100)时路由及负载均衡方面有着较大的CPU消耗从采集的火焰图分析高达30%。为此我们针对vivo内部常用路由策略及负载均衡进行相关优化并取得了较好的效果。接下来主要跟大家分析一下相关问题产生的根源以及我们采用怎样的方式来解决这些问题。当前vivo内部使用的Dubbo的主流版本是基于2.7.x进行相关定制化开发。
二、背景知识
2.1 Dubbo客户端调用流程
1. 相关术语介绍 2. 主要流程
客户端通过本地代理Proxy调用ClusterInvokerClusterInvoker从服务目录Directory获取服务列表后经过路由链获取新的服务列表、负载均衡从路由后的服务列表中根据不同的负载均衡策略选取一个远端Invoker后再发起远程RPC调用。 2.2 Dubbo路由机制
Dubbo的路由机制实际是基于简单的责任链模式实现同时Router继承了Comparable接口自定义的路由可以设置不同的优先级进而定制化责任链上Router的顺序。基于责任链模式可以支持多种路由策略串行执行如就近路由标签路由或条件路由就近路由等且路由的配置支持基于接口级的配置也支持基于应用级的配置。常见的路由方式主要有就近路由条件路由标签路由等。具体的执行过程如下图所示 1. 核心类
Dubbo路由的核心类主要有RouterChain、RouterFactory 与 Router 。
1RouterChain
RouterChain是路由链的入口其核心字段有 invokersListinvoker 类型
初始服务列表由服务目录Directory设置当前RouterChain要过滤的Invoker集合 builtinRoutersList类型
当前RouterChain包含的自动激活的Router集合 routersList类型
包括所有要使用的路由由builtinRouters加上通过addRouters()方法添加的Router对象
RouterChain核心逻辑
public class RouterChainT {// 注册中心最后一次推送的服务列表private ListInvokerT invokers Collections.emptyList();// 所有路由,包括原生Dubbo基于注册中心的路由规则如“route://” urls .private volatile ListRouter routers Collections.emptyList();// 初始化自动激活的路由private ListRouter builtinRouters Collections.emptyList();private RouterChain(URL url) {//通过ExtensionLoader加载可自动激活的RouterFactoryListRouterFactory extensionFactories ExtensionLoader.getExtensionLoader(RouterFactory.class).getActivateExtension(url, ROUTER_KEY);// 由工厂类生成自动激活的路由策略ListRouter routers extensionFactories.stream().map(factory - factory.getRouter(url)).collect(Collectors.toList());initWithRouters(routers);}// 添加额外路由public void addRouters(ListRouter routers) {ListRouter newRouters new ArrayList();newRouters.addAll(builtinRouters);newRouters.addAll(routers);Collections.sort(newRouters, comparator);this.routers newRouters;}public ListInvokerT route(URL url, Invocation invocation) {ListInvokerT finalInvokers invokers;// 遍历全部的Router对象执行路由规则for (Router router : routers) {finalInvokers router.route(finalInvokers, url, invocation);}return finalInvokers;}
}
2RouterFactory为Router的工厂类
RouterFactory接口定义
SPI
public interface RouterFactory {Adaptive(protocol)Router getRouter(URL url);
}
3Router
Router是真正的路由实现策略由RouterChain进行调用同时Router继承了Compareable接口可以根据业务逻辑设置不同的优先级。
Router主要接口定义
public interface Router extends ComparableRouter {/**** param invokers 带过滤实例列表* param url 消费方url* param invocation 会话信息* return routed invokers* throws RpcException*/T ListInvokerT route(ListInvokerT invokers, URL url, Invocation invocation) throws RpcException;/*** 当注册中心的服务列表发现变化或有动态配置变更会触发实例信息的变化* 当时2.7.x的Dubbo并没有真正使用这个方法可基于此方法进行路由缓存* param invokers invoker list* param T invokers type*/default T void notify(ListInvokerT invokers) {}}
2. 同机房优先路由的实现
为方便大家了解路由的实现给大家展示一下就近路由的核心代码逻辑
public T ListInvokerT route(ListInvokerT invokers, URL consumerUrl, Invocation invocation) throws RpcException {if (!this.enabled) {return invokers;}// 获取本地机房信息String local getSystemProperty(LOC);if (invokers null || invokers.size() 0) {return invokers;}ListInvokerT result new ArrayListInvokerT();for (Invoker invoker: invokers) {// 获取与本地机房一致的invoker并加入列表中String invokerLoc getProperty(invoker, invocation, LOC);if (local.equals(invokerLoc)) {result.add(invoker);}}if (result.size() 0) {if (fallback){// 开启服务降级available.ratio 当前机房可用服务节点数量 集群可用服务节点数量int curAvailableRatio (int) Math.floor(result.size() * 100.0d / invokers.size());if (curAvailableRatio availableRatio) {return invokers;}}return result;} else if (force) {return result;} else {return invokers;}}
2.3 Dubbo负载均衡
Dubbo的负载均衡实现比较简单基本都是继承抽象类进行实现主要作用就是根据具体的策略在路由之后的服务列表中筛选一个实例进行远程RPC调用默认的负载均衡策略是随机。
整体类图如下所示 LoadBalance接口定义
SPI(RandomLoadBalance.NAME)
public interface LoadBalance {/*** 从服务列表中筛选一个.** param invokers invokers.* param url refer url* param invocation invocation.* return selected invoker.*/Adaptive(loadbalance)T InvokerT select(ListInvokerT invokers, URL url, Invocation invocation) throws RpcException;}
随机负载均衡核心代码解析 // 预热过程权重计算static int calculateWarmupWeight(int uptime, int warmup, int weight) {int ww (int) (uptime / ((float) warmup / weight));return ww 1 ? 1 : (Math.min(ww, weight));}int getWeight(Invoker? invoker, Invocation invocation) {int weight;URL url invoker.getUrl();// 多注册中心场景下的注册中心权重获取if (UrlUtils.isRegistryService(url)) {weight url.getParameter(REGISTRY_KEY . WEIGHT_KEY, DEFAULT_WEIGHT);} else {weight url.getMethodParameter(invocation.getMethodName(), WEIGHT_KEY, DEFAULT_WEIGHT);if (weight 0) {// 获取实例启动时间long timestamp invoker.getUrl().getParameter(TIMESTAMP_KEY, 0L);if (timestamp 0L) {long uptime System.currentTimeMillis() - timestamp;if (uptime 0) {return 1;}// 获取预热时间int warmup invoker.getUrl().getParameter(WARMUP_KEY, DEFAULT_WARMUP);if (uptime 0 uptime warmup) {weight calculateWarmupWeight((int)uptime, warmup, weight);}}}}return Math.max(weight, 0);}Overrideprotected T InvokerT doSelect(ListInvokerT invokers, URL url, Invocation invocation) {// Number of invokersint length invokers.size();// Every invoker has the same weight?boolean sameWeight true;// the weight of every invokersint[] weights new int[length];// the first invokers weightint firstWeight getWeight(invokers.get(0), invocation);weights[0] firstWeight;// The sum of weightsint totalWeight firstWeight;for (int i 1; i length; i) {int weight getWeight(invokers.get(i), invocation);// save for later useweights[i] weight;// SumtotalWeight weight;if (sameWeight weight ! firstWeight) {sameWeight false;}}if (totalWeight 0 !sameWeight) {// If (not every invoker has the same weight at least one invokers weight0), select randomly based on totalWeight.int offset ThreadLocalRandom.current().nextInt(totalWeight);// Return a invoker based on the random value.for (int i 0; i length; i) {offset - weights[i];if (offset 0) {return invokers.get(i);}}}// If all invokers have the same weight value or totalWeight0, return evenly.return invokers.get(ThreadLocalRandom.current().nextInt(length));}
预热解释
预热是为了让刚启动的实例流量缓慢增加,因为实例刚启动时各种资源可能还没建立连接相关代码可能还是处于解释执行仍未变为JIT执行此时业务逻辑较慢不应该加载过大的流量否则有可能造成较多的超时。Dubbo默认预热时间为10分钟新部署的实例的流量会在预热时间段内层线性增长最终与其他实例保持一致。Dubbo预热机制的实现就是通过控制权重来实现。如默认权重100预热时间10分钟则第一分钟权重为10第二分钟为20以此类推。
具体预热效果图如下 三、问题分析
使用Dubbo的业务方反馈他们通过火焰图分析发现Dubbo的负载均衡模块路由模块占用CPU超过了30%框架层面的使用率严重影响了业务逻辑的执行效率急需进行优化。通过火焰图分析具体占比如下图其中该机器在业务忙时的CPU使用率在60%左右闲时在30%左右。 通过火焰图分析负载均衡主要的消耗是在 getWeight方法。 路由的主要消耗是在route方法 同机房优先路由 接口级标签路由应用级标签路由 这些方法都有一个特点那就是遍历执行。如负载均衡针对每一个invoker都需要通过getWeight方法进行权重的计算就近路由的router方法对于每一个invoker都需要通过url获取及机房信息进行匹配计算。
我们分析一下getWeight及router时间复杂度发现是O(n)的时间复杂度而且路由是由路由链组成的每次每个 Router的route方法调用逻辑都会遍历实例列表那么当实例列表数量过大时每次匹配的计算的逻辑过大那么就会造成大量的计算成本导致占用大量cpu同时也导致路由负载均衡效率低下。
综上所述罪恶的的根源就是遍历导致的当服务提供方数量越多影响越大。
四、优化方案
知道了问题所在我们来分析一下是否有优化空间。
4.1 路由优化
1. 优化一关闭无效路由
通过火焰图分析我们发现有部分业务即使完全不使用应用级的标签路由原生的TagRouter也存在遍历逻辑原因是为了支持静态的标签路由其实这部分的开销也不少那对于根本不会使用应用级标签路由的可以手动进行关闭。关闭方式如下 客户端统一关闭
dubbo.consumer.router-tag
服务级别关闭 注解方式
DubboReference(parameters {router,-tag}) xml方式
dubbo:reference iddemoService checkfalse interfacecom.dubbo.study.n.api.DemoService router-tag / 2. 优化二提前计算路由结果并进行缓存
每次路由目前都是进行实时计算但是在大多数情况下我们的实例列表是稳定不变的只有在发布窗口或配置变更窗口内实例列表才会发生变更那我们是否可以考虑缓存呢。如就近路由可以以机房为key进行机房实例的全量缓存。针对接口级标签路由可以缓存不同标签值指定的实例信息。
我们知道路由的执行过程是责任链模式每一个Router的实例列表入参实际上是一个Router的结果可参考公式target rn(…r3(r2(r1(src))))。那么所有的路由可以基于注册中心推送的原始服务列表进行路由计算并缓存然后不同的路由结果相互取交集就能得到最终的结果当实例信息发生变更时缓存失效并重新计算。
3. 缓存更新时机
当注册中心或者动态配置有变更时相关通知会给到服务目录Directory,Directory收到通知后会重新创建服务列表并把服务列表同步到路由链RouterChainRouterChain再按顺序通知其链上的Router,各个Router再进行缓存清除并重新进行路由结果的计算及进行缓存。相关时序图如下所示 4. 具体路由流程
进入具体路由方法时先判断是否存在缓存的路由值且缓存值的epoch必须与上一个路由的epoch需一致此时缓存才生效然后缓存值与上个Router的结果取交集。
如果不存在缓存或epoch不一致则重新进行实时的路由计算。 引入epoch的原因主要是保证各个路由策略缓存信息的一致性保证所有的缓存计算都是基于同一份原始数据。当实例信息发生变更时epoch会自动进行更新。
5. BitMap引入
上文我们说到不同的路由策略之间的结果是取交集的然后最终的结果才送入负载均衡流程。那如何在缓存的同时加快交集的计算呢。答案就是基于位图BitMap。
BitMap的基本原理就是用一个bit位来存放某种状态适用于大规模数据的查找及位运算操作。如在路由场景先基于全量的推送数据进行计算缓存。如果某个实例被路由选中则其值为1若两个路由的结果要取交集那直接对BitMap进行运行即可。
全量缓存示意图 路由交集计算示步骤
按照路由链依次计算tagRouter-vivoTag-vivoNearestRouter
1tagRouter计算逻辑 按照Invocation计算出目标的Tag假设是tag1 然后从缓存Cache根据key:tag1取出对应的targetAddrPool 将原始传入的addrPool与targetAddrPool得到结果resultAddrPool 将resultAddrPool传入vivoTagRouter
2vivoTag计算逻辑 按照Invocation计算出目标的Tag,假设是tabB 然后从缓存Cache根据key:tag1取出对应的targetAddrPool 将上一次传入的addrPool与targetAddrPool得到结果resultAddrPooll 将resultAddrPool传入vivoNearestRouter
3vivoNearestRouter计算逻辑 从环境变量取出当前机房假设是bj01 然后从缓存Cache根据key:bj01取出对应的targetAddrPool 将上一次传入的addrPool与targetAddrPool取出resultAddrPool 将上一次传入的addrPool与targetAddrPool得到结果resultAddrPool 将resultAddrPool为最终路由结果传递给LoadBalance 6. 基于缓存的同机房优先路由源码解析
缓存刷新
/*** Notify router chain of the initial addresses from registry at the first time.* Notify whenever addresses in registry change.*/public void setInvokers(ListInvokerT invokers) {// 创建带epoch的BitListthis.invokers new BitListInvokerT(invokers null ? Collections.emptyList() : invokers,createBitListEpoch());routers.forEach(router - router.notify(this.invokers));} 同机房优先路由源码解读
public T ListInvokerT route(ListInvokerT invokers, URL consumerUrl, Invocation invocation) throws RpcException {…………//省略非核心代码BitListInvokerT bitList (BitListInvokerT) invokers;//获取路由结果BitListInvokerT result getNearestInvokersWithCache(bitList);if (result.size() 0) {if (fallback) {// 开启服务降级available.ratio 当前机房可用服务节点数量 集群可用服务节点数量int curAvailableRatio (int) Math.floor(result.size() * 100.0d / invokers.size());if (curAvailableRatio availableRatio) {return invokers;}}return result;} else if (force) {return result;} else {return invokers;}} /*** 获取缓存列表* param invokers* param T* return*/private T BitListInvokerT getNearestInvokersWithCache(BitListInvokerT invokers) {ValueWrapper valueWrapper getCache(getSystemProperty(LOC));// 是否存在缓存if (valueWrapper ! null) {BitListInvokerT invokerBitList (BitListInvokerT) valueWrapper.get();// 缓存的epoch与源列表是否一致if (invokers.isSameEpoch(invokerBitList)) {BitListInvokerT tmp invokers.clone();// 结果取交集return tmp.and(invokerBitList);}}// 缓存不存在 实时计算放回return getNearestInvokers(invokers);}/*** 新服务列表通知* param invokers* param T*/Overridepublic T void notify(ListInvokerT invokers) {clear();if (invokers ! null invokers instanceof BitList) {BitListInvokerT bitList (BitListInvokerT) invokers;// 设置最后一次更新的服务列表lastNotify bitList.clone();if (!CollectionUtils.isEmpty(invokers) this.enabled) {// 获取机房相同的服务列表并进行缓存setCache(getSystemProperty(LOC), getNearestInvokers(lastNotify));}}}
4.2 负载均衡优化
1. 优化一
针对getWeight方法我们发现有部分业务逻辑较为消耗cpu,但是在大多数场景下业务方并不会使用到于是进行优化。
getWeight方法优化
优化前
//这里主要要用多注册中心场景下注册中心权重的获取绝大多数情况下并不会有这个逻辑if (UrlUtils.isRegistryService(url)) {weight url.getParameter(REGISTRY_KEY . WEIGHT_KEY, DEFAULT_WEIGHT);}
优化后if (invoker instanceof ClusterInvoker UrlUtils.isRegistryService(url)) {weight url.getParameter(REGISTRY_KEY . WEIGHT_KEY, DEFAULT_WEIGHT);} 2. 优化二
遍历是罪恶的源泉而实例的数量决定这罪恶的深浅我们有什么办法减少负载均衡过程中的遍历呢。一是根据group及version划分不同的集群但是这需要涉及到业务方代码或配置层面的改动会带来额外的成本。所以我们放弃了。
二是没有什么是加一层解决不了的问题为了尽量减少进入负载均衡的节点数量考虑新增一个垫底的路由策略在走完所有的路由策略后若节点数量自定义数量后进行虚拟分组虚拟分组的策略也可进行自定义然后随机筛选一组进入负载均衡。此时进入负载均衡的实例数量就会有倍数的下降。
需要注意的是分组路由必须保证是在路由链的最后一环否则会导致其他路由计算错误。 分组路由示意
/*** * param invokers 待分组实例列表* param groupNum 分组数量* param T* return*/public T ListInvokerT doGroup(ListInvokerT invokers, int groupNum) {int listLength invokers.size() / groupNum;ListInvokerT result new ArrayList(listLength);int random ThreadLocalRandom.current().nextInt(groupNum);for (int i random; i invokers.size(); i i groupNum) {result.add(invokers.get(i));}return result;}
五、优化效果
针对优化前和优化后我们编写Demo工程分别压测了不配置路由/配置就近标签路由场景。Provider节点梯度设置100/500/1000/2000/5000TPS在1000左右记录了主机的cpu等性能指标并打印火焰图。发现配置路由后采用相同并发优化后的版本tps明显高于优化前版本且新版本相较于没有配置路由时tps显著提高下游节点数大于2000时tps提升达到100%以上下游节点数越多AvgCpu优化效果越明显并且路由及负载均衡CPU占比明显更低详细数据可见下表 备注-tag表示显示禁用原生Dubbo应用级标签路由。该路由默认开启。
六、总结
经过我们关闭不必要的路由逻辑、对路由缓存异步化计算、新增分组路由等优化后Dubbo在负载均衡及路由模块整体的性能有了显著的提升为业务方节省了不少CPU资源。在正常业务场景下当提供方数量达到2000及以上时tps提升可达100%以上消费方平均CPU使用率下降约27%且提供方数量越多优化效果越明显。但是我们也发现当前的随机负载均衡依然还是会消耗一定的CPU资源且只能保证流量是均衡的。当前我们的应用基本部署在虚拟机及容器上。这两者均存在超卖的状况且同等配置的宿主机性能存在较大差异等问题。最终会导致部分请求超时、无法最大化利用提供方的资源。我们下一步将会引入Dubbo 3.2的自适应负载均衡并进行调优减少其CPU使用率波动较大的问题其次我们自身也扩展了基于CPU负载均衡的单一因子算法最终实现不同性能的机器CPU负载趋于均衡最大程度发挥集群整体的性能。
参考资料 Dubbo 负载均衡 Dubbo 流量管控 Dubbo 3 StateRouter下一代微服务高效流量路由