HAProxy 的 tcp-request 延迟问题,绝大多数时候不是 HAProxy 本身的 Bug,而是配置逻辑、网络链路或后端服务的行为与你的预期不符。直接表现通常是客户端建立连接时卡顿、SSL 握手超时、或者代理层面莫名其妙的几秒等待。要解决它,必须从 HAProxy 处理 TCP 请求的完整生命周期入手,逐一排查。

检查 tcp-request 规则里的 inspect-delay 和 timeout connect

这是最容易引发延迟的配置项。当你在 frontend 或 listen 里使用 tcp-request content 规则(例如根据 SNI 做内容路由、根据 payload 做协议检测)时,HAProxy 会等待客户端发送足够的数据来满足检查条件。默认的 inspect-delay 是 0,但如果你显式设置了 tcp-request inspect-delay,或者使用了某些需要等待数据的 ACL,HAProxy 就会在这个阶段停留。很多人误以为 timeout connect 只控制后端连接,实际上在 tcp 模式下,timeout connect 也控制客户端在完成 tcp-request 检查前的最大等待时间。如果你设了很长的 timeout connect,而客户端又没有及时发送数据,就会出现数秒甚至数十秒的延迟。解决办法很直接:要么调低 inspect-delay 到毫秒级,要么确保 timeout connect 只比 inspect-delay 略大一点,避免空等。

tcp-request content 规则顺序导致的意外等待

HAProxy 的 tcp-request 规则是按顺序匹配的,一旦某条规则要求等待数据,后续所有规则都会受到影响。比如你写了一条 tcp-request content accept if { req.len 0 },然后又写了一条基于 SSL hello 的规则,那么即使客户端已经发来了完整 ClientHello,HAProxy 也可能因为前面的规则还在等待更多数据而延迟处理。更隐蔽的是,某些 ACL 比如 req.ssl_hello_type 或 req.payload 在低版本 HAProxy 中需要显式声明等待长度,否则会立即返回不匹配,导致规则被跳过,连接直接落到默认后端,看似延迟实际上是路由错误。检查时请把所有 tcp-request content 规则按依赖关系重新排序,把最精确、需要数据最少的规则放在前面,并且显式使用 tcp-request content reject 来快速关闭不符合条件的连接,而不是让它们一直悬着。

TCP 层面的延迟与内核参数强相关

HAProxy 工作在用户态,但它依赖内核的 TCP 栈来完成三次握手和窗口管理。如果你观察到 tcp-request 阶段的延迟总是集中在连接建立后的前几百毫秒,很可能是内核的 TCP 延迟确认(Delayed ACK)或 Nagle 算法在作祟。当客户端发送的数据包很小(比如 TLS ClientHello 刚好塞满一个段),而 HAProxy 的内核在等待更多数据再回复 ACK,或者 Nagle 算法在等待缓冲区填满,就会出现 40ms 到 200ms 的额外延迟。对于高并发短连接场景,这种延迟会被放大。直接在 HAProxy 的配置里加上 option tcp-smart-accept 和 option tcp-smart-connect 可以部分缓解,但更彻底的方案是在系统层面调整 net.ipv4.tcp_slow_start_after_idle 和 net.ipv4.tcp_autocorking,或者在 HAProxy 前端绑定内核参数 tcp_fastopen 来减少握手往返。

SSL 卸载时的 tcp-request 延迟陷阱

很多人在 HAProxy 上做 SSL 卸载,却忘了 tcp-request 是在解密之前执行的。如果你在 tcp-request 里试图检查 HTTP 头或者解密后的内容,那必然会失败,HAProxy 会一直等到超时。正确的做法是让 tcp-request 只检查原始 TCP 数据,比如 SNI 域名(通过 req.ssl_sni)、ALPN 协商值或者 SSL 版本号。一旦 SSL 握手完成,后续的检查应该放到 http-request 里去。还有一个常被忽略的点:tcp-request 里的 SSL SNI 检测需要客户端在 ClientHello 中携带 SNI 扩展,如果客户端不发送(比如某些老旧 SDK 或内网工具),HAProxy 会等待 inspect-delay 超时后才走默认路由,造成明显延迟。针对这种情况,可以设置一条短超时的回退规则,快速把无 SNI 的连接导向一个默认后端或直接拒绝。

后端健康检查与 tcp-request 的联动影响

