Mogul 1995 — 为什么 HTTP 必须改成"一根连接复用多次请求"
是什么
Mogul 1995 是一篇论证报告,回答的问题是:HTTP/1.0 的”每次请求新开一条 TCP 连接、用完就关”——这种打法到底有多浪费?该怎么改?
日常类比:你去便利店买 11 样东西。HTTP/1.0 的做法相当于—— 每买一样,你都从家里出发、走到店、付款、走回家、关门。买 11 样要往返 11 次。 持久连接(这篇论文力推的方案)相当于:进店一次,11 样一起买完,再回家。
论文给出实测数据 + 推理两条线,证明”复用连接”在大多数场景能把页面加载时间砍 2-4 倍,同时减轻服务器负担。这套论据后来直接进了 HTTP/1.1(RFC 2068, 1997),成为今天每一次网页加载的默认行为。
为什么重要
不理解这篇论文,下面的事都解释不通:
- 为什么 HTTP/1.1 的
Connection: keep-alive是默认值,而 HTTP/1.0 要显式打开 - 为什么”加载一张图”的瓶颈常常不是带宽而是 RTT(往返时延)
- 为什么 HTTP/2 的多路复用是延着同一条思路再走一步——既然连接这么贵,干脆一根连接里塞所有请求
- 为什么早期 Web 服务器经常死于 TIME_WAIT 占满 socket 表(连接关了,OS 还要保留 1-4 分钟)
这是 Web 性能史上最关键的一次默认值翻转。
核心要点
论文的论据可拆成 四笔账,每笔都让旧方案吃亏:
-
握手账(RTT 税):建一条 TCP 连接要先做 3 次握手(SYN / SYN-ACK / ACK),至少花掉 1 个 RTT 才能开始发 HTTP 请求。一个页面 11 个对象就是 11 个 RTT 全花在”打招呼”上。
-
慢启动账:TCP 拥塞窗口(cwnd)每次新连接都从 1-2 个数据包开始,指数增长。小对象在窗口长大前就传完了——你永远摸不到带宽上限。
-
TIME_WAIT 账:TCP 关闭一条连接后,操作系统要把这个 (源 IP, 源端口, 目的 IP, 目的端口) 四元组保留 2*MSL(约 60-240 秒)防止旧包混进新连接。繁忙服务器的 socket 表分分钟被这堆”已经关了但还占着位”的连接挤爆。
-
包头账:每条新连接都要发 SYN/FIN 这些纯控制包,没运业务数据——同样的 HTTP 字节,旧方案要多发一堆 TCP 包头。
改造方案:连接发完一个响应先别关,约定好(HTTP header Connection: keep-alive)继续接下一个请求。进一步,可以流水线(pipeline)——客户端不等响应回来,连发 req2、req3,服务器按顺序回——把每请求的 RTT 也省掉。
实践案例
案例 1:一张页面 11 个对象,时间花哪了
页面 = 1 个 HTML + 10 张图,假设 RTT = 100ms,每对象传输只需 50ms。
HTTP/1.0(一请求一连接):
- 11 次握手:11 × 100ms = 1100ms
- 11 次传输:11 × 50ms = 550ms
- 总共 ≈ 1650ms
持久连接 + 流水线:
- 1 次握手:100ms
- 11 次传输首尾相接:~50ms × 11 = 550ms(实际更少,因为 cwnd 已经长大)
- 总共 ≈ 650ms
这就是 2-4 倍提速从哪来的。RTT 越大(移动网络 / 跨海光缆),节省越夸张。
案例 2:服务器视角的 TIME_WAIT 灾难
1995 年常见的 Apache 服务器在高并发下经常报错”address already in use”或”too many open files”。一查,绝大多数 socket 是 TIME_WAIT 状态——已经关了的连接还要再占 60-240 秒。
旧方案下,每秒 1000 请求 ≈ 每秒新建 1000 连接 ≈ 同一时刻有 60000-240000 个 TIME_WAIT。Linux 默认 socket 表撑不住。
持久连接让一个客户端一段时间只占一条连接,TIME_WAIT 数量直接降一两个数量级。
案例 3:今天的浏览器还在受这篇论文影响
打开 Chrome 的 DevTools → Network 面板 → 看 Connection ID 这一列。你会发现:
- 同一域名下的多个请求复用同一个 Connection ID——这是持久连接
- HTTP/2 站点甚至多个并发请求都共用一个连接——这是把这篇论文的思想推到极致(多路复用)
- 当连接 idle 几十秒后浏览器才主动关——这是”keep-alive 超时策略”,论文里讨论过的取舍
案例 4:API 客户端为什么要复用 HTTP client
写过 Python 调外部 API 的人都见过这种代码:
# 错误做法:每次请求新 clientfor url in urls: requests.get(url) # 每次都做 TCP+TLS 握手
# 正确做法:复用 sessionsession = requests.Session()for url in urls: session.get(url) # 复用连接池第一种写法在循环量大时慢得离谱。原因正是这篇论文 1995 年就讲过的——握手 + 慢启动 + TIME_WAIT 三笔账,在每次新建连接时都重交。requests.Session 在底层维护持久连接池(keep-alive),把这三笔账摊销到一次。
这个坑跟 1995 年浏览器遇到的坑完全一致——只是换成了 Python 调 REST API。论文的论据 30 年没过期。
踩过的坑
-
流水线(pipelining)的队头阻塞:req1 慢,req2/req3 必须排队等。这是 HTTP/1.1 流水线没普及的根本原因,也是 HTTP/2 必须用多路复用(multiplexing,多个 stream 在一条连接里交错)才彻底解决的痛点。
-
代理转发 keep-alive 头的 bug:HTTP/1.0 时代的旧代理把
Connection: keep-alive原封不动转给上游,但它自己又不持久连接——客户端以为打开了,代理那边断了,连接卡死。HTTP/1.1 引入Connection是 hop-by-hop(逐跳)头,必须删掉再转,专门解决这个坑。 -
idle 超时的两难:保留连接太久 → 服务器资源耗尽;太短 → 又要新建。今天浏览器一般是 5-10 分钟,服务器 60 秒,且双方约定”先谁谁关”。这个调参从 1995 年讨论到今天还没完美方案。
-
HTTPS 让 RTT 税更狠:TLS 握手又要 1-2 个 RTT。这篇论文写时还没普及 HTTPS,但论据对 HTTPS 加倍成立——这也是 HTTP/2 + TLS 1.3 + 0-RTT 优化的合理性来源。
适用 vs 不适用场景
适用:
- 任何”一个会话里要拉多个小对象”的场景(网页 / API 客户端 / RPC)
- RTT 远大于传输时间的场景(移动网络 / 跨数据中心 / 卫星链路)
- 服务器资源受限(连接数、文件描述符、内存)的场景
不适用:
- 单个超大对象传输(一次握手摊销已经很低)
- 客户端只发一个请求就消失(重连机会几乎为零)
- 极端短命的连接(CDN 边缘节点对完全陌生 IP 的首次握手必然要付一次 RTT)
历史小故事(可跳过)
- 1989-1991:Tim Berners-Lee 在 CERN 设计 HTTP/0.9,简单到只有 GET,每次新连接,断开就完事——彼时 Web 上几乎只有纯文本。
- 1992-1995:Mosaic 浏览器加了
<img>标签,一张网页十几个对象。原本”轻量”的 HTTP 突然要每秒开几十条 TCP 连接。性能崩溃。 - 1995 年 5 月:Mogul(DEC WRL)发表本文,算细账 + 给数据 + 给方案。同年 Padmanabhan 与 Mogul 在 SIGCOMM 95 上发表配套测量论文。
- 1997:HTTP/1.1(RFC 2068)把持久连接定为默认。Web 加载速度肉眼可见提升一档。
- 2015:HTTP/2(RFC 7540)把”一连接复用多请求”推到极致——用 stream 多路复用彻底解决队头阻塞。
这是一篇 20 多页的工程论证报告改写整个 Web 性能史的范例。
学到什么
- 协议默认值的力量:把 keep-alive 从”opt-in”改成”opt-out”,全球带宽利用率立刻翻倍。默认值即政策。
- 算清账才有说服力:论文不是凭”应该更快”立论,是把每条连接的握手 / 慢启动 / TIME_WAIT / 包头税逐项加起来给数据。这是工程改进文章的标准范式。
- TCP 与 HTTP 的耦合:HTTP 性能问题的根因常常在 TCP 层(slow start / RTT)。跨层思考才看得清。
- 优化空间不在算法在场景:算法没换,只是少握几次手——但场景对了,收益是数量级。
延伸阅读
- 论文 PDF:The Case for Persistent Connections in HTTP(Mogul 1995, DEC WRL 95/4)
- 配套测量:Padmanabhan & Mogul, “Improving HTTP Latency”, SIGCOMM 1995
- HTTP/1.1 标准:RFC 2068(1997)→ RFC 2616(1999)
- 进一步:HTTP/2 RFC 7540 看多路复用如何把”一连接复用”推到极致
关联
- http-2 —— 把”一连接复用多请求”推到极致:多路复用 + 二进制帧
- tcp —— 持久连接节省的握手 / 慢启动税都来自 TCP 的工作原理
- tcp-vegas-1995 —— 同年关注 TCP 性能的另一支线(拥塞控制改进)
- fielding-rest-2000 —— REST 论文,理论化 Web 设计原则;持久连接是其中一项工程后果
- akamai-2002 —— CDN 进一步把”减少握手”做到地理层面(边缘节点离用户近 → RTT 小)
- websocket-rfc-6455 —— 同样基于”一根连接长寿”的思路,但允许双向推送
反向链接
- akamai-2002 —— Akamai 2002 — 把网站搬到离用户 10 毫秒的地方
- http-2 —— HTTP/2 — 把 HTTP 从文本协议改造成二进制多路复用
- krishnamurthy-1999-http11 —— Krishnamurthy 1999 — HTTP/1.0 到 1.1 究竟改了什么
- padmanabhan-1995-http-latency —— Padmanabhan-Mogul 1995 — 把 HTTP 三种提速方案放一起跑,看谁真的快
- tcp —— TCP — 在不可靠的 IP 上凿出一条 reliable 字节流
- tcp-vegas-1995 —— TCP Vegas 1995 — 不等丢包,靠 RTT 早一步看见拥塞
- websocket-rfc-6455 —— WebSocket RFC 6455 — 让浏览器和服务器开一条不挂断的双向电话