CDN 加速

限流之后,新的问题来了

限流上线后,系统不再崩溃了。但新的投诉随之而来——“你们的短链接打不开了”、“太慢了,等了好几秒才跳转”。

我查看监控数据,发现被限流的那 20% 请求,用户体验确实很差。而且随着用户量增长,海外用户的投诉越来越多:

“我在纽约,每次点击短链接都要等 300 多毫秒才能跳转,这也太慢了吧?”

我意识到,限流只是治标不治本。它保护了系统,但牺牲了用户体验。我需要找到一个方案,既能减轻服务器压力,又能让全球用户快速访问。

发现 CDN 加速的可能性

那天晚上,我研究了国内外短链接服务的架构,发现一个共同点:他们都用了 CDN。

“CDN 不是用来加速静态资源的吗?短链接跳转是动态请求,也能用 CDN?” 我有些疑惑。

深入研究后,我恍然大悟:短链接的跳转操作,本质上是一个只读的静态映射。同一个短码,永远返回同一个长 URL。这个特性让 CDN 缓存成为可能。

核心思想

传统模式下,所有用户都请求我的服务器:

全球用户 → 我的服务器(北京)

      查询数据库

      返回长 URL

这导致:

  • 纽约用户要跨半个地球访问北京,延迟 200ms+
  • 我的服务器要承受所有流量,QPS 压力巨大

CDN 模式下,把跳转结果缓存到边缘节点:

全球用户 → 最近的 CDN 节点

      直接返回长 URL(缓存命中)
      
CDN 未命中 → 我的服务器 → 缓存结果

我画了一张架构图:

理论上,CDN 能承担 80-90% 的流量,用户延迟从 200ms+ 降到 20ms 左右。

选择 CDN 服务商

我对比了几家主流 CDN 服务商:

CDN 提供商全球节点数国内覆盖价格特点
Cloudflare300+一般免费/按量生态丰富
AWS CloudFront400+一般按量付费AWS 集成
阿里云 CDN2800+优秀按量付费国内首选
腾讯云 CDN2800+优秀按量付费国内优秀
七牛云 CDN2000+良好按量付费性价比高

考虑到我的用户主要在国内,但也有一些海外用户,我选择了阿里云 CDN。它的国内节点覆盖最好,海外也有不少节点。

CDN 配置实战

第一步:添加加速域名

我在阿里云 CDN 控制台添加了加速域名 s.url,回源地址设置为我的服务器 IP。

配置很简单,但有一个关键点:CDN 默认缓存所有响应,包括错误响应。这会导致问题,后面我会详细说。

第二步:配置 Nginx 返回 CDN 友好的响应

CDN 是否缓存,取决于响应头中的 Cache-Control。我需要让 Nginx 返回正确的缓存头。

我修改了 Nginx 配置,使用 Lua 脚本查询 Redis 并返回带缓存头的 302 响应:

入口配置要点

  • 入口层负责接住流量,并把请求转发到后端服务。
  • 缓存规则要明确哪些响应能缓存、缓存多久、什么时候回源。

这里的 Cache-Control 头是关键:

  • public:允许 CDN 缓存
  • s-maxage=300:CDN 缓存 300 秒(5 分钟)
  • max-age=60:浏览器缓存 60 秒

第三步:针对不同类型短链接的缓存策略

我意识到,不同的短链接应该有不同的缓存策略:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。
参数含义建议值
public允许 CDN 缓存始终使用
private只允许浏览器缓存不推荐
max-age=60浏览器缓存 60 秒10-60 秒
s-maxage=300CDN 缓存 300 秒60-300 秒
no-cache每次都要验证不推荐
no-store完全不缓存用于 404 等错误响应

为什么用 302 而不是 301?

有人问我:“为什么用 302 临时重定向,而不是 301 永久重定向?301 不是性能更好吗?”

我解释道:“301 会被浏览器永久缓存。如果短链接的目标 URL 变了,用户看到的还是旧的 URL,体验很差。302 每次都会请求服务器(或 CDN),能及时响应变化。”

这对 CDN 的影响是:301 响应 CDN 几乎永久缓存,302 响应按照 s-maxage 缓存。对于可能变化的短链接,302 更合适。

缓存失效机制

配置好 CDN 后,我遇到了一个新问题:缓存更新延迟。

某天,一个客户投诉:“我更新了短链接的目标 URL,但用户还是跳转到旧地址!”

我检查配置,发现 s-maxage=300,意味着 CDN 会缓存 5 分钟。对于营销活动,5 分钟的延迟可能无法接受。

方案一:等待过期(最简单)

最简单的方式是让缓存自然过期。

Cache-Control: public, s-maxage=60

