如何在香港服务器的Ubuntu环境中,配置与优化CDN回源限速策略,降低跨境视频播放卡顿

凌晨 02:10,香港将军澳机房的工单又一次飙红:华南多个城市用户视频播放卡顿,重缓冲比一度冲到 3.7%。我盯着 NMS 的 egress 曲线——香港到内地的 CN2 出口在晚高峰被 CDN 的回源洪峰“打满”,每当热点剧集更新、CDN 边缘 miss 率上升,PoP 同时向香港源站发起上百路并发拉流,把跨境窄口挤成细颈漏斗,用户侧就开始一顿一顿地卡。
那一刻我很清楚:不是“带宽不够”这四个字能解释一切,而是回源的节奏需要被温柔地“控速”。下面是我在那一夜到天亮的全过程,全部细节、命令、坑与回滚,都整理在这篇一文里。
1. 场景、目标与边界
目标:
- 控制 CDN 回源洪峰,稳定跨境出口利用率 < 80%,避免瞬时拥塞。
- 保障首屏与续播:HLS/DASH 分片首包时间 < 300ms(PoP 与源站 RTT 条件允许的情况下),分片下载耗时 p95 < 1.2s。
- 降低用户侧重缓冲比例至 < 0.6%,并保持 7 天稳定。
边界:
- 源站位于香港(HK),用户主要在中国内地;CDN 在内地多 PoP 覆盖。
- 重点优化回源限速与节流策略(Nginx 层 + 内核队列 + 流量整形),并配套缓存、TCP/内核与监控。
- 操作系统限定为 Ubuntu 22.04 LTS(文末附 20.04 差异点)。
2. 拓扑与硬件基线
逻辑拓扑(简述):
[ 内地用户 ] ←→ [ CDN 边缘 PoP ] ←→ (回源) ←→ [ 香港源站集群 ] ←→ [ 存储/对象 ]
│
[CN2/GIA/专线]
硬件/系统配置(一台典型源站):
| 项 | 参数 |
|---|---|
| 机型 | 1U 服务器,双电冗余 |
| CPU | Intel Xeon Silver 4410Y(12C/24T)或 AMD EPYC 7313(16C) |
| 内存 | 64–128 GB |
| 系统盘 | NVMe 960GB(SAMSUNG PM9A3) |
| 媒体盘 | NVMe 2TB × 2(RAID1,读多写少) |
| 网卡 | 25GbE(主)+ 10GbE(备),支持 RSS、TSO/GRO/GSO |
| OS | Ubuntu Server 22.04.4 LTS |
| Web | Nginx/OpenResty 1.21+(含 VTS/Stub 状态模块) |
| 监控 | Node Exporter + Prometheus + Grafana |
网络与带宽合同(示例):
- 香港 → 内地 CN2/GIA 出口:1 Gbps 承诺 + 2 Gbps 突发(计费 95th)。
- 香港本地与国际出口:10 Gbps(不敏感)。
3. 现网测量(问题如何被复现)
3.1 路由与抖动
# 从源站对某内地 PoP 测试(示例 IP)
mtr -rw -c 200 203.0.113.10
观测:跨境 3–4 跳处 RTT 抖动 12–35ms 不等,偶发 1% 丢包;晚高峰 PoP → 源站 RTT 从 15ms 抬升到 40ms。
3.2 回源并发洪峰
# 统计最近 1 分钟内与 CDN PoP 的 ESTABLISHED 连接
ss -tan dst 203.0.113.0/24 | wc -l
观测:热点更新时 PoP 网段到源站同时建立 600+ 条连接,请求多为 HLS .ts 或 CMAF .m4s 切片,Range 请求比例 ≥ 70%。
3.3 单分片拉取时延
curl -s -w 'dns:%{time_namelookup} conn:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n' \
-o /dev/null 'https://origin.example.com/video/xxx/seg-1001.ts'
观测:TTFB 偶发 > 900ms,total > 1.8s(PoP miss & 源站挤压时尤甚)。
3.4 基线数据(节流前)
| 指标 | 峰值前 | 峰值时 | 备注 |
| 源站 egress(CN2)利用率 | 45% | 98% | 瞬时打满,出现队列拥塞 |
| PoP→源站 并发连接 | ~120 | 600+ | 多 PoP 同时 miss 回源 |
| 分片下载 p95 | 0.85s | 1.92s | 用户端重缓冲明显 |
| 用户重缓冲率 | 0.7% | 3.7% | 来自 QoE 探针 |
4. 策略设计:为什么“回源限速”而不是“加带宽”?
- CDN miss 不可避免:热剧上新、冷门长尾、版本切换都会造成 PoP 冷启动。
- PoP 回源是“洪峰行为”:少数分片在短时间被多 PoP 并发拉取,源站出口瞬时挤满,跨境窄口出现队列积压,RTT 抬高,TCP 重传增多,用户端缓冲。
- “加带宽”并不经济也不稳定:跨境专线/优质线路单价高、调配周期长;此外洪峰仍可能“追着带宽跑”。
核心思路:
- 识别 CDN 回源流量(按目的 IP/ASN/网段),与直连用户/运维流量区分;
- 在源站 Nginx 层做“微观限速”(per-req:limit_rate_after + limit_rate + limit_req);
- 在 内核 tc 层做“宏观整形”(HTB 分级 + fq_codel 排队),给 CDN 回源一个可持续的“配额”;
- 配套 缓存/锁与回退策略:proxy_cache_lock 抑制放大、use_stale 保前台稳定;
- 完整监控闭环:PoP miss→回源 p95→CN2 egress→用户 QoE。
5. 实施(Ubuntu 22.04):从内核到 Nginx,再到 tc
5.1 系统内核与文件句柄
/etc/sysctl.d/99-origin-tuning.conf:
# 拥塞控制与队列
net.core.default_qdisc = fq_codel
net.ipv4.tcp_congestion_control = bbr
# 套接字缓冲
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456
net.ipv4.tcp_rmem = 4096 87380 268435456
net.ipv4.tcp_wmem = 4096 65536 268435456
# 连接追踪与 TIME_WAIT 优化
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_mtu_probing = 1
# 端口范围
net.ipv4.ip_local_port_range = 10000 65535
# backlog
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
生效:sudo sysctl --system
文件句柄:
echo '* soft nofile 1048576' | sudo tee -a /etc/security/limits.conf
echo '* hard nofile 1048576' | sudo tee -a /etc/security/limits.conf
5.2 识别 CDN 回源(nftables 集合 + 标记)
思路:维护一个 CDN PoP 目的地址集合,命中后为数据包打 fwmark=0x1,后续 tc 依据该标记分流。
sudo apt-get update && sudo apt-get install -y nftables
sudo nft add table inet throttle
sudo nft 'add set inet throttle cdn4 { type ipv4_addr; flags interval; }'
# 示例:加入若干 PoP 网段(示意,实际请填自家 CDN 公告网段)
sudo nft 'add element inet throttle cdn4 { 203.0.113.0/24, 198.51.100.0/24, 192.0.2.0/24 }'
# 输出链做标记
sudo nft 'add chain inet throttle out { type filter hook output priority 0; }'
sudo nft 'add rule inet throttle out ip daddr @cdn4 meta mark set 0x1'
# 持久化
sudo sh -c 'nft list ruleset > /etc/nftables.conf'
如果有 IPv6 回源,同样建立 type ipv6_addr 的 set 并在 ip6 daddr 规则中标记。
5.3 tc 流量整形(HTB + fq_codel)
思路:为被标记的 CDN 回源流量设定一个“可持续速率”和“峰值上限”,其余流量走默认队列。
DEV=eth0
sudo tc qdisc del dev $DEV root 2>/dev/null || true
sudo tc qdisc add dev $DEV root handle 1: htb default 30
# 根类:以物理口为上限(示例 10g)
sudo tc class add dev $DEV parent 1: classid 1:1 htb rate 10gbit ceil 10gbit
# CDN 回源:给 700M 可持续,峰值 900M(按你 CN2/GIA 合同调整)
sudo tc class add dev $DEV parent 1:1 classid 1:10 htb rate 700mbit ceil 900mbit burst 15m cburst 15m quantum 30000
sudo tc qdisc add dev $DEV parent 1:10 handle 10: fq_codel
# 其他业务:保留更高上限,避免互相抢占
sudo tc class add dev $DEV parent 1:1 classid 1:30 htb rate 2gbit ceil 5gbit
sudo tc qdisc add dev $DEV parent 1:30 handle 30: fq_codel
# 依据 fwmark 分流到 1:10
sudo tc filter add dev $DEV protocol ip parent 1: prio 1 handle 0x1 fw classid 1:10
验证:
sudo tc -s qdisc show dev $DEV
sudo tc -s class show dev $DEV
rate/ceil 的设定要略低于跨境承诺带宽,留出 TCP 重传与管理头部开销,经验值预留 10–20%。
5.4 Nginx 源站的“微观限速”
目的:在单请求层面,避免 PoP 以过快速度抽走分片,导致大量并发共振;同时配合缓存锁,减少回源放大。
/etc/nginx/nginx.conf 重点段落(示例):
worker_processes auto;
worker_rlimit_nofile 1048576;
events { worker_connections 65535; multi_accept on; use epoll; }
http {
sendfile on; aio threads; tcp_nopush on; tcp_nodelay on;
keepalive_timeout 65; keepalive_requests 10000;
types_hash_max_size 4096;
# 日志与状态
log_format main '$remote_addr - $host "$request" $status $body_bytes_sent $request_time $upstream_response_time';
access_log /var/log/nginx/access.log main;
# 变量化限速:命中 CDN 时启用
map $remote_addr $is_cdn {
default 0;
203.0.113.0/24 1;
198.51.100.0/24 1;
192.0.2.0/24 1;
}
# 按文件类型给出限速(单位:B/s;可用 k/m 后缀)
map $is_cdn$sent_http_content_type $limit_rate_val {
default 0; # 非 CDN 或未匹配:不限速
~^1video/.* 2500k; # 命中 CDN 且是视频:~2.5 MB/s
~^1application/octet-.* 2500k; # CMAF m4s 也按视频处理
}
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=segcache:10g max_size=500g inactive=7d use_temp_path=off;
server {
listen 443 ssl http2 reuseport;
server_name origin.example.com;
# 省略 SSL 证书配置...
location /video/ {
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 缓存与反放大
proxy_cache segcache;
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_ignore_headers Set-Cookie;
# Range 支持,适配 HLS/DASH
proxy_force_ranges on;
# 微观限速:先放 1MB 再按速率匀速回写给 PoP
limit_rate_after 1m;
limit_rate $limit_rate_val;
# 基于请求速率的保护(可选,防抖)
limit_req_zone $binary_remote_addr zone=percdn:10m rate=200r/s;
limit_req zone=percdn burst=400 nodelay;
# 其它缓冲与超时
proxy_buffering on;
proxy_buffers 256 32k;
proxy_busy_buffers_size 64m;
proxy_read_timeout 60s;
send_timeout 60s;
}
location = /status {
stub_status on; # 或使用 vts 模块
allow 127.0.0.1; deny all;
}
}
}
说明:
- limit_rate_after 1m 允许前 1MB 先“冲一口气”,后续按 limit_rate 匀速,更接近音视频分片的突发+平滑特性。
- map 里按 remote_addr 识别 CDN PoP 网段,与 nftables 的集合保持一致(可由自动化同步)。
- HLS .ts、CMAF .m4s、MP4 伪流都走该策略,静态小图等无需限速。
5.5 OpenResty/Lua(可选:按时段/负载自适应)
峰值时段(20:00–23:00)或当 CN2 egress > 70% 时,自动下调 limit_rate。
lua_shared_dict dyn 1m;
init_by_lua_block {
local hour = tonumber(os.date('%H'))
if hour >= 20 and hour <= 23 then
ngx.shared.dyn:set('rate', '2200k')
else
ngx.shared.dyn:set('rate', '2800k')
end
}
set_by_lua_block $dyn_rate {
return ngx.shared.dyn:get('rate') or '2500k'
}
# 替换上文 limit_rate $limit_rate_val; 为:
limit_rate $dyn_rate;
更高级的做法:由 Prometheus 指标触发(CN2 egress、重缓冲率),通过 Nginx API 动态调整共享字典。
5.6 自动化与回滚
systemd 单元(/etc/systemd/system/tc-cdn.service):
[Unit]
Description=CDN Back-to-Origin Traffic Shaper
After=network-online.target nftables.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/tc-cdn.sh start
ExecStop=/usr/local/sbin/tc-cdn.sh stop
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
脚本(/usr/local/sbin/tc-cdn.sh):
#!/usr/bin/env bash
set -euo pipefail
DEV=${DEV:-eth0}
start() {
tc qdisc del dev $DEV root 2>/dev/null || true
tc qdisc add dev $DEV root handle 1: htb default 30
tc class add dev $DEV parent 1: classid 1:1 htb rate 10gbit ceil 10gbit
tc class add dev $DEV parent 1:1 classid 1:10 htb rate 700mbit ceil 900mbit burst 15m cburst 15m quantum 30000
tc qdisc add dev $DEV parent 1:10 handle 10: fq_codel
tc class add dev $DEV parent 1:1 classid 1:30 htb rate 2gbit ceil 5gbit
tc qdisc add dev $DEV parent 1:30 handle 30: fq_codel
tc filter add dev $DEV protocol ip parent 1: prio 1 handle 0x1 fw classid 1:10
}
stop() {
tc qdisc del dev $DEV root 2>/dev/null || true
}
case "${1:-}" in
start) start;;
stop) stop;;
restart) stop; start;;
*) echo "Usage: $0 {start|stop|restart}"; exit 1;;
esac
启用:
sudo chmod +x /usr/local/sbin/tc-cdn.sh
sudo systemctl enable --now tc-cdn.service
回滚:
sudo systemctl stop tc-cdn.service
sudo tc qdisc del dev eth0 root || true
sudo systemctl reload nginx
6. 缓存与 206/Range 策略
- 启用 proxy_cache_lock:同一 key 首个请求拉源,其余等待,避免 N×放大;
- proxy_cache_use_stale updating:后台刷新失败不影响前台;
- Range 与缓存一致性:建议 按分片维度缓存(HLS/DASH/CMAF),而不是大 MP4 切范围缓存;
- 缓存 TTL:热点分片 s-maxage=600,长尾 s-maxage=86400,控制 CDN 复用;
- ETag/Last-Modified:稳定验证与 304 友好;
- 避免 Vary: * 或多维度 Vary 造成缓存碎片。
HTTP 头示例:
Cache-Control: public, s-maxage=600, max-age=60
ETag: "seg-1001-abcdef"
Accept-Ranges: bytes
7. 监控与 SLO:我如何判断“够了”
关键指标:
- CN2 egress 利用率:均值 < 70%,p95 < 80%;
- PoP→源站分片下载耗时:p95 < 1.2s;
- 用户重缓冲率:7 天均值 < 0.6%;
- Nginx upstream 状态:499/5xx 不连续抬升。
PromQL 片段(示例):
# 物理口出方向带宽
(irate(node_network_transmit_bytes_total{device="eth0"}[1m]) * 8) / 1e6
# Nginx 响应时间 p95(需导出直方图)
histogram_quantile(0.95, sum(rate(nginx_http_request_duration_seconds_bucket[5m])) by (le))
# 回源命中:按 remote_addr 匹配 PoP 网段的请求量
sum(rate(nginx_ingress_controller_requests{remote_addr=~"203.0.113.*|198.51.100.*"}[1m]))
压测(离峰时段):
# HTTP/2 多路复用压测
h2load -n 50000 -c 200 -m 100 https://origin.example.com/video/playlist.m3u8
8. 灰度与验证(真实数据)
灰度半小时后观测:
| 指标 | 灰度前 | 灰度后(15 分钟) | 变化 |
| CN2 egress p95 | 98% | 74% | ▼ 24pp |
| 分片下载 p95 | 1.92s | 1.06s | ▼ 45% |
| 用户重缓冲 | 3.7% | 0.8% | ▼ 2.9pp |
| 5xx 比例 | 0.3% | 0.2% | 稳定 |
注:限速参数采用 2.5MB/s/请求、HTB 700M/900M;PoP miss 率在更新窗口内下降 20%(缓存锁生效)。
9. 常见坑 & 现场解法
- 把 limit_rate 设太低:PoP 端积压,Range 请求增多,反而放大连接数。解法:结合 limit_rate_after,并按分片码率 × 1.2–1.5 设定(例如 8Mbps 视频 → 1.2–1.5MB/s 足够)。
- 206/Range 与缓存不友好:有的 CDN 对 Range 缓存策略不一致。解法:分片化存储,尽量避免大文件随机 Range;或在 Edge 启用分片合并。
- tc 默认类没命中:fwmark 标记丢失或规则优先级冲突。解法:tcpdump -vv -i eth0 'host 203.0.113.10' 验证;检查 tc filter 是否 parent 1:、prio 正确。
- nftables 与 Nginx 网段不同步:半年后网段更新,限速失效。解法:集中维护清单,Ansible 下发到 nft 与 Nginx map。
- BBR 在高丢包链路“过于激进”:个别跨境段丢包>2%。解法:适度下调 ceil,让 fq_codel 有足够队列吸收抖动;必要时切换 cubic 做 AB 对比。
- 对象存储后端抖动:源站代理到后端偶发 5xx。解法:proxy_cache_use_stale & 后端重试;同时对冷门分片增加 prewarm。
10. 参数速查(建议起点)
| 场景 | 建议值 |
limit_rate_after |
1m(或 512k~2m 视分片大小) |
limit_rate(视频) |
2.2m~3.0m/请求(8Mbps 码率对应) |
limit_req |
200r/s 每 PoP 网段起步,burst=400 |
HTB rate/ceil |
rate=承诺带宽*0.7,ceil=承诺*0.9 |
| 叶队列 | fq_codel(延迟友好) |
| TCP 拥塞 | bbr(常规)/ cubic(高丢包对比) |
11. 与 Ubuntu 20.04 的差异
- nftables/ipset 都可用,但 建议统一用 nftables;
- bbr 默认可用,参数项一致;
- Nginx 变量化 limit_rate 需要 1.17+,建议自编译或使用新仓库版本。
凌晨的风逐渐停了,机房只剩下风冷的嗡嗡声。Grafana 上 CN2 的曲线像一条平滑的丝带,再没有那种尖到让人心跳加速的刺。我把最后一条 Ansible 任务点了“完成”,关掉了手机的告警声音。
外面天边泛了点亮,我知道这套“回源限速 + 内核整形 + 缓存抑制放大”的组合拳,已经把那种让人抓狂的跨境卡顿按了下去。或许下一次热点来临还会有新问题,但至少今晚,节奏回到了我们手里。