概述
背景介绍
想象一下,当一位广州的用户访问部署在北京的网站时,仅仅是物理距离带来的网络延迟就可能高达30-50毫秒。再加上TCP握手、TLS协商、服务器处理等环节,首字节时间轻松超过200毫秒。内容分发网络的核心思路就源于此——将内容推送到离用户最近的边缘节点,通过缩短物理链路和减少回源次数来提升访问速度。
一次典型的CDN请求链路是这样的:用户发起DNS查询,智能DNS根据用户的地理位置返回最近的边缘节点IP。边缘节点首先查找本地缓存,如果命中则直接返回内容。如果未命中,则会向中间层节点请求,若中间层也未命中,最终才回源站拉取内容,并在返回给用户的路上逐级缓存。这套多级缓存架构能将源站的请求流量压缩到总流量的5%到15%。
要搭建一套高效的CDN,涉及以下几个关键技术栈:
- 智能DNS调度:利用GeoDNS根据地理位置进行解析,或者使用Anycast技术让多个节点共享同一个IP,由网络层将用户路由到最近的节点。
- 内容缓存:基于HTTP缓存协议实现内容的存储和校验,核心是理解
Cache-Control、ETag、Last-Modified 等头部。
- 回源优化:通过长连接复用减少TCP握手开销,并使用HTTP/2等多路复用协议降低回源延迟。
- HTTPS加速:采用TLS 1.3将握手过程从2-RTT减少到1-RTT,利用OCSP Stapling消除证书验证的额外请求,并通过Session Resumption实现0-RTT连接恢复。
技术特点
- 就近访问:边缘节点覆盖主要运营商和地区,用户请求可在同城甚至同机房完成响应。
- 多级缓存:L1边缘节点 → L2中间层 → 源站,逐级收敛回源流量,可将源站压力降低85%以上。
- 智能调度:基于实时网络质量、节点负载和地理位置进行综合决策,动态选择最优节点。
- 协议优化:结合TLS 1.3、HTTP/2、Brotli压缩等技术栈,显著降低传输延迟和带宽消耗。
- 故障容灾:节点故障自动摘除,支持在源站宕机时返回过期缓存进行兜底。
自建 CDN vs 云 CDN 对比:
| 维度 |
自建 CDN(Nginx/Varnish) |
云 CDN(阿里云/CloudFront) |
| 节点覆盖 |
受限于自有机房,通常 5-20 个节点 |
全球数百到数千个 PoP 节点 |
| 初始成本 |
高(服务器采购、带宽、机房) |
低(按量付费,无前期投入) |
| 运维复杂度 |
高(需自行处理节点部署、监控、故障切换) |
低(全托管,开箱即用) |
| 定制灵活性 |
高(VCL/Lua 可编程,缓存策略完全可控) |
中(受限于厂商提供的配置项) |
| 适用规模 |
中小规模或特殊合规要求 |
任意规模,尤其适合全球化业务 |
缓存代理软件对比:
| 特性 |
Nginx(proxy_cache) |
Varnish 7.x |
Squid |
Apache Traffic Server |
| 性能 |
高,事件驱动 |
极高,内存缓存为主 |
中等 |
高,大规模部署验证 |
| 配置语言 |
nginx.conf 指令 |
VCL(专用 DSL) |
squid.conf |
records.config |
| 缓存存储 |
磁盘 + 共享内存 |
内存(可扩展磁盘) |
磁盘 + 内存 |
磁盘 + 内存分层 |
| HTTPS 支持 |
原生支持 |
需前置 Hitch/Nginx |
原生支持 |
原生支持 |
| 可编程性 |
Lua(OpenResty) |
VCL + VMOD |
有限 |
插件 + Lua |
| 社区活跃度 |
极高 |
高 |
下降 |
中等(Apache 基金会) |
| 推荐场景 |
全站加速 + 反向代理一体 |
纯缓存加速,性能极致 |
正向代理 |
大规模 CDN 平台 |
适用场景
- 静态资源加速:图片、CSS、JS、字体文件,缓存命中率可达95%以上。
- 全站加速:静态资源走缓存,动态请求通过智能路由优化链路。
- 视频点播/直播:对大文件进行分片缓存,对直播流进行边缘分发。
- API 加速:对响应可缓存的GET接口设置短时缓存,降低后端压力。
- 安全防护:边缘节点天然具备DDoS吸收能力,可配合WAF规则过滤恶意请求。
环境要求
| 组件 |
版本要求 |
说明 |
| 操作系统 |
Ubuntu 22.04+ / Rocky Linux 9+ |
推荐 Ubuntu 22.04 LTS |
| Nginx |
1.26.x |
需编译 ngx_cache_purge、Brotli 模块 |
| Varnish |
7.x |
推荐 7.5+,VCL 4.1 语法 |
| OpenSSL |
3.x |
TLS 1.3 支持,OCSP Stapling |
| 硬件配置 |
8C16G 起步,SSD 存储 |
缓存盘建议独立 NVMe SSD |
| 网络 |
1Gbps+ 带宽 |
边缘节点建议 10Gbps |
详细步骤
HTTP 缓存机制详解
强缓存
强缓存命中时,浏览器直接使用本地缓存,不发送任何请求到服务器,状态码显示 200 (from disk cache) 或 200 (from memory cache)。
Cache-Control 指令(HTTP/1.1,优先级高于 Expires):
Cache-Control: public, max-age=31536000, s-maxage=86400
max-age=N:客户端缓存有效期,单位秒。max-age=31536000 表示缓存一年,适合带哈希的静态资源。
s-maxage=N:CDN/代理缓存有效期,会覆盖 max-age。源站可以让浏览器缓存1小时,而CDN缓存24小时。
public:允许任何中间缓存存储响应。
private:仅允许浏览器缓存,CDN不得缓存(用于包含用户个人数据的响应)。
no-cache:不是“不缓存”,而是每次使用前必须向服务器验证。
no-store:真正的不缓存,任何环节都不存储。
stale-while-revalidate=N:缓存过期后N秒内,先返回旧缓存,后台异步回源更新。
Expires(HTTP/1.0 遗留,仅作兜底):
Expires: Thu, 01 Jan 2027 00:00:00 GMT
绝对时间格式,依赖客户端和服务器时钟同步,生产环境应以 Cache-Control 为准。
协商缓存
强缓存过期后,浏览器会携带验证信息向服务器确认内容是否变化。如果未变化,服务器返回 304 Not Modified,节省传输带宽。
ETag / If-None-Match(优先级高):
# 首次响应
ETag: "a1b2c3d4e5f6"
# 后续请求
If-None-Match: "a1b2c3d4e5f6"
ETag 是内容的哈希指纹,内容不变则值不变。注意:Nginx默认使用 last_modified_time-content_length 生成 ETag,多台源站需确保文件修改时间和大小一致,否则会导致同一文件在不同源站产生不同的 ETag。
Last-Modified / If-Modified-Since:
# 首次响应
Last-Modified: Wed, 25 Feb 2026 10:00:00 GMT
# 后续请求
If-Modified-Since: Wed, 25 Feb 2026 10:00:00 GMT
精度只到秒级,1秒内多次修改无法感知。当 ETag 和 Last-Modified 同时存在时,服务器会校验两者。
缓存决策流程
请求到达 CDN 节点
├─ 查找缓存键(Cache Key)对应的缓存条目
│ ├─ 未找到 → MISS → 回源获取 → 缓存并返回
│ └─ 找到缓存条目
│ ├─ 未过期(max-age/s-maxage 内)→ HIT → 直接返回
│ └─ 已过期
│ ├─ 有 ETag/Last-Modified → 带条件回源验证
│ │ ├─ 源站返回 304 → REVALIDATED → 刷新过期时间,返回缓存
│ │ └─ 源站返回 200 → EXPIRED → 更新缓存,返回新内容
│ └─ 无验证信息 → EXPIRED → 回源获取新内容
└─ Cache-Control: no-store → BYPASS → 直接回源,不缓存
Vary 头与缓存键设计
Vary 头告诉缓存服务器:相同URL但不同请求头值的响应需要分别缓存。
Vary: Accept-Encoding
这意味着 Accept-Encoding: gzip 和 Accept-Encoding: br 的响应会被分别缓存。常见的 Vary 值:
Accept-Encoding:按压缩算法区分。
Accept-Language:多语言站点按语言区分。
Cookie:慎用,Cookie变化多会导致缓存命中率暴跌。
缓存键默认是 scheme + host + URI,可根据业务需要加入查询参数、特定Header等。原则是:缓存键越简单,命中率越高;包含的变量越多,缓存碎片越严重。
Nginx 缓存代理配置
缓存路径与基础配置
# /etc/nginx/conf.d/cache_zone.conf
# 定义缓存存储区域(放在 http 块内)
# 缓存路径配置
# levels=1:2 表示两级目录结构,避免单目录文件过多影响性能
# keys_zone=cdn_cache:256m 在共享内存中分配256MB存储缓存键索引
# max_size=50g 磁盘缓存上限50GB,超出后LRU淘汰
# inactive=7d 7天内未被访问的缓存自动清除
# use_temp_path=off 直接写入缓存目录,避免跨磁盘拷贝
proxy_cache_path /data/nginx/cache
levels=1:2
keys_zone=cdn_cache:256m
max_size=50g
inactive=7d
use_temp_path=off;
# 缓存键设计:scheme + host + URI + 查询参数
# 不要把 Cookie 放进缓存键,否则命中率会很低
proxy_cache_key "$scheme$host$request_uri";
缓存策略与回源控制
# /etc/nginx/conf.d/cdn_proxy.conf
server {
listen 80;
server_name cdn.example.com;
# 添加缓存状态头,方便排查(HIT/MISS/EXPIRED/BYPASS)
add_header X-Cache-Status $upstream_cache_status always;
# 静态资源缓存
location ~* \.(jpg|jpeg|png|gif|webp|ico|css|js|woff2|ttf|svg)$ {
proxy_pass http://backend_origin;
proxy_cache cdn_cache;
# 按源站响应码设置缓存时间
proxy_cache_valid 200 301 7d; # 正常响应缓存 7 天
proxy_cache_valid 302 1h; # 临时重定向缓存 1 小时
proxy_cache_valid 404 1m; # 404 缓存 1 分钟,防止穿透
proxy_cache_valid any 0; # 其他状态码不缓存
# 缓存锁:同一资源并发请求时,只有第一个回源,其余等待
# 防止缓存击穿(热点资源过期瞬间大量请求打到源站)
proxy_cache_lock on;
proxy_cache_lock_timeout 5s; # 等待超时后放行回源
proxy_cache_lock_age 5s; # 锁持有超时,防止死锁
# 源站故障时返回过期缓存(兜底策略)
proxy_cache_use_stale error timeout updating
http_500 http_502 http_503 http_504;
# 后台异步更新过期缓存,用户不感知延迟
proxy_cache_background_update on;
# 缓存最小使用次数,冷门资源不缓存(减少磁盘写入)
proxy_cache_min_uses 2;
# 传递必要头部给源站
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
expires 30d; # 客户端浏览器缓存 30 天
}
# 动态请求不缓存,直接代理
location / {
proxy_pass http://backend_origin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 带 Cookie 或特定参数的请求绕过缓存
proxy_cache_bypass $http_cookie;
proxy_no_cache $http_cookie;
}
}
缓存清除(Purge)
需要编译 ngx_cache_purge 模块。
# 缓存清除接口(仅允许内网访问)
location ~ /purge(/.*) {
allow 10.0.0.0/8;
allow 172.16.0.0/12;
deny all;
proxy_cache_purge cdn_cache "$scheme$host$1";
}
# 清除指定资源缓存
curl -X PURGE http://cdn.example.com/purge/static/css/main.abc123.css
Varnish 缓存加速
Varnish 架构原理
Varnish 的请求处理是一条VCL状态机流水线:
客户端请求 → vcl_recv(接收请求,决定是否查缓存)
→ vcl_hash(计算缓存键)
→ 缓存查找
├─ 命中 → vcl_hit(决定是否直接返回)→ vcl_deliver(返回响应)
└─ 未命中 → vcl_miss → vcl_backend_fetch(回源)
→ vcl_backend_response(处理源站响应,决定是否缓存)
→ vcl_deliver(返回响应)
Varnish 默认将缓存存储在内存中(malloc),性能极高。大容量场景可使用 file 存储引擎或商业版的MSE。
VCL 配置
# /etc/varnish/default.vcl
vcl 4.1;
# 源站定义
backend default {
.host = "10.0.1.10";
.port = "80";
.connect_timeout = 3s; # 连接超时
.first_byte_timeout = 10s; # 首字节超时
.between_bytes_timeout = 5s; # 字节间超时
.max_connections = 256; # 最大连接数
# 健康检查
.probe = {
.url = "/health";
.interval = 5s;
.timeout = 2s;
.window = 5;
.threshold = 3;
}
}
# 接收请求阶段
sub vcl_recv {
# 缓存清除接口(仅允许内网)
if (req.method == "PURGE") {
if (!client.ip ~ internal) {
return (synth(403, "Forbidden"));
}
return (purge);
}
# BAN 模式批量失效(正则匹配)
if (req.method == "BAN") {
if (!client.ip ~ internal) {
return (synth(403, "Forbidden"));
}
ban("req.url ~ " + req.http.X-Ban-Url);
return (synth(200, "Banned"));
}
# 只缓存 GET 和 HEAD 请求
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
# 移除不影响缓存的 Cookie(保留登录态相关的)
# 静态资源完全去除 Cookie
if (req.url ~ "\.(jpg|jpeg|png|gif|webp|css|js|woff2|svg|ico)(\?.*)?$") {
unset req.http.Cookie;
return (hash);
}
# 动态页面带 session Cookie 的不缓存
if (req.http.Cookie ~ "session_id") {
return (pass);
}
# 其他请求去除无关 Cookie 后尝试缓存
unset req.http.Cookie;
return (hash);
}
# 源站响应处理阶段
sub vcl_backend_response {
# 静态资源缓存 7 天
if (bereq.url ~ "\.(jpg|jpeg|png|gif|webp|css|js|woff2|svg|ico)(\?.*)?$") {
set beresp.ttl = 7d;
set beresp.grace = 24h; # Grace 期:源站故障时可返回过期 24 小时内的缓存
unset beresp.http.Set-Cookie;
}
# HTML 页面缓存 5 分钟
if (beresp.http.Content-Type ~ "text/html") {
set beresp.ttl = 5m;
set beresp.grace = 1h;
}
# 源站返回 5xx 时不缓存错误页面
if (beresp.status >= 500) {
set beresp.ttl = 0s;
set beresp.uncacheable = true;
return (deliver);
}
}
# 返回响应阶段
sub vcl_deliver {
# 添加缓存命中标识
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
set resp.http.X-Cache-Hits = obj.hits;
} else {
set resp.http.X-Cache = "MISS";
}
}
# 内网 ACL
acl internal {
"10.0.0.0"/8;
"172.16.0.0"/12;
"192.168.0.0"/16;
}
全站 HTTPS 加速配置
TLS 1.3 与性能优化
# /etc/nginx/conf.d/ssl.conf
server {
listen 443 ssl http2;
server_name cdn.example.com;
# 证书配置
ssl_certificate /etc/nginx/ssl/cdn.example.com.pem;
ssl_certificate_key /etc/nginx/ssl/cdn.example.com.key;
# 协议版本:只保留 TLS 1.2 和 1.3
ssl_protocols TLSv1.2 TLSv1.3;
# TLS 1.3 密码套件(性能优先)
ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# OCSP Stapling:由服务器代替客户端查询证书吊销状态
# 省去客户端单独请求 CA 的 OCSP 服务器,减少 100-300ms 延迟
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/ca-chain.pem;
resolver 8.8.8.8 223.5.5.5 valid=300s;
resolver_timeout 5s;
# Session 复用:避免每次连接都做完整 TLS 握手
ssl_session_cache shared:SSL:50m; # 50MB 共享缓存,约可存 20 万个 Session
ssl_session_timeout 1d; # Session 有效期 1 天
ssl_session_tickets on; # Session Ticket 支持分布式场景
# 多台 Nginx 需共享 ticket key 文件
# ssl_session_ticket_key /etc/nginx/ssl/ticket.key;
# 103 Early Hints
location / {
add_header Link "</static/css/main.css>; rel=preload; as=style" always;
add_header Link "</static/js/app.js>; rel=preload; as=script" always;
proxy_pass http://backend_origin;
}
}
Brotli 与 Gzip 压缩
# /etc/nginx/conf.d/compression.conf
# Brotli 需编译 ngx_brotli 模块
# Brotli 压缩(压缩率比 Gzip 高 15-25%)
brotli on;
brotli_comp_level 6; # 1-11,6 是性能和压缩率的平衡点
brotli_min_length 256; # 小于 256 字节不压缩
brotli_types text/plain text/css text/javascript
application/javascript application/json
application/xml image/svg+xml;
# Gzip 兜底(不支持 Brotli 的客户端)
gzip on;
gzip_comp_level 5; # 1-9,5 是常用平衡点
gzip_min_length 256;
gzip_vary on; # 输出 Vary: Accept-Encoding
gzip_types text/plain text/css text/javascript
application/javascript application/json
application/xml image/svg+xml;
回源策略优化
长连接复用与超时控制
# /etc/nginx/conf.d/upstream.conf
upstream backend_origin {
server 10.0.1.10:80 weight=5;
server 10.0.1.11:80 weight=3;
server 10.0.1.12:80 backup; # 备用源站,主源全挂时启用
# 长连接复用:减少 TCP 握手开销
keepalive 64; # 每个 worker 保持 64 个空闲长连接
keepalive_timeout 60s; # 空闲连接超时
keepalive_requests 1000; # 单连接最大请求数
}
server {
location / {
proxy_pass http://backend_origin;
# 必须设置 HTTP/1.1 和清空 Connection 头才能启用 keepalive
proxy_http_version 1.1;
proxy_set_header Connection "";
# 回源超时控制
proxy_connect_timeout 5s; # 连接源站超时
proxy_send_timeout 10s; # 发送请求超时
proxy_read_timeout 30s; # 读取响应超时(大文件适当加大)
# 失败重试:连接错误或超时时尝试下一台源站
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2; # 最多重试 2 次
proxy_next_upstream_timeout 10s; # 重试总超时
}
}
Range 回源(大文件分片)
大文件按客户端请求的 Range 分片回源,减少带宽和存储压力。
# 大文件分片回源
location ~* \.(mp4|zip|iso|tar\.gz)$ {
proxy_pass http://backend_origin;
proxy_cache cdn_cache;
# 启用 Range 回源,CDN 按分片缓存
slice 1m; # 每片 1MB
proxy_cache_key "$scheme$host$uri$slice_range";
proxy_set_header Range $slice_range;
proxy_cache_valid 200 206 7d; # 206 Partial Content 也缓存
proxy_http_version 1.1;
}
回源请求头改写
# 传递真实客户端信息给源站
proxy_set_header Host $host; # 保持原始 Host
proxy_set_header X-Real-IP $remote_addr; # 真实客户端 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # 原始协议(http/https)
# 自定义回源标识,源站可据此区分 CDN 回源和直接访问
proxy_set_header X-CDN-Node $hostname;
proxy_set_header X-CDN-Request-ID $request_id;
示例代码和配置
Nginx 全站加速完整配置
# /etc/nginx/nginx.conf
user nginx;
worker_processes auto; # 自动匹配 CPU 核数
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
events {
worker_connections 16384;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式:包含缓存状态字段
log_format cdn '$remote_addr - [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'cache:$upstream_cache_status '
'rt:${request_time}s '
'upstream:$upstream_addr';
access_log /var/log/nginx/access.log cdn;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
# 缓存存储区域
proxy_cache_path /data/nginx/cache
levels=1:2
keys_zone=cdn_cache:256m
max_size=50g
inactive=7d
use_temp_path=off;
proxy_cache_key "$scheme$host$request_uri";
# Brotli 压缩
brotli on;
brotli_comp_level 6;
brotli_min_length 256;
brotli_types text/plain text/css text/javascript
application/javascript application/json
application/xml image/svg+xml;
# Gzip 兜底
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_vary on;
gzip_types text/plain text/css text/javascript
application/javascript application/json
application/xml image/svg+xml;
# 源站集群
upstream backend_origin {
server 10.0.1.10:80 weight=5;
server 10.0.1.11:80 weight=3;
server 10.0.1.12:80 backup;
keepalive 64;
keepalive_timeout 60s;
keepalive_requests 1000;
}
# HTTP → HTTPS 强制跳转
server {
listen 80;
server_name cdn.example.com;
return 301 https://$host$request_uri;
}
# HTTPS 主配置
server {
listen 443 ssl http2;
server_name cdn.example.com;
ssl_certificate /etc/nginx/ssl/cdn.example.com.pem;
ssl_certificate_key /etc/nginx/ssl/cdn.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/ca-chain.pem;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
resolver 8.8.8.8 223.5.5.5 valid=300s;
# 缓存状态响应头
add_header X-Cache-Status $upstream_cache_status always;
# 缓存清除接口
location ~ /purge(/.*) {
allow 10.0.0.0/8;
allow 172.16.0.0/12;
deny all;
proxy_cache_purge cdn_cache "$scheme$host$1";
}
# 静态资源:长缓存
location ~* \.(jpg|jpeg|png|gif|webp|ico|css|js|woff2|ttf|svg)$ {
proxy_pass http://backend_origin;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache cdn_cache;
proxy_cache_valid 200 301 7d;
proxy_cache_valid 404 1m;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_cache_min_uses 2;
expires 30d;
}
# 大文件分片缓存
location ~* \.(mp4|zip|iso|tar\.gz)$ {
proxy_pass http://backend_origin;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_cache cdn_cache;
slice 1m;
proxy_cache_key "$scheme$host$uri$slice_range";
proxy_set_header Range $slice_range;
proxy_cache_valid 200 206 7d;
}
# 动态请求:直接代理
location / {
proxy_pass http://backend_origin;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
}
}
}
缓存预热脚本
#!/bin/bash
# cdn_warmup.sh - CDN 缓存预热脚本
# 从 sitemap 或 URL 列表批量预热缓存
set -euo pipefail
# ========== 配置区 ==========
CDN_DOMAIN="https://cdn.example.com"
URL_LIST="/opt/cdn/warmup_urls.txt" # URL 列表文件,每行一个路径
SITEMAP_URL="" # 可选:从 sitemap.xml 提取 URL
CONCURRENCY=10 # 并发数,根据源站承受能力调整
LOG_FILE="/var/log/cdn/warmup_$(date +%Y%m%d_%H%M%S).log"
TIMEOUT=10 # 单个请求超时(秒)
USER_AGENT="CDN-Warmup-Bot/1.0"
# ========== 函数定义 ==========
# 日志输出
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"
}
# 从 sitemap.xml 提取 URL
extract_from_sitemap() {
local sitemap_url="$1"
local tmp_file="/tmp/sitemap_urls_$$.txt"
log "从 sitemap 提取 URL: ${sitemap_url}"
curl -s --max-time 30 "${sitemap_url}" \
| grep -oP '<loc>\K[^<]+' \
| sed "s|${CDN_DOMAIN}||" \
> "${tmp_file}"
local count
count=$(wc -l < "${tmp_file}")
log "提取到 ${count} 个 URL"
echo "${tmp_file}"
}
# 预热单个 URL
warmup_single() {
local path="$1"
local url="${CDN_DOMAIN}${path}"
local http_code
local cache_status
# 发送请求,提取状态码和缓存状态头
local response
response=$(curl -s -o /dev/null -w "%{http_code}" \
--max-time "${TIMEOUT}" \
-H "User-Agent: ${USER_AGENT}" \
-D - "${url}" 2>/dev/null)
http_code="${response}"
echo "${http_code}${path}" >> "${LOG_FILE}"
}
# 并发预热
warmup_batch() {
local url_file="$1"
local total
total=$(wc -l < "${url_file}")
local completed=0
log "开始预热,共 ${total} 个 URL,并发数 ${CONCURRENCY}"
# 使用 xargs 控制并发
cat "${url_file}" | xargs -P "${CONCURRENCY}" -I {} bash -c '
url="'${CDN_DOMAIN}'{}"
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time "'${TIMEOUT}'" \
-H "User-Agent: '${USER_AGENT}'" "${url}" 2>/dev/null)
echo "${code} {}"
' | tee -a "${LOG_FILE}"
log "预热完成"
}
# ========== 主流程 ==========
mkdir -p "$(dirname "${LOG_FILE}")"
log "===== CDN 缓存预热开始 ====="
# 确定 URL 来源
if [[ -n "${SITEMAP_URL}" ]]; then
URL_FILE=$(extract_from_sitemap "${SITEMAP_URL}")
elif [[ -f "${URL_LIST}" ]]; then
URL_FILE="${URL_LIST}"
else
log "错误:未找到 URL 列表文件 ${URL_LIST},也未配置 SITEMAP_URL"
exit 1
fi
warmup_batch "${URL_FILE}"
# 统计结果
log "===== 预热统计 ====="
log "200 响应: $(grep -c '^200' "${LOG_FILE}" || true) 个"
log "非 200 响应: $(grep -cv '^200\|^====\|^开始\|^预热\|^从\|^提取\|^\[' "${LOG_FILE}" || true) 个"
log "===== CDN 缓存预热结束 ====="
缓存命中率分析脚本
#!/bin/bash
# cdn_cache_stats.sh - CDN 缓存命中率分析
# 解析 Nginx 访问日志,统计 HIT/MISS/EXPIRED/BYPASS 比例
set -euo pipefail
# ========== 配置区 ==========
ACCESS_LOG="${1:-/var/log/nginx/access.log}"
REPORT_FILE="/var/log/cdn/cache_report_$(date +%Y%m%d).txt"
# ========== 参数校验 ==========
if [[ ! -f "${ACCESS_LOG}" ]]; then
echo "错误:日志文件不存在 ${ACCESS_LOG}"
exit 1
fi
mkdir -p "$(dirname "${REPORT_FILE}")"
# ========== 分析函数 ==========
analyze_cache() {
local log_file="$1"
echo "=========================================="
echo " CDN 缓存命中率分析报告"
echo " 日志文件: ${log_file}"
echo " 分析时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="
echo ""
# 提取缓存状态字段(日志格式中 cache: 后面的值)
local total hit miss expired bypass stale updating revalidated
total=$(grep -c 'cache:' "${log_file}" || echo 0)
hit=$(grep -c 'cache:HIT' "${log_file}" || echo 0)
miss=$(grep -c 'cache:MISS' "${log_file}" || echo 0)
expired=$(grep -c 'cache:EXPIRED' "${log_file}" || echo 0)
bypass=$(grep -c 'cache:BYPASS' "${log_file}" || echo 0)
stale=$(grep -c 'cache:STALE' "${log_file}" || echo 0)
updating=$(grep -c 'cache:UPDATING' "${log_file}" || echo 0)
revalidated=$(grep -c 'cache:REVALIDATED' "${log_file}" || echo 0)
echo "【总体统计】"
echo " 总请求数: ${total}"
echo " HIT: ${hit}"
echo " MISS: ${miss}"
echo " EXPIRED: ${expired}"
echo " BYPASS: ${bypass}"
echo " STALE: ${stale}"
echo " UPDATING: ${updating}"
echo " REVALIDATED: ${revalidated}"
echo ""
# 计算命中率(HIT + STALE + UPDATING + REVALIDATED 都算有效命中)
if [[ "${total}" -gt 0 ]]; then
local effective_hit=$((hit + stale + updating + revalidated))
local hit_rate
hit_rate=$(awk "BEGIN {printf \"%.2f\", ${effective_hit}/${total}*100}")
local miss_rate
miss_rate=$(awk "BEGIN {printf \"%.2f\", ${miss}/${total}*100}")
local bypass_rate
bypass_rate=$(awk "BEGIN {printf \"%.2f\", ${bypass}/${total}*100}")
echo "【命中率】"
echo " 有效命中率: ${hit_rate}%(含 HIT/STALE/UPDATING/REVALIDATED)"
echo " MISS 率: ${miss_rate}%"
echo " BYPASS 率: ${bypass_rate}%"
echo ""
# 命中率预警
if (( $(echo "${hit_rate} < 80" | bc -l) )); then
echo "⚠️ 警告:缓存命中率低于 80%,建议检查:"
echo " - Cache-Control 头是否正确设置"
echo " - 缓存键是否包含不必要的变量(如 Cookie)"
echo " - 是否有大量带查询参数的请求未归一化"
echo ""
fi
fi
# MISS 最多的 URL(Top 20)
echo "【MISS 最多的 URL(Top 20)】"
grep 'cache:MISS' "${log_file}" \
| awk '{print $4}' \
| sed 's/"//g' \
| sort | uniq -c | sort -rn \
| head -20 \
| awk '{printf " %6d 次 %s\n", $1, $2}'
echo ""
# BYPASS 请求分析
echo "【BYPASS 请求来源(Top 10)】"
grep 'cache:BYPASS' "${log_file}" \
| awk '{print $4}' \
| sed 's/"//g' \
| sort | uniq -c | sort -rn \
| head -10 \
| awk '{printf " %6d 次 %s\n", $1, $2}'
}
# ========== 主流程 ==========
analyze_cache "${ACCESS_LOG}" | tee "${REPORT_FILE}"
echo ""
echo "报告已保存至: ${REPORT_FILE}"
最佳实践和注意事项
最佳实践
缓存策略设计
CDN缓存的核心是按资源类型分层设计,不同类型的资源对实时性和一致性的要求差异很大:
- HTML 页面:短缓存(60s~300s),保证内容更新能快速生效。
- CSS/JS 静态资源:长缓存(30d+),通过文件名哈希指纹实现版本控制。
- 图片/视频/字体:长缓存(90d+),变更频率极低,命中率收益最大。
- API 接口:原则上不缓存,或仅对幂等GET请求做极短缓存(5s~10s)。
Cache-Control 最佳实践矩阵:
| 资源类型 |
缓存策略 |
TTL |
缓存键 |
说明 |
| HTML |
no-cache |
60s~300s |
URI |
每次回源验证,保证内容新鲜 |
| CSS/JS(带 hash) |
public, immutable |
365d |
URI |
文件名含 hash,内容变更即换 URL |
| CSS/JS(无 hash) |
public, must-revalidate |
7d |
URI |
依赖 ETag/Last-Modified 验证 |
| 图片/视频 |
public |
90d |
URI |
大文件长缓存,节省回源带宽 |
| API(GET) |
private, no-store |
不缓存 |
— |
动态数据禁止 CDN 缓存 |
| 字体文件 |
public |
180d |
URI |
变更极少,注意 CORS 头 |
缓存一致性保障
缓存最棘手的问题不是“缓存了什么”,而是“什么时候该失效”。
主动清除:发布部署后通过API精确清除变更资源的缓存。
# Nginx 缓存清除(需要 ngx_cache_purge 模块)
curl -X PURGE https://cdn.example.com/css/main.abc123.css
版本化URL:前端构建工具自动在文件名中注入内容哈希,内容变更时URL自然变化,无需手动清缓存。
# 构建产物示例
main.3a7b2c1d.js → 内容变更后 → main.9e4f8a2b.js
style.f1c2d3e4.css → 内容变更后 → style.b5a6c7d8.css
stale-while-revalidate策略:缓存过期后先返回旧内容,同时异步回源更新。
proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
# 或通过 Cache-Control 头控制
add_header Cache-Control "public, max-age=600, stale-while-revalidate=30";
性能优化
Brotli压缩:相比Gzip,Brotli在同等压缩率下体积更小约15%~25%。生产环境建议静态资源预压缩用级别11,动态压缩用级别4~6。
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
# 静态预压缩(优先使用 .br 文件)
brotli_static on;
连接预热:在HTML <head> 中提前建立与CDN域名的连接,减少首次请求延迟。
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//cdn.example.com">
<!-- 预连接(含 TLS 握手) -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
安全防护
防盗链配置:
# 基于 Referer 的防盗链
valid_referers none blocked server_names *.example.com;
if ($invalid_referer) {
return 403;
}
速率限制:
# 定义限速区域
limit_req_zone $binary_remote_addr zone=cdn_limit:10m rate=100r/s;
limit_conn_zone $binary_remote_addr zone=cdn_conn:10m;
server {
# 突发允许 200,超出部分排队
limit_req zone=cdn_limit burst=200 nodelay;
# 单 IP 最大并发连接数
limit_conn cdn_conn 50;
}
源站IP隐藏:CDN架构下源站IP不应暴露在公网DNS中。源站防火墙仅放行CDN节点回源IP段。
注意事项
配置注意事项
警告:动态内容误缓存是CDN事故的头号元凶
用户登录态、购物车、个人中心等包含 Set-Cookie 的响应一旦被CDN缓存,会导致严重安全事故。必须确保:
-
动态接口响应头包含 Cache-Control: private, no-store。
-
在Nginx配置中通过 proxy_no_cache 和 proxy_cache_bypass 排除带Cookie的请求。
-
在VCL中对 req.http.Cookie 存在的请求执行 pass。
# 排除带 Cookie 的请求进入缓存
set $no_cache 0;
if ($http_cookie ~* "session_id|token|logged_in") {
set $no_cache 1;
}
proxy_no_cache $no_cache;
proxy_cache_bypass $no_cache;
-
缓存空间不足:proxy_cache_path 的 max_size 设置过小,热点资源被反复淘汰又重新回源。
-
Vary头滥用:源站返回 Vary: * 或过多Vary字段会导致缓存碎片化严重。
-
HTTPS回源证书校验:CDN回源使用HTTPS时,源站证书必须合法有效。
常见错误
| 错误现象 |
原因分析 |
解决方案 |
| 缓存命中率长期低于 60% |
Cache Key 包含了查询参数、Cookie 等变量 |
精简 proxy_cache_key,忽略无关查询参数 |
| 接入 CDN 后源站压力未下降 |
缓存规则未生效或 TTL 过短 |
检查 X-Cache-Status,延长静态资源 TTL |
| 用户反馈看到过期/错误内容 |
缓存未及时清除,或多层缓存中某层残留旧数据 |
全链路清除缓存,响应头禁止浏览器强缓存 |
| 回源频繁出现 5xx 错误 |
源站过载或回源超时设置过短 |
启用 proxy_cache_lock 合并回源,配置源站限流 |
兼容性问题
- HTTP/2 与旧客户端:HTTP/2 要求 TLS 1.2+,老旧客户端不支持。需保留 HTTP/1.1 降级能力。
- Brotli 浏览器支持:Brotli 仅在 HTTPS 下生效,且 IE 全系列不支持。需同时保留 Gzip。
故障排查和监控
故障排查
缓存状态诊断
排查CDN问题的第一步是查看响应头。三个关键头字段:
# 查看缓存相关响应头
curl -sI https://cdn.example.com/css/main.abc123.css | grep -iE "x-cache|age|via|cf-cache"
# 典型输出解读
# X-Cache-Status: HIT → 缓存命中
# X-Cache-Status: MISS → 缓存未命中
# X-Cache-Status: EXPIRED → 缓存过期,重新回源
# Age: 3600 → 缓存已存活3600秒
常见问题排查
问题一:缓存命中率低
排查方向:Cache Key设计不合理、Vary头过多、查询参数差异导致缓存碎片化。
# 忽略指定查询参数,规范化 Cache Key
set $clean_uri $uri;
if ($args ~* "^(.*)(?:&|^)(_t|timestamp|_=\d+)(.*)$") {
set $clean_uri $uri?$1$3;
}
proxy_cache_key "$scheme$host$clean_uri";
问题二:缓存内容过期不更新
排查方向:PURGE请求是否到达所有节点、浏览器本地强缓存未清除、多层缓存未同步清除。
问题三:回源异常
排查方向:源站超时、DNS解析失败、回源SSL证书问题。
Varnish 调试
# 实时查看请求处理流程
varnishlog -g request -q "ReqURL ~ '/api/'"
# 查看缓存统计概览
varnishstat -1 | grep -E "cache_hit|cache_miss|backend_fail|n_object"
性能监控
关键指标
| 指标名称 |
正常范围 |
告警阈值 |
说明 |
| 缓存命中率 |
> 85% |
< 70% |
核心指标,低于阈值需优化策略 |
| 回源带宽 |
< 总带宽 20% |
> 总带宽 40% |
回源占比过高说明缓存未起效 |
| 响应时间 P95 |
< 100ms |
> 500ms |
边缘节点响应应在毫秒级 |
| 5xx 错误率 |
< 0.1% |
> 1% |
突增通常意味源站或节点故障 |
总结与进阶
技术要点回顾
- 缓存分层是基础:按资源类型设计差异化策略,静态长缓存+内容哈希,动态内容严禁缓存。
- 一致性保障是关键:版本化URL是最可靠方案,PURGE API作为补充,多层缓存需全链路同步清除。
- 监控驱动运维:缓存命中率、回源带宽、延迟、错误率四个核心指标必须持续监控。
- 安全不可忽视:防盗链、速率限制、源站IP隐藏构成基本防护面。
- 应急预案要提前:缓存雪崩、节点宕机等场景处置流程需提前演练。
进阶学习方向
- 边缘计算:将计算逻辑下沉到CDN边缘节点,减少回源依赖,如A/B测试分流、请求改写。
- Serverless@Edge:在边缘运行JavaScript/Wasm,实现动态内容的边缘生成,适合个性化内容注入。
- QUIC/HTTP3 加速:基于UDP消除TCP队头阻塞,0-RTT连接建立显著降低延迟。
- P2P CDN:利用终端用户上行带宽进行内容分发,适合直播、大文件等高带宽场景。
结语
构建一个高效、可靠的CDN系统,远不止是配置一个反向代理那么简单。它要求开发者深入理解HTTP协议、网络架构,并具备精细化的运维能力。从精准的缓存策略设计,到严密的安全防护,再到全面的监控告警,每一个环节都至关重要。希望本文详尽的原理剖析、配置示例和最佳实践,能为你构建或优化自己的CDN架构提供切实的帮助。如果你在实践中遇到更复杂的问题,或者想与其他开发者交流 Nginx 与 网络 优化的心得,欢迎来 云栈社区 一起探讨。
附录
命令速查表
# ========== curl 缓存调试 ==========
curl -sI https://cdn.example.com/path # 查看响应头
curl -X PURGE https://cdn.example.com/path # 清除指定 URL 缓存
# ========== Nginx 缓存管理 ==========
nginx -t # 检查配置语法
nginx -s reload # 平滑重载配置
grep "cache=" /var/log/nginx/access.log | awk '{print $6}' | sort | uniq -c # 统计缓存命中分布
# ========== Varnish 管理命令 ==========
varnishadm ban req.url "~" "^/api/" # 按正则批量清除缓存
varnishstat -1 | grep -E "hit|miss|backend" # 快速查看命中率和后端状态
配置参数详解
Nginx proxy_cache 核心指令:
| 指令 |
默认值 |
说明 |
proxy_cache_path |
— |
定义缓存存储路径、分级目录、共享内存区、最大容量 |
proxy_cache_key |
$scheme$proxy_host$request_uri |
缓存键,决定如何区分不同的缓存对象 |
proxy_cache_valid |
— |
按状态码设置缓存有效期 |
proxy_cache_lock |
off |
开启后同一资源只允许一个请求回源,防止回源风暴 |
proxy_cache_use_stale |
off |
源站异常时允许返回过期缓存的条件 |