由于公司面向广大C端用户,每日需要承担十万级的QPS,每个节点可能会高达数千QPS的,每一秒的抖动都可能对大量用户造成影响,在高频产品迭代的前提下,平稳的进行服务发布和新老服务替换是一个必要的能力。
可以看到部署优雅功能前,启动阶段会导致100ms+的响应抖动。这个说明启动瞬间会有部分用户体验受到较大的影响,是值得研究优化的点。

nacos源码下载
为了方便研究源码,把nacos下载下来。下载地址我已经打包好了,免积分下载:
https://download.csdn.net/download/fyihdg/90461118https://download.csdn.net/download/fyihdg/90461118
如果编译nacos源码遇到问题,可以参考:
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以下
