上一篇把 mux 与 stream 分层钉住。本篇进入协议表示轴:HTTP 在 HAProxy 内部以什么形态流动,改写落在哪,以及为何「先转成 H1 字节再处理」是一条已经否定的弯路。
本文不是 http-request set-header 菜谱——那是
network/56
的范围。这里只回答机制问题:规则引擎看见的是结构化消息还是原始套接字缓冲?H2
与 H1 在何处分叉、在何处汇合?相对
mode tcp,这条路径贵在哪里?
本文是「HAProxy / 数据面代理内核」系列第 5 篇(共 14 篇)。→ 系列目录
篇目 核心内容 第 4 篇 · Stream Mux 连接与流 第 5 篇 · HTX HTTP 内部表示与改写落点 第 6 篇 · ACL Rules 求值顺序与副作用 第 10 篇 · SSL TLS 与 HTTP 路径的正交轴
版本锚定:HAProxy 3.4.3;内部文档
doc/internals/api/htx-api.txt(HTX 动机与块模型);源码 tag v3.4.3 的src/htx.c、src/http_htx.c、src/mux_h1.c、src/mux_h2.c。不写未测的改写 QPS 对比。
一、HTX 是什么:版本无关的内部 HTTP 消息
内部 API
文档(htx-api.txt)把历史动机写得很直白:
- 早期:HTTP 消息以接近原始字节的方式躺在 buffer 里,解析状态另存;利于转发,不利于改写;且以 HTTP/1 为中心。
- H2 第一代弯路:把 H2 消息转成 H1 再走旧路径——两遍解析 + 中间转换,反向再转回去;甚至长期只能较好支持客户端侧 H2。
- HTX:用与线协议版本无关、自描述的内部表示替换旧模型;解析在 mux(h1/h2/…),处理在 stream/规则,二者分离。
工作定义:
- HTX 消息藏在 buffer 里:中间层(channel / stream-interface)仍操作 buffer,但 mux 与 stream 知道其中是 HTX。
- 消息由有序 block
组成:元数据(
htx_blk)与 payload;payload 与 block 元数据从缓冲区两端相向生长,中间留空隙——细节见内部文档图式。 - 块类型覆盖起始行、首部、数据、trailer 结束标记等;H2 没有起始行时,由 H2 mux 从伪首部合成 HTX 起始行(方法/路径或 authority、以及文档所述的版本字面等)。
flowchart LR
H1["HTTP/1 wire"] --> M1["mux_h1"]
H2["HTTP/2 wire"] --> M2["mux_h2"]
M1 --> HTX["HTX blocks"]
M2 --> HTX
HTX --> Rules["ACL / http-request rules"]
Rules --> HTX2["HTX possibly rewritten"]
HTX2 --> MOut["egress mux_h1/h2/..."]
MOut --> Wire2["Wire bytes"]
对排障:规则与改写看到的是 HTX 视图;「抓包看到的字节」是 mux 重新编码后的线格式。两者不一致时,先问改写是否发生,再问上游协议是否不同。
HTX 消息在缓冲中「看起来已满」的表象,来自其把结构塞进
buffer
的方式:中间层若仍按「普通字节缓冲还有空位」去推理,会误判流控。内部文档明确:从
HTX 视角,承载消息的 buffer
常表现为满——这是表示法后果,不是一定发生了应用层背压。读源码或调
tune 相关缓冲参数时,要把「HTX 容器满」与「TCP
窗口满」分开。
二、改写 / 重定向落点:HTX,而非「永远抠原始缓冲」
http-request / http-response
等动作(增删改首部、路径改写、redirect、reject…)在机制上针对已解析进
HTX
的消息(或基于其字段的分析结果)。这带来几条不变量:
| 不变量 | 含义 |
|---|---|
| 解析先于处理 | 畸形消息在 mux 阶段失败时,规则可能根本看不到「半包原文」 |
| 改写改的是内部块 | 随后由出口 mux 编码为 H1 或 H2 帧;不是在 socket 上 splice 一段历史字节再魔改 |
| 下游协议 ≠ 上游协议 | 同一 HTX 可被另一侧 mux 编码成不同线协议 |
| L4 路径可跳过 | mode tcp 不走这套 HTTP 分析;硬在 TCP
前端上套 HTTP 改写是配置错误,不是「HTX 丢了」 |
与「应用菜谱」的边界:本篇不解释某个
set-header 表达式怎么写;只强调——你在
Runtime/配置里改的 HTTP 语义,落点是 HTX
管道,成本也在这条管道上。
Body 与 trailer:分块 H1、H2 DATA/HEADERS 的 trailer 出现条件不同;mux 必须能处理「有无 trailer」两种世界(内部文档明示)。大 body 场景下,改写若触及 body,成本模型与「只改首部」完全不同——平台规范应默认限制 body 改写面。
三、HTTP/1 与 HTTP/2:在 mux 边界分叉,在 HTX 汇合
| 阶段 | HTTP/1 | HTTP/2 |
|---|---|---|
| 线格式 | 文本起始行 + 首部 + 可选分块 body | 二进制帧、流 ID、HPACK、窗口 |
| Mux 职责 | 扫描/状态机解析进 HTX;连接级 keep-alive / 空闲超时任务 | 帧复用、流状态、窗口;伪首部 → HTX 起始行 |
| 并发模型 | 同连接事务多串行(或受限 pipelining,代理侧通常谨慎) | 同连接多 stream |
| 汇合点 | HTX 块序列 | 同左 |
| 出口 | 再编码为 H1 或交给另一 mux | 再编码为 H2 或交给另一 mux |
分叉的运维含义:
- 失败粒度:H2 可重置单流;H1 常表现为连接级关闭或串扰下一条事务。
- 头压缩与表状态:H2 的 HPACK 状态在连接上;改写头部集合会影响编码大小,但不改变「规则看见的是 HTX 字段」这一层。
- 观测:不要用「H1 时代的一条 access log = 一个 FD」硬套 H2;应按 stream/请求关联(第 13 篇收束字段)。
HTTP/3/QUIC 若在构建与配置中启用,属于另一条传输分叉(UDP、独立监听与故障转移经济学);本篇不展开成熟度争论——第 14 篇选型会把它列为开放问题,避免在无环境声明下写「已与 H2 完全同构」。
四、成本:相对纯 L4 路径贵在哪里
不做本机 benchmark,只给可检验的成本项清单(每一项都应对应源码路径或配置开关,而不是形容词):
| 成本项 | L4 / mux_pt |
HTTP + HTX |
|---|---|---|
| 解析 | 基本不解析应用 | mux 必须解析到可建 HTX |
| 规则引擎 | 内容切换手段有限(端口/IP/TCP 检查等) | ACL 与 http 动作可深度读字段 |
| 改写 | 通常不改应用语义 | 首部/起始行改写 + 可能的 defrag |
| 缓冲形态 | 字节缓冲转发;或条件允许的快速转发 | HTX 块管理;文档描述的空洞与整理 |
| 协议转换 | 无 | H2↔︎H1 等在出口重新编码 |
| 超时集合 | 连接/隧道类为主 | 另加 http-request / keep-alive 等 |
工程判断(标明为判断):需要按 Host/路径/Cookie
做切换或改写时,付 HTX 税是买能力;纯透传 TLS
或二进制协议时,强行 mode http
是买噪音。
反向判断也成立:为了「统一观测」把一切抬到 HTTP,可能把 L4
故障伪装成应用层 400——第 13 篇会要求先看模式。
与第 1 篇争论表对齐:「一切经 HTTP」vs「保留瘦 TCP」没有全局赢家;赢家是故障域是否与业务语义同层。
五、与规则引擎、TLS 的衔接(预告)
- ACL / 规则(第 6 篇):匹配器读取的样本大量来自 HTX 字段或连接元数据;求值顺序与副作用(set / track / 限流)在 HTX 已就绪之后才有稳定语义。
- TLS(第 10 篇):终结发生在 xprt/bind,早于或围绕 HTTP 解析;透传则可能根本不进 HTX。SNI 选证书与「HTTP Host 选 backend」是不同层——混为一谈是经典误诊。
相对 Envoy:Envoy 用 codec 事件喂 HTTP filter;HAProxy 用 HTX 喂规则与分析器。两边都把「线协议细节」挡在可编程层之外,也都在半关闭与错误传播上把协议差异泄漏回运维面。
六、安全与一致性:IR 边界上的两类事故
HTX 作为 IR,还有两个常被配置菜谱忽略的机制后果:
- 规范化与重复首部:规则按 HTX
字段读写时,对「同名多首部」「大小写」「连接级 hop-by-hop
首部」的处理必须与出口编码一致。若在错误阶段删除/保留
Connection/Transfer-Encoding一类字段,故障会表现为上游断流或客户端诡异重试,而不是清晰的 ACL 未命中。 - 部分成功的改写:起始行已改、首部改到一半、或
redirect 与
use_backend同时存在时,分析器阶段顺序决定最终语义(第 6 篇展开)。本篇只强调:HTX 让「半改写」变得可表达——因此也变得需要严格的阶段纪律。
内部文档还描述 payload 区可能环绕、block 区整理(defrag)等行为:这意味着在极端缓冲压力下,改写不只是 CPU,还可能触发整理与二次拷贝。没有实测前,不得把「HTX 一定比旧 raw 路径更慢/更快」写成结论;只能写清可能多出来的工作项。
对平台约束的建议(工程判断):默认允许的动作集应以「首部与起始行」为主;body 检查走专门路径并设硬限制;禁止在热路径上做无界正则扫 body——否则线程模型篇的阻塞警告会以「某条 http-request 规则」的形式再现。
七、谱系、争论与开放问题
谱系:HTTP/1.1(RFC 7230 一系)把消息语义标准化;HTTP/2(RFC 7540 一系)把多路复用变成默认心理模型;HAProxy HTX(Christopher Faulet 等,内部 API 文档)是代理内核侧的工程回答——拒绝 H2→H1 字节桥,改用中立 IR。这与编译器「前端→IR→后端」同构:mux 是前端,规则/分析器是过遍,出口 mux 是后端。
争论:
- IR 的表达力 vs 转发延迟:块数组 + 可能的整理,是否在「只转发不改写」热路径上可被足够多的快速路径绕开。
- 是否让规则作者直接碰线格式:可得奇技,但会毁掉 H2/H1 对称并放大安全面——HTX 边界同时是安全边界。
开放问题:
- Body 级改写与零拷贝 / 快速转发旗标如何共存;平台默认策略应禁止哪些动作。
- HTX defrag 在高并发小对象改写下是否成为尾延迟主因——需第 13 篇可观测钩子,本篇不下数字结论。
- H3 进入同一 IR 时,传输失败(UDP 阻断)与 HTTP IR 失败如何在排障轴上切开(第 14 篇)。
工程间隙:内部 API 文档更新时间戳可能落后于 3.4.3
小版本;行为以 tag
内源码为准,文档负责讲清动机与块模型。阅读顺序:htx-api.txt
→ htx.c → http_htx.c → 某一侧
mux_h*.c。
八、参考资料
规范 / 官方文档(A)
- HAProxy 3.4.3, Configuration
Manual:
mode http、http-request/http-response动作族(机制落点,非菜谱全集)。 haproxy/haproxyv3.4.3(或同线文档树)内部:doc/internals/api/htx-api.txt— HTX 背景、块布局、H1/H2 起始行合成、trailer 规则。
源码(A)
- tag
v3.4.3:
src/htx.c、src/http_htx.c、src/mux_h1.c、src/mux_h2.c,及include/haproxy/htx.h等。
标准谱系(A)
- RFC 7230 一系(HTTP/1.1 消息);RFC 7540 / 9113 一系(HTTP/2)——用于理解伪首部与多路复用为何逼出 IR。
站内对照
实验台账
- 本篇无改写前后的吞吐数字;成本以机制清单给出。
九、小结
- HTX 是版本无关的内部 HTTP IR;mux 负责线协议↔︎HTX,规则负责处理。
- 改写/重定向落在 HTX;出口再编码——不要用抓包字节否定内部视图,也不要在 L4 模式上找 HTTP 动作。
- H1/H2 在 mux 分叉、在 HTX 汇合;失败粒度与并发模型仍泄漏到运维面。
- 相对 L4 的成本是解析、IR、规则与可能的再编码;用能力换税,或把故障域留在正确的层。
- IR 边界同时是安全与一致性边界;半改写与 hop-by-hop 首部需要阶段纪律,细节交给第 6 篇。
→ 上一篇:Stream 与 Mux · 系列目录 · 下一篇:ACL / 规则引擎求值
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【HAProxy 数据面】Stream 与 Mux:连接之上的协议分层
厘清 connection → session → stream 与 mux 的分层;对比 TCP pass-through 与 HTTP mux;说明超时挂在哪一层对象上,以及失败归因如何区分连接级与流级;源码目录级指向 stream 与 mux_*。
【HAProxy 数据面】数据面全景:从 Frontend 到 Seamless Reload 的代理内核
定位 HAProxy 相对 network/56 配置运维、Nginx 多进程与 Envoy Filter/xDS 的生态位;钉住请求路径、线程共享、协议表示、动态运维、失败归因五条坐标系,并给出 14 篇阅读路线。
HAProxy / 数据面代理内核:从 Frontend 到 Seamless Reload
补齐站内 HAProxy 工程单篇与 Nginx/Envoy 选型之间的数据面内核层:线程组、stream/mux/HTX、ACL 求值、stick-table、health-check、Runtime API 与 seamless reload,并以排障与选型收束。
【HAProxy 数据面】线程与调度:nbthread、Thread-group 与共享内存边界
钉住 HAProxy 单进程多线程模型:每线程事件循环、连接单线程服务;对照 nbproc 下 stick-table 不共享,以及 Nginx 多进程与 Envoy Main/Worker;说明 poller 线程上阻塞的软警告。