欢迎光临
我们一直在努力

跟着 main 方法走了 700 行,终于看懂 Nacos 是怎么启动的

跟着 main 方法走了 700 行,终于看懂 Nacos 是怎么启动的


第一次 clone 源码,在 IDEA 里找了半个小时

不是开玩笑。Nacos 是个 Maven 多模块项目。根目录下十几个子模块。我点开每个模块的 src/main/java,翻来覆去找 main 方法,找了半小时。

最后在 console 模块里找到了:

// console/src/main/java/com/alibaba/nacos/Nacos.java
@SpringBootApplication
@EnableScheduling
public class Nacos {

public static void main(String[] args) {
SpringApplication.run(Nacos.class, args);
}
}

就三行。一个标准的 Spring Boot 启动类。

但接下来发生的事,让这三行背后展开了一个庞大的初始化链条。这篇文章跟着 main 方法往下走,把 Nacos 服务端从启动到就绪的完整路径拆开——每个关键 Bean 是什么时候初始化的,核心模块按什么顺序加载,standalone 和 cluster 模式在哪里分叉。

🔗 读完启动流程想继续挖代码?下一篇讲注册流程:一个服务注册请求在服务端走了哪些类


从 @SpringBootApplication 到自动装配

Spring Boot 的 @SpringBootApplication 触发 @EnableAutoConfiguration,后者扫描 classpath 下所有 spring.factories 文件。

Nacos 的四个核心模块各自有一份 spring.factories:

