一、Web 缓存(代理服务器)
1.1 机制
在客户机和源服务器之间放一台缓存服务器(proxy server)。浏览器把所有请求发给它:
浏览器 ──①请求──→ ┌──────────┐ ──③若未命中,向源请求──→ 源服务器
←④响应── │ Web 缓存 │ ←──────────────────────
└──────────┘
②若命中,直接返回
缓存既是服务器(对浏览器),也是客户机(对源服务器)——这个双重身份是理解代理的关键。
1.2 三重收益
- 降低响应时延:命中时不用跨越广域网
- 降低接入链路带宽消耗(对机构最实在的收益:省钱)
- 让小内容提供商也能有效交付内容——不需要自己建全球基础设施
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 好,而是因为:
- UDP 能穿过现有的 NAT 和防火墙(部署可行性)
- QUIC 在用户态实现,可以随浏览器更新而迭代,不必等操作系统内核升级——这是关键的工程理由
- 传输层的可靠性、拥塞控制、加密全部重新实现,这次带上了「流」的概念
📌 **协议僵化(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%。
七、随堂自测
no-cache和no-store的区别是什么?哪一个用于银行页面?- 「深入部署」和「邀请做客」两种 CDN 哲学各自的取舍是什么?
- CDN 通过 DNS 做节点选择时,最大的准确性缺陷是什么?如何缓解?
- HTTP/2 消除了哪一层的队头阻塞?没消除哪一层?请解释根本原因。
- QUIC 为什么建在 UDP 上而不是直接定义一个新的传输层协议?
- 一位工程师说:「我们网站升级到 HTTP/2 后,在东南亚用户那里反而更慢了。」给出一个技术上说得通的解释。
- 为什么 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(服务器提示客户端「你稍后会需要这些,可以先去取」),把决定权交回客户端。