Day 02 / 共 20 天 · 第 1 周 架构与线程

启动路径

敲下 envoy -c config.yaml 回车后发生了什么?今天追一遍启动全链路:main()MainCommonServer::InstanceImpl 初始化 → 主线程阻塞在事件循环。

📍 第 1 周(架构与线程)· 你在这里
D1 全景 D2 启动路径 D3 线程模型 D4 事件循环 D5 请求入口
L01

启动全链路

🤔 痛点:昨天知道了 Envoy 是"门卫",可门卫是怎么"上岗"的? 你敲 envoy -c config.yaml 回车,进程就跑起来开始接流量了。中间发生了一连串事:读配置、建各种管理器、拉起干活的线程……如果这套"开机顺序"错一步(比如还没读配置就开门接客),就会翻车。今天就把这条开机链路走一遍。
💡 本质:启动 = 开一家门店的"开业筹备流程"(今天的类比世界观) 把 Envoy 进程想成开一家新门店:①拿到营业执照(main())→ ②总经理到岗(MainCommon)→ ③先给全体员工装好私人储物柜(TLS,最先建)→ ④备料、砌灶台、排班(InstanceImpl 初始化)→ ⑤正式开门营业、站在门口不停接客(run 事件循环,一直不返回)。顺序不能乱:必须先备好料才能开门。
main()                          # source/exe/main.cc:16
 → MainCommon::main             # main_common.cc:155
 → 构造 MainCommon → createFunction() lambda 创建 Server::InstanceImpl  # main_common.cc:43
 → server->initialize()         # main_common.cc:47
 → MainCommonBase::run()        # main_common.cc:77
 → runServer() → InstanceBase::run()  # server.cc:1040
 → dispatcher_->run(Block)      # server.cc:1061 —— 主线程阻塞在事件循环
读法:启动 = "构造 server 对象 → 初始化(加载配置、建集群管理器等)→ 跑主线程事件循环"。最后一行 dispatcher_->run(Block) 是"心跳"起点——进程就阻塞在这里处理事件,直到收到退出信号。
开机五步(对照"开门店"流程) main()拿营业执照 MainCommon总经理到岗 TLS 最先建员工储物柜 InstanceImpl备料建灶台排班 run(Block)开门营业(不返回) ⟳ 最后这步进入无限循环,主线程"住"在这里
图注:启动顺序不可乱——储物柜(TLS)必须最早建,营业(事件循环)必须最后开,且一开就不停。
🧠 记忆口诀:证—经—柜—料—开(main)→ 理到岗(MainCommon)→ 装(TLS)→ 备(初始化)→ 门(run 事件循环)。五个字记住整条开机链,面试被问"Envoy 怎么启动的"就照这个顺序讲。
L02

main()

// source/exe/main.cc:16
int main(int argc, char** argv) {
  ...
  return Envoy::MainCommon::main(argc, argv);   // main.cc:24
}
读法:main() 极薄——只做站点特定初始化,立刻转入 MainCommon::main真正的逻辑在 MainCommon
L03

MainCommon::main

// source/exe/main_common.cc:155
int MainCommon::main(int argc, char** argv) {
  Thread::MainThread main_thread;                    // :161 标记主线程身份
  try {
    auto main_common = std::make_unique<MainCommon>(argc, argv);  // :168 构造+初始化整个 server
  } catch (...) { ... }
  return main_common->run() ? EXIT_SUCCESS : EXIT_FAILURE;  // :189 事件循环放 try 外,便于崩溃 core dump
}
为什么事件循环放在 try/catch 外面? 注意 run()(事件循环)故意包在 try 块里。因为如果运行期崩溃(比如某扩展 bug),你希望进程直接 core dump(生成崩溃现场),方便调试;而不是被 catch 悄悄吞掉、丢失现场。构造/初始化阶段的错误才 catch(那些是配置错误,能友好报错退出)。Thread::MainThread 标记"我是主线程"——后面大量代码会断言"这个操作只能在主线程做"(Day 03 线程安全)。
L04

TLS 最先创建

// source/exe/stripped_main_base.cc:44-75(StrippedMainBase 构造)
tls_ = std::make_unique<ThreadLocal::InstanceImpl>();   // :68 创建线程本地存储实例
// :69 创建 Stats::ThreadLocalStoreImpl
读法:注意 线程本地存储(TLS)实例在最早期就创建,早于一切配置加载。因为它是整个 Envoy 无锁并发的基础设施(Day 03 详讲),几乎所有东西都依赖它。createFunction()main_common.cc:43)随后创建 Server::InstanceImplinitialize()
L05

