一、从一次发布事故聊起

先举个例子:你正在带一个电商项目,平时流量不大,Spring Boot应用跑得好好的。这天中午,你打算发一个新版本,修复了几个小bug。按照老流程,先停掉旧服务,再把新服务启动,然后注册到Nacos。刚启动完不到半分钟,监控屏幕上CPU使用率一下子飙到90%,紧接着一堆超时报警,用户开始反馈页面打不开。你手忙脚乱地回滚,才算稳住局面。

这种场景是不是很眼熟?其实不是代码写错了,而是新实例刚上线时“体温”还没上来,就被扔到了暴晒的马路中央。我们把这种现象叫“冷启动”,而解决它的办法之一,就是给实例做一个“预热”。

二、为什么新实例容易“受伤”

要搞懂预热的价值,得先搞清楚新实例为什么“虚”。咱们用大白话来说。

第一,程序内部的很多资源是“懒加载”的。比如连接数据库的线程池、Redis连接池、各种缓存,还有那些复杂的业务配置。默认情况下,只有第一次有人访问到一个功能时,才会去初始化对应的组件。这就好比新买的房子,水管、电线、家具看着都在,但还没通水通电,你突然请一堆客人来家里吃饭,那狼狈局面肯定不断。

第二,JVM这个“运行容器”也需要时间热身。很多Java服务启动后,需要运行一段时间,让及时编译器(JIT)把热点代码编译成本地机器码,执行效率才会高。如果刚开始流量就很大,大量的请求会挤在解释执行阶段,CPU消耗自然猛增。

第三,业务上的“缓存黑洞”。你服务里可能用了本地缓存,比如Caffeine或Guava。刚启动时缓存里都是空的,所有请求都得穿透到数据库或者下游服务,一旦流量集中,数据库可能直接被打挂。而运行了一段时间后,缓存命中率上来,压力就小很多了。

现在再来看看Nacos的角色。Nacos是服务注册和发现的中心,服务提供者启动后会把自己的地址注册上去,消费者会从Nacos拉取地址列表,然后进行负载均衡调用。如果新实例注册后立刻被分配和旧实例一样多的流量,那它大概率会“消化不良”。

三、Nacos里的“预热”是怎么做到的

很多同学以为预热是Nacos内置的一个按钮,其实Nacos没有专门叫“预热”的功能,但它的“权重”机制,可以非常方便地实现类似预热的效果。

在Nacos中,每个实例都可以设置一个权重值,范围从0到100。权重越高,被分到的流量比例越大。默认情况下,所有实例权重都是100,大家平分流量。那我们可以这样设计预热流程:

  1. 新实例启动后,向Nacos注册时,故意把权重设置得很低,比如10。
  2. 等服务运行了两三分钟,各种连接池、缓存、JIT都“热”起来了,再把权重慢慢调高到100。
  3. 整个过程中,消费者侧不需要做任何改动,Nacos会自动根据权重计算每个实例应该接收的流量比例。

这就是一个典型的“预热”过程。它并没有真正拦截流量,而是通过逐渐放量,让新实例有个适应期。

3.1 用OpenAPI调整权重

Nacos提供了丰富的OpenAPI,我们可以通过一个简单的HTTP接口来修改实例的权重。这样不管是手工执行命令,还是写个脚本定时调整,都很方便。

假设你的Nacos服务端地址是localhost:8848,服务名是order-service,集群名是DEFAULT,实例IP是1.2.3.4,端口是8080。要修改权重为20,可以这样调用:

# 通过Nacos OpenAPI修改实例权重
curl -X PUT "http://localhost:8848/nacos/v1/ns/instance?serviceName=order-service&ip=1.2.3.4&port=8080&weight=20"

这里weight=20就表示权重20。调用成功后,Nacos会返回ok。然后你可以循环调用它,每次把权重增加一点,比如每隔30秒增加10,四五次之后就到了100。

3.2 在Java应用启动时设置低权重

如果不想用外部命令,也可以在应用程序启动时,通过Nacos的客户端API来注册一个低权重的实例。下面是一个完整的演示。

技术栈:Java + Spring Cloud Alibaba + Nacos。

先看项目里需要依赖哪些包。这里假设你已经有一个Spring Boot项目,我们只需要加上对应的starter和注册发现组件。

<!-- pom.xml 关键依赖 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>2.2.5.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.3.4.RELEASE</version>
</dependency>

接下来写一个启动类,在注册成功后,主动调整自己的权重。

