spring boot 微服务优雅预热上线方案

由于公司面向广大C端用户,每日需要承担十万级的QPS,每个节点可能会高达数千QPS的,每一秒的抖动都可能对大量用户造成影响,在高频产品迭代的前提下,平稳的进行服务发布和新老服务替换是一个必要的能力。

可以看到部署优雅功能前,启动阶段会导致100ms+的响应抖动。这个说明启动瞬间会有部分用户体验受到较大的影响,是值得研究优化的点。

nacos源码下载

为了方便研究源码,把nacos下载下来。下载地址我已经打包好了,免积分下载:

https://download.csdn.net/download/fyihdg/90461118https://download.csdn.net/download/fyihdg/90461118

如果编译nacos源码遇到问题,可以参考:

idea 编译打包nacos2.0.3源码,生成可执行jar 包常见问题-CSDN博客文章浏览阅读112次。idea 编译打包nacos2.0.3源码,生成可执行jar 包常见问题https://blog.csdn.net/fyihdg/article/details/146341263

1.Nacos优化

代码分析

java 复制代码
package com.performance.optimization.config;
import com.alibaba.cloud.nacos.registry.NacosAutoServiceRegistration;
import com.alibaba.cloud.nacos.registry.NacosRegistration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.concurrent.CompletableFuture;

