一、Web 缓存(代理服务器)

1.1 机制

在客户机和源服务器之间放一台缓存服务器(proxy server)。浏览器把所有请求发给它:

   浏览器 ──①请求──→ ┌──────────┐ ──③若未命中,向源请求──→ 源服务器
          ←④响应──  │ Web 缓存  │ ←──────────────────────
                    └──────────┘
                     ②若命中,直接返回

缓存既是服务器(对浏览器),也是客户机(对源服务器)——这个双重身份是理解代理的关键。

1.2 三重收益

  1. 降低响应时延:命中时不用跨越广域网
  2. 降低接入链路带宽消耗(对机构最实在的收益:省钱)
  3. 让小内容提供商也能有效交付内容——不需要自己建全球基础设施

1.3 定量分析(经典必考题)

场景:一所学校,内部 1 Gbps 局域网,通过一条 15 Mbps 的接入链路连到互联网。

  • 平均对象大小 1 Mbit
  • 请求速率 15 个/秒
  • 从机构路由器到源服务器的平均往返时延(含互联网排队)= 2 秒

无缓存时:

局域网流量强度 = 15 req/s × 1 Mbit ÷ 1000 Mbps = 0.015   ← 完全没压力
接入链路流量强度 = 15 × 1 Mbit ÷ 15 Mbps = 1.0          ← 灾难 ⚠️

流量强度 = 1.0 意味着什么?回顾第 3 讲的排队曲线:排队时延趋于无穷。实际表现是分钟级的响应时间。

方案 A:把接入链路升级到 100 Mbps

接入链路流量强度 = 15 ÷ 100 = 0.15  ← 排队时延可忽略
总时延 ≈ 2 s(互联网)+ 微秒级

有效,但很贵——接入链路是按月付费的固定成本。

方案 B:装一台缓存服务器,命中率 0.4

40% 的请求由缓存满足,时延约 10 ms
60% 的请求要走接入链路:

接入链路流量强度 = 0.6 × 15 × 1 Mbit ÷ 15 Mbps = 0.6
→ 排队时延约几十到几百毫秒(取 0.5 s 估算)

平均总时延 = 0.4 × 0.01 s + 0.6 × (2 + 0.5) s
           = 0.004 + 1.5 = 1.504 s

📌 结论:缓存方案的时延(约 1.5 s)比升级链路(约 2 s)还要好,而成本是一台服务器的一次性投入。

🎯 这就是缓存的力量:它不是「稍微优化一点」,而是改变了流量强度所处的区间——从 1.0 的爆炸区拉回到 0.6 的可用区。

1.4 Cache-Control:现代缓存的控制手段

指令 语义
max-age=3600 该副本在 3600 秒内是新鲜的,期间不必发请求
no-cache ⚠️ 不是「不缓存」,而是「每次使用前必须向服务器验证」
no-store 真正的「不许缓存」,用于敏感数据
private 只允许浏览器缓存,不允许共享缓存(CDN、代理)缓存
public 允许任何缓存存储
immutable 内容永不改变,即使刷新也不必重新验证
stale-while-revalidate=60 过期后仍可先用旧的,同时后台异步更新

⚠️ no-cache vs no-store 是面试高频陷阱no-cache 允许存,但每次用之前要问一句(发一个条件 GET);no-store 连存都不许。

与条件 GET 的关系(第 5 讲):

有 max-age 且未过期  →  0 个 RTT,直接用本地副本  ⭐ 最优
过期 / no-cache      →  1 个 RTT,条件 GET,可能收到 304(省实体体,不省 RTT)
无缓存               →  1 个 RTT + 完整传输

二、内容分发网络(CDN)

2.1 问题:如何把视频送到全球十亿用户

Netflix 高峰时段占北美下行流量的相当比例。假设用一个巨型数据中心:

  • 单点故障
  • 跨洲链路成为瓶颈,且要为峰值付费
  • 时延不可接受(跨洲传播时延物理上消不掉)
  • ❌ 同一个视频被反复通过同一条跨洋链路传送

解决办法只有一个:把内容搬到用户附近。

2.2 两种部署哲学

深入部署(Enter Deep) 邀请做客(Bring Home)
代表 Akamai Limelight 及多数新兴 CDN
做法 把服务器集群直接放进接入 ISP 的机房 在少量 IXP 建大型集群
集群数 数千到十万+ 数十到数百
优势 时延最低(离用户最近) 管理简单、成本低
劣势 管理与维护极其复杂 时延略高

2.3 CDN 如何把你导向最近的节点

⭐ **答案:通过 DNS。**这是 DNS 最重要的现代用途之一(第 7 讲会补齐 DNS 细节)。

