优雅启动:如何避免流量打到没有启动完成的节点?

简介: 本讲介绍RPC中的优雅启动,重点讲解启动预热与延迟暴露。通过预热机制让新实例逐步承接流量,避免冷启动高负载;结合延迟暴露,在服务完全初始化后再注册上线,确保平滑启动,提升系统稳定性与用户体验。

上一讲我们介绍了优雅停机,就是为了让服务提供方在停机应用的时候,保证所有调用方都能「安全」地切走流量,不再调用自己,从而做到对业务无损。其中实现的关键点就在于,让正在停机的服务提供方应用有状态,让调用方感知到服务提供方正在停机。接着上一讲的内容,今天我们来聊聊优雅启动。
是不是很诧异?应用启动居然也要这么讲究吗?这就好比我们日常生活中的热车,行驶之前让发动机空跑一会,可以让汽车的各个部件都热起来,减小磨损。
换到应用上来看,原理也是一样的。运行了一段时间后的应用,执行速度会比刚启动的应用更快。这是因为在 Java 里面,在运行过程中,JVM 虚拟机会把高频的代码编译成机器码,被加载过的类也会被缓存到 JVM 缓存中,再次使用的时候不会触发临时加载,这样就使得「热点」代码的执行不用每次都通过解释,从而提升执行速度。
但是这些「临时数据」,都在我们应用重启后就消失了。重启后的这些「红利」没有了之后,如果让我们刚启动的应用就承担像停机前一样的流量,这会使应用在启动之初就处于高负载状态,从而导致调用方过来的请求可能出现大面积超时,进而对线上业务产生损害行为。
在上一讲我们说过,在微服务架构里面,上线肯定是频繁发生的,那我们总不能因为上线,就让过来的请求出现大面积超时吧?所以我们得想点办法。既然问题的关键是在于「刚重启的服务提供方因为没有预跑就承担了大流量」,那我们是不是可以通过某些方法,让应用一开始只接少许流量呢?这样低功率运行一段时间后,再逐渐提升至最佳状态。
这其实就是我今天要和你分享的重点,RPC 里面的一个实用功能——启动预热。
启动预热
那什么叫启动预热呢?简单来说,就是让刚启动的服务提供方应用不承担全部的流量,而是让它被调用的次数随着时间的移动慢慢增加,最终让流量缓和地增加到跟已经运行一段时间后的水平一样。
那在 RPC 里面,我们该怎么实现这个功能呢?
我们现在是要控制调用方发送到服务提供方的流量。我们可以先简单地回顾下调用方发起的 RPC 调用流程是怎样的,调用方应用通过服务发现能够获取到服务提供方的 IP 地址,然后每次发送请求前,都需要通过负载均衡算法从连接池中选择一个可用连接。那这样的话,我们是不是就可以让负载均衡在选择连接的时候,区分一下是否是刚启动不久的应用?对于刚启动的应用,我们可以让它被选择到的概率特别低,但这个概率会随着时间的推移慢慢变大,从而实现一个动态增加流量的过程。
现在方案有了,我们就可以考虑具体实现了。
首先对于调用方来说,我们要知道服务提供方启动的时间,这个怎么获取呢?我这里给出两种方法,
● 一种是服务提供方在启动的时候,把自己启动的时间告诉注册中心;
● 另外一种就是注册中心收到的服务提供方的请求注册时间。
这两个时间我认为都可以,不过可能你会犹豫我们该怎么确保所有机器的日期时间是一样的?这其实不用太关心,因为整个预热过程的时间是一个粗略值,即使机器之间的日期时间存在 1 分钟的误差也不影响,并且在真实环境中机器都会默认开启 NTP 时间同步功能,来保证所有机器时间的一致性。
不管你是选择哪个时间,最终的结果就是,调用方通过服务发现,除了可以拿到 IP 列表,还可以拿到对应的启动时间。我们需要把这个时间作用在负载均衡上,在 第 11 讲 我们介绍过一种基于权重的负载均衡,但是这个权重是由服务提供方设置的,属于一个固定状态。现在我们要让这个权重变成动态的,并且是随着时间的推移慢慢增加到服务提供方设定的固定值,整个过程如下图所示:

