2254 字
约 7 分钟
22
S02-A25 · HTTP/1.1 的 pipelining:为什么几乎没人用

一个前端团队在"减少请求数"上做过尝试:既然 HTTP/1.1 支持 pipelining(在同一个连接上连续发多个请求,不等响应),那"一次发出十个图片请求"应该更快。实验结果是"更慢",而且"偶尔整个连接卡死"。

pipelining 是"协议允许、现实不用"的典型:它的设计假设(响应按序返回、无中间层干扰)在今天的网络上几乎不成立,这个"更糟"是结构性的:多开连接是"多几条队",而 pipelining 是"把三队并成一队"——**于是"慢的那个"从"影响自己"变成"影响全部".
而它的失败模式(队头阻塞)比"多开几个连接"更糟。

1. pipelining 想解决什么

问题 pipelining 的答案
每请求一个 RTT 连续发,省 RTT
连接受限(6 个/域) 一个连接跑多个请求

它的"理想收益"是"把 N 个 RTT 变成 1 个"——而前提是"响应顺序固定".

"代理不兼容"这条,是"pipelining 在生产里死掉"的直接原因:

// 三类"中间层"的处理
1 支持流水线:按序转发(少见)
2 拆成"独立请求"重发:顺序打乱(常见)
3 直接"丢弃/拒绝":连接卡死(偶发)
// 判据:链路上有"任何一跳"是 2 或 3,pipelining 就不能用

2. 为什么"更慢":三种失败

失败 机制
队头阻塞 第一个响应慢 → 后续全等
代理不兼容 中间层"打乱/丢弃"流水线
无法重试 失败时"哪些已执行"不明

第一项的"场景"很具体:第一个请求是个"2 秒的大文件",后面九个是"50ms 的小图"——**于是"九个小的"要等"两秒"才能开始".
第一项是"根本缺陷":pipelining 要求"响应按请求顺序返回"——于是"一个大响应"会把"后面的小响应"全堵住.

"队头阻塞"可以"用一次实验看见",而它比"读协议"直观:

// 三步"看见队头阻塞"(本地起一个慢接口)
1 同一连接上"先请求 /slow(2s)、再请求 /fast(10ms)"(pipelining)
2 观察 /fast 的响应时间:≈ 2s(被堵)
3 换成"两个连接各发一个":/fast ≈ 10ms
// 判据:同连接"串行等待",多连接"并行"——这就是"6 个连接"的理由

3. "六个连接"为什么更好

机制 效果
并行独立 一个慢不影响别的
失败隔离 一个连接坏,其余正常
普遍支持 所有客户端都这么干

第三项是"现实选择":**浏览器宁愿"开 6 个连接"**第三项的"代价"是"握手成本":6 个连接 = 6 次 TCP+TLS——于是它"用握手换并行",而在"h2/h3"时代这个代价被"多路复用"消掉了.
——因为"并行"比"省 RTT"更重要.

"域名分片"在 h2 时代是"反模式",而它曾是"标准优化":

// 两个时代的"最优"
HTTP/1.1:分片到 2–4 个域名(多开连接,绕过 6 连接限制)
HTTP/2  :合并到 1 个域名(同一连接多路复用,省握手)
// 判据:你的流量是"h2 为主"还是"1.1 为主"——协议决定"分还是合"

4. "想省 RTT"的三种现代方案

方案 机制
HTTP/2 多路复用 一个连接、多个流(仍有 TCP 层队头)
HTTP/3(QUIC) 流独立,解决 TCP 层队头
域名分片(反例) 多开连接(HTTP/1.1 时代的做法)

第二项的"关键差别"是"丢包的粒度":**h2 里"一个包丢了",所有流一起等(TCP 队头);h3 里只有"那一条流"等.
第二项是"真正的解法":QUIC 把"流"做在传输层——于是"一个流丢包"不阻塞别的流.

"协议演进"的这条线索能解释"为什么老优化会失效":

// 三代"减少 RTT"的手段
1.1 时代:多连接 + 域名分片 + 合并(粗但有效)
h2 时代:多路复用(连接数不再重要)+ 头部压缩
h3 时代:流独立(连"丢包"都不互相影响)
// 判据:优化手段要"跟着协议走",否则会"互相抵消"

