土法炼钢 · 系统与基础设施

【HAProxy 数据面】线程与调度:nbthread、Thread-group 与共享内存边界

文章导航

分类入口
networkproxy
标签入口
#haproxy#nbthread#thread-group#scheduler#stick-table#nbproc#event-loop#3.4

目录

上一篇给出了请求路径与线程共享两条坐标系。本篇只钉一条轴:多线程下谁跑事件循环、什么状态在进程内共享、什么必须跨进程复制——以及在 poller 线程里阻塞意味着什么。

常见误区有两个:把 HAProxy 想成「像 Nginx 一样每个 worker 一份 stick-table」;或把 nbthread 想成「像 Envoy Main 那样有一条专职控制面线程不碰连接」。两者都会把排障方向带偏——前者去进程间找「粘性丢失」却忘了自己开了多进程;后者去 Main 上找延迟,却发现 HAProxy 默认根本没有那条对称的 Main。

本文说明:nbthread / thread-group 如何组织 CPU;每线程事件循环与连接亲和;共享内存边界相对 nbproc;以及与 Nginx / Envoy 的对照句。

本文是「HAProxy / 数据面代理内核」系列第 2 篇(共 14 篇)。→ 系列目录

篇目 核心内容
第 1 篇 · 数据面全景 生态位与五条坐标系
第 2 篇 · 线程与调度 事件循环、共享边界、阻塞警告
第 3 篇 · Frontend Accept bind / accept / FD
第 8 篇 · Stick-table 容量、锁、peers 复制

版本锚定:HAProxy 3.4.3 Management Guide / Starter Guide / Configuration Manual(docs.haproxy.org/3.4/);源码树 haproxy/haproxy tag v3.4.3。不粘贴伪造的 show info / 线程 dump。


一、单进程多线程:官方怎么钉死

Management Guide(3.4)开篇把 HAProxy 写成:

multi-threaded, event-driven, non-blocking daemon

要点不是口号,而是三条可检验不变量:

不变量 含义 排障后果
事件多路复用调度 用 poller 调度活动,而不是「每连接一 OS 线程」 单回调里阻塞 = 该线程上所有连接一起饿
默认单进程、每允许 CPU 一 worker 线程 除非显式改配置,流量摊到各线程,同一套事件循环 ps 里常只有一个 haproxy(reload 时可能短暂并存旧进程)
连接由单线程服务 限制跨线程依赖,追求近线性扩展 单连接尖刺看该线程,不要先平均所有 CPU

Starter Guide 补充:多线程主要用于 SSL 或极高转发吞吐;并警告——在跨物理 CPU / 跨 NUMA 通信时,减少 HAProxy 使用的 CPU 数有时反而更好。这与「能绑多少核就绑多少」的直觉相反,却与缓存/中断局部性一致。

flowchart LR
  subgraph proc ["Single process address space"]
    T0["Thread 0<br/>event loop"]
    T1["Thread 1<br/>event loop"]
    T2["Thread 2<br/>event loop"]
    ST["Shared stick-tables / stats"]
  end
  C0["Conn A"] --> T0
  C1["Conn B"] --> T1
  C2["Conn C"] --> T2
  T0 --> ST
  T1 --> ST
  T2 --> ST

二、nbthread 与 thread-group

2.1 nbthread

全局 nbthread 指定工作线程数。不显式写时,常见行为是按进程被允许运行的 CPU 集合自动起步——具体自动策略随 3.2+ 拓扑感知增强;强制写死 nbthread 会关掉「按本机拓扑自动选优」的那条路径(配置教程与 3.4 公告均强调:大机上优先理解自动策略,再手工覆盖)。

线程数必须与连接并发匹配:官方写明,要用满处理能力,连接数至少要到线程量级——长连接稀少时会出现「核很多、却喂不饱」的假空闲。

2.2 Thread-group(线程组)