① 用户请求  http://video.netflix.com/xyz
② 浏览器向本地 DNS 服务器查询 video.netflix.com
③ Netflix 的权威 DNS 服务器不直接返回 IP,
   而是返回一个 CNAME:  a1105.kxcdn.com
④ 本地 DNS 转而查询 CDN 的权威 DNS
⑤ ⭐ CDN 的 DNS 看到「请求来自哪个本地 DNS 服务器」,
   据此判断用户大致位置,返回一个「近」的集群 IP
⑥ 浏览器与该集群建立 TCP 连接,取内容

集群选择的依据

  • 地理距离(基于 IP 地理库)
  • 实时网络测量:定期探测各 LDNS 到各集群的时延与丢包
  • 集群当前负载
  • 内容是否已在该集群缓存

⚠️ 一个经典缺陷:CDN 看到的是本地 DNS 服务器的位置,不是用户的位置。如果你用 8.8.8.8(Google Public DNS),CDN 可能把你导到离 Google DNS 节点近、而离你远的集群。补救方案是 EDNS Client Subnet (ECS) 扩展,让 LDNS 把用户所在网段一并转发——但这本身又是一个隐私问题。

2.4 Netflix 的案例

① 用户在 Netflix 的 AWS 上完成登录、浏览、鉴权(控制面)
② Netflix 返回一个 manifest 文件,列出各清晰度版本的 URL 和所在 CDN
③ 客户端播放器直接向 Open Connect 边缘节点拉取视频块(数据面)
④ 播放器根据实测带宽在不同清晰度间切换 —— 这就是 DASH(第 8 讲)

控制面在云上,数据面在 CDN 上——这个分离是所有大规模流媒体系统的通用架构。


三、HTTP/1.1 的困境

HTTP/1.1 于 1997 年定稿。它假设的网页是几个 HTML 加十几张图。今天一个网页平均要加载 70+ 个资源。

3.1 队头阻塞(Head-of-Line Blocking)

HTTP/1.1 的持续连接上,响应必须按请求顺序返回

连接上:  [请求A][请求B][请求C]
响应:    [———— 响应A(很大,2 秒)————][响应B][响应C]
                                        ↑
                        B 和 C 早就准备好了,但必须等 A

这就是应用层的队头阻塞

3.2 浏览器的粗暴补救:开 6 条连接

浏览器对同一域名并行开 6 条 TCP 连接。但这带来新问题:

  • 每条连接都要单独握手(HTTPS 还要 TLS 握手)
  • 每条连接都要单独经历慢启动,谁也长不大
  • ⚠️ 6 条连接对拥塞控制是不公平的——一个用户占了 6 份带宽份额
  • 开发者被迫用各种 hack:域名分片(sharding)、雪碧图(CSS sprites)、内联小资源

📌 这些 hack 本质上都是在对抗协议的缺陷,而不是在解决业务问题。这是一个协议该升级的明确信号。


四、HTTP/2(2015,RFC 7540 / 9113)

4.1 核心改进

1️⃣ 二进制分帧(Binary Framing)

不再是纯文本,报文被切成二进制的(HEADERS 帧、DATA 帧等)。代价是不能再用 telnet 直接手打请求,收益是解析高效且无歧义。

2️⃣ 多路复用(Multiplexing) ⭐ 最关键

一条 TCP 连接上并发多个(stream),每个流承载一个请求/响应对:

一条 TCP 连接
┌──────────────────────────────────────────────┐
│ [S1帧][S3帧][S3帧][S1帧][S5帧][S3帧][S1帧]... │  ← 帧交错传输
└──────────────────────────────────────────────┘
   接收端按 Stream ID 重新组装

**结果:应用层的队头阻塞被消除了。**响应可以乱序返回,小的先到。

3️⃣ 首部压缩(HPACK)

HTTP 首部有大量重复(同样的 User-Agent、Cookie、Accept 出现在每个请求里,动辄几百字节)。HPACK 用静态表 + 动态表 + 霍夫曼编码,压缩率常常超过 80%。

4️⃣ 流优先级与依赖

客户端可以声明「先给我 CSS 和关键 JS,图片可以慢一点」。

5️⃣ 服务器推送(Server Push)

服务器可以在客户端请求 HTML 时主动推送它必然会要的 CSS。

⚠️ 注意:Server Push 实践中效果不佳(经常推送客户端已缓存的资源,浪费带宽),主流浏览器已在 2022 年前后移除支持,被 103 Early Hints 取代。这是一个「理论上好、实践中失败」的经典案例,值得记住。

4.2 HTTP/2 没能解决的问题

⚠️ TCP 层的队头阻塞依然存在。

原因很根本:TCP 提供的是一条有序字节流。它不知道上面跑着 5 个独立的流。

TCP 字节流:  [S1数据][S3数据][S5数据][S3数据]...
                       ↑
                  这个段丢了