5. "pipelining 还在吗"的三种现状

现状 说明
协议仍允许 RFC 未删除
客户端默认关闭 浏览器均不启用
服务器多半不启用 有的直接"不支持"

第二项是"事实标准":**"协议里有"不等于"能用"**第二项的"识别"很直接:看"客户端与服务端是否都启用"——浏览器默认关,于是"你在服务端开它"没有意义;而"以为开了就快了"是这类误判的典型.
——而"看到"RFC 支持"就去用"是典型误判.

"连接复用是否生效"看两个头,而不必"猜":

// 两个"证据"
1 Connection: keep-alive(1.1)或"复用同一连接"的 h2 流
2 抓包的 TCP 端口:多次请求"同一端口" → 复用生效
// 判据:每次请求都"新端口" → 没复用(握手成本吃掉收益)

6. 排错:五种"连接复用问题"

现象 成因 处理方向
开了 pipelining 更慢 队头阻塞 关掉
偶发连接卡死 代理不兼容 关掉/换 h2
请求数多但很快 连接复用生效 正常
每请求都新建连接 未开 keep-alive 开 keep-alive
首屏慢 关键路径串行 减少依赖层级

"请求合并"常常比"换协议"更有效,而它被"工程习惯"忽略:

// 三种"合并"的收益
1 首屏 10 个小 JS → 打包成 1 个(省 9 个 RTT)
2 10 个接口 → 1 个聚合接口(省 9 次往返)
3 小图 → 雪碧图/内联(省请求数)
// 判据:RTT 主导时,"少发请求"永远比"发得更聪明"有效

7. 边界:什么情况"多连接"反而不好

场景 为什么 做法
移动端(弱网) 多连接=多次握手 用 h2
大量小请求(> 50) 6 连接也排队 合并/h2
服务端"连接数受限" 每客户端 6 个太贵 用 h2/长连接
高延迟链路(跨洲) RTT 主导 h2/h3 + 合并

最后一条与"验收"有关的边界:"协议换了有没有用"要"按链路测"——"内网 0.2ms RTT"时,h2 的收益"几乎为零";"跨洲 200ms RTT"时,"省下的往返"才是大头——而"同一份优化"在这两种链路上"结论相反".
判断口径一句话:
"瓶颈是"连接数"还是"RTT""**。是连接数(请求太多、域太少),用 h2 多路复用;是 RTT(跨洲、弱网),先"减少请求"再谈协议;而 pipelining 在两个方向上都不是答案.

再补一个"教学"价值:pipelining 是"协议设计与现实妥协"的最好案例——它在"理想网络"里能省 RTT,在"有中间层、有慢请求、有重试"的现实里全线崩溃。所以"读 RFC 看到某个特性"时,应当多问一句"它为什么没被采用"——答案通常不是"大家不知道",而是"它的失败模式比收益更贵"。这条判断方式,对"技术选型"同样适用:一个长期没人用的特性,几乎总有原因,而那个原因往往就是"它在你这里也会失败"。

而"问一句为什么没人用",比"读一遍协议"更有收获。

这也解释了"技术选型"里最省力的一步:先看"它有没有被大规模使用",再看"它的理论优势"——因为"被使用"本身就是"经过现实检验"的证据,而"理论优势"往往只在"理想条件"下成立。 对一个要上线的东西,"被现实检验过"比"理论上更好"重要得多。

8. 自测

  1. pipelining 的理想收益是什么?它的前提是什么?
  2. "队头阻塞"为什么是"根本缺陷"?举一个"大响应堵住小响应"的场景。
  3. 为什么"浏览器宁愿开 6 个连接"?三个机制分别解决什么?
  4. HTTP/2 与 HTTP/3 在"队头阻塞"上有什么差别?
  5. "协议允许"与"能用"的差别是什么?为什么这条容易误判?
  6. 判断"该优化连接还是优化 RTT"的那个问题是什么?
S02-A25 · HTTP/1.1 的 pipelining:为什么几乎没人用
https://www.clxhxhhr.top/posts/6691/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。