@Component
@Slf4j
public class NacosDelayRegisterRunner implements ApplicationRunner {
    @Resource
    private NacosAutoServiceRegistration nacosAutoServiceRegistration;
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 在这里编写应用程序启动后要执行的逻辑
        System.out.println("---开始执行应用程序已启动,执行runner逻辑---");
        try {
            // 临时获取权限拿参数
            Field declaredField = nacosAutoServiceRegistration.getClass().getDeclaredField("registration");
            declaredField.setAccessible(true);
            NacosRegistration nacosRegistration = (NacosRegistration) declaredField.get(nacosAutoServiceRegistration);
            declaredField.setAccessible(false);
            // 如果开启了自动注册 那么就直接返回
            if (nacosRegistration.isRegisterEnabled()) {
                log.warn("---nacos已打开自动注册,跳过手动注册---");
                return;
            }
            // 手动注册
            log.warn("---nacos异步注册开始,15s后执行---");
            CompletableFuture.supplyAsync(() -> {
                log.warn("---nacos异步手动注册开始---");
                // 等待几秒才注册
                try {
                    log.warn("---开始等待15秒---");
                    Thread.sleep(15000); // 模拟耗时操作
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                nacosRegistration.getNacosDiscoveryProperties().setRegisterEnabled(true);
                nacosAutoServiceRegistration.start();
                log.warn("---nacos异步手动注册完成---");
                return true;
            }).thenAccept(result -> log.warn("---nacos异步手动注册完成---"));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        // 获取并处理命令行参数和应用程序参数
        handleCommandLineArguments(args);
    }
    private void handleCommandLineArguments(ApplicationArguments args) {
        // 获取并处理命令行参数
        System.out.println("---命令行参数:---");
        for (String arg : args.getSourceArgs()) {
            System.out.println(arg);
        }
        // 获取并处理应用程序参数
        System.out.println("---应用程序参数:---");
        for (String name : args.getOptionNames()) {
            System.out.println(name + "=" + args.getOptionValues(name));
        }
    }
}

在阅读了源码之后,可以得出结论nacos的注册时机是依托于spring的生命周期机制,我们发现监听的是WebServerInitializedEvent,也就是内置的Tomcat启动完成的时刻,所以需要确保所有服务依赖都准备好之后再进行服务注册。

审视了一遍nacos的设计和架构,发现还可以通过权重制定了进一步的优化方向:对用户流量进行控流,逐步预热上线。依托于nacos的权重weight机制,可以对用户流量进行设置从0.01至1的权重配置,逐步放大用户流量至全量,这样做可以更好预热服务,防止瞬间高请求量导致扩tomcat线程等操作的耗时。各台机器的weight形成各自的区间,依靠随机数去命中区间,以此达到权重的效果。com.alibaba.nacos.client.naming.utils.Chooser#randomWithWeight

java 复制代码
    public T randomWithWeight() {
        Ref<T> ref = this.ref;
        double random = ThreadLocalRandom.current().nextDouble(0, 1);
        int index = Arrays.binarySearch(ref.weights, random);
        if (index < 0) {
            index = -index - 1;
        } else {
            return ref.items.get(index);
        }
        
        if (index < ref.weights.length) {
            if (random < ref.weights[index]) {
                return ref.items.get(index);
            }
        }
        
        /* This should never happen, but it ensures we will return a correct
         * object in case there is some floating point inequality problem
         * wrt the cumulative probabilities. */
        return ref.items.get(ref.items.size() - 1);
    }

注册流程加入权重,添加坐标

java 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version> <!-- 版本号可以根据需要调整 -->
        </dependency>
java 复制代码
package com.performance.optimization.config;

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.NacosServiceManager;
import com.alibaba.cloud.nacos.registry.NacosAutoServiceRegistration;
import com.alibaba.cloud.nacos.registry.NacosRegistration;
import com.alibaba.nacos.api.naming.NamingMaintainService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.actuate.health.HealthComponent;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.concurrent.CompletableFuture;
import static org.springframework.boot.actuate.health.Status.UP;

@Component
@Slf4j
public class NacosDelayRegisterRunner2 implements ApplicationRunner {
    /**
     * 最大健康检查次数
     */
    private static final int CHECK_HEALTH_NACOS_REGISTER_MAX_TIMES = 10;
    @Resource
    private NacosAutoServiceRegistration nacosAutoServiceRegistration;
    @Resource
    private NacosServiceManager nacosServiceManager;
    @Resource
    private HealthEndpoint healthEndpoint;
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 在这里编写应用程序启动后要执行的逻辑
        log.warn("---开始执行应用程序已启动,执行runner逻辑---");
        // 你还可以获取并处理命令行参数和应用程序参数
        handleCommandLineArguments(args);
    }
    /**
     * 读取程序启动参数并执行
     * @param args 启动参数
     */
    private void handleCommandLineArguments(ApplicationArguments args) {
        // 获取并处理命令行参数
        System.out.println("---命令行参数:---");
        for (String arg : args.getSourceArgs()) {
            System.out.println(arg);
        }
        // 获取并处理应用程序参数
        System.out.println("---应用程序参数:---");
        for (String name : args.getOptionNames()) {
            System.out.println(name + "=" + args.getOptionValues(name));
        }
        // 如果在启动参数手动设置了不注册nacos,就跳过手动注册,为了开发环境和backend
        if ( !checkDisableNacos(args.getSourceArgs()) ) {
            // 初次健康检查,预热
            this.firstHealthCheck();
            // 异步健康检查
            CompletableFuture.supplyAsync(() -> {
                log.warn("异步监测健康状态开始");
                Boolean isUp = false;
                // 等待5秒才注册
                try {
                    for (int i = 1; i <= CHECK_HEALTH_NACOS_REGISTER_MAX_TIMES; i++) {
                        isUp = this.isUpStatus();
                        log.warn("第{}次异步健康检测:{}", i, isUp);
                        if (isUp){
                            // 如果已启动,注册并中断循环
                            this.doNacosRegister();
                            break;
                        }
                        Thread.sleep(5000); // 模拟耗时操作
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                return isUp;
            }).thenAccept(result -> {
                if (result) {
                    log.warn("异步监测健康状态结束");
                } else {
                    System.exit(99);
                    log.error("异步监测健康状态一直失败,请检查!");
                }
            });
        }
    }
    private boolean checkDisableNacos(String[] args){
        System.out.println(System.getProperty("spring.cloud.nacos.discovery.register-enabled"));
        for (String arg : args) {
            if (StringUtils.contains(arg, "spring.cloud.nacos.discovery.register-enabled") && StringUtils.contains(arg,"false")
            || StringUtils.equals(System.getProperty("spring.cloud.nacos.discovery.register-enabled"), "false")){
                return true;
            }
        }
        return false;
    }
    /**
     * 进行nacos手动注册
     */
    private void doNacosRegister(){
        log.warn("nacos手动注册流程开始");
        try {
            // 临时获取权限拿参数
            Field declaredField = nacosAutoServiceRegistration.getClass().getDeclaredField("registration");
            declaredField.setAccessible(true);
            NacosRegistration nacosRegistration = (NacosRegistration) declaredField.get(nacosAutoServiceRegistration);
            declaredField.setAccessible(false);
            // 如果开启了自动注册 那么就直接返回
            if (nacosRegistration.isRegisterEnabled()) {
                log.warn("nacos已打开自动注册,跳过手动注册!");
                return;
            }
            NacosDiscoveryProperties nacosDiscoveryProperties = nacosRegistration.getNacosDiscoveryProperties();
            // 手动注册,初始0.1流量
            nacosDiscoveryProperties.setRegisterEnabled(true);
            nacosDiscoveryProperties.setWeight(0.1F);
            nacosAutoServiceRegistration.start();
            // TODO 这里start() 偶现权重设置不生效 下面用maintain重新设置0.1
            // 获取维护client
            NamingMaintainService maintainService = nacosServiceManager.getNamingMaintainService(nacosDiscoveryProperties.getNacosProperties());
            String serviceName = nacosDiscoveryProperties.getService();
            String groupName = nacosDiscoveryProperties.getGroup();
            // 创建要更新的实例
            Instance instance = new Instance();
            instance.setIp(nacosDiscoveryProperties.getIp());
            instance.setPort(nacosDiscoveryProperties.getPort());
            // 预热30秒
            // 更新实例
            instance.setWeight(0.1);
            maintainService.updateInstance(serviceName, groupName, instance);
            Thread.sleep(30 * 1000);
            log.warn("预热结束:1500, 0.1");
            // 更新实例
            instance.setWeight(0.5);
            maintainService.updateInstance(serviceName, groupName, instance);
            // 预热30秒
            Thread.sleep(30 * 1000);
            log.warn("预热结束:1500, 0.5");
            // 更新实例
            instance.setWeight(1);
            maintainService.updateInstance(serviceName, groupName, instance);
            log.warn("预热结束:1");
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            log.warn("nacos手动注册流程结束");
        }
    }
    /**
     * 进行初次健康检查
     */
    private void firstHealthCheck(){
        log.warn("开始进行初次预热健康检查");
        // 进行初次健康检查
        HealthComponent endpoint = healthEndpoint.health();
        log.warn("初次预热健康检查完成:" + endpoint.getStatus());
    }
    /**
     * 打印当前健康状态
     */
    private void logHealth(){
        log.warn("!!!!当前实例状态:" + healthEndpoint.health().getStatus());
    }
    /**
     * 是否已启动
     * @return 是/否
     */
    private Boolean isUpStatus(){
        return UP.equals( healthEndpoint.health().getStatus() );
    }
}

整体完整流程

● 关闭自动注册进行手动注册,且应在spring runner阶段

● 注册前进行spring actuator的health check

● health check返回成功后进行0.01weight的小流量注册

● 逐步放大weight直至到1

● 服务发布结束

其他注意事项

●healthcheck必须确保没有废弃中间件的引入,以免healthcheck一直不过

●启动时CPU资源一定要给足,否则启动过慢

●weight参数的使用要谨慎,要确保各种客户端的兼容性

2.中间件优化

使用监控工具( Prometheus、Grafana)观察 MySQL 和 Redis 的内存使用情况,发现内存使用量随着访问量逐渐增加,说明采用了懒加载机制,不会主动创建连接,这样有可能会造成卡顿,导致首次访问时的延迟较高,尤其是在数据量较大或访问压力较大的场景下。

配置 innodb_buffer_pool_load_at_startup 和 innodb_buffer_pool_dump_at_shutdown:

将 innodb_buffer_pool_load_at_startup 设置为 ON,MySQL 会在启动时加载上次关闭时保存的缓冲池数据。

将 innodb_buffer_pool_dump_at_shutdown 设置为 ON,MySQL 会在关闭时保存缓冲池中的数据页。

手动预热:

在 MySQL 启动后,手动执行一些查询(如全表扫描)来加载常用数据到缓冲池中。

使用工具如 mysqlslap 或自定义脚本模拟访问。

Redis 在启动时如果采用懒加载机制,首次访问时会有较高的延迟。可以通过以下方式预热缓存:

手动加载数据:

在 Redis 启动后,通过脚本或工具将常用数据加载到内存中。

例如,使用 SCAN 命令遍历所有键,并访问这些键以触发加载。

持久化文件优化:

如果使用 RDB 持久化,可以定期生成 RDB 文件,并在启动时加载。

如果使用 AOF 持久化,可以优化 AOF 文件大小,减少加载时间。

部署后很直观可以看到,平均响应能保持正常的10ms以下

相关推荐
Vic101013 小时前
基于Zookeeper的微服务配置管理与灰度发布实战指南
分布式·微服务·zookeeper
咯拉咯啦3 小时前
Docker安装 Nacos 微服务
docker·微服务
2301_764602235 小时前
网络体系架构
网络·架构
桂月二二5 小时前
云原生时代的智能流量治理体系设计与实践
云原生
小杨4045 小时前
架构系列二十三(全面理解IO)
java·后端·架构
秋说6 小时前
【区块链安全 | 第二篇】区块链概念详解
安全·架构·区块链
云上艺旅6 小时前
K8S学习之基础四十三:k8s中部署elasticsearch
学习·elasticsearch·云原生·kubernetes
无级程序员6 小时前
银行分布式新核心的部署架构(两地三中心)
分布式·架构
小样vvv6 小时前
【Redis】架构演进:从基础到卓越的技术之旅
redis·架构
靖靖桑6 小时前
微服务中的服务发现与注册中心
微服务·架构·服务发现