渲染错误: Mermaid 渲染失败: Parse error on line 2: graph TB Main[console/Nacos.j… ————^ Expecting 'SEMI', 'NEWLINE', 'SPACE', 'EOF', 'subgraph', 'end', 'acc_title', 'acc_descr', 'acc_descr_multiline_value', 'AMP', 'COLON', 'STYLE', 'LINKSTYLE', 'CLASSDEF', 'CLASS', 'CLICK', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', 'direction_tb', 'direction_bt', 'direction_rl', 'direction_lr', 'direction_td', got 'LINK_ID'

四个模块通过 spring.factories 注册各自的自动配置类。Spring Boot 扫描到后按顺序实例化。core 先跑,然后是 naming 和 config,最后是 console。

实际启动顺序是 core → naming → config → console,因为 naming 和 config 依赖 core 里的 Bean(比如 DataSource),console 依赖前三个模块的 Service。


启动流程:从 main 到就绪的 8 个关键节点

端口监听

Console 模块

Config 模块

Naming 模块

Core 模块

main()

端口监听

Console 模块

Config 模块

Naming 模块

Core 模块

main()

#mermaid-svg-xBFNhpg89ZMNrbaJ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xBFNhpg89ZMNrbaJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xBFNhpg89ZMNrbaJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xBFNhpg89ZMNrbaJ .error-icon{fill:#552222;}#mermaid-svg-xBFNhpg89ZMNrbaJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xBFNhpg89ZMNrbaJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xBFNhpg89ZMNrbaJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xBFNhpg89ZMNrbaJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xBFNhpg89ZMNrbaJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xBFNhpg89ZMNrbaJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xBFNhpg89ZMNrbaJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xBFNhpg89ZMNrbaJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xBFNhpg89ZMNrbaJ .marker.cross{stroke:#333333;}#mermaid-svg-xBFNhpg89ZMNrbaJ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xBFNhpg89ZMNrbaJ p{margin:0;}#mermaid-svg-xBFNhpg89ZMNrbaJ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xBFNhpg89ZMNrbaJ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-xBFNhpg89ZMNrbaJ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-xBFNhpg89ZMNrbaJ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-xBFNhpg89ZMNrbaJ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-xBFNhpg89ZMNrbaJ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-xBFNhpg89ZMNrbaJ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-xBFNhpg89ZMNrbaJ .sequenceNumber{fill:white;}#mermaid-svg-xBFNhpg89ZMNrbaJ #sequencenumber{fill:#333;}#mermaid-svg-xBFNhpg89ZMNrbaJ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-xBFNhpg89ZMNrbaJ .messageText{fill:#333;stroke:none;}#mermaid-svg-xBFNhpg89ZMNrbaJ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xBFNhpg89ZMNrbaJ .labelText,#mermaid-svg-xBFNhpg89ZMNrbaJ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-xBFNhpg89ZMNrbaJ .loopText,#mermaid-svg-xBFNhpg89ZMNrbaJ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-xBFNhpg89ZMNrbaJ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-xBFNhpg89ZMNrbaJ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-xBFNhpg89ZMNrbaJ .noteText,#mermaid-svg-xBFNhpg89ZMNrbaJ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-xBFNhpg89ZMNrbaJ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xBFNhpg89ZMNrbaJ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xBFNhpg89ZMNrbaJ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xBFNhpg89ZMNrbaJ .actorPopupMenu{position:absolute;}#mermaid-svg-xBFNhpg89ZMNrbaJ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-xBFNhpg89ZMNrbaJ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xBFNhpg89ZMNrbaJ .actor-man circle,#mermaid-svg-xBFNhpg89ZMNrbaJ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-xBFNhpg89ZMNrbaJ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

alt

[standalon-

e 模式]

[cluster

模式]

1. 初始化 DataSource、加载配置

2. ClusterManager 读取 cluster.conf

3. 判断 standalone vs cluster

加载内嵌 Derby

连接 MySQL

4. 创建 ServiceManager

5. 初始化一致性服务

(Distro / Raft)

6. 创建 ConfigService

7. 启动长轮询线程池

8. 初始化 Controller

HTTP(8848) + gRPC(9848) 开始监听

8 个关键节点:DataSource → ClusterManager → 模式判断 → ServiceManager → 一致性服务 → ConfigService → 长轮询 → Controller。缺任何一个环节服务都不可用。


启动入口的三处关键逻辑

回到 Nacos.java 的 main 方法。SpringApplication.run 只是表象。Nacos 在启动参数里做了三件事。

// 第一处:解析启动模式
// 来自 core 模块的启动逻辑
if (STANDALONE_MODE.equals(System.getProperty("nacos.standalone"))) {
// standalone 模式
// 用内嵌 Derby,不走 Raft 选举
System.setProperty("nacos.standalone", "true");
} else {
// cluster 模式
// 读 cluster.conf,走 Raft 选举
}

这段逻辑决定了一个全局变量,后面所有模块都会根据它做分支。后面的命名模块用这个变量来决定是走 Distro(AP)还是 Raft(CP),配置模块用这个变量来决定用内嵌 Derby 还是外部 MySQL。

// 第二处:JVM 参数解析
// 来自 bin/startup.sh 传入的 -Xms -Xmx 等
// Nacos 自己也会根据模式调整默认值
// standalone 默认 -Xms512m -Xmx512m
// cluster 建议至少 -Xms2g -Xmx2g

// 第三处:日志框架初始化
// Nacos 用 logback,启动时加载 nacos-logback.xml
// 如果找不到,fallback 到默认控制台输出


四个模块初始化——一个在另一个的肩膀上

Core 模块:地基

core 模块第一个被初始化。它不提供业务功能,只提供基础设施。

// 关键 Bean:DataSource
// 根据 standalone/cluster 模式决定用 Derby 还是 MySQL
@Bean
public DataSource dataSource() {
if (standalone) {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.DERBY)
.build();
} else {
return DataSourceBuilder.create()
.url(dbUrl)
.username(dbUser)
.password(dbPassword)
.build();
}
}

// 关键 Bean:ClusterManager
// 读取 cluster.conf,维护集群节点列表
@Bean
public ClusterManager clusterManager() {
ClusterManager cm = new ClusterManager();
cm.load(); // 读 cluster.conf 文件
cm.startHealthCheck(); // 启动节点健康检查
return cm;
}

DataSource 和 ClusterManager 被后续所有模块引用。没有 DataSource,naming 存不了服务数据,config 存不了配置。

Naming 模块:服务发现的骨架

naming 模块初始化时创建两个核心 Bean。

// 关键 Bean:ServiceManager
// 它是命名模块的"总统"——所有服务注册/发现/订阅都经过它
@Bean
public ServiceManager serviceManager() {
ServiceManager sm = new ServiceManager();
sm.setConsistencyService(consistencyService); // 注入一致性服务
return sm;
}

