You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

76 lines
10 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 14 | 优雅启动:如何避免流量打到没有启动完成的节点?
你好,我是何小锋。上一讲我们介绍了优雅停机,就是为了让服务提供方在停机应用的时候,保证所有调用方都能“安全”地切走流量,不再调用自己,从而做到对业务无损。其中实现的关键点就在于,让正在停机的服务提供方应用有状态,让调用方感知到服务提供方正在停机。
接着上一讲的内容,今天我们来聊聊优雅启动。
是不是很诧异?应用启动居然也要这么“讲究”吗?这就好比我们日常生活中的热车,行驶之前让发动机空跑一会,可以让汽车的各个部件都“热”起来,减小磨损。
换到应用上来看原理也是一样的。运行了一段时间后的应用执行速度会比刚启动的应用更快。这是因为在Java里面在运行过程中JVM虚拟机会把高频的代码编译成机器码被加载过的类也会被缓存到JVM缓存中再次使用的时候不会触发临时加载这样就使得“热点”代码的执行不用每次都通过解释从而提升执行速度。
但是这些“临时数据”,都在我们应用重启后就消失了。重启后的这些“红利”没有了之后,如果让我们刚启动的应用就承担像停机前一样的流量,这会使应用在启动之初就处于高负载状态,从而导致调用方过来的请求可能出现大面积超时,进而对线上业务产生损害行为。
在上一讲我们说过,在微服务架构里面,上线肯定是频繁发生的,那我们总不能因为上线,就让过来的请求出现大面积超时吧?所以我们得想点办法。既然问题的关键是在于“刚重启的服务提供方因为没有预跑就承担了大流量”,那我们是不是可以通过某些方法,让应用一开始只接少许流量呢?这样低功率运行一段时间后,再逐渐提升至最佳状态。
这其实就是我今天要和你分享的重点RPC里面的一个实用功能——启动预热。
## 启动预热
那什么叫启动预热呢?
简单来说,就是让刚启动的服务提供方应用不承担全部的流量,而是让它被调用的次数随着时间的移动慢慢增加,最终让流量缓和地增加到跟已经运行一段时间后的水平一样。
**那在RPC里面我们该怎么实现这个功能呢**
我们现在是要控制调用方发送到服务提供方的流量。我们可以先简单地回顾下调用方发起的RPC调用流程是怎样的调用方应用通过服务发现能够获取到服务提供方的IP地址然后每次发送请求前都需要通过负载均衡算法从连接池中选择一个可用连接。那这样的话我们是不是就可以让负载均衡在选择连接的时候区分一下是否是刚启动不久的应用对于刚启动的应用我们可以让它被选择到的概率特别低但这个概率会随着时间的推移慢慢变大从而实现一个动态增加流量的过程。
**现在方案有了,我们就可以考虑具体实现了。**
首先对于调用方来说我们要知道服务提供方启动的时间这个怎么获取呢我这里给出两种方法一种是服务提供方在启动的时候把自己启动的时间告诉注册中心另外一种就是注册中心收到的服务提供方的请求注册时间。这两个时间我认为都可以不过可能你会犹豫我们该怎么确保所有机器的日期时间是一样的这其实不用太关心因为整个预热过程的时间是一个粗略值即使机器之间的日期时间存在1分钟的误差也不影响并且在真实环境中机器都会默认开启NTP时间同步功能来保证所有机器时间的一致性。
不管你是选择哪个时间最终的结果就是调用方通过服务发现除了可以拿到IP列表还可以拿到对应的启动时间。我们需要把这个时间作用在负载均衡上在[\[第 11 讲\]](https://time.geekbang.org/column/article/210893) 我们介绍过一种基于权重的负载均衡,但是这个权重是由服务提供方设置的,属于一个固定状态。现在我们要让这个权重变成动态的,并且是随着时间的推移慢慢增加到服务提供方设定的固定值,整个过程如下图所示:
![](https://static001.geekbang.org/resource/image/e7/d4/e796da8cf26f056479a59fd97b43d0d4.jpg "预热过程图")
通过这个小逻辑的改动,我们就可以保证当服务提供方运行时长小于预热时间时,对服务提供方进行降权,减少被负载均衡选择的概率,避免让应用在启动之初就处于高负载状态,从而实现服务提供方在启动后有一个预热的过程。
看到这儿,你可能还会有另外一个疑问,就是当我在大批量重启服务提供方的时候,会不会导致没有重启的机器因为扛的流量太大而出现问题?
关于这个问题,我是这么考虑的。当你大批量重启服务提供方的时候,对于调用方来说,这些刚重启的机器权重基本是一样的,也就是说这些机器被选中的概率是一样的,大家都是一样得低,也就不存在权重区分的问题了。但是对于那些没有重启过的应用提供方来说,它们被负载均衡选中的概率是相对较高的,但是我们可以通过[\[第 11 讲\]](https://time.geekbang.org/column/article/210893) 学到的自适应负载的方法平缓地切换,所以也是没有问题的。
启动预热更多是从调用方的角度出发,去解决服务提供方应用冷启动的问题,让调用方的请求量通过一个时间窗口过渡,慢慢达到一个正常水平,从而实现平滑上线。但对于服务提供方本身来说,有没有相关方案可以实现这种效果呢?
当然有,这也是我今天要分享的另一个重点,和热启动息息相关,那就是延迟暴露。
## 延迟暴露
我们应用启动的时候都是通过main入口然后顺序加载各种相关依赖的类。以Spring应用启动为例在加载的过程中Spring容器会顺序加载Spring Bean如果某个Bean是RPC服务的话我们不光要把它注册到Spring-BeanFactory里面去还要把这个Bean对应的接口注册到注册中心。注册中心在收到新上线的服务提供方地址的时候会把这个地址推送到调用方应用内存中当调用方收到这个服务提供方地址的时候就会去建立连接发请求。
但这时候是不是存在服务提供方可能并没有启动完成的情况因为服务提供方应用可能还在加载其它的Bean。对于调用方来说只要获取到了服务提供方的IP就有可能发起RPC调用但如果这时候服务提供方没有启动完成的话就会导致调用失败从而使业务受损。
**那有什么办法可以避免这种情况吗?**
在解决问题前我们先看下出现上述问题的根本原因。这是因为服务提供方应用在没有启动完成的时候调用方的请求就过来了而调用方请求过来的原因是服务提供方应用在启动过程中把解析到的RPC服务注册到了注册中心这就导致在后续加载没有完成的情况下服务提供方的地址就被服务调用方感知到了。
这样的话其实我们就可以把接口注册到注册中心的时间挪到应用启动完成后。具体的做法就是在应用启动加载、解析Bean的时候如果遇到了RPC服务的Bean只先把这个Bean注册到Spring-BeanFactory里面去而并不把这个Bean对应的接口注册到注册中心只有等应用启动完成后才把接口注册到注册中心用于服务发现从而实现让服务调用方延迟获取到服务提供方地址。
这样是可以保证应用在启动完后才开始接入流量的但其实这样做我们还是没有实现最开始的目标。因为这时候应用虽然启动完成了但并没有执行相关的业务代码所以JVM内存里面还是冷的。如果这时候大量请求过来还是会导致整个应用在高负载模式下运行从而导致不能及时地返回请求结果。而且在实际业务中一个服务的内部业务逻辑一般会依赖其它资源的比如缓存数据。如果我们能在服务正式提供服务前先完成缓存的初始化操作而不是等请求来了之后才去加载我们就可以降低重启后第一次请求出错的概率。
**那具体怎么实现呢?**
我们还是需要利用服务提供方把接口注册到注册中心的那段时间。我们可以在服务提供方应用启动后接口注册到注册中心前预留一个Hook过程让用户可以实现可扩展的Hook逻辑。用户可以在Hook里面模拟调用逻辑从而使JVM指令能够预热起来并且用户也可以在Hook里面事先预加载一些资源只有等所有的资源都加载完成后最后才把接口注册到注册中心。整个应用启动过程如下图所示
![](https://static001.geekbang.org/resource/image/3c/bd/3c84f9cf6745f2d50e34bd8431c84abd.jpg "启动顺序图")
## 总结
包括[\[第 11 讲\]](https://time.geekbang.org/column/article/210893) 在内到今天为止我们就已经把整个RPC里面的启停机流程都讲完了。就像前面说过的那样虽然启停机流程看起来不属于RPC主流程但是如果你能在RPC里面把这些“微小”的工作做好就可以让你的技术团队感受到更多的微服务带来的好处。
另外我们今天的两大重点——启动预热与延迟暴露它们并不是RPC的专属功能我们在开发其它系统时也可以利用这两点来减少冷启动对业务的影响。
## 课后思考
在启动预热那部分,我们特意提到过一个问题,就是“当大批量重启服务提供方的时候,会导致请求大概率发到没有重启的机器上,这时服务提供方有可能扛不住”,不知道你是怎么看待这个问题的,是否有好的解决方案呢?
欢迎留言和我分享你的思考,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!