Book Notes · 09 / 14 · 综合进阶
High Performance Browser Networking
精读笔记
Ilya Grigorik 把浏览器性能从「把图片压得更小」拉回到物理与协议:光要走完的距离、TCP 慢启动要爬的窗口、TLS 要付的握手、无线电台要醒的时间。书在 O'Reilly 出版,作者维持免费在线版 hpbn.co 。本笔记按七个主题转述,HTTP/3 用站内 RFC 补书出版后的演进。
全书骨架:性能是往返次数的税
Web 传输里大多数对象并不大。大带宽救得了一次下载电影,救不了「握手 + 证书 + 几十个小文件」这种被往返次数支配的页面。书从物理层的时延预算讲到应用层的并发模型,主线始终是:少付 RTT,少吵醒电台。
光纤里的光速大约是真空的三分之二。跨洋 RTT 有硬下限,加带宽买不到这段时间。
慢启动和握手都按 RTT 收费。页面由许多小对象组成时,窗口还没爬起来,传输已经结束。
电台休眠、唤醒、空口调度和尾能量,让「多开几个连接」在手机上变成耗电策略,而不只是吞吐策略。
域名分片、合并文件、多路复用,都是在给 TCP 的队头阻塞和连接税打补丁。QUIC 把补丁下沉了一层。
延迟与带宽:为什么延迟更致命
带宽是「单位时间能灌多少比特」,延迟是「第一个比特要多久才到」。浏览器里的一次页面加载,是一串依赖着的短传输,不是一条灌满的水管。
四段时延,只有一段能靠加带宽缩小
跨城、跨洋的传播时延是硬地板:纽约到伦敦量级的光纤往返,仅光速就要数十毫秒。把接入从十兆升到百兆,只能削掉发送时延,削不掉这段光程。页面若要来回好几次才能画出首屏,用户感知就被 RTT 锁死。
带宽时延积:管子里能装多少
带宽乘以往返时延,得到链路上尚未确认的数据量,也就是要把链路灌满所需要的在途字节。窗口小于这个积,发送方会先停下来等 ACK,链路空转。窗口大于它,又可能把中间缓冲撑爆。讨论 TCP 性能而不写 RTT,等于只谈水管口径、不谈水管长度。
最后一公里与对等互连
用户感知的延迟很少等于骨干网上的传播时延。接入网、运营商之间的绕行、缺少对等互连、以及服务器本身的排队,都会在光程之外再加几段。CDN 和任播的价值,首先是缩短这段「逻辑距离」,而不只是缓存字节。书把性能问题从「前端打包」拉开到「包要走哪条物理与商业路径」。
同一条「带宽很大」的接入,晚高峰排队一变,RTT 的中位数和长尾会一起坏掉。bufferbloat 把延迟藏进巨大缓冲:吞吐看起来还行,交互却像泡在糖浆里。测性能时要同时看吞吐、RTT 分布和丢包,而不是只截一张测速截图。依赖链上的每多一次重定向、一次阻塞脚本、一次跨域预检,都是在已经很贵的传播时延上再乘一笔。
读这一章时养成「先数 RTT」的习惯: DNS 一次、TCP 一次、TLS 一次或两次、首个请求一次、 阻塞脚本再串几次。 把这些加起来,往往已经超过用户愿意等的首屏。 优化顺序因此是:减往返、减依赖、再减字节。 把顺序倒过来,会做出体积很小却依然很慢的页面。
TCP 性能:慢启动与拥塞控制
TCP 先保证不把网络挤垮,再谈速度。三次握手、慢启动、拥塞避免和丢包恢复,每一段都按 RTT 计费。对 Web 这种短流,计费往往还没摊完,流就结束了。
握手是第一笔 RTT 税
数据不能在 SYN 交换完成前按普通流发送(后续的 TCP Fast Open 是例外,需要两端支持)。一次全新连接至少先花掉一个往返,才能开始应用字节。页面若对每个源、每个短对象都新建连接,这笔税会乘在对象个数上。保持连接、会话复用、减少源的个数,都是在少付这笔税。
慢启动:窗口按 RTT 翻倍往上爬
新连接的拥塞窗口从很小的初值长起,大约每个 RTT 翻一倍,直到触达慢启动阈值或出现丢包,再转入更温和的拥塞避免。初值后来在业界被抬高(常见讨论是从几个报文段升到十个量级),正是因为 Web 短流还没爬完窗口就已经传完。即使如此,跨洋高 RTT 加上证书链、重定向、多层依赖,仍会让首屏停在「窗口还小」的阶段。
队头、Nagle 与延迟 ACK
一条 TCP 连接上,前面的字节没被确认,后面的字节即使已到接收方应用也可能被卡住——这是传输层的队头阻塞,HTTP/2 在单连接上复用流时会再碰到它。Nagle 算法想把小写入攒成段,延迟
ACK 想把确认攒一攒,两者叠在一起可以让小请求多等出一个 RTT。Web 与 RPC 路径上通常关掉 Nagle(TCP_NODELAY
),并避免一次逻辑消息拆成多次小write。
丢包恢复同样按 RTT 计费。三次重复确认触发快重传,可以不必等超时;超时重传则往往伴随窗口骤降,短流可能直接在恢复里结束。中间盒若乱改缓冲或乱丢,应用看到的是「偶发变慢」,抓包才能分辨是慢启动没爬完、是丢包恢复,还是排队。书反复让读者用时间轴而不是用「网速」两个字解释瀑布图上的空隙。
TLS 的开销与优化
加密不是「多几次 CPU 运算」那么简单。对短连接,TLS 更贵的是额外往返、证书链验证和会话无法复用。书按握手时序来算账,而不是按算法名称来恐吓。
握手叠在 TCP 之上
先完成 TCP 握手,再交换密钥与证书,应用数据才能在对称密钥下发送。TLS 1.2 时代,完整握手常常再要一到两个往返;会话恢复(session ID 或 ticket)可以把后续连接减到更少的往返。证书链过长、必须在线查吊销信息、或密钥交换选了昂贵参数,都会在这条时序上加毛刺。CPU 上的对称加密,对现代硬件通常不是第一瓶颈。
把 TLS 1.3 读进同一张账单
书的主体写于 TLS 1.3 全面铺开之前,但成本模型可以直接接上去:1.3 把常见握手收成一个往返,并提供 0-RTT 恢复(带重放风险,只适合幂等请求)。前向保密成为常态,旧的静态 RSA 密钥交换退出舞台。优化清单变成:升级协议版本、打开会话票证、裁短证书链、启用 OCSP stapling、在 HTTP 层坚持长连接,让握手摊到许多请求上。
证书验证还可能再引出一次联网:客户端若要在线问吊销状态,等于在握手里嵌套一次 HTTP。把应答钉在服务器自己的握手消息里(stapling),是「把计算挪出客户关键路径」的典型手法。密钥与证书选得过大,会让握手的 CPU 在移动设备上重新变成可见延迟;选得过旧,又会丢掉前向保密。性能和密码学在这里必须坐在同一张表上,不能各写各的清单。
部署清单可以写得很短: 新连接走 TLS 1.3,旧会话走票证恢复; 证书链只留必要的中间证书, stapling 打开; HTTP 层坚持长连接,让握手摊薄; 0-RTT 只用于可重放也无害的请求。 其余的密码套件争论, 很少比「又做了一次完整握手」更影响用户。
无线与移动:能耗和延迟是一对状态机
桌面以太网几乎总是「醒着」。蜂窝电台不是。移动网络用一套无线电资源控制状态,在耗电、延迟和空口容量之间折中。浏览器若按桌面习惯轮询,会把电池和首包时延一起烧掉。
Wi-Fi 已经不是免费的低延迟
共享介质要先听再发,冲突规避、速率自适应、节能休眠都会把一帧的发送变成一次调度。同一热点下的邻居、微波炉与雷达残留、漫游切换,都会让 RTT 的方差远大于有线。对应用来说,这意味着超时和重试策略不能按机房里的几个毫秒来写。
蜂窝:唤醒、传输、尾巴
典型模型分三拍。空闲时电台几乎不耗电,但要发数据得先向网络申请专用资源,这段「提升」可能加上数百毫秒量级的延迟。传输阶段功耗最高。传完后电台不会立刻睡死,而会在较高功耗状态逗留一段时间(尾时间),以防马上又有数据。许多小请求若刚好错过彼此、每次都单独唤醒并各自拖一条尾巴,能量会按次数而不是按字节收费。
给应用的推论
合并请求、服务端推送或长连接、避免每几秒一次的轮询、在前台一次性把近期要用的数据拉完,都是在减少「唤醒次数 ×(提升 + 尾巴)」。三代、四代的具体状态名不同,结构没变:无线是带滞后的状态机。bufferbloat 在移动网上同样存在——基站侧巨大缓冲会把 RTT 拉成秒级,实时音视频会先死掉。
信号差时,链路速率会主动降下来换可靠,发送同一对象的时间变长,电台醒着的时间也变长。应用若在这种时候还按固定间隔重试,会把自己锁在高功耗态上。可见性策略(页面不可见就停轮询)、指数退避、以及把非紧急同步留到充电或 Wi-Fi,都是无线章对产品经理也能讲清的句子。不要用实验室里一根满格的网线,给手机客户端定超时。
从 HTTP/1.1 技巧到 HTTP/2 多路复用
应用层花了十年给 TCP 打补丁:连得不够多就开多个连接,对象太碎就合并,头部太大就砍 Cookie。HTTP/2 把其中最正经的那条——一条连接上同时跑多个请求——收进协议。
HTTP/1.1 的工具箱与它的副作用
这些技巧在书中被当成「在错误层做对的事」:它们有效,是因为传输层一次只好好服务一条字节流。缓存、压缩、CDN 则仍是正道——少传字节、就近传字节,任何版本的 HTTP 都吃这一套。
HTTP/2:流、帧、头部压缩
HTTP/2 把会话切成二进制帧,用流标识把多个请求/响应在同一条连接上交织。小图片不必再等大文件的字节全部离开窗口,域名分片和雪碧图的动机被削弱。HPACK 压缩重复的头部,长 Cookie 不再每个请求原价重传。优先级与服务器推送写在协议里,但推送后来被证明难用对:推错了会浪费带宽,也和缓存状态打架。
单连接的新脆弱点
所有流共享拥塞窗口和丢包恢复。一次丢包会卡住连接上的每一条流——这就是「TCP 队头阻塞」,也是下一章 QUIC 要搬走的东西。运维上还要避免再对 HTTP/2 做过度分片:多条连接意味着多个慢启动,正好抵消多路复用。调试时抓到的不再是纯文本,需要能解帧的工具。
头部压缩维护的是连接级对照表。一条连接上的后续请求越「像」,压缩比越高;连接一切,表就没了。这进一步惩罚短连接。优先级若配置得当,可以让关键 CSS 压过巨型图片;配得不好,只会在同一条拥塞窗口里制造另一种饥饿。书的态度很明确:先把连接复用做对,再谈优先级艺术。
迁移检查表: 是否还在做无意义的域名分片? 是否还在为了减少请求而把不该绑在一起的资源捆成一个大包? 服务器是否只开了一条 HTTP/2 连接却又在前面加了一层会拆连接的代理? 瀑布图上的空隙,是握手、慢启动,还是应用自己的依赖链? 能回答这四个问题,HTTP 两章就算读过,而不只是记住了「多路复用」四个字。
WebRTC:实时传输是另一份合同
页面加载可以等;通话不行。WebRTC 要在浏览器里做媒体采集、NAT 穿越、加密和拥塞控制,目标函数从「尽快传完」换成「按时送到,迟到就丢掉」。
先打洞,再传媒体
两端往往都在 NAT 后面。ICE 收集候选地址,用 STUN 发现经过 NAT 映射后的公网地址,走不通再改道 TURN 中继。连通性检查成功后,媒体走 SRTP,密钥由 DTLS 导出。数据通道则把 SCTP 架在 DTLS 上,给应用一条可不可靠、可有序可乱序的消息管线。信令本身(谁邀请谁)不在 WebRTC 里规定,站点要自己选一条信令通道。
延迟预算不允许「可靠地重传一切」
音视频帧有截止时间。TCP 式的重传会把后续帧一起拖晚,听起来就是卡顿。实时拥塞控制更愿意降码率、丢旧帧、保时延,而不是把窗口减半再慢慢爬。这也是它走 UDP 家族的原因:应用必须能自己决定「丢还是重传」。回声消除、抖动缓冲、编解码选型(书中讨论当时的常见编解码)都在同一条延迟预算里抢时间。
和页面加载的性能观不要混用
HTTP 优化追求完整交付和缓存命中;WebRTC 优化追求端到端时延和连续性。同一页面上两者共存时(边看直播边拉资料),不要让大文件下载把实时流的队列挤爆。测实时质量要用往返、抖动、丢包和主观卡顿,而不是只看 Mbps。
抖动缓冲用一点点延迟换平滑:来早的包等一等,来晚的包还有机会赶上播放时间线。缓冲太浅,卡顿;太深,通话像对讲机。它是实时栈里少数「主动加延迟」的部件,必须和无线章的长尾 RTT 一起调。数据通道若选了可靠有序,行为会重新靠近 TCP,只适合文件或信令,不适合已经过期的位置更新。
WebRTC 与前面几章共用同一张物理账单: 传播时延仍是地板,无线尾巴仍会在静音后突然咬人, NAT 后的绕行可能比媒体编码更贵。 差别只是目标函数。 页面加载可以牺牲这一帧换完整文件; 通话牺牲的是「现在这帧」,去保下一帧还来得及。
注记:用 HTTP/3 与 QUIC 补全书出版之后
HPBN 把故事讲到 HTTP/2 与 WebRTC。HTTP/3 把传输换成 QUIC,目标正是书里反复指出、却无法在 TCP 上拆掉的那块:传输层队头阻塞,以及把加密和握手合成一次往返。
QUIC 把书中的几张账单并到一层
QUIC 跑在 UDP 上,自己做流复用、丢失恢复和拥塞控制。一条流丢包,不必卡住同会话里的其它流——这是对 FIG.5 里「TCP 队头仍在」的直接回答。加密是强制的,握手与 TLS 1.3 集成,常见情况一个往返就能带上应用数据,恢复路径还可以 0-RTT。连接用连接标识而不是四元组粘住,迁移网络(蜂窝切 Wi-Fi)时不必重来一整套 TCP+TLS。
阅读顺序建议
先读完本书的延迟、TCP、TLS 与 HTTP/2 四章,再进站内的 QUIC 与 HTTP/3 精读。会发现 HTTP/3 几乎不是新的应用语义,而是把 HPBN 已经算清楚的税,改到一个能同时付「安全 + 多路 + 迁移」的运输层去。无线章里的唤醒与尾巴,并没有因为换成 UDP 而消失。
部署上还要面对中间盒对 UDP 的偏见:部分网络把非常见 UDP 端口丢弃或限速,浏览器会回落到 HTTP/2。性能对比必须按「最终协商到的协议」分组,不能把所有 HTTPS 混成一张图。QPACK 为了不让头部压缩再引入流间队头,把对照表的更新设计得比 HPACK 更小心——又一次证明:队头会在你以为已经消灭它的那一层重新长出来。
把这本书当作「往返税手册」来合上: 延迟章解释税为什么存在, TCP 与 TLS 章解释税在哪两层征收, 无线章解释手机为什么还要多付唤醒, HTTP 与 WebRTC 章解释应用如何少缴或改签合同, HTTP/3 注记说明运输层后来把几张税单合并了。 官方免费版仍在 hpbn.co, 适合对照抓包和瀑布图精读原文图示。 读纸书或在线版时,把每一章的图当成「税单」, 用本页的站内 RFC 与无线专题把出版之后的运输层补齐。