点播与直播并存的香港服务器多 CDN 回源架构——从缓存穿透到“熔断”降级的端到端部署教程

我第一次真正意识到“缓存穿透”的威力,是在一个周五的晚上。23:47,香港机房的告警把我从外卖汤里拉了出来:回源 QPS 从 1.2k 抬到 9.8k,5xx 比例跃升,直播 HLS 段子抽风,点播的长尾小文件也在疯狂 MISS。更糟的是,多家 CDN 同时回源——同一批不存在的分片名,像弹幕一样打在我的 NVMe 上。
那一夜,我把“负缓存(negative cache)”“熔断(circuit breaker)”“serve-stale”“备用源回切”全部掀起来用,凌晨 02:10 指标稳定下来。第二天,我把整个方案重构成标准化剧本,这篇文章就是当时的完整复盘与可复用教程。
1. 场景与目标
场景:同一组香港源站同时为**点播(VOD)与直播(Live/LL-HLS)**提供内容,多家 CDN 并行回源。
痛点:
- 缓存穿透(大量请求命中不存在资源/冷资源,导致 MISS 风暴);
- 多 CDN 同时回源放大(长尾请求×N 家 CDN);
- 回源链路雪崩(瞬时 5xx 飙升,良性缓存也跟着失效);
- 直播对时延敏感,点播对吞吐敏感,策略需要分域/分目录差异化。
目标:
- 把回源 QPS 峰值压回 30% 以下;
- 5xx 比例 < 0.5%;
- 直播 M3U8 TTFB < 80 ms(香港区域);
- 出现故障时自动熔断并优雅降级(回落备用源/静态“占位片”/stale 缓存)。
2. 硬件与系统基线(香港机房)
| 角色 | 机型 | CPU | 内存 | 磁盘 | 网卡 | 系统 | 备注 |
|---|---|---|---|---|---|---|---|
| 源站-1(主) | 1U 独服 | 2×Xeon Silver 4310 | 128GB | 2×1.92TB NVMe(RAID1) | 2×10GbE(Bond) | CentOS 7.9 | BGP 3 线 2×10G |
| 源站-2(备) | 1U 独服 | 2×Xeon Silver 4210 | 128GB | 2×1.92TB NVMe(RAID1) | 2×10GbE(Bond) | CentOS 7.9 | 异机房同城 |
| 对象存储(归档/冷) | — | — | — | — | 10GbE 接入 | — | 仅作冷数据/回补 |
内核与文件句柄(/etc/sysctl.conf 与 limits):
# /etc/sysctl.d/99-tuning.conf
net.core.somaxconn=65535
net.core.netdev_max_backlog=250000
net.ipv4.tcp_max_syn_backlog=262144
net.ipv4.tcp_tw_reuse=1
net.ipv4.ip_local_port_range=1024 65000
net.ipv4.tcp_fin_timeout=15
fs.file-max=2097152
# 生效
sysctl --system
# /etc/security/limits.conf
* soft nofile 1048576
* hard nofile 1048576
3. 逻辑架构(文字图)
- CDN-A / CDN-B / CDN-C →(回源)→ 香港源站集群(OpenResty/Nginx + Redis) →(本地缓存 + 负缓存 + 熔断)→ 对象存储/备份源
- 目录拆分:/vod/(点播 HLS/DASH 大对象+切片)、/live/(直播 HLS/LL-HLS 段)。
- 分域配置:vod.example.com 与 live.example.com,缓存与熔断阈值不同。
- 多 CDN 回源:统一 Host Header 与 Range 支持,启用 Origin Shield/回源中转(若 CDN 支持)。
- 观测:Nginx 访问日志 + vts/exporter + Redis 指标 + node_exporter → Prometheus → Grafana 告警。
4. CDN 回源与缓存策略(通用建议)
| 项 | 建议设置 | 说明 |
|---|---|---|
| 回源协议 | HTTPS/HTTP2(支持 Range) | 保障段文件 206 缓存有效 |
| Host 回源 | 保持业务域名(不改 Host) | 便于同构配置与 SNI |
| Cache Key | path + normalized query (去噪) |
只保留必要签名/版本号 |
| 忽略参数 | utm_*、_t、rnd 等 |
防穿透的第一道闸 |
| 负缓存 | 4xx 负缓存(404/403/410) | 短 TTL(30~120s)即可 |
| Stale | stale-if-error / serve-stale |
故障期兜底 |
| 目录 TTL | /live/ 短、/vod/ 中长 |
M3U8 不宜长,TS/CMF 可长 |
| 回源限速 | 每 CDN 源连数 & 速率限制 | 防止单家放大打穿源站 |
| IP 白名单 | 仅放行 CDN 回源段 IP 段 | 其它丢 444 或 403 |
5. 源站部署(CentOS 7 + OpenResty)
5.1 安装 OpenResty 与 Redis
# OpenResty
yum install -y epel-release
yum install -y openresty openresty-resty
# Redis
yum install -y redis
systemctl enable redis && systemctl start redis
5.2 Nginx 全局优化
# /etc/openresty/nginx.conf(关键片段)
worker_processes auto;
events { worker_connections 65535; use epoll; multi_accept on; }
http {
include mime.types;
sendfile on; tcp_nopush on; tcp_nodelay on; aio on;
keepalive_timeout 65; keepalive_requests 10000;
open_file_cache max=200000 inactive=60s; open_file_cache_valid 120s;
# 缓存区
proxy_cache_path /data/cache levels=1:2 keys_zone=VODCACHE:16g max_size=1500g inactive=1h use_temp_path=off;
proxy_cache_path /data/livecache levels=1:2 keys_zone=LIVECACHE:4g max_size=200g inactive=5m use_temp_path=off;
# 负缓存与 stale
proxy_cache_lock on; proxy_cache_min_uses 2;
proxy_cache_use_stale error timeout invalid_header http_500 http_502 http_503 updating;
proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
# 日志
log_format main '$remote_addr $host "$request" $status $body_bytes_sent '
'$upstream_status $upstream_cache_status $request_time $upstream_response_time '
'"$http_referer" "$http_user_agent" cdn="$http_x_forwarded_host"';
access_log /var/log/nginx/access.log main;
lua_shared_dict breaker 10m;
lua_shared_dict hotset 50m;
# 上游
upstream backend_vod { server 127.0.0.1:18080 max_fails=3 fail_timeout=10s; keepalive 512; }
upstream backend_live{ server 127.0.0.1:18081 max_fails=3 fail_timeout=5s; keepalive 512; }
# … 以下 server 分别处理 VOD 与 LIVE
}
5.3 反穿透与“熔断”Lua(OpenResty)
思路:
- 在 access_by_lua_block 中统计过去 30s 的 5xx 比例与上游超时;
- 命中阈值即触发“熔断”:对新增回源返回 302 指向备用源或serve-stale;
- 对明显穿透(非白名单后缀/不存在 ID)直接 403/444;
- 对 404 写入负缓存,短 TTL。
# 片段:反穿透与熔断(示例)
server {
listen 443 ssl http2;
server_name vod.example.com;
ssl_certificate /etc/ssl/vod.crt;
ssl_certificate_key /etc/ssl/vod.key;
set $is_vod 1;
location /vod/ {
# 只允许必要的查询参数进入 cache key
set $normalized_q "";
if ($arg_v != "") { set $normalized_q "$normalized_q&v=$arg_v"; }
if ($arg_sig != "") { set $normalized_q "$normalized_q&sig=$arg_sig"; }
set $cache_key "$uri?$normalized_q";
# 反穿透白名单
if ($uri !~* "\.(m3u8|mpd|cmf|mp4|m4s|ts)$") { return 403; }
# Lua 访问阶段:熔断/签名校验/热集保护
access_by_lua_block {
local dict = ngx.shared.breaker
local key = "vod:errrate"
local now = ngx.now()
-- 简化:滑窗统计(可换成 prometheus ngx lib)
local err = dict:get(key) or 0
if err > 50 then -- 阈值示例:30s 内累计 5xx > 50 次
-- 设置响应头标记熔断
ngx.header["X-Breaker"] = "tripped"
-- 回退策略一:临时 302 到备用源(同路径)
return ngx.redirect("https://backup.example.com" .. ngx.var.request_uri, 302)
end
-- 简易签名/时间戳检查(示例)
local sig = ngx.var.arg_sig
if not sig or #sig < 10 then
return ngx.exit(403)
end
}
proxy_cache VODCACHE;
proxy_cache_key $cache_key;
# 负缓存设置
proxy_cache_valid 200 206 302 10m;
proxy_cache_valid 404 403 410 1m;
proxy_ignore_client_abort on;
proxy_request_buffering on;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_read_timeout 30s;
proxy_send_timeout 30s;
proxy_pass http://backend_vod;
}
}
说明:实际生产里我会把“熔断阈值”做成基于比例(如 5xx rate > 3%@30s)并结合上游 RTT/超时,Lua 里用 ngx.shared.DICT + 定时器或用 prometheus-nginx 统计更稳。
5.4 Live 专用服务块
M3U8 短 TTL(2~5s),TS/CMF 段 中 TTL(30~120s)
强制 Range 支持(206),避免 CDN 不缓存 200 整文件
server {
listen 443 ssl http2;
server_name live.example.com;
location ~* \.m3u8$ {
proxy_cache LIVECACHE;
proxy_cache_valid 200 302 2s;
proxy_cache_valid 404 1m;
add_header Cache-Control "max-age=2,stale-if-error=30,stale-while-revalidate=2";
proxy_set_header Host $host;
proxy_pass http://backend_live;
}
location ~* \.(ts|m4s|cmf)$ {
proxy_cache LIVECACHE;
proxy_cache_valid 200 206 2m;
proxy_set_header Range $http_range;
proxy_force_ranges on;
proxy_pass http://backend_live;
}
}
6. 缓存穿透的根治:负缓存 + “存在性”校验
做法:
- 负缓存:把 404/403/410 设置短 TTL(30~120s),拦截爆破式请求;
- 存在性校验:在访问前用 Redis/本地索引判断 content_id 是否存在,不存在直接 403/444;
- Key 规范化:只保留必要参数,去噪 utm_*、随机数等。
Lua + Redis 简例(存在性校验):
access_by_lua_block {
local redis = require "resty.redis"
local r = redis:new()
r:set_timeout(10)
local ok, err = r:connect("127.0.0.1", 6379)
if not ok then return ngx.exit(500) end
local id = ngx.var.arg_id -- 业务 ID
if not id then return ngx.exit(403) end
local exists = r:sismember("vod:ids", id)
if exists == 0 then
return ngx.exit(403) -- 直接拒绝,避免打到源业务
end
}
进阶:把 vod:ids 替换成 Bloom Filter(如 redisbloom),在海量 ID 时更节省内存。
7. “熔断”与优雅降级的几种策略
当回源失败率/超时率超过阈值,我的三招顺序如下(从温和到激进):
serve-stale:优先发陈旧缓存(proxy_cache_use_stale ... updating),用户侧几乎无感;
限速/限并发(对单 CDN 限制):
limit_req_zone $http_x_forwarded_host zone=cdn_rps:10m rate=200r/s;
limit_req zone=cdn_rps burst=400 nodelay;
limit_conn_zone $http_x_forwarded_host zone=cdn_conn:10m;
limit_conn cdn_conn 2000;
302 回切备用源:仅对新增 MISS;HLS 可回家目录下的低码率备用片或静态“占位片”。
8. 部署步骤(可照抄执行)
系统调优:sysctl/limits/Bond/MTU 9000(如网络允许)。
安装:OpenResty + Redis。
目录:/data/cache、/data/livecache、/data/logs。
Nginx 配置:按章节 5.2~5.4。
CDN 回源配置:
- Host 保持业务域名;
- 启用 Range;
- Cache Key 仅含必要参数;
- 开启负缓存(若 CDN 支持 4xx 缓存);
- 源连并发与速率限额(每家不同配额)。
预热(Warm-up):上线前跑一遍热点清单,避免冷启动全 MISS。
# 简单预热(并发 200,忽略失败重试)
cat hotlist.txt | xargs -n1 -P200 -I{} curl -sS -m 10 "https://vod.example.com{}" > /dev/null
监控告警:Prometheus + Grafana,设置
- upstream_5xx_rate > 3% @ 1m
- upstream_rt_p95 > 300ms @ 5m
- cache_hit_ratio < 80% @ 5m
压测:
wrk -t8 -c2000 -d60s --timeout 5s https://vod.example.com/vod/xxx.ts
9. 实测数据(节选)
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 回源 QPS 峰值 | 9.8k | 2.7k |
| 源站 5xx 比例(峰时) | 3.1% | 0.28% |
| CDN→源 RTT p95 | 210 ms | 84 ms |
| VOD 命中率(小时) | 71% | 89% |
| LIVE m3u8 TTFB(HK) | 135 ms | 72 ms |
关键改动:负缓存 + key 规范化 + per-CDN 限流 + serve-stale + 302 回切。
10. 常见坑与现场解法
- 206 不缓存:部分 CDN 对 206 处理异常,源站务必 proxy_force_ranges on;,并确认 CDN 端可缓存 206。
- M3U8 过期策略:M3U8 TTL 设长会造成延后切片,建议 2~5s,并配 stale-if-error。
- Host 改写:CDN 回源“改 Host”导致证书/SNI 不匹配,统一要求保持 Host。
- Query 污染:rnd/_t 导致低命中率,清洗 query 后命中率立刻上去。
- 跨 CDN 互相放大:按 X-Forwarded-Host 或 CDN 自带头区分限额,避免一家爆了拖累全局。
- 负缓存过长:404 负缓存 TTL 过长会影响回补上线,建议**≤120s** 并支持手动 purge(按 URL 前缀)。
11. 安全与成本
- 签名鉴权(HMAC/JWT,时效 5~10 分钟),降低爬虫穿透;
- 只放行 CDN 源段 IP(其他来路直接 444);
- 日志抽样(1/10 抽样)降低 IO 压力;
- 冷热分层(NVMe 热集、对象存储冷数据)控制成本。
12. 从“被打懵”到“打不穿”
那晚之后,我给团队立了规矩:任何一次抖动,都要能复盘成“可复制的剧本”。
现在,当我看到回源曲线轻轻抖一下,我知道负缓存在挡脏流,熔断在保护上游,stale在稳用户体验。最重要的是,多 CDN 不再是放大器,而是冗余与弹性。
如果你也在香港机房守夜,愿这套剧本能让你睡得更稳。
附录:完整示例(可直接落地)
A) systemd 服务
# /usr/lib/systemd/system/openresty.service
[Unit]
Description=OpenResty
After=network.target
[Service]
Type=forking
PIDFile=/usr/local/openresty/nginx/logs/nginx.pid
ExecStartPre=/usr/local/openresty/nginx/sbin/nginx -t
ExecStart=/usr/local/openresty/nginx/sbin/nginx
ExecReload=/usr/local/openresty/nginx/sbin/nginx -s reload
ExecStop=/usr/local/openresty/nginx/sbin/nginx -s quit
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target
B) IP 白名单(只放行 CDN 源段)
map $remote_addr $allow_cdn {
default 0;
# 示例:真实环境请同步各家 CDN 源站段
1.2.3.0/24 1;
5.6.7.0/24 1;
}
server {
if ($allow_cdn = 0) { return 444; }
# ...
}
C) 手工 PURGE(简易)
location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
proxy_cache_purge VODCACHE $1$is_args$args;
}
D) Nginx vts/exporter(观测)
vhost_traffic_status_zone;
server {
location /status {
vhost_traffic_status_display;
vhost_traffic_status_display_format html;
}
}
# Prometheus exporter 以 sidecar 方式拉 /status