上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-27 10:05 阅读量:167


我第一次真正意识到“缓存穿透”的威力,是在一个周五的晚上。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
目录结构
全文