自建 CDN 服务器:取一次,分发千万次
CDN 服务器把每个文件从源站取一次,之后成千上万次都由它自己分发。源站只会看到其中一小部分请求,访客则拿到很短的加载时间。本文讲怎么把它搭起来。
访客从您站点上下载的每个文件,都要由源站从文件系统里取出来、装进一个响应再发走。每天十位访客时谁都察觉不到。而在一万位访客同时在线时,这恰恰就是页面变慢的原因,哪怕真正的应用程序根本无事可做。
CDN 服务器就摆在它前面。它把每个文件从源站取一次,之后需要多少次就自己分发多少次。源站因此只会看到其中一小部分请求,而访客拿到文件的那台服务器除了分发什么都不干。本文说明这到底能带来多少好处、怎么用 nginx 搭这样一台服务器,以及流量和机房位置上要注意什么。
CDN 服务器到底做什么
整个流程用三句话就能讲完:
- 一位访客向 CDN 服务器请求某个文件。
- 文件已经在那里,就立刻发出去。这叫命中。
- 文件不在那里,CDN 服务器就去源站取、存下来再发出去。这叫未命中,每个文件在每个有效期里只发生一次。
原理和流媒体服务器一样:取一次,分发多次。区别只在于流媒体服务器分发的是持续的数据流,而 CDN 服务器分发的是现成的文件。
算一算:缓存能减掉多少负载
决定性的是命中率,也就是缓存自己答掉的请求占比。按每天一百万次请求算,减负效果是这样:
| 命中率 | 到源站的请求 | 减负 |
|---|---|---|
| 50% | 500,000 | 2 倍 |
| 80% | 200,000 | 5 倍 |
| 95% | 50,000 | 20 倍 |
| 99% | 10,000 | 100 倍 |
对静态文件来说,95% 以上是常态,因为图片、脚本和字体很少改动。而从 95% 提到 99% 的意义比数字看起来更大:它把剩下的负载又砍到五分之一。
同时流量也会转移。源站几乎不再传东西,CDN 服务器则传得更多。每天 500,000 次图片请求、每个 400 KB,就是每天 200 GB,也就是每月约 6 TB,仅图片一项。所以对一个 CDN 节点来说,重要的数字不是 CPU,而是网络接入和流量。
自建 CDN 服务器什么时候划算
商业 CDN 服务商按传输的每 GB 收费。这很方便,流量少时也便宜。但超过一定量,关系就反过来了:
| 情形 | 自建 CDN 服务器 | 按 GB 计费 |
|---|---|---|
| 每月几个 GB | 几乎不划算 | 更便宜 |
| 每月好几个 TB | 月费固定 | 线性增长 |
| 大体积下载、视频存档 | 明显占优 | 很快就贵了 |
| 对规则和协议的完全控制 | 完全掌握 | 受限 |
还有一点很难用数字衡量:在自己的节点上,什么被缓存、缓存多久、什么时候消失,都由您自己定。您不受别家服务商的规则约束,也可以分发那些塞不进别人既定方案的东西。
用 nginx 作缓存的搭法
安装
apt update
apt install -y nginx
就这些,缓存功能是 nginx 自带的。建好存放文件的目录:
mkdir -p /var/cache/cdn
chown www-data:www-data /var/cache/cdn
配置
缓存在 http 块里描述一次,然后在各个 location 里使用:
proxy_cache_path /var/cache/cdn
levels=1:2
keys_zone=cdn:100m
max_size=200g
inactive=30d
use_temp_path=off;
proxy_cache_key "$scheme$request_method$host$request_uri";
这些值分别表示:keys_zone=cdn:100m 够放大约 800,000 条记录,max_size 限制占用的硬盘空间,inactive=30d 会丢掉 30 天无人请求的文件,use_temp_path=off 则避免在目录之间做多余的搬移。
下面是真正的 server 段:
server {
listen 443 ssl;
http2 on;
server_name cdn.example.com;
ssl_certificate /etc/letsencrypt/live/cdn.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cdn.example.com/privkey.pem;
location / {
proxy_pass https://origin.example.com;
proxy_set_header Host origin.example.com;
proxy_http_version 1.1;
proxy_cache cdn;
proxy_cache_valid 200 301 302 24h;
proxy_cache_valid 404 1m;
# 把多个同时发生的未命中合并成一次请求
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
# 源站出问题时继续分发
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
# 会话 Cookie 不能把缓存搞坏
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
add_header X-Cache-Status $upstream_cache_status always;
}
}
其中三行值得特别注意。proxy_cache_lock 保证在遇到新文件时,不会有一百位访客同时去问源站,而是只有一位去问、其余等着。proxy_cache_use_stale 在源站重启或响应变慢时维持分发不中断。而隐去 Set-Cookie 则防止某一条带会话 Cookie 的响应把整个缓存弄成废物。
检查是否真的生效
nginx -t && systemctl reload nginx
curl -sI https://cdn.example.com/bild.jpg | grep -i x-cache-status
curl -sI https://cdn.example.com/bild.jpg | grep -i x-cache-status
第一次请求显示 MISS,第二次显示 HIT。如果第二次还是 MISS,几乎总是三个常见嫌疑之一:响应里带了 Set-Cookie、源站发了 Cache-Control: no-store,或者缺少长度信息。
什么会被缓存,什么不会
干净的做法是让源站自己说。它了解自己的内容,缓存不了解。为此它要设置合适的响应头:
| 内容 | 源站上的响应头 |
|---|---|
| 文件名里带校验和的文件 | Cache-Control: public, max-age=31536000, immutable |
| 没有版本化的图片或下载包 | Cache-Control: public, max-age=86400 |
| 公开页面的 HTML | Cache-Control: public, max-age=300 |
| 登录区、购物车 | Cache-Control: private, no-store |
如果您想在缓存端自己决定,可以按文件类型写各自的 location:
location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico|css|js|woff2|mp4|zip)$ {
proxy_pass https://origin.example.com;
proxy_cache cdn;
proxy_cache_valid 200 7d;
add_header Cache-Control "public, max-age=604800";
add_header X-Cache-Status $upstream_cache_status always;
}
定点清掉内容
最简单的路是根本不用删:谁给每张改动过的图片都起个新文件名,比如 logo.a1b2c3.png,就没有这个问题。旧文件迟早会自己从缓存里掉出去。
做不到的话,nginx 这边可以用自由软件模块 ngx_cache_purge。不加额外模块也行,路径可以从缓存键算出来,因为 nginx 用它的 MD5 值当文件名:
KEY="httpsGETcdn.example.com/bild.jpg"
H=$(printf '%s' "$KEY" | md5sum | cut -d' ' -f1)
rm -f "/var/cache/cdn/${H: -1}/${H: -3:2}/$H"
日常用一个小脚本,把这几行套在一份路径清单上执行,通常就够了。
大文件和视频
文件超过几百 MB 时,就需要切块,否则观众每次在视频里跳转,缓存都会重新下载整个文件:
location /video/ {
proxy_pass https://origin.example.com;
proxy_cache cdn;
proxy_cache_valid 200 206 7d;
slice 1m;
proxy_set_header Range $slice_range;
proxy_cache_key "$uri$slice_range";
}
这样 nginx 只取真正被请求的那些块。观众跳到影片中间时,线路上走的是几 MB,而不是好几 GB。
TLS、HTTP/2 与 HTTP/3
CDN 节点要把很多小文件分发到很多条连接上,新协议正好在这种场合划得来。HTTP/2 一行 http2 on; 就搞定。HTTP/3 主要在移动网络上有效果,因为换网不会把连接断掉:
listen 443 quic reuseport;
listen 443 ssl;
http2 on;
add_header Alt-Svc 'h3=":443"; ma=86400';
别忘了在防火墙里放行 UDP 443。这是 HTTP/3 已经配好却依然没人用得上的最常见原因。
多个机房
单个节点就已经带来大部分收益,也就是给源站减负。第二份收益,即到访客的路程更短,要有多个机房才拿得到。
分流通过 DNS 完成:同一个名字会按访客来源指向不同的节点。为此您需要一家支持地理解析的 DNS 服务商。所有节点用同一份配置、同一个源站,彼此之间毫不知情,而这恰恰让整个架构很结实:某个节点挂了,您把它从 DNS 里摘掉,其余的接着干。
KernelHost 提供德国、美国西海岸、美国东海岸以及新加坡和韩国的机房。带延迟数据的概览见服务器机房位置。经验法则是:受众里有相当一部分人所在的每个大洲,放一个节点。
测量,别猜
把缓存状态写进日志,这样随时都能统计命中率:
log_format cdn '$remote_addr $status $body_bytes_sent '
'$upstream_cache_status "$request"';
access_log /var/log/nginx/cdn.log cdn;
一行命令就能统计出来:
awk '{print $4}' /var/log/nginx/cdn.log | sort | uniq -c | sort -rn
如果静态文件的 HIT 占比低于 90%,几乎总值得去找原因:多半是响应里的 Cookie、有效期太短,或者地址末尾带着变化参数的请求,这些在缓存看来每次都像一个新文件。
该选哪种服务器
CDN 节点是一台分发机器。所以重要特性的排序和应用服务器不一样:
- 流量与网络接入。全部负载都压在这里。一个节点在运行中每月经常要传好几个 TB。
- 硬盘。NVMe 明显占优,因为缓存由大量零碎的随机访问构成。容量取决于您的内容里有多少是热数据,而不是内容总共有多大。
- 内存。操作系统会把空闲内存当作硬盘前面的第二层缓存。内存越大,这里就直接越快。
- CPU。最不关键,只在 TLS 上用得到,而现代处理器顺手就做完了。
我们的无限流量根服务器正是为这种场景做的:每台服务器真正的 2 × 1 Gbit/s,不限速,无限流量且月费固定,RAID 下的数据中心级 NVMe,以及出厂自带的 3.2 Tbps 洪水防护。开通不到 30 秒,带完整 Root 权限、IPv4 和一个 /64 的 IPv6 网段。因为计费采用预付费、按月可退订,所以您可以先从一个节点起步,等受众分散开来,随时再加第二个机房。
常见问题
CDN 服务器具体做什么?
实际能减掉多少负载?
我必须用商业服务商吗?
哪些文件该缓存,哪些不该?
缓存该把文件留多久?
怎么把一个文件从缓存里删掉?
源站宕掉会怎样?
大视频文件也行吗?
我需要多少个机房?
我怎么知道缓存在起作用?
2026 KernelHost GmbH。保留所有权利。本教程受著作权法保护,未经我们书面同意,不得在其他网站上转载,节选转载或改写后转载同样不被允许。欢迎在注明出处并附上链接的前提下引用。