// 关键 Bean:一致性服务(代理模式)
// standalone 模式 → DistroConsistencyServiceImpl(AP)
// cluster 模式 → 委托给 RaftConsistencyServiceImpl(CP)
@Bean
public ConsistencyService consistencyService() {
if (standalone) {
return new DistroConsistencyServiceImpl();
}
return new DelegateConsistencyServiceImpl(
new RaftConsistencyServiceImpl()
);
}

这个代理模式在第二篇讲过。这里不展开。重点是:一致性服务在启动时就被确定了,运行时不切换。

Config 模块:配置管理的骨架

// 关键 Bean:ConfigService
@Bean
public ConfigService configService() {
ConfigService cs = new ConfigService();
cs.setDataSource(dataSource);
cs.setDumpService(dumpService);
return cs;
}

// 关键 Bean:DumpService
// 从数据库加载配置到内存,定期做全量 dump
@Bean
public DumpService dumpService() {
DumpService ds = new DumpService();
ds.init(); // 启动时全量加载一次
return ds;
}

// 关键 Bean:LongPollingService
// 1.x 模式下的长轮询线程池
// 2.x 模式切换到 gRPC Push,这个 Bean 仍然存在但几乎不用
@Bean
public LongPollingService longPollingService() {
return new LongPollingService();
}

Console 模块:最后一层

// Console 全是 Controller
// 不初始化自己的数据模型
// 只把前三个模块的 Service 注入进来,通过 REST API 暴露
@RestController
@RequestMapping("/nacos/v1/ns")
public class InstanceController {

@Autowired
private ServiceManager serviceManager;

@PostMapping("/instance")
public String register(...) {
// 委托给 naming 模块的 ServiceManager
serviceManager.registerInstance(...);
}
}

初始化到这里,8848 端口开始接收 HTTP 请求,9848 开始接收 gRPC 连接。启动完成。


standalone vs cluster:代码在哪里分叉

启动流程里最关键的分支点在这里:

// 来自 Nacos 源码(简化后的核心逻辑)
public void start() {
// 1. 读模式
boolean isStandalone = Boolean.getBoolean("nacos.standalone");

// 2. 初始化存储
if (isStandalone) {
initDerby(); // 内嵌 Derby
consistencyService = new DistroConsistencyServiceImpl();
} else {
initMySQL(dataSource); // 外部 MySQL
initClusterManager(); // 读 cluster.conf
initRaft(); // 启动 Raft 选举
}

// 3. 初始化业务模块(两种模式都一样)
initServiceManager();
initConfigService();

// 4. 启动端口监听
startHttpServer(8848);
startGrpcServer(9848);
}

分支点只有两处:存储层和一致性协议。业务层的 ServiceManager 和 ConfigService 代码完全一样。


启动失败的三个常见断点

现象代码断点位置原因
启动一半卡住 ClusterManager.load() cluster.conf 里写了不存在的 IP,load 超时
8848 起不来 EmbeddedServletContainer.start() 端口被占,Spring Boot 默认不做端口冲突检测
gRPC 连不上 GrpcServer.start() 9848 被防火墙挡了,或者与 8848 同端口冲突

总结

跟着 main 方法走完 700 行初始化代码,核心就三件事:

  • 模式判断:standalone 用 Derby + Distro,cluster 用 MySQL + Raft。这是在 core 模块启动时就决定的。
  • 模块顺序:core → naming → config → console。后一个依赖前一个的 Bean,不能调换。
  • Bean 注入:ServiceManager 管命名,ConfigService 管配置,DataSource 被所有模块共享。
  • 如果想改 Nacos 内核,从 Nacos.java 的 main 方法开始跟,比从 application.properties 瞎改参数高效得多。


    你读过 Nacos 源码吗?评论区留个数字:1=只调API不读源码 2=看过启动流程 3=跟踪过某条调用链 4=改过源码做过二次开发。顺便说说你最想搞清楚 Nacos 哪一块的源码逻辑。

    赞(0)
    未经允许不得转载:171主机测评 » 跟着 main 方法走了 700 行,终于看懂 Nacos 是怎么启动的
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址