通过这个小逻辑的改动,我们就可以保证当服务提供方运行时长小于预热时间时,对服务提供方进行降权,减少被负载均衡选择的概率,避免让应用在启动之初就处于高负载状态,从而实现服务提供方在启动后有一个预热的过程。
看到这儿,你可能还会有另外一个疑问,就是当我在大批量重启服务提供方的时候,会不会导致没有重启的机器因为扛的流量太大而出现问题?
关于这个问题,我是这么考虑的。当你大批量重启服务提供方的时候,对于调用方来说,这些刚重启的机器权重基本是一样的,也就是说这些机器被选中的概率是一样的,大家都是一样得低,也就不存在权重区分的问题了。但是对于那些没有重启过的应用提供方来说,它们被负载均衡选中的概率是相对较高的,但是我们可以通过 第 11 讲 学到的自适应负载的方法平缓地切换,所以也是没有问题的。
启动预热更多是从调用方的角度出发,去解决服务提供方应用冷启动的问题,让调用方的请求量通过一个时间窗口过渡,慢慢达到一个正常水平,从而实现平滑上线。但对于服务提供方本身来说,有没有相关方案可以实现这种效果呢?
当然有,这也是我今天要分享的另一个重点,和热启动息息相关,那就是延迟暴露。
延迟暴露
我们应用启动的时候都是通过 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 里面事先预加载一些资源,只有等所有的资源都加载完成后,最后才把接口注册到注册中心。整个应用启动过程如下图所示:

总结
包括 第 11 讲 在内,到今天为止,我们就已经把整个 RPC 里面的启停机流程都讲完了。就像前面说过的那样,虽然启停机流程看起来不属于 RPC 主流程,但是如果你能在 RPC 里面把这些微小的工作做好,就可以让你的技术团队感受到更多的微服务带来的好处。
另外,我们今天的两大重点——启动预热与延迟暴露,它们并不是 RPC 的专属功能,我们在开发其它系统时,也可以利用这两点来减少冷启动对业务的影响。

相关文章
|
IDE 安全 API
copilot在pycharm的应用
copilot在pycharm的应用
1080 4
copilot在pycharm的应用
|
开发工具 git
Git查看/修改/添加远程仓库地址
Git查看/修改/添加远程仓库地址
896 0
官宣!杭州市地铁集团与阿里云达成战略合作
官宣!杭州市地铁集团与阿里云达成战略合作
951 9
|
9月前
|
域名解析 网络协议 安全
DNS服务器地址大全
DNS(域名系统)是互联网的“电话簿”,将域名解析为IP地址。选择优质DNS服务器可提升网络速度、降低延迟。以下是全球及中国各运营商的DNS服务器列表,包括公共DNS(如Google DNS、Cloudflare DNS)、中国电信、联通、移动等。根据地理位置、稳定性、安全性与隐私保护等因素选择适合的DNS服务器,优化上网体验。
27374 6
|
9月前
|
存储 缓存 Java
理解Java引用数据类型:它们都是对象引用
本文深入探讨了Java中引用数据类型的本质及其相关特性。引用变量存储的是对象的内存地址而非对象本身,类似房子的地址而非房子本身。文章通过实例解析了引用赋值、比较(`==`与`equals()`的区别)以及包装类缓存机制等核心概念。此外,还介绍了Java引用类型的家族,包括类、接口、数组和枚举。理解这些内容有助于开发者避免常见错误,提升对Java内存模型的掌握,为高效编程奠定基础。
471 0
|
SQL 关系型数据库 MySQL
mysql分页读取数据重复问题
在服务端开发中,与MySQL数据库进行数据交互时,常因数据量大、网络延迟等因素需分页读取数据。文章介绍了使用`limit`和`offset`参数实现分页的方法,并针对分页过程中可能出现的数据重复问题进行了详细分析,提出了利用时间戳或确保排序规则绝对性等解决方案。
534 1
|
Prometheus Kubernetes 网络协议
k8s学习笔记之CoreDNS
k8s学习笔记之CoreDNS
|
Java 数据库连接 mybatis
mybatis判断批量操作是否全部执行成功
mybatis判断批量操作是否全部执行成功
609 1
|
消息中间件 弹性计算 监控
【Serverless架构组成及优势适用场景】
Serverless的弹性伸缩、按需计费、无状态等特性使得开发者能够更加专注于业务逻辑,摆脱繁琐的服务器管理。它的优势在于灵活应对突发性工作负载、降低成本、提高开发效率,尤其在事件驱动、微服务、后端API等场景中表现出色。虽然Serverless仍然在不断发展,但其已经在云计算领域掀起了一场革命,成为当今应用开发的热门选择。随着技术的不断演进,我们有理由期待Serverless将继续推动应用开发的创新,为我们构建更加高效、可靠的应用提供更多可能。
631 0
springfox-bridge:随心所欲地为非restful接口生成swagger api文档
### 一、引言     目前,利用swagger框架为restful接口编写API文档非常流行,在spring web项目中,利用springfox+swagger更是可以通过注解的方式直接进行API文档的生成,这样开发者在项目开发的同时就直接把文档准备好了,利用springfox的配置,可以在项目启动后直接浏览器访问查看API文档,同时还能