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

点播热度阶梯策略:以香港服务器为边缘节点的切片缓存、冷热回收与多 CDN 选路

发布人:Minchunlin 发布时间:2025-09-24 18:57 阅读量:667


去年“双 11”前两周,我在香港机房跟着两台 1U 机器熬了三个通宵。一个地方剧集临时买了海外发行权,点播用户暴增,国内跨境访问全压到香港边缘。最开始我们只跑了单 CDN + 简单缓存,结果晚高峰命中率掉到 60% 以下、回源把主站打穿。第三个夜里我决定“动刀”:把香港边缘改造成热度分层(阶梯)的切片缓存,配上多 CDN 选路与冷热回收,再把风控和演练补齐。下面是我当时的完整打法——设备、拓扑、参数、配置、踩坑与复盘,全部摊开。

1. 目标与约束

目标

  • 晚高峰边缘缓存命中率 ≥ 92%
  • 回源带宽峰值降低 ≥ 60%
  • 多 CDN 自动选路,端到端 95 线性延迟降低 20%+
  • 故障演练 30 分钟内完成回切

约束

  • 机房在香港(BGP 多线),需兼顾大陆与海外访问。
  • 操作系统用 CentOS 7(历史原因,团队脚本体系依赖)。
  • 点播为 HLS/DASH 切片(2s/4s segment),加密和鉴权在上游完成。

2. 硬件与系统参数

2.1 边缘节点硬件(香港机房)

角色 型号/形态 CPU 内存 本地盘 网卡 备注
edge-01 1U Supermicro/同档品牌 AMD EPYC 7313P(16C) 128GB 2× 3.84TB NVMe(RAID1) 2× 25GbE(Intel E810 / CX5) NVMe 做热层
edge-02 同上 同上 同上 2× 3.84TB NVMe(RAID1) 2× 25GbE 与 edge-01 互为热备
obs-01(监控) 1U Xeon 银牌 64GB 2× SSD 10GbE Prometheus/Grafana/日志汇聚

NVMe 只做热数据与缓存索引;冷数据在中心仓(对象存储/分布式存储)回源。NVMe RAID1 不是为了极致性能,而是为了“边缘机单盘挂了还能撑到维护窗口”。

2.2 系统与内核

OS:CentOS 7(最小化安装)

内核:用 elrepo 安装 LTS(≥ 5.4),启用 BBR(CentOS7 自带 3.10 不建议直接折腾 BBR)

磁盘:xfs,禁用 atime,I/O 调度器 none(NVMe)

文件描述符:fs.file-max=2097152,nginx/worker 打开到 1M 以上

时钟同步:chrony 指向本地 Stratum2 + 公共源

/etc/sysctl.d/99-edge.conf(核心段):

net.core.somaxconn = 65535
net.ipv4.ip_local_port_range = 10000 65000
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_congestion_control = bbr
vm.swappiness = 1
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
fs.inotify.max_user_watches = 1048576

3. 网络拓扑与数据流

[User/Player] 
    │  (DNS 选路/HTTP 302)
    ▼
[CDN_A / CDN_B / CDN_C]  <--- 健康与成本打分
    │  命中则直接下发
    │  未命中回源 → Edge (HK)
    ▼
[Edge(OpenResty/Nginx) 25G]
    │  命中则下发
    │  未命中回源 → 中心对象存储/主站
    ▼
[Origin/ObjStore/Packager]

 

选路层:我们同时支持 DNS 层 CNAME 与 HTTP 302 两种;高峰时以 302 为主(更灵活,RUM 直连测 RTT)。

边缘层:OpenResty 负责切片缓存、热度分层、鉴权与签名透传、prefetch 与 背景更新。

回源层:对象存储(S3 兼容/MinIO/Ceph 任一),主站仅处理元数据与授权。

4. 热度“阶梯”模型(T0/T1/T2)

层级 定义 载体 目标命中 TTL/失活 回收策略
T0 热门 最近 30 分钟 PV/分 ≥ 阈值;正在热播或新上架 NVMe 本地(proxy_cache) ≥ 98% TTL=30m + background_update 低延迟淘汰,保护 Top-N(LRU+热度权重)
T1 温热 过去 24 小时内有持续访问但非爆款 NVMe 本地 ≥ 92% TTL=5m~10m max_size 触顶先淘汰 T1
T2 冷门 长尾内容,偶发访问 不常驻,靠 快速回源 + 切片预取 - TTL=30s~1m 近零常驻,省 NVMe

