如何通过香港服务器的大带宽结合 CDN 缓存,解决跨境点播平台的峰值卡顿?

那天晚上 19:42,我在香港葵涌机房 9 楼看着Grafana 的 95 线曲线突然抬头,我们的跨境点播业务正迎来春节档第一个高峰。香港源站的 10G 专线被打到 8.7Gbps,回源触发雪崩,CDN 侧 206 命中率异常。运维群里有人说“北方移动卡”,有人说“iOS 端 Seek 失败”。我把咖啡放在机柜门上,SSH 进源站,三件事同时开做:限流保护、Range 切片缓存回退、手动预热 Top100 热点。20 分钟后,Rebuffer Ratio 从 7.6% 退回 1.9%,告警渐灭。那一夜之后,我把整个方案梳成了这篇文档。**
下面是我“如何用香港大带宽 + CDN 缓存”把跨境点播的峰值卡顿问题一件件拆解并修好的全过程。内容尽量细,新手能照抄上线,老手能直接套到自己的架构里。
1)业务背景与目标指标
| 指标 | 峰值前(旧方案) | 目标/上线后 | 说明 |
|---|---|---|---|
| 并发播放数(P95) | 22k | 45k | 节假日、晚高峰 20:00–23:00 |
| 平均码率 | 3.5 Mbps | 3.5–4.2 Mbps | 自适应码率 ABR |
| Rebuffer Ratio(全量) | 6–9% | ≤2% | 主观体验最敏感 |
| 首帧时间(P95) | 2.8 s | ≤1.5 s | 含跨境 RTT |
| CDN 命中率(静态片段) | 83% | ≥96% | 含 206/Range 命中 |
| 回源带宽(P95) | 6.5 Gbps | ≤2.5 Gbps | 降压源站和专线 |
| 端到端可用性 | 99.85% | ≥99.95% | 像素级报障对齐 |
关键挑战:跨境 RTT 高、晚高峰长尾回源。解决思路:香港源站大带宽兜底 + “能不回源就不回源”的 CDN 细粒度缓存策略。
2)总体架构与流量路径(跨境点播)
用户终端(移动/电信/联通, CN)
│ DNS -> 智能解析(权重/健康/区域/运营商)
▼
CDN 边缘节点(内地/东南亚)
│ 命中则直接 200/206 返回
│ 未命中 -> 向 CDN 中心/Shield 请求
▼
CDN Shield(香港/深圳边界)
│ 命中则返回;未命中回源
▼
香港源站(10G~20G 专线, 多运营商 BGP)
│ Nginx/OpenResty + 切片缓存/防雪崩
▼
后端对象存储/编目服务(内网)
要点:
- 源站只做“最后一跳”兜底:香港大带宽抗回源尖刺。
- CDN 开启 Shield 层:边缘未命中先打 Shield,显著减少对源站的并发扇出。
- 视频分片化 + Range 切片缓存:把“整大文件请求”拆成可缓存的小片段,命中更高。
3)香港机房与硬件配置(我实际投产的一套)
3.1 机房与网络
- 机房:香港葵涌/荃湾任一 Tier 3 水平,冷通道+N+1 UPS,带 24/7 远维。
- 上联:双运营商 BGP(例:PCCW/HGC)+ 增补一条 CN 系列优质回国线路(避免长途绕路),合计 10Gbps 保底,可 20Gbps 短时突发(按 95th 百分位计费)。
- 路由:BGP 社区策略压低劣质路径,启用 RTBH(黑洞)以防某些恶意透传。
3.2 服务器(源站缓存/回源网关)
| 角色 | 机型 | CPU | 内存 | 系统盘 | 数据盘 | 网卡 | 其他 |
|---|---|---|---|---|---|---|---|
| 源站 A/B | 1U 双路 | AMD EPYC 7313P ×1 | 256GB | 2×480G SATA SSD (RAID1) | 4× 3.84TB NVMe U.2 (JBOD) | 2×10GbE (Mellanox CX-4 Lx) | IPMI, AES-NI |
| Shield/前置缓存 | 1U | Intel Xeon 4310 | 128GB | 同上 | 2× 3.84TB NVMe | 2×10GbE | 同上 |
OS:CentOS 7(业务要求),内核升级到 4.14/5.4 elrepo 以启用 BBR、更好的 NVMe/AIO 支持。
4)带宽规划与容量估算
并发 × 平均码率 ≈ 峰值带宽:
45,000 × 3.5 Mbps ≈ 157.5 Gbps(总分发)
若 CDN 命中 96%,回源仅 4%:
157.5 × 4% ≈ 6.3 Gbps(接近我们旧值)
再叠加 Shield 命中 70% 的未命中流量:
源站实际 ≈ 6.3 × (1 - 70%) ≈ 1.9 Gbps(目标达成)
建议:香港源站保底 10Gbps,可突发到 20Gbps,并启用 95th 计费;再配合 CDN Shield,可把源站 P95 稳在 2–3Gbps。
5)系统调优(CentOS 7)
/etc/sysctl.d/99-tuning.conf
net.core.somaxconn = 10240
net.core.netdev_max_backlog = 250000
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_mtu_probing = 1
net.ipv4.ip_local_port_range = 10240 65535
fs.file-max = 2000000
vm.max_map_count = 262144
/etc/security/limits.d/99-nofile.conf
* soft nofile 1048576
* hard nofile 1048576
nginx soft nofile 1048576
nginx hard nofile 1048576
NVMe/IO 调整
- scheduler=none(NVMe 默认),irqbalance 开启,numactl --interleave=all 对 nginx worker 生效。
- fio 验证 128k 顺序读吞吐,确保单盘 > 2.5GB/s,四盘 JBOD 足够扛热片段。
6)Nginx/OpenResty 源站关键配置
目标:高并发、避免雪崩回源、支持 Range/206 切片缓存、错误退化不影响播放。
完整示例(节选) /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
worker_rlimit_nofile 1048576;
events { worker_connections 65535; }
http {
include mime.types;
default_type application/octet-stream;
# 基础优化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
aio threads; # 结合 NVMe
directio off; # 大文件+Range时更灵活
open_file_cache max=200000 inactive=60s;
open_file_cache_valid 120s;
open_file_cache_min_uses 2;
# 日志中打出 CDN 回源标识
log_format main '$remote_addr - $host "$request" $status $body_bytes_sent '
'"$http_range" "$upstream_cache_status" '
'rt=$request_time urt=$upstream_response_time '
'cdn="$http_x_edge_ip" xff="$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main buffer=512k flush=5s;
# 缓存目录与键
proxy_cache_path /data/ngcache levels=1:2 keys_zone=vodcache:10g
max_size=2000g inactive=7d use_temp_path=off;
# 防止缓存击穿(雪崩)与狗堆效应
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
proxy_cache_min_uses 1;
# 切片缓存,配合 Range
# 需要编译带 slice 模块(openresty 可)
slice 1m;
proxy_cache_key "$scheme$host$uri$is_args$args-$slice_range";
# 源站上游
upstream storage {
server 127.0.0.1:9000; # 你实际的对象存储/文件网关
keepalive 256;
}
map $request_uri $vod_ttl {
default 7d;
~*\.m3u8$ 10m;
~*\.mpd$ 10m;
}
server {
listen 80 default_server reuseport;
listen 443 ssl http2 reuseport;
server_name vod-origin.example.com;
# TLS 省略:ECDHE+AESGCM,OCSP Stapling 开启
# 统一强制 Range 可缓存
proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
add_header Cache-Control "public, max-age=604800, stale-while-revalidate=120, stale-if-error=600";
# 对大文件/视频
location ~* \.(mp4|m4v|mkv|mov|ts|m4s|mpd|m3u8)$ {
proxy_pass http://storage;
proxy_http_version 1.1;
proxy_set_header Connection "";
# Range/206 & 切片
proxy_request_buffering off;
proxy_buffering on;
proxy_buffers 256 64k;
proxy_busy_buffers_size 256k;
proxy_max_temp_file_size 0;
# 缓存策略
proxy_cache vodcache;
proxy_cache_valid 200 206 $vod_ttl;
proxy_cache_valid 404 10s;
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
# 防盗链(示例,可改签名)
valid_referers none blocked *.example.com;
if ($invalid_referer) { return 403; }
}
# 其余静态
location / {
proxy_pass http://storage;
proxy_cache vodcache;
proxy_cache_valid 200 10m;
}
}
}
解释与经验
- slice + Range:将 Range 请求按 1MB 切片,缓存单位更细,CDN/Shield 更容易命中小片段。
- proxy_cache_lock:热点首次未命中只让一个回源,避免瞬间 N× 回源放大。
- stale-while-revalidate/stale-if-error:回源慢/失败时,端上仍能拿旧片,不黑屏。
- m3u8/mpd TTL 短(10 分钟),片段长 TTL(7 天),列表短、片段长是 VOD 的通用准则。
- 不要对视频开压缩(gzip/br),毫无收益还费 CPU。
7)CDN 侧的关键开关(以通用能力为例)
- 开启 “206/Range 可缓存”,并把缓存键包含 Range/切片标识(与源站 slice 对齐)。
- 启用 Origin Shield(香港/深圳),边缘未命中先打 Shield,大幅减少源站并发。
- Collapse Forwarding/Request Coalescing:同一资源未命中时只放行一个向下游请求,其余等待结果复用。
- Cache Key 规范化:忽略无关查询参数,如 utm_*、_=timestamp,保留版本号或签名参数。
- 签名/防盗链:建议 URL Token(过期时间+HMAC),CDN 验证;Referer 仅做辅助手段。
- 分区域/运营商调度:CN 移动/联通/电信分开权重;高峰时动态调整权重到表现好的集群。
- 热度保护:开启自动预热或回源负载阈值切断(超阈值时返回旧缓存/降级码率提示)。
- HTTP/3/QUIC:移动网络场景强烈建议开启(CDN 层提供即可,源站无需支持)。
- 跨境合规:中国内地节点通常需要备案/域名 ICP。没有内地节点也可走港/深边界 Shield + 优质跨境路由,体验略逊但可用。
8)预热与回源减压 —— 我用的两个脚本
(1)Top 热门内容预热:每天 18:30 跑一次)
#!/bin/bash
# prewarm.sh
CDN_HOST="cdn.example.com"
TOP_LIST="/var/data/top100_urls.txt" # 按前一日访问排序生成
UA="prewarm-bot/1.0"
xargs -I{} -P16 curl -sS -A "$UA" -I "https://${CDN_HOST}{}" < "$TOP_LIST" | grep -E "HTTP/|X-Cache"
(2)命中 CDN API 的精确刷新(仅列表,不刷新片段)
# purge_m3u8.py
import requests, json, sys
API="https://api.cdn.example.com/v1/purge"
TOKEN="YOUR_TOKEN"
urls=[l.strip() for l in open(sys.argv[1]) if l.strip().endswith(('.m3u8','.mpd'))]
payload={"urls": urls, "type":"invalidate"}
r=requests.post(API, headers={"Authorization":"Bearer "+TOKEN,"Content-Type":"application/json"}, data=json.dumps(payload), timeout=15)
print(r.status_code, r.text)
原则:只刷新 m3u8/mpd,不刷新分片。列表很小,片段巨大且高命中,刷新会自杀式回源。
9)日志与监控:我盯的 12 个指标
| 层级 | 指标 | 说明/阈值 |
|---|---|---|
| 终端 | Startup/首帧、Rebuffer、错误码 | P95 追到区域/运营商 |
| CDN 边缘 | 命中率(200/206 分开)、5xx、RTT | 206 单独看,很多平台忽略了 |
| CDN Shield | 命中率、回源速率、并发 | 未命中放大器 |
| 源站 | 带宽、连接数、平均/95 响应时间 | >400ms 就危险 |
| 源站 | upstream_cache_status 比例 | HIT/MISS/EXPIRED/UPDATING |
| 系统 | load、IOwait、软中断、磁盘队列 | iowait > 10% 警戒 |
| 网络 | 丢包、重传、ECN、突刺 | QUIC 开启后重传下降 |
PromQL 示例(以节点导出器为例):
rate(node_network_transmit_bytes_total[1m])
histogram_quantile(0.95, sum(rate(nginx_request_duration_seconds_bucket[5m])) by (le))
sum by (status)(rate(nginx_http_requests_total{status=~"5.."}[5m]))
10)真实踩坑与处理过程(血的教训)
- 206 默认不缓存:某 CDN 的默认行为是只缓存 200。症状:边缘命中高但回源依旧大。处理:明确开启 206 缓存,且缓存键含切片范围。
- 雪崩回源:一部新剧上架,前 3 分钟片段全 MISS。症状:源站并发飙升 >40k,CPU 空间却富余。处理:打开 proxy_cache_lock + Shield 层 collapse,MISS 变单飞,其余复用结果。
- ETag/If-Range 异常:对象存储升级后 ETag 变化,CDN 以为资源更新。症状:持续刷片段。处理:稳定 ETag 策略,或改用 Cache-Control 强 TTL。
- Range 计算不一致:客户端 Seek 到非切片边界,CDN 与源站切片策略不同,导致反复错位 MISS。处理:统一 1MB slice,在源站 proxy_cache_key 引入 $slice_range。
- 端口耗尽:高并发回源导致源站对上游短连接过多。症状:connect() failed (99: Cannot assign requested address)。处理:开启 keepalive,扩大 ip_local_port_range。
- 文件句柄打满:EMFILE: Too many open files。处理:nofile 拉满 + open_file_cache + 限制目录遍历。
- CDN 节点区域调度错误:华北电信被分到华东联通集群,症状:体验局部“卡”。处理:回收该地区权重、单点压测对齐。
- m3u8 频繁刷新 导致片段被动过期:处理:只刷新列表,片段让其自然过期;紧急时预热 Top 热门片段而非全量。
- 对象存储限速:后端默认 1Gbps 限速,源站稳定但上游堵。处理:放开桶/前缀限速,或在源站做本地热片段缓存。
11)上线步骤与回滚预案(我这么走的)
- 灰度:DNS 按 5%→20%→50%→100% 逐步权重切换到新 CDN 策略;同时 Shield 只对灰度域名生效。
- 压测:wrk/h2load 对 10 个热点片段做 2 万并发读,观测 206 命中、回源并发、TTFB。
- 保护阈值:源站连接 > 30k 或回源带宽 > 8G 时触发降级:stale-if-error + 固定低码率清单。
- 自动化:Ansible 一键发版 Nginx 配置(含回滚),CDN API 批量下发规则。
- 回滚:DNS 5 分钟 TTL;一键关闭 slice 与 proxy_cache_lock(必要时),切回旧规则。
12)效果复盘(真实区间数据)
| 指标 | 上线前(周均) | 上线后(周均) | 备注 |
|---|---|---|---|
| Rebuffer Ratio | 6.8% | 1.7% | 峰值夜 1.9% |
| 首帧时间 P95 | 2.8 s | 1.3 s | QUIC 带来 10–15% 改善 |
| CDN 命中率(含206) | 83% | 96.4% | 切片后显著提升 |
| 回源带宽 P95 | 6.5 Gbps | 2.1 Gbps | Shield 命功不可没 |
| 源站连接 P95 | 32k | 9k | 雪崩被抑制 |
| 故障分钟数/月 | 65 | 18 | 以 P1/P2 计 |
13)常见问题(我被问最多的 8 个)
- 必须多 CDN 吗? 不一定,先把一个 CDN 配到极致(206、Shield、Key、预热);高峰再考虑多 CDN 做 A/B 或熔断切换。
- 视频要不要 Gzip? 不要。视频本身已压缩,开启只会伤 CPU。
- HLS 还是 DASH? 两者都行,关键是片长 2–4s、独立版本化,与缓存策略对齐。
- 源站要对象存储吗? 我倾向 源站做前置缓存 + 后端对象存储;对象存储的 ETag/限速要提前打点验证。
- 内地没有备案怎么办? 走 香港/深圳 Shield + 优质跨境路由,体验不如内地边缘,但比直连源站强太多。
- 如何防盗链? 签名 URL + CDN 防盗链 + 限速与 QPS 限制,必要时黑名单国家/ASN。
- 切片大小怎么定? 512KB–2MB 之间选;我最终用 1MB,命中与开销折中。
- 回源 500/504 如何处理? stale-if-error 先保体验,随后溯源到对象存储或网关。
14)Checklist(上线前最后过一遍)
- CDN 已开启 206 缓存 与 Collapse Forwarding
- Cache-Key 已包含切片标识,忽略无关 query
- Shield 生效且观测到命中
- 源站 proxy_cache_lock 开启,stale-* 策略就绪
- nofile、sysctl、BBR、生效校验
- 监控面板含:命中率(分 200/206)、回源并发、TTFB、Rebuffer
- 预热任务 18:30 定时执行
- DNS TTL ≤ 300s,可随时回滚
- 压测报表保存,作为回归基线
凌晨 0:15,我把最后一条预热任务 push 进去,机房空气依然冷,曲线像一条安静的河。微信里产品经理发来一句“今晚没被骂,感谢”。我回酒店的路上想明白了:跨境点播要稳,不是靠一根更粗的专线,而是靠“尽可能不回源”的工程自觉。
如果你也在香港做源站,面对内地用户的晚高峰,这套 “香港大带宽 + CDN 细粒度缓存 + Shield + 防雪崩 + 预热” 的组合拳,八成能帮你把 Rebuffer 打到 2% 左右。剩下的 1%——是你对数据的执着、对每个 206 的尊重,以及对一次次“ Seek ”失败背后那条路由的好奇心。
附:一键校验脚本(快速体检你的源站与 CDN)
#!/bin/bash
URL="https://cdn.example.com/path/to/video/seg-00001.m4s"
echo "[1] 边缘命中状态"
curl -sI "$URL" | egrep "HTTP/|Cache|Age|Range|ETag|Via|CDN|X-Cache"
echo -e "\n[2] 206 Range 可缓存验证"
curl -sI -H "Range: bytes=0-1048575" "$URL" | egrep "HTTP/|Cache|Age|X-Cache"
echo -e "\n[3] Shield 回源路径观察(需你CDN开放调试头)"
curl -sI "$URL" | egrep "Shield|Parent|X-"
echo -e "\n[4] 源站命中观察(直打源站域名)"
ORIGIN="https://vod-origin.example.com/path/to/video/seg-00001.m4s"
curl -sI "$ORIGIN" | egrep "HTTP/|Upstream-Cache-Status|X-Accel|Cache"