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

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

发布人:Minchunlin 发布时间:2025-09-13 10:28 阅读量:1133


凌晨 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.7ceil=承诺*0.9
叶队列 fq_codel(延迟友好)
TCP 拥塞 bbr(常规)/ cubic(高丢包对比)

11. 与 Ubuntu 20.04 的差异

  • nftables/ipset 都可用,但 建议统一用 nftables;
  • bbr 默认可用,参数项一致;
  • Nginx 变量化 limit_rate 需要 1.17+,建议自编译或使用新仓库版本。

凌晨的风逐渐停了,机房只剩下风冷的嗡嗡声。Grafana 上 CN2 的曲线像一条平滑的丝带,再没有那种尖到让人心跳加速的刺。我把最后一条 Ansible 任务点了“完成”,关掉了手机的告警声音。

外面天边泛了点亮,我知道这套“回源限速 + 内核整形 + 缓存抑制放大”的组合拳,已经把那种让人抓狂的跨境卡顿按了下去。或许下一次热点来临还会有新问题,但至少今晚,节奏回到了我们手里。

目录结构
全文