→ CDN 最多缓存 60 秒
→ 60 秒后 CDN 会回源获取最新 URL

优点:简单,无需额外逻辑 缺点:有缓存不一致时间窗口

方案二:主动刷新缓存(我选择了这个)

我实现了 CDN 缓存刷新 API,当短链接目标 URL 变化时,主动刷新 CDN 缓存:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

这样,目标 URL 更新后,缓存会在几秒内失效。

方案三:URL 版本化

另一种方式是在 URL 中加入版本号:

https://s.url/abc123?v=1  ← 旧版本
https://s.url/abc123?v=2  ← 新版本,CDN 视为新 URL

但这种方式会改变短链接,不适合我的场景。

上线效果

配置好 CDN 后,我上线了。效果立竿见影:

性能对比

指标上线前上线后改善
纽约用户延迟350ms35ms降低 90%
伦敦用户延迟280ms30ms降低 89%
东京用户延迟120ms20ms降低 83%
北京用户延迟50ms15ms降低 70%
源站 QPS5000010000降低 80%
CDN 缓存命中率0%80%-

全链路性能优化

CDN 只是其中一环,看看加上 CDN 后的全链路性能:

链路                 优化前      优化后
─────────────────────────────────────────
DNS 解析            50ms       5ms    (CDN DNS 加速)
TCP 连接            100ms      10ms   (就近节点)
TLS 握手            100ms      20ms   (CDN TLS 卸载)
服务端处理          50ms       0ms    (CDN 直接响应)
─────────────────────────────────────────
总计                300ms      35ms   ⚡ 快了 8.5 倍

用户投诉大幅减少,海外用户反馈”现在访问快多了”。

CDN 的坑

CDN 上线后,效果很好。但我也遇到了一些问题。

问题一:缓存成本超出预期

月底我看账单,发现 CDN 流量费 ¥870/月。而我的服务器租费才 ¥500/月。CDN 比服务器还贵!

我算了笔账:

每天处理 1 亿次重定向
每次重定向约 1KB 数据
CDN 缓存命中率 80%

CDN 流量:
- 总流量:1 亿 × 1KB = 100GB/天
- 回源流量:100GB × 20% = 20GB/天

费用(阿里云 CDN):
- CDN 流量费:100GB × ¥0.24/GB ≈ ¥24/天
- 回源流量费:20GB × ¥0.5/GB ≈ ¥10/天
- 月费用:约 ¥1020

但如果没有 CDN:
- 服务器需要抗 100K QPS
- 至少 10 台高配服务器
- 月费用:¥50,000+

虽然有 CDN 成本,但还是省了 98%!💰

虽然 CDN 有成本,但相比扩容服务器,还是划算的。

问题二:缓存命中率不稳定

运行一段时间后,我发现缓存命中率波动很大。有时 90%,有时只有 60%。

我分析了日志,发现:

  • 很多短链接只被访问一次(比如一次性验证链接)
  • 这些短链接缓存了也没用,反而占用 CDN 空间
  • 热门短链接的缓存命中率很高

我调整了策略:只对热门短链接开启 CDN 缓存,冷门短链接不缓存。

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

调整后,缓存命中率稳定在 85% 以上。

问题三:404 也被缓存了

有用户反馈:“我创建了一个短链接,但访问时显示 404。后来我发现是创建失败了,但 404 被 CDN 缓存了,一直显示错误。”

我意识到,CDN 默认会缓存所有响应,包括 404。我调整策略,确保 404 不被缓存:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

问题四:缓存雪崩

有一天,我发现源站 QPS 突然飙升。查看日志,发现大量请求同时回源。

原来是很多缓存同时过期,导致”缓存雪崩”。

我给缓存时间加了随机偏移:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

问题五:Vary 头导致缓存失效

有一次,我发现某些用户访问正常,某些用户访问异常。排查后发现,CDN 根据 Accept-Language 头分别缓存了响应,导致缓存命中率下降。

我检查了响应头,发现有个地方设置了 Vary: Accept-Language。对于短链接跳转,响应内容与语言无关,我移除了这个头。

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

完整的请求流程

加上 CDN 后,完整的请求流程变成了这样:

用户点击 https://s.url/abc123

[1] DNS 解析 → CDN 的 Anycast IP

[2] TCP/TLS → 就近的 CDN 节点(30ms 内)

[3] CDN 检查缓存
   ├── 命中 → 302 重定向 ✅(总耗时 ~35ms)
   └── 未命中 ↓
[4] CDN 回源 → 我的服务器

[5] 服务器查 Redis → 查数据库

[6] 返回 302 + Cache-Control

[7] CDN 缓存结果 → 响应用户

[8] 后续请求直接走 CDN ✅