InstanceBase 初始化

// source/server/server.cc 构造(:78)+ initializeOrThrow(:464)
:98  dispatcher_(api_->allocateDispatcher("main_thread"))  // 主线程 Dispatcher/事件循环
:478 thread_local_.registerThread(*dispatcher_, true);     // 主线程注册进 TLS
:719 创建 listener_manager_(其构造里创建 N 个 worker)
:723 stats_store_.initializeThreading(...)                 // 此后可跨线程写 stats
:819 config_.initialize(...)                               // 解析 bootstrap 配置、建集群管理器
:861 创建看门狗 main_thread_guard_dog_ / worker_guard_dog_
读法:初始化按依赖顺序建好核心对象:主线程 Dispatcher → 注册主线程 → ListenerManager(含创建 worker)→ stats 线程化 → 解析配置 → 看门狗。看门狗(guard dog)监控线程是否卡死——某线程长时间不"喂狗"就报警/杀进程(防死锁静默)。
L06

run 事件循环

// source/server/server.cc:1040 InstanceBase::run
const auto run_helper = RunHelper(..., [this] {
    notifyCallbacksForStage(Stage::PostInit);
    startWorkers();                                  // :1047 集群就绪后拉起 worker 线程
});
dispatcher_->post([this] { notifyCallbacksForStage(Stage::Startup); });  // :1060
dispatcher_->run(Event::Dispatcher::RunType::Block);  // :1061 —— 阻塞在此!
terminate();                                          // :1068 退出后清理
"阻塞在事件循环"是什么意思? dispatcher_->run(Block) 这一行不会返回(直到退出)——主线程就"住"在这个事件循环里,不停地处理事件:有新连接?处理。定时器到点?处理。收到信号?处理。这就是 Envoy 的"主循环",Day 04 会拆开看它内部。注意 startWorkers() 是在"集群初始化完成"后才调的——因为要等控制面配置就绪(Day 13 的初始化顺序),worker 才开始接流量。

👶 小白:既然 run(Block) 会一直卡住不返回,那 Envoy 岂不是"死"在这一行了?后面的代码怎么执行?

👨‍🏫 老师:它没"死",是在"忙"。这行不是傻等,而是主线程钻进了一个无限循环,循环体里不停处理事件(新连接、定时器、信号)。就像门店的前台"一直站在门口"——看着不动,其实一有客人(事件)就立刻招呼。只有收到关店信号(SIGTERM)循环才结束、往下走到 terminate() 清理。真正接客的活由 worker 线程干(Day 03),主线程主要管调度和配置。

L07

信号与 worker 启动

RunHelperserver.cc:964)注册信号处理::980 SIGTERM、:985 SIGINT(优雅退出)、:990 SIGUSR1(重开日志)、:995 SIGHUP(热重启)。集群就绪回调触发 startWorkers():919)→ listener_manager_->startWorkers(...) 真正拉起 worker 线程。

读法:信号处理让 Envoy 能响应运维操作(优雅关闭、重载日志、热重启不断流)。worker 启动是"集群配置就绪"这个事件触发的——事件驱动贯穿始终。下一课看 worker 线程模型。
📝 举个例子:kill 一下 Envoy 会发生什么 你执行 kill -TERM <pid>(等价于 docker stop)→ 进程收到 SIGTERM → 触发注册的处理器 → 主线程的事件循环从"接客"转为"打烊":停止接受新连接、让已有请求处理完(优雅排空)→ 循环退出 → 执行 terminate() 清理 → 进程退出。
对比:直接 kill -9 是 SIGKILL,进程被内核当场打死,正在处理的请求全断——所以生产环境关服务要用 SIGTERM 而非 -9
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 启动全链路 main → MainCommon → InstanceImpl → run 的大致步骤?
  • 为什么事件循环放在 try/catch 外?
  • 为什么 TLS 最先创建?
  • dispatcher_->run(Block) 为什么"阻塞"?worker 什么时候启动?
  • 看门狗和信号处理各干什么?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/envoy
sed -n '16,25p' source/exe/main.cc
sed -n '155,190p' source/exe/main_common.cc
sed -n '1040,1069p' source/server/server.cc
明天预告 · Day 03线程模型(Envoy 招牌设计)——1 主线程 + N worker + 线程本地存储 + 热路径无锁。worker 数怎么定、每个 worker 是什么、TLS 怎么让配置无锁地分发到每个线程。
← Day 01 全景 Day 03 · 线程模型 →