核心服务优雅上线方案设计与实践

75 阅读5分钟

背景‌:搜索服务承载5k+ QPS,单机500+ QPS,传统发布流程导致100ms+响应抖动,影响用户体验。


一、问题定位与根因分析

1.1 核心问题现象
  • 启动阶段流量冲击‌:服务注册过早,实例未完成初始化即接收生产流量

  • 响应时间尖刺‌:首次请求触发懒加载组件初始化(DB连接池/Redis连接/动态配置)

    image.png

1.2 技术根因溯源
问题点具体表现影响范围
Nacos注册时机过早监听WebServerInitializedEvent事件注册,此时部分Bean未完成初始化所有依赖延迟加载的组件
中间件连接懒加载Hikari连接池、Lettuce客户端等采用首次调用时建立连接数据库/Redis操作
动态配置加载滞后@RefreshScope Bean在首次请求时触发初始化配置相关业务逻辑
未使用jit优化没有jit优化,代码是边解释边执行全部代码

通过Spring生命周期追踪与源码插桩,发现‌Nacos注册与资源就绪存在时序差‌。 在阅读了源码之后,可以得出结论nacos的注册时机是依托于spring的生命周期机制,我们发现监听的是WebServerInitializedEvent,也就是内置的Tomcat启动完成的时刻,所以你会看到nacos的beat心跳紧贴在tomcat的17000端口启动之后进行注册并完成的。意味着从这一刻开始,nacos上就有这台机器,对应的请求也会过来。

image.png


二、解决方案设计

2.1 优化方案
方案优点
延迟注册简单易实现
健康检查驱动注册精准判断服务真实可用状态
接口预热主动触发
逐步预热上线自定义权重配置
2.2 最终方案架构
    A[Spring容器启动] --> B{触发ApplicationRunner}
    B --> C[执行首次健康检查]
    C --> D[启动异步健康监控线程]
    D --> E{持续检查就绪状态?}
    E -->|未就绪| F[等待5秒重试]
    G -->|已就绪| H[进行接口预热]
    I -->|预热完毕| J[触发Nacos手动注册]
    K --> L[10%流量接入] --> M[50%流量接入] --> N[100%流量接入] 

三、关键技术实现

3.1 注册时机控制
  • 注册时机后移 关闭自动注册,进行手动注册,并进行流量的逐步接入‌:
/**
 * 进行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手动注册流程结束");
    }
}
3.2 健康检查增强
  • 使用健康检查进行连接预热‌:
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("异步监测健康状态一直失败,请检查!");
            }
        });
    }
}
  • 中间件预热‌:
    以mysql举例,使用的Hikari连接池在datasource创建的时候采用的是懒加载模式,直到第一次调用getConnection才会真正和mysql进行连接。而spring actuator的健康检查机制可以解决此类问题,针对所有的中间件,不管你是否有主动进行getConnection,它都会在检查时主动getConnection。

    image.png

通过DataSourceHealthIndicator主动触发连接池初始化,避免首次请求时建立连接。


四、实施效果验证

4.1 性能指标对比
指标优化前优化后
启动阶段最大响应时间100ms7ms
服务注册到接收流量间隔3-5s按需可控
发布期间错误率0.8%0.02%

image.png

4.2 典型日志轨迹
textCopy Code
[14:52:59] 首次健康检查:OUT_OF_SERVICE  
[14:53:04] 异步检测通过 → DB/Redis连接就绪  
[14:53:04] 触发手动注册 → 服务进入可用状态

五、方案扩展性

  1. 动态等待策略‌:根据健康检查结果动态调整重试间隔(指数退避算法)
  2. 分级就绪机制‌:区分核心依赖与次要依赖的健康状态
  3. 熔断降级集成‌:与Sentinel联动实现异常状态自动下线

后续计划‌:将该能力沉淀为Spring Cloud Starter组件,实现企业级标准化发布流程