Day 02 / 共 20 天 · 第 1 周 架构与线程
启动路径
敲下 envoy -c config.yaml 回车后发生了什么?今天追一遍启动全链路:main() → MainCommon → Server::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) 是"心跳"起点——进程就阻塞在这里处理事件,直到收到退出信号。图注:启动顺序不可乱——储物柜(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::InstanceImpl 并 initialize()。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 启动
RunHelper(server.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 怎么让配置无锁地分发到每个线程。