结果:S3 的这个段没到,TCP 就不会把后面的 S1、S5 数据交给应用
      —— 即使它们已经完整收到、且与 S3 毫无关系

一个流丢包,全部流卡住。在丢包率高的网络(移动、跨国)上,HTTP/2 有时甚至比 HTTP/1.1 的 6 条连接更慢——因为 6 条连接里一条卡住,另外 5 条还能跑。

要解决这个问题,必须让传输层知道流的存在。而 TCP 在内核里、在中间件(防火墙、NAT)里根深蒂固,改不动

于是有了下一步。


五、HTTP/3 与 QUIC(2021,RFC 9000 / 9114)

5.1 核心决定:放弃 TCP,建在 UDP 之上

        HTTP/2                    HTTP/3
   ┌─────────────┐           ┌─────────────┐
   │   HTTP/2    │           │   HTTP/3    │
   ├─────────────┤           ├─────────────┤
   │   TLS 1.3   │           │             │
   ├─────────────┤           │    QUIC     │  ← 可靠传输 + 拥塞控制
   │     TCP     │           │  (含 TLS1.3) │     + 加密,全在用户态
   ├─────────────┤           ├─────────────┤
   │     IP      │           │     UDP     │
   └─────────────┘           ├─────────────┤
                             │     IP      │
                             └─────────────┘

