大家好,我是楼下的小黑哥~今天文章的内容来自于一位朋友外出面试遇到的问题:“你了解Dubbo服务预热流程吗?详细说说它的原理吧。”这个问题朋友不好回答,因为之前没看懂。说实话,一开始我也只是大致知道预热代码在什么位置,具体原理还是没看懂。所以这次仔细看了一下代码,查了一些Github代码提交记录,终于摸清了这块的原理,分享给大家。预热首先我们来看看什么是服务预热?让我们举一个现实生活中的例子。买过新车的同学应该都知道,新车是有磨合期的。大约需要一两千公里才能达到最佳性能。地位。其实服务预热也是这个意思。刚开始服务时会有一个“磨合期”。在此期间,服务运行状态并不理想。如果一下子把业务流量提升到正常状态,可能会有大量的请求。超时或立即使系统不堪重负。因此,在服务刚启动时,我们需要慢慢增加流量,直到一段时间后达到阈值上限,给系统一个“预热过程”,使其运行状态达到最佳.那为什么刚启动服务时系统状态没有达到最优状态呢?大概原因如下:Java应用程序有一个类加载过程,这个过程是按需加载的。即在服务首次启动时,JVM只加载启动过程所必需的类。在调用服务之前,我们需要的类不会被真正加载。另外,对于一些“热点代码”,JVM会使用JIT编译器将其编译成本地代码,以提高运行速度。以上两个过程是由于JVM系统层面的影响。另外,我们的服务系统可能需要一些缓存资源。刚启动时,服务需要加载这些资源,因为资源不存在。Dubbo预热的实现已经准备就绪。了解了预热是怎么回事,我们回归正题,看看Dubbo是如何实现预热的。首先我们来看一下Dubbo服务模型:服务提供者启动后,会将节点相关信息注册到注册中心,服务消费者可以通过注册中心及时获取所有服务节点。当服务消费者调用服务时,会通过负载均衡组件在内部选择一个节点进行服务调用。如上图所示,假设B节点服务刚刚启动,需要一个预热过程,需要服务消费者逐步向B节点分发流量。下面我们从Dubbo源码入手,观察服务预热的具体实现。具体源码位于AbstractLoadBalance#getWeightps:目前源码Dubbo版本为2.7.4,此版本以下代码实现有一些差异。详情见下文。这段代码主要分为三步:获取服务提供者启动时间时间戳当前时间减去服务提供者启动时间,计算服务提供者运行时间uptime根据运行时间动态计算服务预热过程的权重动态权重计算方法如下:这里的计算方法其实很简单。简单的说,服务运行的时间越长,权重越高,直到正常权重。如果服务提供者已经运行了1分钟,那么权重将最终为10。如果服务提供者已经运行了5分钟,那么权重将最终为50。如果服务提供者已经运行了11分钟,超过了默认的预热时间阈值10分钟,那么就不会再进行计算,直接返回weight的默认权重。这里需要注意的是,Dubbo默认提供了五种负载均衡策略:RandomLoadBalance:“加权随机”策略RoundRobinLoadBalance:“加权轮询”策略LeastActiveLoadBalance:“最小活跃调用数”策略ConsistentHashLoadBalance:“一致性”“Hash”策略ShortestResponseLoadBalance:“最短响应时间”策略“ShortestResponseLoadBalance”策略小伙伴可能比较陌生,官方文档中也没有提到这个策略,其实这是Dubbo2.7.7版本新增的负载均衡策略,而且官方文档估计还没更新。ps:有兴趣的小伙伴,可以修改官方文档,加入这个新的负载均衡策略,为开源贡献自己的一份力量。回到正文,我们可以看到从AbstractLoadBalance#getWeight调用关系得知“ConsistentHashLoadBalance”实现类不支持服务预热,需要n注意到了。Dubbo预热历史bug——反复跳转虽然Dubbo预热的相关代码总体上不难,但是历史版本还是有几个bug,导致预热失败。Dubbo2.5.5之前的版本在Dubbo2.5.5之前的版本中,AbstractLoadBalance#getWeight的实现如下:本版本与当前代码相同,都是从节点的时间戳获取服务启动时间。但是,此版本中存在一些问题。Dubbo不会将服务提供者的启动时间传递给消费者。结果这里得到的timestamp是consumer的启动时间,导致warm-up失败。等到Dubbo2.5.6修复这个问题。源码如下:该版本将服务提供者启动时间单独保存在remote.timestamp属性中,源码位于ClusterUtils#mergeUrl。这样就解决了预热失败的问题。Dubbo2.7.2预热再次失败。Dubbo版本升级到2.7.2后,预热失效bug回归。出现这个问题的主要原因是ClusterUtils#mergeUrl源码中清除了remote.timestamp,统一使用timestamp来节省服务启动时间。但由于修改不完整,AbstractLoadBalance#getWeight仍然使用remote.timestamp获取服务启动时间,导致预热失败。预热代码中隐藏的bug在Dubbo2.7.4版本中得到了彻底修复,顺便优化了代码中的缺陷。先看原代码中国缺陷,原预热代码实现采用如下方法计算服务启动和运行时间。intuptime=(int)(System.currentTimeMillis()-时间戳);但是这里有一个问题。如果服务提供者和消费者两端的时钟不一致,服务提供者的启动时间很可能大于消费者的本地时间。这种情况下uptime计算结果为负值,会导致权重使用配置的默认值,预热也会失败。因此,“@aftersss”针对这种情况提供了修复方案,并添加了相关判断。当uptime为负数时,权重会直接返回1。不过,在“Codereview”过程中,“@beiwei30”觉得不需要额外添加if判断,直接使用Math.max即可兼容性。但是,这种修改仍然存在一个问题:Integer精度的损失。如果此时System.currentTimeMillis()=1566209746000(2019-08-1918:15:46),timestamp=1561914711000(2019-07-0101:11:51),当两者之差为:"4295035000”。这是一个比Integer.MAX_VALUE大得多的值,因此在将long转换为int时会丢失精度,从而导致实际的int值“67704”。而这个值小于服务预热的默认时间(10*60*1000),所以在进入动态计算权重环节时,最终会得到一个比较小的权重,从而导致“假预热”。所以最终采用了“@aftersss”修复方案,使用long类型存储时间戳计算结果。最终优化后的代码如下:总结今天这篇文章主要介绍服务预热的作用以及Dubbo服务预热的实现。这种实现方法整体来说难度不是很大。简单来说,权重随着运行时间逐渐增加,从而增加服务节点的流量。如果你当前使用的框架没有这个功能,需要服务预热,可以参考Dubbo的实现。另外,由于Dubbo历史版本存在一些bug,如果需要使用服务预热功能,需要注意避免使用以下版本:“Dubbo2.5.5之前的版本”“Dubbo2.7.2/2.7.3》好了,今天的文章就到这里啦~我是楼下的小黑哥,知道的越多,不知道的越多。下周见~相关资料https://github.com/apache/dubbo/issues/6242https://github.com/apache/dubbo/issues/306https://github.com/apache/dubbo/pull/4870本文转载自微信公众号“程序通识”,可通过以下二维码关注。转载本文,请联系程序通讯号。