/**
 * 服务提供者启动类
 * 通过ApplicationRunner在应用启动完成后,设置当前实例的权重
 * 这里演示的是“先低权重,再逐步升高”的预热思路
 */
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {

    public static void main(String[] args) {
        // 启动Spring Boot应用
        SpringApplication.run(OrderServiceApplication.class, args);
    }

    /**
     * 在应用启动后执行权重调整逻辑
     * 这里只是演示,实际生产环境建议通过运维脚本或控制台手动调整
     */
    @Bean
    public ApplicationRunner preheatWeight(NacosServiceManager nacosServiceManager,
                                           NacosDiscoveryProperties discoveryProperties) {
        return args -> {
            // 获取当前服务的IP和端口
            String ip = discoveryProperties.getIp();
            int port = discoveryProperties.getPort();
            // 服务名,需要和环境一致
            String serviceName = discoveryProperties.getService();
            // 集群名,一般为DEFAULT
            String clusterName = "DEFAULT";
            
            // 模拟预热:先设置权重为10
            nacosServiceManager.getNacosServiceManager().getNamingService()
                .setInstanceWeight(serviceName, ip, port, 10, clusterName);
            
            System.out.println("新实例已注册,初始权重设为10,开始预热...");

            // 线程中每10秒增加权重,直到100
            new Thread(() -> {
                try {
                    for (int weight = 10; weight <= 100; weight += 10) {
                        // 每隔10秒调高一次权重
                        Thread.sleep(10000);
                        nacosServiceManager.getNacosServiceManager().getNamingService()
                            .setInstanceWeight(serviceName, ip, port, weight, clusterName);
                        System.out.println("当前权重已调整:" + weight);
                    }
                    System.out.println("预热完成,权重已到100");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        };
    }
}

这段代码中有一些需要注意的地方。NacosServiceManager和NacosDiscoveryProperties都是Spring Cloud Alibaba提供的类,你可以直接注入。setInstanceWeight方法对应的是Nacos OpenAPI的修改权重接口。实际使用时,建议把权重提升的间隔和步长做成配置,方便调整。

不过,上面这种自己写线程的方式有点“野”。更稳妥的做法是,在应用启动时只设置一个低权重,然后由外部的发布系统或运维脚本,在观察一段时间后再逐步调高。因为预热期间可能出现问题,外部人为控制更容易暂停或者回滚。

3.3 消费者侧需要做什么

前面说了,预热机制对消费者是透明的。消费者只需要正常使用@LoadBalanced或OpenFeign即可。Nacos客户端在获取服务列表时,会自动拿到每个实例的权重,并在负载均衡时按权重分配流量。

比如你有一个订单服务,里面调用了用户服务。正常情况下,所有用户服务实例的权重都是100,那么每个实例收到的请求数差不多。当其中某个实例权重被调低到10,它收到的请求就会明显减少。我们可以简单理解为:权重20的实例,收到的流量大约是权重100实例的五分之一。实际上Nacos对权重的计算还有一套公式,但咱们不需要记那么细,只要知道“权重越大,流量越多”就够了。

为了验证效果,我们可以用一段简单的代码,在消费者侧打印出每次调用的实例地址,这样就能直观地看到预热前后流量的变化。

技术栈:Java + Spring Cloud Alibaba + Nacos。

/**
 * 消费者控制器
 * 每次都调用用户服务,并打印出实际访问的实例信息
 */
@RestController
public class OrderController {

    // 注入RestTemplate,它已经具备了负载均衡能力
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/call")
    public String callUserService() {
        // 通过服务名调用,实际会由负载均衡算法选择一个实例
        String result = restTemplate.getForObject("http://user-service/user/info", String.class);
        return "调用完成,服务端返回:" + result;
    }
}

其中RestTemplate需要加上@LoadBalanced注解才具备负载均衡能力,这个大家应该已经熟悉了。只要Nacos那边权重在变,这里的调用就会自动跟着走。

四、应用场景:什么时候需要预热

预热不是银弹,但有几个场景特别值得用它。

第一个场景是版本发布。就像开头说的,你刚上线一个新版本,如果所有流量瞬间打过去,新实例可能会因为冷启动而拖垮整个服务。通过预热,可以先放10%的流量进去,接着逐步放大,确保新实例能扛住。

第二个场景是大促扩容。每年双十一前,系统都会预先扩容一批新的实例。这批实例刚创建出来的时候,如果没有预热,峰值流量来了它们根本接不住。提前用脚本把权重从0慢慢升上去,让它们充分“热身”,等流量爆发时就能从容应对了。

第三个场景是服务迁移。有时候你需要把服务从一个机房迁移到另一个机房,或者从一个集群迁移到另一个集群。新集群的服务启动后,如果没有预热,可能一下子被大量跨机房请求打爆。用权重预热,可以平滑地将流量从旧集群切到新集群。

第四个场景是压测演练。在做全链路压测时,往往需要逐步增加压测实例的流量,而不是一把梭哈。通过动态调整权重,可以精确控制每个实例的压力,方便定位问题。

五、技术优缺点:好用,但别神化它

5.1 优点

实现简单,完全不侵入业务代码。你不需要在服务里写任何和预热相关的逻辑,只要通过Nacos的控制台或者OpenAPI改一下权重就行。虽然没有强依赖,但确实能显著降低新实例上线时被打挂的概率。

灵活度高。权重可以实时调整,瞬间生效。想调高就调高,想调低就调低,对正在进行中的业务没有任何影响。而且消费者不需要重启,不需要发版,真是方便。

可结合自动化。我们可以写一个发布流水线,在服务启动后自动执行权重调整脚本,比如每30秒调高20,等权重到100后再开始继续后续的健康检查。整个过程完全无人值守。

5.2 缺点

权重调整是“盲调”。它只负责按比例放流量,不管实例当前是否真的健康。如果实例内部已经出问题了,比如JVM内存溢出、死锁,你再把权重调高,只会让它死得更快。所以权重预热必须搭配健康检查来使用。

没有自动感知能力。Nacos并不知道一个实例“热”好了没有。它只知道权重是多少。所以需要运维人员根据自己的经验,决定提升权重的时机和步长。如果系统规模很大,手动调权重很辛苦。

对瞬时突发流量无能为力。如果流量峰值来得太快,而你的权重提升速度又比较慢,那前端压力还是会变大,预热的效果会打折扣。所以预热适合“可预期”的流量变化,不适合应对毫无征兆的流量洪峰。

六、注意事项:别把预热玩成“背锅侠”

先说第一点,权重不能从0直接跳。如果你把新实例的权重设为0,Nacos是不会给它分配流量的。但如果你在短时间内把它直接从0调到100,那和没预热没区别。最好设置一个递增序列,比如0->10->30->50->80->100,每次间隔几十秒。

第二点,要配合健康检查。Nacos本身有健康检查机制,临时实例会通过心跳来上报状态,非临时实例则需要服务端主动探测。我们在调高权重前,最好先确认实例处于“健康”状态。可以通过Nacos控制台查看,也可以通过API查询健康状态:

# 查询实例详情,注意返回结果中的healthy和weight字段
curl -X GET "http://localhost:8848/nacos/v1/ns/instance?serviceName=order-service&ip=1.2.3.4&port=8080"

如果返回的healthy为true,再继续调高权重。

第三点,别混淆临时实例和持久实例。临时实例默认是心跳方式,服务停止后会自动删除。持久实例则是服务端主动探测,如果机器宕机了,实例依然会存在一段时间,只是标记为不健康。预热时,如果用的是持久实例,需要确保它没有因为端口不通而被服务端摘掉流量。对于大多数微服务项目,使用临时实例就够了。

第四点,对外公布的“权重”和负载均衡器的权重不是一回事。Nacos的权重是给服务消费者内部的负载均衡算法用的,比如Spring Cloud的Ribbon或LoadBalancer。如果你还有外部的负载均衡器(比如Nginx),那它们的权重是独立的,别混为一谈。

第五点,在预热期间要盯紧监控。至少要看这几个指标:JVM的CPU、老年代内存使用率、线程池活跃线程数、下游数据库和中间件的连接数。如果发现有问题,要能立刻把权重降下来,或者直接摘除实例。

七、总结一下

Nacos的实例预热,本质上就是利用Nacos的权重调整能力,让新上线的实例从“少接活”逐渐过渡到“多接活”。它没有改变注册中心本身的功能,也没有引入额外的框架,只是换了一种优雅的使用姿势。这种做法能有效缓解冷启动带来的流量冲击,降低服务上线时的风险。

当然,预热只是整个上线流程中的一个环节。真正稳定的发布体系还需要有健康检查、自动回滚、全链路监控来配合。但至少,当你下一次再遇到新实例刚上线就CPU飙升的情况时,可以先想到这个简单的机制,然后会心一笑:哦,原来是忘了预热。