HAProxy 的 tcp-request 规则可以基于后端状态做决策,但如果健康检查配置不当,会导致连接在 tcp-request 阶段就被卡住。例如你用 tcp-request content switch-mode 根据后端可用性切换模式,而某个后端恰好处于 DOWN 状态,HAProxy 在等待切换逻辑完成时会产生短暂的阻塞。更严重的是,如果后端健康检查本身因为网络问题超时,而你的 tcp-request 规则又依赖于这个后端的 ACL,整个前端连接处理都会被拖慢。务必确保健康检查的间隔和超时时间远小于客户端的 timeout connect,并且用 default_backend 配合 use_backend 来保证至少有一个可用的后端路径,避免 tcp-request 规则陷入死循环等待。

使用 tcp-request session 规则时的隐蔽延迟源

tcp-request session 规则在连接建立后、任何数据交换前执行,通常用于基于源 IP 的限速或黑白名单。这里的延迟主要来自两个方面:一是 stick-table 的查找开销,如果你在高并发下使用复杂的 stick-table 匹配(比如结合多个 key 或大容量表),每次查找可能耗时几毫秒,累积起来就很可观;二是外部文件或 API 调用的阻塞,虽然 HAProxy 支持 lua 脚本在 tcp-request 阶段运行,但任何阻塞操作都会直接拖慢连接处理。如果你用了 lua 做实时 IP 信誉查询,务必设置超时和异步回退机制,否则一个慢速的外部查询就能让所有新连接排队等待。

网络链路中的 SYN 包重传和中间设备干扰

有时延迟根本不在 HAProxy 自身,而在客户端到 HAProxy 之间的网络路径上。防火墙、负载均衡器或云服务商的 SDN 可能会对 SYN 包进行速率限制或深度包检测,导致三次握手迟迟完不成。HAProxy 的 tcp-request 延迟统计只会记录从连接进入内核到规则处理完毕的时间,如果 SYN 包被中间设备丢弃或延迟,HAProxy 根本感知不到这个连接,直到重传的 SYN 到达。排查这种问题需要在内核层面抓包,看 SYN 包的时间戳和重传间隔。如果确认是中间设备问题,可以尝试在 HAProxy 前端开启 TCP Fast Open,或者调整客户端侧的重传策略来绕过限制。

内核连接队列溢出引发的隐性排队

当 HAProxy 的 listen 队列(somaxconn 和 net.core.somaxconn)被填满时,新的 SYN 包会被内核丢弃或排队,客户端看到的就是连接超时或长时间等待。这种情况在高并发突发流量下尤其明显,表面上看是 tcp-request 处理慢,实际上连接还没到 HAProxy 用户态就已经在内核里排队了。解决办法是调大 somaxconn 和 HAProxy 配置里的 maxconn,同时监控 netstat -s 里的 listen queue overflow 计数器。另外,HAProxy 的 nbproc 或 nbthread 设置也会影响连接分发效率,多线程模式下如果某个线程的局部队列满了,连接会被内核重新分配到其他线程,这个过程也会引入微秒到毫秒级的延迟。

日志和统计信息里的延迟线索

HAProxy 的日志格式里有一个专门的 Tc 字段,记录的是从客户端连接建立到 HAProxy 收到完整请求的总时间,这个时间包含了 tcp-request 阶段的所有等待。如果你开启了 option tcplog,就能在日志里看到每个连接的 Tc 值。通过分析 Tc 的分布,可以快速定位是偶发延迟还是普遍延迟。如果 Tc 值普遍偏高,问题在配置或系统层面;如果只有少数连接 Tc 很高,很可能是特定客户端行为或网络抖动。结合 Tr(后端响应时间)和 Tt(会话总时间),就能把延迟精确归因到 tcp-request 阶段还是后端处理阶段。

实战排查步骤总结

遇到 tcp-request 延迟,按以下顺序排查最有效率:第一步,抓包确认三次握手是否正常完成,排除网络和内核队列问题;第二步,检查 HAProxy 配置中所有 tcp-request 规则,重点看 inspect-delay 和 timeout connect 的数值是否合理;第三步,用 strace 或 HAProxy 自带的 stats socket 观察连接在哪个阶段耗时最长;第四步,临时关闭 SSL 卸载或复杂 ACL,逐项排除规则影响;第五步,调整内核 TCP 参数和 HAProxy 性能调优项,如 tcp_fastopen、多线程绑定等。绝大多数 tcp-request 延迟都能通过这几步快速定位并解决,不需要盲目升级版本或更换硬件。