判定方法:边缘上报 segmentKey → QPS 到 Redis(local primary + obs 备份),每 10 秒聚合成滑窗。Lua 按阈值把内容提级/降级。

5. OpenResty/Nginx:切片缓存与防雪崩

5.1 核心缓存区与切片

/etc/nginx/conf.d/cache.conf

proxy_cache_path /nvme_cache levels=1:2 keys_zone=video_cache:64g
                 max_size=1800g inactive=10m use_temp_path=off;

# 切片大小:对 HLS/DASH segment 再切 1MB 片,提升并行回源吞吐
proxy_cache_key "$scheme$proxy_host$uri$is_args$args";

5.2 业务 server

server {
    listen 80 reuseport;
    server_name edge.hk.example.com;

    # 热度分层:通过 Lua 动态决定 TTL/Cache-Control
    set $ttl "5m";   # 默认
    set $tier "T1";

    location ~* \.(m3u8|mpd)$ {
        # 清单文件短 TTL,避免黑屏,但允许 background 更新
        proxy_cache video_cache;
        proxy_cache_valid 200 1m;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_pass http://origin_packager;
        add_header X-Cache-Tier $tier always;
    }

    location ~* \.(ts|m4s|cmfv|cmfa)$ {
        # 切片
        proxy_cache video_cache;
        proxy_ignore_headers Set-Cookie;
        proxy_cache_background_update on;   # 后台刷新
        proxy_cache_lock on;                # 防止雪崩
        proxy_cache_lock_age 5s;
        proxy_cache_min_uses 1;

        # 二次切片提升并发
        proxy_request_buffering off;
        proxy_buffering on;
        proxy_buffers 512 16k;
        proxy_busy_buffers_size 64m;
        proxy_max_temp_file_size 0;

        # Lua 决定 TTL + 热度标记(见下)
        set $cache_ttl $ttl;
        add_header X-Cache-Tier $tier always;

        proxy_pass http://origin_segment;
        proxy_cache_valid 200 $cache_ttl;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    }
}

5.3 热度感知(Lua)

/usr/local/openresty/nginx/lua/hotness.lua

local redis = require "resty.redis"
local r = redis:new()
r:set_timeout(30)
assert(r:connect("127.0.0.1", 6379))

-- key: hot:<segmentKey>  value: qps(rolling)
local uri = ngx.var.uri
local key = "hot:" .. uri

-- 滑窗 QPS → 分层
local qps = tonumber(r:get(key)) or 0
local ttl = "5m"
local tier = "T1"

if qps >= 200 then
  ttl = "30m"    -- T0:强缓存
  tier = "T0"
elseif qps >= 20 then
  ttl = "10m"    -- T1
  tier = "T1"
else
  ttl = "45s"    -- T2
  tier = "T2"
end

ngx.var.ttl  = ttl
ngx.var.tier = tier


在 nginx.conf 中挂载:

lua_shared_dict hot_cache 128m;

init_by_lua_block {
  package.path = "/usr/local/openresty/nginx/lua/?.lua;;";
}

# 在 location 中:
access_by_lua_file /usr/local/openresty/nginx/lua/hotness.lua;

5.4 预取与背景更新

proxy_cache_background_update on:用户命中旧副本时后台刷新,避免抖动。

清单文件(m3u8/mpd) TTL 极短,配合 segment 预取:解析清单的前 5 个分片 URI 放入本地队列,Nginx 子请求拉取,冷启动秒开率显著提升。

6. 多 CDN 选路(成本 × 质量 × 健康)

6.1 评分模型

我们对每个地区(CN North/CN South/SEA/EU/US)和每家 CDN 维护一个 10s 刷新的打分:

score = w1 * normalized_RTT
      + w2 * normalized_error_rate
      + w3 * normalized_bitrate_drop
      + w4 * cost_factor

数据来源:

RUM:播放器上报 dns+tconnect+tfirstbyte;

合成:香港、多地轻量探针直连各 CDN 边缘;

成本:各家实时单价/包量余量(接口或手工录入)。

6.2 302 选路(OpenResty)

/usr/local/openresty/nginx/lua/cdn_route.lua

-- 输入:用户所在 region、资源路径
-- 输出:重定向到得分最低(最优)的 CDN 域名
local cjson = require "cjson.safe"
local redis = require "resty.redis"
local r = redis:new()
r:connect("127.0.0.1", 6379)

local region = ngx.var.geo_region or "CN_SOUTH"
local path = ngx.var.request_uri

local cdns = {"cdn_a","cdn_b","cdn_c"}
local best, best_score = nil, 1e9

for _, c in ipairs(cdns) do
  local key = string.format("score:%s:%s", c, region)
  local s = tonumber(r:get(key)) or 1000
  if s < best_score then
    best, best_score = c, s
  end
end

-- 生成带签名的回源 URL(各家 CDN 鉴权口径不同,这里示意)
local function sign(cdn, p)
  -- 省略具体实现:带过期时间与 HMAC
  return string.format("https://%s.example-cdn.com%s?token=%s", cdn, p, "xxx")
end

local url = sign(best, path)
return ngx.redirect(url, 302)


nginx 中的入口:

location /r/ {
    # /r/ts/xxx.ts → 选路到某家 CDN
    content_by_lua_file /usr/local/openresty/nginx/lua/cdn_route.lua;
}


生产上我们实际上 双轨:热门区域走 DNS CNAME(降低额外 RTT),边缘抖动或成本阈值触发时临时切回 302 精准控流。

7. 鉴权与防盗链(风控)

签名 URL:path + ts + uid + hmac,过期 ≤ 5 分钟;CDN 与边缘统一验证。

Referer + UA 白名单:防直链(宽松校验,避免误杀播放器 SDK)。

频率限制:

IP/UID 维度的 segment 拉取 QPS 与 并发(ngx limit + Redis 滑窗)。

清单文件频率更严格(反爬关键)。

灰度放行:放大镜规则遇误杀,灰度 10% 放行并聚类分析 UA/Referer。

WAF:只开最小集规则,重点拦自动化爬虫 UA 与异常 Range 请求。

8. 监控、报警与 SLO

8.1 关键指标(边缘)

指标 含义 目标
edge_cache_hit_ratio 分片命中率 ≥ 92%(峰值 ≥ 95%)
origin_bw_mbps 回源带宽 峰值较基线下降 60%+
ttfb_ms_p95 首字节 95 线 降低 20%+
cdn_error_rate CDN 4xx/5xx 比例 < 0.5%
route_switch_count 选路切换次数 < 3 次/小时(非演练)

8.2 Prometheus 示例(导出器略)

# 回源带宽
sum(rate(edge_origin_bytes_total[1m])) / 125000

# 命中率
sum(rate(edge_cache_hit_total[1m])) / sum(rate(edge_cache_req_total[1m]))

# 选路健康
avg by(cdn,region) (cdn_score_gauge)  # 数值越低越好

9. 容量与参数基线(我们线上取值)

数值/范围 说明
segment 大小 1.5MB ~ 3MB HLS 2s/4s
二次切片 1MB 大文件并发拉取更平滑
NVMe 命中层容量 1.8TB(max_size) 预留 10% 余量
keys_zone 64GB 热索引
worker_processes auto(16) 绑核
worker_connections 65535  
proxy_cache_lock_age 5s 防击穿
proxy_cache_background_update on 平滑更新
proxy_cache_valid(清单) 1m m3u8/mpd
proxy_cache_valid(分片) T0:30m/T1:10m/T2:45s Lua 决策

10. 部署流程(零停机)

灰度:edge-02 先改,权重 10% → 30% → 50%。

监控对比:命中率、TTFB、回源流量三联动看 15 分钟窗口。

切主:参数一致后把 70% 流量移到新策略。

回滚预案:保留上一版 conf 与 systemd unit,systemctl revert + ipvs 回切老池。

变更冻结:晚高峰前 1 小时冻结。

11. 我踩过的坑 & 现挂现修

(坑 1)切片预取过猛打穿回源

表现:边缘命中没上来,回源瞬时飙升。

修:预取限速 + 限并发,Top-N 热内容才允许预取,且只预取 下一清单的前 3~5 个 segment。

(坑 2)proxy_cache_lock 误会导致长尾等待

表现:冷门内容大量等待同一锁,用户首开卡顿。