Thread-group 把线程按局部性收成组,降低「远距离线程」之间的通信与争用面。版本脉络(以官方/发布说明为准,不编造本机数字):

对排障:stats 上部分 per-request 计数在 3.4 可按 thread-group 内部分片再聚合上报——报表数字仍是总和,但内部减少热点。这解释了「升级 3.4 后大机 HTTP/1 更稳」的一类机制叙述,却不能在没有本机对照实验时写成固定百分比结论。

2.3 cpu-map 与中断

Management Guide 性能段:网络密集时,把软中断与 HAProxy 钉在共享同一 L3 的不同核上,往往优于让它们抢同一核;cpu-map(配置)或 taskset(外部)是绑定入口。irq_balance 在这类负载上常被点名为「最差默认」。这些是部署层工具,不改变「连接钉在一线程」的语义。


三、每线程事件循环与调度器

每个工作线程跑自己的 poller 循环:等 I/O / 定时器,再跑到期的 task / tasklet。内部时钟用于超时:空闲时也会周期性醒来校正漂移(Management Guide 用 poll/epoll_waitgettimeofday 叙述说明「空闲也有短周期唤醒」是正常现象)。

与站内 libevent 系列 的关系:Reactor 通识在那边;本篇只保留 HAProxy 特有的 多线程同一进程 + 共享表。调度器是优先级感知的(Starter Guidepriority-based, multi-threaded scheduler)——含义是不同类任务可排序,而不是「业务请求有 POSIX 实时优先级」。

源码阅读入口(v3.4.3,目录级):src/ 下与 task / polling / thread 相关的实现,以及 include/ 中线程上下文结构。具体函数名以该 tag 为准;本篇不伪造调用栈。


四、共享内存边界:相对 nbproc

4.1 线程之间(同进程)

同进程内,stick-table、大量配置对象、聚合用的 stats 等位于共享地址空间。正确性靠锁与局部数据结构;吞吐靠减少跨 thread-group 碰触与缩短持锁临界区(3.4 对 stick-table 释放路径等仍有争用相关改动)。共享 ≠ 无锁,也 ≠ 「读配置完全免费」。

4.2 进程之间(nbproc

Management Guide 明文列出多进程限制,其中与本系列直接相关的是:

限制 含义
Stick-tables are per process and are not shared between processes 粘性/限流计数在进程间天然分裂
Health checks per process 上游收到的检查数随进程数倍增
maxconn / 队列 per-process 必须按进程重算,否则打爆后端
Peers section 同时只在单进程跑 复制拓扑不能「每进程各玩各的」却期望同一 peers 语义
CLI 一次作用一个进程 Runtime 操作要意识到目标进程

因此:现代默认叙事是 nbthread,不是靠 nbproc 横向扩 stick-table。 nbproc 仍可能出现在「CPU 极重的 SSL/压缩拆层」等实验拓扑(官方甚至描述过用 UNIX socket + PROXY protocol 串两层),但那是刻意的进程边界,不是「多开几个就更快」。

Peers / reload 时的表复制,是「跨进程或跨节点对齐状态」的正路——第 8、12 篇展开。不要用「再开一个 nbproc」假装共享。


五、对照:Nginx 多进程与 Envoy Main/Worker

维度 HAProxy(默认线程模型) Nginx Envoy
并行单元 同进程多线程 多 worker 进程 Main + 多 Worker 线程
连接亲和 单线程服务连接终生 通常钉在 accept 到的 worker 钉在单个 Worker
配置更新 新进程 reload + Runtime 热改子集 reload / 共享内存区视模块 xDS → TLS 快照
共享粘性状态 进程内 stick-table 默认不跨 worker 共享同构表 集群状态经主线程编排进 TLS
控制面线程 对称「只跑 xDS 的 Main」 master 管进程 Main 跑 xDS/Admin

对照句:

开放争论(与第 1 篇对齐):进程隔离换简单推理,还是线程共享换粘性正确性?没有免费午餐——选 nbproc 就必须接受 Management Guide 那张限制表。


六、软警告:不要在 poller 线程里阻塞

HAProxy 的正确性假设是热路径非阻塞。在线程回调里做同步 DNS、大文件读、长临界区持锁、或调用可能堵死的外部库,会导致:

  1. 该线程上所有连接的定时器与 I/O 一起推迟;
  2. 表现为「随机超时 / 延迟尖刺」,火焰图却不像业务锁竞争;
  3. 多线程下问题被局部化到一核,平均 CPU 利用率看起来「并不高」。

扩展点(Lua、某些 SPOE 用法、自定义 check 等)若把阻塞工作做进 poller,等于放弃事件驱动前提。官方与社区排障直觉一致:先假设线程被堵,再假设上游慢——区分手段是会话年龄、线程 CPU、与上游 RTT 是否同量级(具体命令语义见第 13 篇;本篇不伪造输出)。

Watchdog 式「摸时间戳」在 Envoy 里是显式机制;HAProxy 侧则更依赖「超时全靠内部时钟 + 你别堵 poller」。工程间隙:论文式 lock-free/event-driven 叙述常省略扩展与日志 I/O 对尾延迟的贡献。

可检验的开放问题:

  1. 超大 thread-group 与「多组小群」在容器 cpuset 收缩时,谁的尾延迟更可预测?
  2. Stick-table 极高更新率下,锁外释放等 3.4 改动是否足以支撑目标 QPS——必须按表类型与 key 基数实测(第 8 篇)。
  3. 与 Envoy TLS 快照相比,Runtime 改权重的「生效可见性」如何在多线程下定义(第 11、13 篇)。

七、调度器与「任务」:热路径上还有谁

事件循环不只处理套接字可读可写。HAProxy 还把大量工作建模成 task / tasklet:超时检查、会话收尾、某些内部维护。优先级调度的工程含义是——在同一线程里,紧急的短任务可以插在大块工作之前,但没有任何魔法把阻塞的系统调用变成非阻塞。

因此排障时要建立第二直觉:CPU 不高却延迟高,可能是线程卡在锁或偶发阻塞 syscall;CPU 某一核打满,可能是该核上连接叠加了 SSL/压缩/重规则,而不是「全局 nbthread 不够」。加线程只在「有足够并发连接、且工作可并行」时有效;官方已提醒连接数要到线程量级,否则多核是空转。

与健康检查引擎的关系(第 9 篇预告):检查任务同样要进入调度器。检查过密或脚本型检查过重时,争用的不是「另一个进程的 worker」,而是同进程线程的时间片——这与 Nginx 把某些工作隔离在独立进程里的取舍不同。

Master-worker 模式下,master 负责拉起/回收 worker、协助 reload,仍然不是 Envoy 那种持续解析 xDS 的 Main 数据面旁路。把 master 的 CPU 毛刺当成「数据面锁竞争」会走错方向;把 worker 上的 Lua 阻塞当成「上游 RTT」同样会走错方向。


八、参考资料

规范 / 官方文档(A)

源码(A)

维护者 / 发布说明(B)

站内对照

实验台账


九、小结

  1. 默认模型是单进程、每 CPU 工作线程、每线程事件循环;连接不跨线程迁移
  2. Stick-table 在线程间共享、在 nbproc 进程间不共享——这是 Management Guide 级硬边界。
  3. Thread-group / cpu-map 服务的是局部性与争用,不是换一套代理语义。
  4. Poller 上阻塞会把整线程连接拖进超时;扩展作者要把重活挪出热路径。
  5. Task 调度解释了「同线程上超时与 I/O 如何交错」;它不能拯救阻塞 syscall,也不能替代正确的进程/线程边界选择。

上一篇:数据面全景 · 系列目录 · 下一篇:Frontend / bind / accept

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .