我主要使用 Go 语言做开发,Go 服务自身预热问题并不是特别严重,在部署服务前已经打包好了可执行脚本,预热问题更多是集中在服务依赖的中间件上。比如,服务启动后首次请求第三方服务,第三方可以是一个 HTTP 服务,也可以是底层存储等。
Java 就比 Go 语言差点,它有一个 JVM 预热的过程,而预热就会导致业务被影响。所以,大家也给出了很多预热的解决方案。但处理问题的本质方向是基本一致的:通过控制请求分流的权重来使机器逐步预热。
引出预热问题
我们业务底层有 2 套数据环境,分别是正式环境和容灾环境。当正式环境出现问题时,可以手动将流量切换到容灾环境。但是,正常情况下容灾环境是不承接流量的,突然将流量打到容灾环境,预热问题可能会非常严重。

服务器在刚启动的时候,比如在流量高峰期,触发了机器自动扩容,很容易出现一瞬间 CPU 突然飙高、服务自身接口 RT 飙高 、请求第三方接口超时报错等问题。这些问题都是因为服务启动之后没有预热导致的。
哪些情况下会出现预热问题呢,关键点就是“新”机器。但这个“新”不仅仅指刚刚扩容产生的新机器,还包括之前那些流量为 0 的老机器,突然接收到了新流量。所以说呢,预热的场景是有区分的,但本质是相通的。
我们先看服务器预热的情况,我列举了两种情况,一种是线上服务器扩容的情况,另一种是两个机房流量切换的情况。主要是从服务器的角度来看这个问题。

线上服务器扩容
当线上流量增多,集群cpu水位超过一定阈值,就会触发服务器自动扩容。新扩容的机器如果刚刚启动,网关代理就无脑的进行流量负载均衡,调度过来一波流量,很容易导致请求耗时、失败增多,机器 CPU飙高的情况。更恶劣的情况是 CPU 增高又导致新的扩容,形成一个循环。
如果流量特别大的时候,因为流量问题机房宕机了,如果立即重启服务,大概率服务还是无法恢复,就是因为预热问题。最好的解决方案就是网关先限流,然后把服务器资源扩的足够,然后逐步放开限流。但话说回来,特别高的流量持续的时间会特别短,如果你在流量高峰期没有顶住流量压力,到概率就是这个流量实在超过预期太多了,你真正能做的其实也并不多。
与服务扩容等同的一种方式,是服务分批发布,分批部署的模式有很多,我特意找了一篇网上的博客 灰度发布、蓝绿部署、金丝雀都是啥?,没事的时候看看也能涨涨见识。
解决这种入口流量上的预热问题,大家其实有固有的方案,就是通过调整机器的流量权重来达到预热。机器刚发布上线时,流量权重设置为 1%,间隔1 分钟之后,设置为 5%,最后设置为 100%,承载全部流量。
理论上,目前所有的代理应该都是支持。我记得在 nginx 比较流行那段时间,面试的时候,还经常会考核流量调度算法,其中就有简单轮询、加权轮询策略等。预热过程中,调整的也是机器的权重比例。

不过,实现稳定的一套预热体系要考虑的细节也蛮多的。比如,假设我们服务只有一台机器,这种流量权重的设置就是多余的,单台机器还是得处理了所有流量。
再比如,如果服务整体宕机重启,我们只能保证一部分机器执行预热,另外一部分机器不能执行预热,说白了,就是设置预热的机器比例。因为,无论底层机器流量调度策略怎么变化,入口的流量都是需要 100% 被处理的。预热机器没有处理的流量,需要被其它机器处理。
不过,如果是服务器分批发布,每一批次重启的机器数量和机器的预热比例相匹配的话,就非常完美。
主备机房切换
解决新扩容机器的预热问题有常规方案,对于新扩容的机器,流量负载均衡的时候,逐步调整它的权重,保证初始阶段,新扩容的机器只会处理少量的情况。等一段时间后(通常不需要太长时间),将新机器的权重设置成 100% 就可以了,这是一个调度流量的过程。
但主备机房切换就比较特殊,假设在正常情况下,备机房是不承载线上流量的,只有在主机房出现问题的时候,才会将流量切刀备机房。但因为备机房没有承载过流量,突然让它承载流量,它也会出现预热问题。因为这种主备切换是没有时间过渡的,所以逐步调度增加流量是行不通的。
可行的方式是我们将请求主机房的流量,copy一部分,转换为请求备机房的虚拟流量。当发生机房切换时,我们关闭这一份 copy 的流量。但这个 copy 流量的数量级其实是不好把控的。
我也参加过容灾环境的建设,是一个体系化的复杂工程,当时正值教育行业如日中天,公司还没有降本增效的想法,花了好大力气建设了容灾环境。我记得当时 MySQL、Redis、Kafka 数据同步都做了容灾改造,不过,现在不行了。
当时的容灾环境会承接一部分线上的流量,也正常处理线上请求,但因为容灾环境承接的流量比例较少,容灾环境的机器也就比较少。如果将流量全部切到容灾环境,容灾环境首先需要扩容升级,又回到了上面提到的预热问题上。
容灾数据库
为了保证业务的稳定性,我们创建了容灾数据库,当线上数据库写入出现问题时,我们可以将数据操作切换到容灾数据库。
一般来说,数据库都会有一些缓存优化,如果在数据库没有建立起缓存时,大量请求打到缓存数据库,还是会触发严重的预热问题,搞不好,机器都被打挂了。
数据库都有一些特点,就是扩容的时间会比较长,如果要立即扩容数据库,肯定是不现实的。所以,容灾环境是具备抗下所有请求的能力的。数据库预热的做法就是,虚拟一部分流量持续请求容灾数据库,但不处理响应结果,保证容灾数据库不会有预热问题。
预热场景
在服务启动时,都有哪些场景需要预热呢?大类上可以分为业务逻辑预热和依赖方预热,大多数都是依赖方预热,也就是非业务的。
业务逻辑预热
业务中,我们会有一些热点数据,如果靠 MySQL 这种 DB 来抗热点数据,一方面性能上不去,另一方面 MySQL 的CPU、连接数也面临很大的考验,极端情况可能来个MySQL暂停服务。我遇到过一个真实的情况,只不多存储是 Redis,因为缓存命中率的问题,大量的请求查询缓存,发现缓存中没有数据,就写入缓存,瞬间缓存的CPU和存储就飙升到了停止服务的地步。
我举得这个例子,更多的是逻辑失误,而非预热问题。但我们可以继续推演,对于热点数据,我们在服务启动之前是需要提前创建缓存的,而不能靠 Lazy 模式在启动后动态构建缓存。如果用到了本地缓存,就需要评估本地缓是否需要进行预热,如果不进行预热,直接请求到后面的存储,服务稳定性是否会有风险。
















