土法炼钢兴趣小组的算法知识备份

【Envoy 数据面】Main / Worker 与配置快照:事件循环、TLS 与几乎无锁热路径

文章导航

分类入口
networkproxy
标签入口
#envoy#threading#tls-snapshot#event-dispatcher#worker#xds#v1.39

目录

上一篇给出了请求路径与配置快照两条坐标系。本篇只钉一条轴:配置如何在不停流量下到达 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/envoy tag v1.39.0source/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,并注明有性能代价。

排障含义:


四、TLS 配置快照:解决什么

此处 TLS 指 Thread Local Storage,不是 Transport Layer Security。

机制(官方 Threading model + Klein 文一致):

  1. Main 上为某类对象(cluster manager、listener 配置视图等)分配 slot(全局索引)。
  2. 配置更新时,Main 向各 Worker post 闭包;闭包在 Worker 上创建或替换该线程本地数据。
  3. 热路径用 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 全书):

  1. 大规模 EDS 抖动时,TLS 更新闭包排队是否成为 Worker 尾延迟主因——需结合第 10–11 篇的推送经济学。
  2. CPU affinity + locality balance 在容器 cpuset 收缩时的失效模式,是否被运维正确观测(回退是否可从 stats / 日志识别)。
  3. 扩展生态在「必须共享跨连接状态」时,除 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)

源码(A)

维护者叙述(B)

站内对照

实验台账


八、小结

  1. Main 不碰高并发数据面;Worker 用独立 Dispatcher 处理绑定到本线程的连接。
  2. 连接终生钉在一个 Worker——热路径并行靠 shared-nothing,不靠跨线程抢锁。
  3. TLS 快照让读配置成为 O(1) 本地查找;提供全进程瞬时一致,也不等价于 xDS ACK。
  4. 下一层是 accept 之后如何选 FilterChain——失败常表现为「协议全错」而非「上游 503」。

上一篇:数据面全景 · 系列目录 · 下一篇:Listener 与 FilterChainMatch

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .