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

【HAProxy 数据面】Seamless reload 与 soft-stop:FD 交接、排空与状态保留

文章导航

分类入口
networkproxy
标签入口
#haproxy#seamless-reload#soft-stop#expose-fd#master-worker#stick-table#peers#3.4.3

目录

第 11 篇 把「能热改」与「必须 reload」切开。本文回答下一层:reload 时 listen FD 如何交接、旧进程如何 soft-stop、stick-table 与 peers 保住什么、相对 Nginx reload / Envoy hot-restart 差在哪。

依据 HAProxy 3.4.3 Management Guide 的信号模型、-x / expose-fd listeners / master-worker sockpair@ 说明。不粘贴伪造 reload 日志;不写未测延迟排名。

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

篇目 核心内容
第 11 篇 · Runtime API 热改 vs 结构
第 12 篇 · Seamless reload FD 交接、soft-stop、状态
第 13 篇 · 排障与可观测 五轴清单

版本锚定Management Guide 3.4.3 — graceful/hard stop、reload 窗口、-x、master-worker 对 SIGUSR2 的响应。对照 Envoy Hot restart / drainNginx 深度


一、问题:结构变更如何不停听

结构变更必须由新进程镜像解析配置。若新进程自行 bind 而旧进程仍占端口,会撞车;若先停旧再起新,会空窗。Seamless reload 的目标是:新进程拿到(或共享)listen 能力,旧进程停止 accept 但仍排空已有连接。

文档用语:

信号 / 选项 语义
SIGUSR1-sf finish) Graceful / soft-stop:解绑监听,处理完存量连接后退出
SIGTERM-st terminate) Hard stop:立即退出,切断连接
SIGUSR2(master) master-worker 下由 master 触发 reload 流程
-x <unix_socket> 新进程从旧进程取回 listening sockets,避免漏接新连接(Linux)

「不丢连接」在这里的精确含义是:存量连接留在旧进程排空;不是把 TCP 会话 splice 到新进程。长活连接要么在 soft-stop 窗口内结束,要么被 hard-stop-after 等策略切断——与 Envoy「连接不迁移」同族。

把 seamless reload 理解成「零失败数学保证」会过度承诺。Management Guide 明确写了高新建连接速率下仍可能出现少量失败,并区分两类窗口:pause/resume 竞态,以及 listen 关闭瞬间 backlog/SYN 变 RST。工程目标应是:选择正确交接路径(-x / sockpair)+ 足够新的内核 SO_REUSEPORT 支持 + 控制 reload 频率,把失败压进噪声;而不是在变更流水线里宣称「绝对无感」。

与第 11 篇合读:能 Runtime 解决的不要 reload——每次结构 reload 都引入双进程与状态迁移问题。反过来,为了「躲 reload」把本该进 Git 的结构变更长期堆在内存里,会在下次被迫 reload 时一次性爆掉。健康节奏是低频结构 reload + 高频受控 Runtime。


二、FD 交接:三种路径

sequenceDiagram
  participant Old as OldWorker
  participant New as NewWorker
  participant M as Master
  Note over M,New: master-worker path
  M->>New: Fork after cfg parse
  New->>Old: Retrieve listen FDs via sockpair
  New->>New: Accept new conns
  M->>Old: SIGUSR1 soft-stop
  Note over Old: Drain existing streams

2.1 无 master-worker:-x + expose-fd listeners

非 master-worker 时,新进程用 -x 连接旧进程的 stats socket,取回 listening FD。能力必须在配置里打开:

global
    stats socket /run/haproxy/admin.sock mode 600 level admin expose-fd listeners

expose-fd listeners 让该 socket 允许把 listener FD 交给对端;没有它,-x 取 FD 的路径不可用。典型启动形态(文档示例语义):

haproxy -f /etc/haproxy/haproxy.cfg \
  -D -p /var/run/haproxy.pid \
  -sf $(cat /var/run/haproxy.pid) \
  -x /run/haproxy/admin.sock

-sf 在新进程就绪后向旧 PID 发 finish;-x 负责 FD 交接。init/systemd 单元通常封装上述步骤。

2.2 master-worker:自动 sockpair@,可不配 expose-fd

Management Guide 写明:在 master-worker 下,reload 不需要 expose-fd listeners。Master 在 reload 时自动用 sockpair@ 语法直连 worker,取回 listening sockets,不必经过配置里声明的 stats socket。若要禁用该行为,可传 -x /dev/null

Master 收到 SIGUSR2 后:带着 -sf 与旧 worker PID 列表 re-exec / 解析配置、fork 新 worker,再完成交接与 soft-stop。运维面常见入口是 systemctl reload haproxy(具体 unit 以实现为准)。

Master CLI(-S不能用于取 FD——文档单独警告:该 socket 不可 retrieve listening sockets。

systemd / 发行版 unit 通常把「校验配置 → 发信号 / 带 -sf 拉起」封进 reload 动作。机制文关心的是单元背后的语义是否等价于「新进程取得 listen FD 且旧进程 soft-stop」;若自定义包装脚本丢掉了 -x 又未启用 master-worker 自动 sockpair,就会默默退回 pause 竞态路径。升级 HAProxy 大版本或迁移容器入口时,应重新核对 unit 与文档选项,而不是假设「reload 一词永远同义」。

容器场景额外注意:listen FD 交接依赖旧进程仍可达且 socket 路径一致;若编排层先杀旧容器再起新容器,那是滚动重建,不是 seamless reload。二者都合法,但失败模式不同——前者可能有短暂空窗,后者依赖同机进程协作。

2.3 无 -x 时的 pause/resume 竞态

若新进程无法直接继承 FD,而走「先让旧进程 pause(SIGTTOU)再 bind、失败则 SIGTTIN resume」路径,文档记载存在毫秒级窗口:端口既不在旧进程听、也尚未被新进程绑上。高新建连接速率下可能出现少量失败;SO_REUSEPORT(现代 Linux)可显著缩小该类竞态。另有 backlog/SYN 在旧 listen 关闭瞬间变 RST 的系统相关边角。结论:生产应优先走 -x / master-worker 自动交接,而不是依赖 pause 窗口碰运气。

历史包袱:早期环境若内核过旧、缺少可靠 SO_REUSEPORT,运维有时用防火墙在 reload 前后短暂拦 SYN,逼客户端重传——这是系统相关权宜之计,不是 HAProxy 默认承诺。新部署应先验证交接路径与内核能力,而不是默认上防火墙补丁。WSL/容器网络命名空间若把 listen 行为变得与裸机不同,应用本机实验确认,不要直接抄发行版 unit 假设。

把三种路径写成检查表:

  1. 是否 master-worker?是 → 确认未传 -x /dev/null 关掉自动交接。
  2. 否 → stats socket 是否带 expose-fd listeners,reload 是否带 -x
  3. 任一「否」→ 变更窗口按「可能有竞态」管理:降频、错峰、观察新建失败率。

三、旧进程排空:soft-stop 时间线

1. 新进程解析配置成功并取得 listen 能力
2. 旧进程收到 SIGUSR1:停止(或已停止)accept 新连接
3. 旧进程继续服务已建立 stream,直到自然结束
4. 连接耗尽后旧进程退出;内存池在 graceful 路径上会尽量释放给新进程

运维含义:

Master CLI 的 show proc 在窗口内应能看到 leaving worker;排障时对 @!旧PID 做只读查询,避免误判「reload 已完全切到新语义」。

排空期间可观测性容易骗人:stats 页面或默认 socket 可能主要反映 current worker,而延迟敏感的长连接仍在 leaving 上。第 13 篇把「Reload / Runtime」单列一轴,原因正在于此。发布系统若只探针「新进程 show info 成功」就标绿,可能在 leaving 仍持有大量会话时过早继续下一波变更。

hard-stop-after(及同类超时)是安全阀:没有它,异常客户端可以无限拖住旧进程,导致 FD、内存与线程组长期双倍占用。设多少秒必须看连接时长分布——边缘网关与内部 LB 的答案不同;本篇不写未测「推荐秒数」。


四、Stick-table / peers:reload 保留什么

状态 默认 reload 行为 保留路径
Stick-table 条目 清空(新进程空表) peers 段 + 表声明 peers <section>;单机也可把本机列为 peer,用于跨 reload 同步
Runtime 改的权重/state 丢失(除非写入 state 并加载) show servers state 落盘 + 启动加载;或回写 cfg
Stats 计数器 新进程从零(或 stats-file 预载) 文档中的 stats-file / preload 机制
已建立 TCP/HTTP 会话 留在旧进程排空 无迁移;客户端可能在排空后重建到新进程

Peers 协议本为多机 stickiness / 限流共享设计;「单机 peers 抗 reload 丢表」是同一机制的副作用(维护者博客有操作说明,属 B 级辅证;配置语法以 Configuration Manual 为准)。未配 peers 时,限流桶与粘滞在 reload 瞬间归零——这是事故常见根因,不是「seamless」承诺的一部分。

机制细节与容量失败模式见 第 8 篇。工程上建议把「该表是否 peers 保护」写成配置评审项:凡用于安全封禁或计费粘滞的表,默认要求 peers;仅用于短窗调试计数的表,可接受 reload 清空并在变更说明里写明。

State file 路径与 stick-table/peers 正交:前者主要服务 server 运行态在 reload 后的复现;后者服务表条目同步。只做其中一个,仍可能看到「机器还在、限流桶没了」或「桶还在、权重回到文件值」——排障时按第 13 篇分轴,不要混成一个「状态丢了」黑盒。


五、对照:Nginx reload 与 Envoy hot-restart

维度 HAProxy seamless reload Nginx reload Envoy hot-restart
触发 配置结构变更;master SIGUSR2-sf 新进程 配置变更;master 拉新 worker 二进制/不可热替换状态;epoch
Listen 交接 -x / expose-fd / sockpair 新 worker bind;旧 worker 退出 UDS RPC 传 listen socket
存量连接 旧进程 soft-stop 排空 旧 worker 排空 父进程 drain;不迁移
统计/状态 peers / state file 可选 共享区因产品而异 共享内存对齐 counter/gauge
动态配置主路径 Runtime API(第 11 篇)缩小 reload 面 多数变更仍 reload xDS 优先;hot-restart 是例外

Envoy 第 12 篇 对齐的判决句:三者都是「新听旧排」,都不是连接热迁移。 HAProxy 的差异在于:日常运维大量变更可先吃 Runtime,把 reload 留给结构与版本;Envoy 则把日常留给 xDS,把进程级留给 hot-restart。Nginx 侧见 network/55;产品层横向表见 network/60;配置菜谱见 network/56

文档对高新建连接下的少量失败有数量级描述(约每 \(10^{4}\) 新建/秒量级一次失败量级的经验陈述)。本系列把该数字当本机 benchmark;只提醒:seamless ≠ 数学零失败,生产应以交接路径与内核 SO_REUSEPORT 能力为准。

选型收束层如何把「reload 税」写进排除树,见 第 14 篇Envoy 选型:若组织变更极频繁且必须 ACK 语义,HAProxy 的文件+Runtime 模型可能先触顶;若变更以结构为主且可接受受控 reload,则 seamless 路径足够支撑经典 LB 运维。

争论的另一面:有人主张「永远不要 reload,只滚动 Pod」。在编排层这通常意味着连接仍被切断或由客户端重试吸收——把 soft-stop 责任上移了,而不是消灭了。HAProxy seamless 的价值,是在同一宿主机进程世代内完成结构切换;若平台强制不可变镜像滚动,应把 soft-stop 时间写进 Pod 生命周期,而不是以为关掉 reload 就消灭了排空问题。


运维上把 soft-stop 当成「变更预算」的一部分:每次结构 reload 占用一段双进程资源与一段排空时间;预算用尽前应合并变更、错峰执行。把十次小结构改动拆成十次 reload,比合成一次文件变更再 reload 更伤交接窗口——这与「Git 小步提交」不矛盾:提交可以碎,生效可以批。

对多 frontend 大配置,解析与 fork 成本本身也会拉长「新进程尚未接听」的体感;校验(haproxy -c)必须在发信号前完成,避免解析失败却已扰动旧进程。Master-worker 下 master 常驻,能减少「整进程从零拉起」的惊吓,但不免除配置错误导致新 worker 起不来、旧 worker 已收到 finish 的悲剧剧本——发布系统应对「新 worker 未就绪」有明确回滚或中止语义。

六、参考资料

规范 / 官方文档(A)

工程对照(B)

源码锚点(A,路径级)

读完这篇,下一步读什么

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


By .