修:把 T2 的 lock_age 降到 2~3s,并允许 updating 旧副本下发。

(坑 3)CDN 302 选路引入跨域

表现:部分老播放器不跟随 302 跨域或丢 Referer。

修:补充 DNS 直达通道;302 仅用于抖动期与特定区域。

(坑 4)多家 CDN 鉴权口径不一致

修:在边缘统一签名口径,CDN 端做简单校验;下游回源头部携带 X-Edge-Sign。

(坑 5)NVMe 温度过高降速

修:加风道挡板,I/O 峰值时降低预取并行;SMART 阈值报警提前 10℃。

12. 演练与风控清单(我实际在跑的表)

12.1 故障演练(每两周一次)

场景 触发方式 预期 验收
CDN_A 区域性抖动 屏蔽健康探针或拉高 error_rate 5 分钟内切到 CDN_B/C 选路日志/得分变化
对象存储限速/降级 限制带宽到 50% 边缘命中率 ≥ 90%,TTFB 可控 回源带宽曲线平滑
边缘单机宕机 关机 edge-02 LVS/IPVS 自动摘除,整体 TTFB 波动 < 10% 合成与 RUM 对比
Redis 故障 停 redis 服务 3 分钟 热度策略退化为默认 TTL,不影响下发 日志中降级标记
大面抓取 压测 UA + 频率 触发频控与灰度放行流程 WAF 命中与误杀率

12.2 风控规则基线

规则 阈值 动作
单 UID 并发分片拉取 24 429 + 冷却 30s
单 IP 清单请求 30/min 403(渐进:告警→限速→封禁)
UA 黑名单/异常 Range 403
选路频繁切换 >5 次/10min 锁定成本维度,优先稳定

13. 验收数据(上线后一周)

指标 上线前 上线后(峰值日) 变化
Cache 命中率(分片) 68% 94.7% +26.7pp
回源带宽峰值 42 Gbps 16.3 Gbps -61%
TTFB p95 480 ms 320 ms -33%
播放起播失败率 0.85% 0.32% -0.53pp
选路稳定性(切换/小时) 7.2 2.1 -70%

14. 附:可直接复用的脚本片段

14.1 BBR + 基础优化(CentOS 7 / elrepo 内核)

yum install -y https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
yum --enablerepo=elrepo-kernel install -y kernel-lt
grub2-set-default 0 && reboot

# 重启后
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.d/99-edge.conf
sysctl --system

14.2 Nginx Reload & 语法钩子

nginx -t && systemctl reload nginx || { echo "bad conf"; exit 1; }

14.3 分片预取(示意)

-- parse m3u8 to prefetch first 3-5 segments with subrequests
local segs = parse_m3u8(body)
for i=1, math.min(#segs, 5) do
  ngx.location.capture("/_prefetch" .. segs[i])
end

15. 复盘那天凌晨

复盘那天是凌晨 1 点,机房的空调呼呼直吹。我把最后一条演练记录贴进 Wiki,抬头看了眼机柜门上贴的“别在高峰动配置”。两周的改造,把边缘从“能用”拉到了“能扛”。
说到底,点播不是玄学:把热度做成阶梯,缓存做成切片,多 CDN 做成可度量可演练的系统,问题就定性成“参数、阈值、回收”的组合题。
真正的成就感来自凌晨那张稳定的曲线,以及你敢按下回滚开关的底气。

16. 清单(拿去就能用)

变更前

  •  回滚包/上版 conf 打包
  •  预估峰值与 NVMe 余量 ≥ 10%
  •  演练窗口与灰度比例确定
  •  监控面板对比图就位(命中/回源/TTFB/错误率/选路)

上线中

  •  10% → 30% → 50% 灰度
  •  观察 15 分钟窗口三指标
  •  锁定成本项防止误切

上线后

  •  一周内做 2 次故障演练
  •  校准热度阈值(T0/T1)
  •  盘点误杀与灰度放行样本
  •  复盘报告:参数、曲线、决策记录

如果你也要在香港边缘拉一套这样的点播栈,把上面表格与配置替换成你的域名、对象存储与 CDN 鉴权即可。剩下的,就是按表演练、看图说话、少改多跑。祝你晚高峰曲线也稳得像一条直线。

目录结构
全文