**为什么是 UDP?**不是因为 UDP 好,而是因为:

  1. UDP 能穿过现有的 NAT 和防火墙(部署可行性
  2. QUIC 在用户态实现,可以随浏览器更新而迭代,不必等操作系统内核升级——这是关键的工程理由
  3. 传输层的可靠性、拥塞控制、加密全部重新实现,这次带上了「流」的概念

📌 **协议僵化(ossification)**是这里的核心概念:中间设备对 TCP 的假设太多,导致 TCP 本身几乎无法演进。互联网的「窄腰」(第 4 讲)在实践中比设计上还要窄。

5.2 QUIC 的四个关键收益

1️⃣ 消除传输层队头阻塞

QUIC 把每个流独立地做重传和排序。流 3 丢包,只有流 3 等待,流 1 和流 5 照常交付。

2️⃣ 更快的握手

TCP + TLS 1.3:   1 RTT (TCP) + 1 RTT (TLS) = 2 RTT
QUIC:            1 RTT(连接建立与加密协商合并)
QUIC 0-RTT:      0 RTT(对之前连过的服务器,首个请求即可携带数据)

⚠️ 0-RTT 数据不具备重放保护,因此只能用于幂等请求(第 5 讲的幂等性在这里变成了安全约束)。

3️⃣ 连接迁移(Connection Migration)

TCP 连接由四元组(源 IP、源端口、目的 IP、目的端口)标识。手机从 WiFi 切到 5G,IP 变了,所有 TCP 连接立刻断掉

QUIC 用一个与 IP 无关的 Connection ID 标识连接。IP 变了,连接继续。这对移动场景是决定性的改进。

4️⃣ 默认加密

QUIC 强制 TLS 1.3,而且连大部分传输层头部都加密。副作用是中间设备再也无法窥探和干预——这既是隐私收益,也让网络运营商的流量管理和调试变得困难(一个真实的争议点)。

5.3 QUIC 的代价

代价 说明
CPU 开销更高 用户态处理 + 加密,早期比 TCP 高 2–3 倍,现已大幅优化
UDP 被限速/封禁 部分企业网络阻断 UDP 443,需回落到 TCP
调试困难 头部加密,传统抓包分析受限
生态成熟度 中间件、监控、负载均衡器需要重新适配

5.4 三代对比

HTTP/1.1 HTTP/2 HTTP/3
年份 1997 2015 2021
传输层 TCP TCP UDP + QUIC
报文格式 文本 二进制帧 二进制帧
多路复用 ❌(靠开 6 条连接)
应用层队头阻塞 ❌ 存在 ✅ 已解决 ✅ 已解决
传输层队头阻塞 每连接独立 存在 已解决
首部压缩 HPACK QPACK
加密 可选 事实必需 强制
握手 RTT 1 (+1 TLS) 1 (+1 TLS) 1 或 0
连接迁移

六、例题(Worked Example)

题目:某机构接入链路 100 Mbps,平均对象 800 kbit,请求速率 100 个/秒。到源服务器的平均互联网时延为 1.2 s(不含接入链路排队)。局域网时延与缓存命中时延均忽略不计(按 5 ms 计)。

(a) 无缓存时接入链路的流量强度是多少? (b) 若部署缓存,要把流量强度降到 0.5 以下,命中率至少需要多少? (c) 在命中率 0.6、接入链路排队时延估为 0.3 s 时,平均响应时延是多少?

解答

(a)

比特到达率 = 100 req/s × 800 kbit = 80 Mbps
流量强度 = 80 / 100 = 0.8

已经在排队时延显著上升的区间。

(b) 设未命中率为 m:

m × 80 Mbps / 100 Mbps < 0.5
m < 0.625
→ 命中率 > 0.375,即至少约 38%

(c)

平均时延 = 0.6 × 0.005 + 0.4 × (1.2 + 0.3)
         = 0.003 + 0.6
         = 0.603 s

相比无缓存时的 1.2 s + 大排队时延(0.8 强度下可能 0.5 s 以上,约 1.7 s),改善约 65%


七、随堂自测

  1. no-cacheno-store 的区别是什么?哪一个用于银行页面?
  2. 「深入部署」和「邀请做客」两种 CDN 哲学各自的取舍是什么?
  3. CDN 通过 DNS 做节点选择时,最大的准确性缺陷是什么?如何缓解?
  4. HTTP/2 消除了哪一层的队头阻塞?没消除哪一层?请解释根本原因。
  5. QUIC 为什么建在 UDP 上而不是直接定义一个新的传输层协议?
  6. 一位工程师说:「我们网站升级到 HTTP/2 后,在东南亚用户那里反而更慢了。」给出一个技术上说得通的解释。
  7. 为什么 HTTP/2 的 Server Push 最终被废弃?

八、本讲要点回顾

  • Web 缓存的收益是时延、带宽成本、可达性三重的;关键在于它把接入链路的流量强度从爆炸区拉回可用区。
  • no-cache = 用之前必须验证;no-store = 不许存。
  • CDN 的节点选择通过 DNS 完成,看到的是 LDNS 的位置而非用户位置。
  • HTTP/1.1 的问题是应用层队头阻塞,浏览器用 6 条连接粗暴绕过。
  • HTTP/2 用二进制分帧 + 多路复用解决了应用层队头阻塞,但TCP 层队头阻塞依旧
  • HTTP/3 = QUIC over UDP:流级独立重传、1-RTT/0-RTT 握手、连接迁移、强制加密。
  • 贯穿的主题:协议僵化让 TCP 无法演进,于是创新绕道 UDP 在用户态发生。

九、自测答案

1. no-cache可以存储,但每次使用前必须向源服务器重新验证(发条件 GET)。no-store完全不允许存储在任何缓存中。银行的账户页面应当用 no-store(配合 private),防止副本残留在共享代理或磁盘上。

2. 深入部署(Akamai):集群多、离用户最近、时延最优,但要与数千家 ISP 打交道、运维成本极高。邀请做客:在少数 IXP 建大集群,部署与运维简单、成本低,但用户到集群的距离更远、时延略高,且更依赖 IXP 的可用性。

3. 缺陷是 CDN 只能看到本地 DNS 服务器的 IP,而不是终端用户的 IP。使用公共 DNS(8.8.8.8、1.1.1.1)时,LDNS 可能与用户相距很远,导致选到次优集群。缓解手段是 EDNS Client Subnet (ECS),由 LDNS 附带用户所在的网段前缀;代价是把用户网段暴露给了权威 DNS,构成隐私泄露。

4. 消除了**应用层(HTTP 层)的队头阻塞——响应不再需要按请求顺序返回。没消除传输层(TCP 层)**的队头阻塞。根本原因是 TCP 向上提供的抽象是「一条有序的字节流」,它对上层的多流结构一无所知,因此任何一个字节丢失,都会阻止其后所有已到达的字节被交付给应用——哪怕那些字节属于完全无关的流。

5. 因为部署可行性。定义一个新的 IP 协议号意味着全球的 NAT、防火墙、负载均衡器都要升级才能让它通过,历史证明这需要几十年(参考 SCTP 的命运,以及 IPv6 的部署速度)。UDP 是既有的、被普遍放行的通道。此外,QUIC 在用户态实现,可以随应用更新迭代,而 TCP 的改进要等内核升级和运营商设备更新——这是速度上的决定性差异。

6. 东南亚到该站点的路径可能丢包率较高。HTTP/2 把所有请求集中到一条 TCP 连接上,一旦丢包就触发 TCP 层队头阻塞,所有资源同时卡住;而 HTTP/1.1 的 6 条并行连接中,一条卡住时其余 5 条仍在传输。在高丢包链路上,这个差异可能压过 HTTP/2 的多路复用收益。(正确的解法不是退回 HTTP/1.1,而是升到 HTTP/3。)

7. 主要原因是服务器无法知道客户端缓存里已经有什么。大量推送的资源客户端本来就有,纯属浪费带宽——在带宽受限的移动网络上甚至挤占了真正需要的资源。加上实现复杂、收益难以稳定复现,浏览器厂商陆续移除支持,改用 103 Early Hints(服务器提示客户端「你稍后会需要这些,可以先去取」),把决定权交回客户端。