自建 CDN 最容易犯的错误,是把“节点数量”当成性能指标。真正决定访问体验的,是请求在缓存未命中之后会走哪条链路、在哪一层被拦住,以及源站波动会扩散到多大范围。
如果手上已经有分布在多个地区的机器,更值得做的是把它们组织成一个有层级的分发系统:靠近用户的节点负责接入和缓存;网络条件更可靠的节点负责跨区域回源;最靠近应用的入口负责保护源站。这样优化的不是某一次测速,而是一条可解释、可观测、可降级的请求路径。
先把问题说清:用户访问慢,慢在哪里
“离用户近”只解决了第一跳。静态资源在边缘缓存命中时,这当然很有效;但一旦缓存失效,真正的成本来自边缘节点到源站的回源链路。一个地理位置很近、但跨网质量不稳定的节点,可能比一个稍远、却能稳定接入优质父节点的节点表现更差。
因此设计时应先画出完整链路:DNS 选择了哪个入口、入口命中缓存了吗、未命中后由谁回源、父节点再向哪里请求、最终源站是否承受得住。只有这条链路能被说清楚,节点扩展才有意义。
一个更实用的三层模型
L1:边缘节点,负责接入而非承担一切
L1 节点放在目标用户相对集中的区域,主要缓存图片、脚本、样式表、下载文件和短时间热点接口。它的职责是缩短用户到入口的距离,并尽可能把请求消化在本地。对于缓存未命中的请求,L1 不应直接随机回到真实源站,而应进入指定的父节点。
L2:区域父节点,负责把回源路径固定下来
L2 是整个系统的关键。它不必离每个用户最近,却应该拥有经过验证的跨区域互联、带宽和稳定性。每组 L1 按区域绑定一个或两个 L2:例如东亚边缘回到东亚父节点,欧美边缘回到北美父节点。这样即使新增了普通线路的边缘机,长距离流量仍会汇入可控的主干路径,而不是让每台机器各自“碰运气”回源。
L3:源站防护层,把应用从公网波动中隔离出来
L3 位于真实应用前面,可以是独立反代,也可以是源站所在网络中的缓存层。它承接来自 L2 的请求,执行限流、连接复用、缓存回填和健康检查。这样源站只需要面对少量、稳定的上游,而不是直接暴露给每个边缘节点。
DNS 只做粗分流,不要承诺“绝对就近”
DNS 适合按大区、运营商或可用性做第一次选择,但它看到的是递归解析器,不一定是最终用户。把 DNS 当作精确调度器,往往会得到难以解释的误路由。
更可靠的做法是:为每个区域准备少量可替换入口,设置保守 TTL,并在节点不可用时明确回退。复杂的细粒度策略应建立在长期日志上,而不是一次测速截图。无论使用 GeoDNS、智能解析还是手工分区,始终要给默认路径留一个安全、可访问的兜底节点。
缓存配置的目标:防止同一个未命中打穿源站
下面是一段适用于静态内容或可缓存 GET 请求的 Nginx 思路。它不是完整生产配置,但包含了多级缓存里最容易遗漏的几个点:缓存锁、过期副本兜底和鉴权请求绕过缓存。
proxy_cache_path /var/cache/nginx/cdn levels=1:2
keys_zone=cdn_cache:100m inactive=30m max_size=10g use_temp_path=off;
upstream regional_parent {
server 10.0.0.12:443;
keepalive 32;
}
server {
location / {
proxy_pass https://regional_parent;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache cdn_cache;
proxy_cache_key "$host$request_uri";
proxy_cache_valid 200 10m;
proxy_cache_lock on;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_cache_bypass $http_authorization;
proxy_no_cache $http_authorization;
add_header X-Cache-Status $upstream_cache_status always;
}
}
proxy_cache_lock 的作用是让同一个缓存键在失效时只由少数请求回源,避免高并发下所有请求同时穿透。proxy_cache_use_stale 则允许在上游短暂失败时继续服务旧内容。两者能显著改善“源站一抖,全站一起慢”的情况,但只能用于允许短暂陈旧的数据。
多级分发带来的代价,必须提前接受
层级不是免费的。每增加一层,都会增加故障域、证书管理、缓存清理和排障复杂度。尤其是动态接口、登录态、支付、个性化页面和需要强一致性的内容,不应该因为“也能被代理”就塞进缓存链路。
- 失效策略:提前设计版本化资源、缓存标签或主动清理路径;否则内容更新会变成运维事故。
- 健康检查:DNS 发现节点不可达太慢,父节点和边缘节点都需要自己的上游超时、重试与降级策略。
- 安全边界:限制源站只接受 L3 或可信上游的流量;不能把真实源站 IP 留在公网旁路。
- 合规与风险:服务范围、内容类型和用户所在地会影响合规要求。网络优化不应绕开这些约束。
如何判断它真的变快了
不要只看某个地区的一次总耗时。至少记录不同地区、不同运营商、不同时间段的 DNS 结果、建连时间、TLS 时间、TTFB、完整下载时间和缓存命中率。更重要的是观察 P95 或 P99,而不是只展示最好看的平均值。
在日志中保留节点标识、父节点标识、缓存状态和上游响应时间。发生异常时,才能回答“慢的是用户到边缘、边缘到父节点,还是父节点到源站”。这比增加一台新机器更有价值。
什么时候不该自建
如果没有既有节点、没有明确的区域需求,或者团队无法维护证书、监控、缓存清理与故障切换,商业 CDN 往往是更低风险的选择。自建的优势不在于替代所有产品,而在于你已经拥有一组不同网络特性的资源,并且愿意用工程方式把它们组织起来。
我的结论很简单:先固定回源路径,再扩展边缘节点;先做可观测性,再追求全球覆盖。把优质链路当成父节点能力,而不是某一台机器的“测速成绩”,自建 CDN 才会从节点堆叠变成可维护的分发系统。