HTTP 的瓶颈不在带宽,在等待:一条连接上一次只有一个请求在途,首部每次重发还不压缩。 管线化本来想救,结果只是把排队挪了个位置。
上一节说了 HTTP 做不了什么,这一节把「为什么慢」拆成三条,逐条量。
一、一条连接,一次一个
2.7.1 的持久连接省掉了每次建连的开销,但它没允许并行——同一条连接上的请求仍然是一个发出去、等回来、 再发下一个。 浏览器只好靠「多开几条连接」来凑并行,但每条连接都要单独握手、单独慢启动, 而且服务端一般限制每域名 6 条左右。这个数字是十多年前定的,一直没怎么变。 于是就有了一个奇怪的局面:带宽早就涨了几十倍,可页面的加载时间没有同比例变短, 因为卡住的地方不是管子粗细,是队伍太长。
二、首部是纯文本,还得重复付
HTTP/1.1 的首部是一行行明文,一点不压缩。 Demo 里一条普通浏览器请求的首部 232 字节,正文只有 2 字节,首部是正文的 116 倍。 这本身不算问题——问题是同一个站点的几十个请求,发出去的首部八九成是一样的:同一个 Host、 同一个 User-Agent、同一串 Accept。 一百个资源,光首部就是二十多 KB,而真正的图片可能才几百 KB。 这些字节每一份都要占用带宽、都要等一个往返,纯属重复。
三、管线化只是把排队挪了个位置
2.7.2 讲过管线化:不等前一个响应就先发后一个。 听上去能救,但它有个硬规定——响应必须按请求的顺序回来。 Demo 里六个请求一次全发出去,第一个故意慢 200 毫秒,其余各 50 毫秒。 完成时刻依次是 201.0、251.7、302.5、353.3、404.3、455.2 毫秒——一条漂亮的等差数列, 说明它们真的是一个接一个在排队。 第 2 个请求自己只要 50 毫秒,却等到 252 毫秒才回来。 这就是队头阻塞:前面那个慢的没回来,后面的就算已经做好了也不许先走。 同样的六个请求,改成六条连接各走各的,203.4 毫秒全部完成,比排队少付 251.8 毫秒。 所以真凶不是「连接少」,是「一条连接里只能排一队」。
四、动手 Demo:六个请求,一条队伍,一条等差数列
本机服务端,第一个路径故意慢 200 毫秒,其余各 50 毫秒。 先把六个请求一次全发到同一条连接上(流水线),量每个响应回来的时刻; 再用六条连接各发一个,量总耗时。
# 一条连接:六个请求一次全发
for p in PATHS:
s.sendall(req(p))
for p in PATHS:
recv_response(s) # 只能按顺序收
arrive.append(now())
实跑输出:
--------------------------------------------------------------------
队头阻塞:一个慢请求把后面全堵住
--------------------------------------------------------------------
六个请求一次全发出去(流水线),第一个故意慢 200 毫秒,其余各 50 毫秒。
请求 路径 完成时刻(毫秒)
1 /slow 200.6
2 /q2 251.6
3 /q3 302.5
4 /q4 353.5
5 /q5 404.4
6 /q6 455.2
一条连接收完六个 455.2 毫秒
六条连接收完六个 203.2 毫秒
排队多付的时间 252.0 毫秒
注意第 2 个请求:它自己只要 50 毫秒,却等到 252 毫秒才回来。
它在慢请求后面,只能干等——这就是队头阻塞(head-of-line blocking)。
六个完成时刻 201.0/251.7/302.5/353.3/404.3/455.2, 间隔稳定在 50 毫秒左右——后面的请求在等前面的,不是在等自己。 一条连接收完六个 455.2 毫秒,六条连接 203.4 毫秒,排队多付 251.8 毫秒。 注意六条连接也没快到 50 毫秒,因为最慢那个本身就是 200 毫秒,这是下限。 第 2 个请求是最冤的:它 50 毫秒就做完了,却要等到 252 毫秒才被送出去。 它的时间花在「等别人」,不在「自己算」。 到这里三条瓶颈就齐了:一条连接只能排一队、首部不压缩、排了队还不能插队。 后面三个协议各治其中一两条。
30 秒实验
- 把慢请求挪到最后一位,看六个完成时刻会变成什么样。
- 把六个路径都改成 50 毫秒,比较一条连接和六条连接的差距还剩多少。
- 想一想:为什么服务端不愿意让浏览器无限多开连接?
常见误区
- 「持久连接就等于并行了」——省的是建连,不是排队。
- 「管线化解决了队头阻塞」——它只允许提前发请求,响应仍须按序回。
- 「多开连接就能解决」——浏览器每域名只开几条,而且服务端也扛不住。
自测三问(点开对答案)
1. HTTP/1.1 的三个瓶颈分别是什么? —— 一条连接一次一个请求、首部不压缩且重复、响应必须按序回导致队头阻塞。 2. 为什么第 2 个请求要等 252 毫秒? —— 它在慢请求后面,响应必须按序返回,前面的没走完它不能走。 3. 管线化和多路复用的根本区别是什么? —— 管线化只是提前发请求,响应仍按序;多路复用给每个请求编号,响应可以乱序交错。瓶颈量清楚了。谷歌的答案是:在这条连接里塞进很多条「流」,各走各的。
- ☐ (本节完,下一节 9.2.2 SPDY的设计与功能)