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

去年“双 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 鉴权即可。剩下的,就是按表演练、看图说话、少改多跑。祝你晚高峰曲线也稳得像一条直线。