上一篇给出了请求路径与配置快照两条坐标系。本篇只钉一条轴:配置如何在不停流量下到达 Worker,以及 Worker 热路径为何几乎不为「读配置」加全局锁。
常见误区有两个:把 Main 当成「也会处理一部分请求的 master」;把 TLS 快照当成「全进程瞬间一致的共享内存」。两者都会把排障方向带偏——前者去 Main 上找延迟,后者去锁竞争上找「配置未生效」。
本文说明:Dispatcher 如何组织事件;连接如何绑死到一个 Worker;Thread Local Storage(TLS)槽位如何分发只读快照;以及快照不保证的一致性窗口。
本文是「Envoy / 数据面代理内核」系列第 2 篇(共 16 篇)。→ 系列目录
篇目 核心内容 第 1 篇 · 数据面全景 生态位与五条坐标系 第 2 篇 · Main / Worker 事件循环、TLS 快照、几乎无锁 第 3 篇 · Listener Match accept 与 FilterChainMatch 第 9–11 篇 · xDS 推送如何触发快照更新
版本锚定:Envoy v1.39.0 官方 Architecture overview / Threading model;源码树
envoyproxy/envoytag v1.39.0(source/common/event/、source/common/thread_local/、source/server/)。
一、单进程多线程:谁干什么
官方 Threading model(v1.39.0)把架构写成三块:
| 线程 | 职责 | 不做什么 |
|---|---|---|
| Main | xDS 解析与编排、stats 刷新、Admin、进程信号 / hot restart 协调 | 不承载高并发下游流量 |
Worker(--concurrency) |
每个 listener 上 accept、实例化 filter 栈、连接生命周期内全部 I/O | 不直接连控制面做全量配置解析 |
| File flusher | 把 access log 缓冲刷到磁盘 | 不参与协议处理 |
设计谱系可回指 Matt Klein 的 Envoy threading model(B 级,仍被官方文档点名为 companion):Main 写成像「单线程异步管理面」;Worker 写成像「单线程异步数据面」。与 Nginx 多进程 / HAProxy 单进程多线程相比,Envoy 的不变量是 API 驱动的配置分发,不是「reload 换一批 worker」。
flowchart LR
CP["Control plane / xDS"] --> Main["Main thread"]
Main -->|"post TLS update"| W0["Worker 0 Dispatcher"]
Main -->|"post TLS update"| W1["Worker 1 Dispatcher"]
Client["Downstream"] --> W0
Client --> W1
W0 --> Up["Upstream"]
W1 --> Up
Worker 数默认贴近硬件线程;--concurrency
可显式指定。v1.39.0 还引入了 Linux 上可选的
enable_worker_cpu_affinity 与基于
SO_REUSEPORT BPF 的
cpu_locality_balance(见版本说明)——这是部署层对缓存
/ NUMA 的增强,不改变「连接绑死一个
Worker」的语义。缺任一前置条件时,listener 回退到内核默认
reuseport 哈希,而不是静默变成「假亲和」。
Admin 监听与 stats sink 刷新落在 Main:高基数 stats
或频繁 /config_dump 会挤占 Main
时间片,表现为配置更新变慢,不一定表现为
Worker CPU 打满。把「控制面迟滞」与「数据面慢」分开,是后续
xDS 篇的前置直觉。
二、Event::Dispatcher:每线程一个 Reactor
每个 Main / Worker 线程的根是
Event::Dispatcher:对
libevent(及后续可替换事件环)的封装,管 fd
可读可写、定时器、(Main
上)信号。代码按「单线程、非阻塞、回调」书写;跨线程投递用
Dispatcher::post(),把闭包排进目标线程的队列。
RunType 区分 Block / NonBlock /
RunUntilExit——长生命周期线程用后者。Linux 上
io_uring 若启用,是每 Worker 独立
ring,经 eventfd 挂进同一
Dispatcher,仍遵守 shared-nothing,避免 Worker
间抢同一提交队列。
与站内 libevent 系列 的关系:Reactor 通识在那边;本篇只保留 Envoy 特有的 TLS 槽位 + Main 编排。
Watchdog 另开线程盯 Main / Worker 是否按时「摸」时间戳:长时间不响应会记 Miss / MegaMiss,严重时可杀进程出 core——这是「热路径被阻塞」的工程保险丝,不是业务锁。扩展里若在 Worker 回调中做阻塞 DNS、大文件读或重 CPU,先触发的往往是 Watchdog,而不是互斥锁火焰图。
三、连接钉死在一个 Worker
Threading model 写死:listener accept 之后,连接绑定到单个 Worker,终生不迁移。热路径因此高度并行:同一连接上的 filter、buffer、codec 状态不必与其他 Worker 共享。
默认无 Worker 间协调:各 Worker 独立在同一 listener 上抢 accept,靠内核均衡。长连接极少、HTTP/2 / gRPC egress 等场景下,内核均衡可能偏斜,此时可在 listener 上配置 connection balancer。Windows 上因异步 I/O 与内核均衡限制,文档明确会强制 listener connection balancing,并注明有性能代价。
排障含义:
- 单连接延迟尖刺 → 看该连接所在 Worker 是否卡在某个 filter / 上游 / 日志缓冲,而不是「平均一下所有 Worker」。
- CPU 不均 → 先区分「连接数不均」与「单连接 CPU 重」;后者无法靠迁移连接解决。
四、TLS 配置快照:解决什么
此处 TLS 指 Thread Local Storage,不是 Transport Layer Security。
机制(官方 Threading model + Klein 文一致):
- Main 上为某类对象(cluster manager、listener 配置视图等)分配 slot(全局索引)。
- 配置更新时,Main 向各 Worker post 闭包;闭包在 Worker 上创建或替换该线程本地数据。
- 热路径用 slot 做 O(1) 向量查找,读本线程快照——不为「读路由 / 读 cluster」加进程级读写锁。
因此「几乎无锁」指的是:读配置的数据面路径。仍存在少数共享点(例如 access log 写入内存缓冲再交给 flusher、部分 stats 聚合),官方与 Klein 文都承认「几乎」而非「绝对」。
sequenceDiagram
participant Main
participant W0 as Worker0
participant W1 as Worker1
Main->>W0: post update slot N
Main->>W1: post update slot N
Note over W0: build local snapshot
Note over W1: build local snapshot
W0->>W0: O(1) slot lookup on hot path
W1->>W1: O(1) slot lookup on hot path
五、TLS 解决不了什么(争论与失败模式)
| 期望 | 实际 | 后果 |
|---|---|---|
| 全 Worker 同一时刻切到同一版本 | post 异步,存在新旧快照并存窗口 | 同一秒内两连接可能打到不同路由版本 |
| ACK 等于流量已切新配置 | ACK 是 xDS 协议层接受;warming / 依赖资源见第 9–11 篇 | 「控制面已推送」仍可能旧路由或黑洞 |
| 任意扩展可随意跨线程读全局可变状态 | 扩展若绕过 TLS /
post,会引入隐性锁或数据竞争 |
热路径卡顿、Watchdog Miss |
| 配置更新零成本 | 大对象重建、引用计数释放仍占 Worker 时间片 | 更新风暴时延迟抬升,不是锁竞争假象 |
开放争论(与第 1 篇靶心对齐):静态文件 + reload 用进程边界换「配置瞬间一致」;xDS + TLS 快照 用并存窗口换「不停流量热更新」。没有免费的全局瞬时一致——第 11 篇讲 warming 如何收窄「已 ACK 但仍不可服务」的窗口。
工程间隙:论文式「lock-free dataplane」常省略 Main
编排延迟、slot 更新排队、以及扩展线程(DNS
getaddrinfo 池、GeoIP reload、AsyncFile
等)对整体延迟分布的影响。生产归因要把「Worker
事件环阻塞」与「上游慢」分开。
可检验的开放问题(本篇收束,不提前写 xDS 全书):
- 大规模 EDS 抖动时,TLS 更新闭包排队是否成为 Worker 尾延迟主因——需结合第 10–11 篇的推送经济学。
- CPU affinity + locality balance 在容器 cpuset 收缩时的失效模式,是否被运维正确观测(回退是否可从 stats / 日志识别)。
- 扩展生态在「必须共享跨连接状态」时,除 TLS 外有哪些半官方模式,代价各是什么(第 13 篇)。
六、开发与排障落点(本篇边界)
扩展作者应使用 Thread::ThreadFactory,经
runOnAllThreads / Dispatcher::post
投递;不要直接 std::thread 绕过线程跟踪。官方
Threading model
还列出若干合法的旁路线程池:AsyncFile、Cache
eviction、GeoIP reload、getaddrinfo DNS
等——共同点是把阻塞工作挪出 Worker,再经 post
回事件环。自写扩展若「图省事」在 filter
里同步堵死,等于放弃整条 shared-nothing 假设。
Admin /stats、mutex
tracing(--enable-mutex-tracing)属于观测入口——具体命令输出留到有环境实跑的篇目,本篇不伪造。
源码阅读入口(v1.39.0):source/common/event/(Dispatcher)、source/common/thread_local/(slot)、source/server/(Worker
/ Main 生命周期)。具体函数名以该 tag
为准,后续篇再钉调用链。
与第 1 篇坐标系对齐:本篇只钉线程与快照轴;accept 后如何选链属匹配与改写轴(下一篇);xDS ACK 与 warming 属一致性轴(第 9–11 篇)。
七、参考资料
规范 / 官方文档(A)
- Envoy Proxy, Threading model, docs v1.39.0:Main / Worker / File flusher、连接绑定、Dispatcher、TLS slot、Watchdog。
- Envoy Proxy, Architecture overview(同版本 docs 树)。
- Envoy Proxy, 1.39.0
版本说明:
enable_worker_cpu_affinity、cpu_locality_balance。
源码(A)
envoyproxy/envoytag v1.39.0:source/common/event/、source/common/thread_local/、source/server/。
维护者叙述(B)
- Matt Klein, Envoy threading model(官方 Threading model 仍引用为 companion)。
站内对照
实验台账
- 本篇无命令输出、无 QPS / 延迟数字。
八、小结
- Main 不碰高并发数据面;Worker 用独立 Dispatcher 处理绑定到本线程的连接。
- 连接终生钉在一个 Worker——热路径并行靠 shared-nothing,不靠跨线程抢锁。
- TLS 快照让读配置成为 O(1) 本地查找;不提供全进程瞬时一致,也不等价于 xDS ACK。
- 下一层是 accept 之后如何选 FilterChain——失败常表现为「协议全错」而非「上游 503」。
→ 上一篇:数据面全景 · 系列目录 · 下一篇:Listener 与 FilterChainMatch
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Envoy 数据面】xDS 资源树:LDS→RDS→CDS→EDS 的依赖、命名与顺序
把 Listener、RouteConfiguration、Cluster、ClusterLoadAssignment(及 SDS)画成依赖树,说明资源命名如何串起引用、warming 与 make-before-break 顺序为何决定短暂黑洞,并衔接到 ADS/SotW/Delta。
【Envoy 数据面】数据面全景:从 Listener 到 xDS 的可编程代理内核
定位 Envoy 相对 Nginx/HAProxy 静态配置代理与 Mesh 选型叙事的生态位;钉住请求路径、配置快照、FilterChainMatch、xDS warming、上游资源五条坐标系,并给出与 network/57 的分工及 16 篇阅读路线。
【Envoy 数据面】Listener 与 FilterChainMatch:accept、listener filters 与选链失败
拆解 Envoy accept 之后到 network filter 之前:listener filters 如何 enrich 元数据,FilterChainMatch 如何按 SNI/ALPN/目的地址选链,以及选错 chain 的典型失败模式。
【Envoy 数据面】Network filter 与 Connection:字节流、水印与 TCP Proxy / HCM 分叉
在 FilterChain 已选定之后,说明 network filter 如何在连接字节流上工作、读写水印如何反压、连接生命周期事件,以及 TCP Proxy 相对 HTTP Connection Manager 的路径分叉。