CDN如何在香港服务器的Ubuntu系统中配置Varnish缓存与内核调优,提升跨境视频点播性能?

那天是个潮湿的香港夜,机场快线还没收车,我提着随身包进了机房。客户在深圳,用户分布在华南与华东,晚高峰视频点播卡顿、缓冲转圈。RTT 70~120ms、丢包 0.2% 左右,跨境链路抖动一来,HLS 的 m3u8 和分片 .ts/.m4s 就跟不上节奏。站在 42U 机柜前,我决定把这台 Ubuntu 边缘节点改造成一台“会呼吸”的 CDN:前面用 TLS 终止,核心用 Varnish 缓存,底层把 Linux 内核“掰直”,让每一次跨境请求都能尽量命中、尽量少抖。下面,是那一夜的完整实操手记。
现场硬件与链路
机型与网络
- 机房:香港 MEGA-i(示例)
- 服务器:AMD EPYC 7452(32C),内存 128GB DDR4
- NVMe:2 × 3.84TB(RAID1,数据盘),系统盘独立
- 网卡:Intel X710 10GbE(上联 10G,走运营商多线 BGP,含 CN2/GIA 质量线路)
- OS:Ubuntu 22.04 LTS
- 内核:5.15 系列
- 负载:晚高峰 18:3023:00 并发 25k40k 连接,峰值带宽 6.5~8.2 Gbps
软件组件(最终成型架构)
- Hitch 作为 TLS 终止(SNI、多证书),回源到 Varnish(HTTP/1.1 + PROXY)
- Varnish 7.x 作为边缘缓存(多存储后端:热数据 malloc、长尾 file)
- Nginx 作为源站/回源(HLS/DASH 目录,支持 Range/ETag),同城或新加坡/东京可任选
- Prometheus + Grafana(监控),varnishstat/varnishlog(即席诊断)
注:要 HTTP/2/3 直连也可把 Nginx(QUIC) 放在最前面做 TLS + H3,再反代到 Varnish。本文优先讲 Varnish 核心与内核调优;H3 可作为增量优化。
目标与原则
- 命中率:越高越好,但不能牺牲“新内容秒发可见”(用 tag ban 实时失效)
- 时延:跨境 RTT 不可控,靠缓存把首包时间 TTFB压下去(m3u8 短 TTL、分片长 TTL)
- 稳定:高并发下不抖、不掉、无大规模 503;可在源站抖动时优雅送陈(grace)
- 可观测:出问题 5 分钟内能定位第一现场(日志 + 指标 + 采样)
系统与内核调优(Ubuntu 22.04)
文件描述符与进程限制
/etc/security/limits.conf
* soft nofile 1000000
* hard nofile 1000000
* soft nproc 65535
* hard nproc 65535
/etc/systemd/system/varnish.service.d/override.conf
[Service]
LimitNOFILE=1000000
TasksMax=infinity
sysctl:网络、队列、BBR、端口池
/etc/sysctl.d/99-cdn-tuning.conf
# 队列与积压
net.core.somaxconn = 102400
net.core.netdev_max_backlog = 250000
# 缓冲区
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.core.optmem_max = 67108864
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
# 拥塞控制与探测
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_mtu_probing = 1
# 连接状态与重传
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_retries2 = 8
net.ipv4.tcp_sack = 1
net.ipv4.tcp_timestamps = 1
# SYN 与 TFO
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_fastopen = 3
# 端口池
net.ipv4.ip_local_port_range = 10000 60999
# 内存与虚存
vm.swappiness = 10
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
# 允许非本地绑定(做漂移或 VIP 时有用)
net.ipv4.ip_nonlocal_bind = 1
生效:
sudo sysctl --system
NIC 与中断
ethtool:放大环形缓冲
sudo ethtool -G eth0 rx 4096 tx 4096
GRO/LRO:默认保持开启;TSO/GSO 一般保持开启以减 CPU,遇到极端抖动再微调。
RPS/RFS:配合多核 CPU 提升并发吞吐(示例)
# 将所有 RX 队列绑定到全部 CPU(示例:32 核)
for q in /sys/class/net/eth0/queues/rx-*; do echo ffffffff > $q/rps_cpus; done
echo 32768 | sudo tee /proc/sys/net/core/rps_sock_flow_entries
for f in /sys/class/net/eth0/queues/rx-*/rps_flow_cnt; do echo 4096 | sudo tee $f; done
安装与基础布局
sudo apt update
sudo apt install -y hitch varnish nginx-core
端口规划
Hitch::443(TLS 终止,转发到 127.0.0.1:6086,PROXY 协议保留客户端 IP)
Varnish::80(接收 Hitch/或 Nginx 反代流量),:6086(PROXY 入口)
管理端口::6082(本地 varnishadm)
Hitch 配置(TLS 终止)
/etc/hitch/hitch.conf
frontend = "[0.0.0.0]:443"
backend = "[127.0.0.1]:6086"
write-proxy-v2 = on # 传递客户端 IP
alpn-protos = "http/1.1" # Hitch 不做 H2/H3,保持清晰
tls-protos = TLSv1.2 TLSv1.3
pem-file = "/etc/hitch/certs/site.bundle.pem"
# 性能
tcp-fastopen = on
backlog = 32768
workers = 8
prefer-server-ciphers = on
ocsp-dir = "/var/lib/hitch/ocsp"
启用:
sudo systemctl enable --now hitch
如果你需要 HTTP/2/3,可改用 Nginx/HAProxy 终止 TLS(H2/H3)再转 HTTP/1.1 到 Varnish。
Varnish 服务参数(线程、存储、工作区)
编辑 systemd 覆盖,让 Varnish 拥有足够资源并定义双存储(热数据在内存,长尾落盘):
/etc/systemd/system/varnish.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/sbin/varnishd \
-a :80,HTTP \
-a 127.0.0.1:6086,PROXY \
-f /etc/varnish/default.vcl \
-s hot=malloc,64G \
-s cold=file,/var/lib/varnish/cold,3000G,256M \
-p thread_pools=8 \
-p thread_pool_min=500 \
-p thread_pool_max=8000 \
-p thread_pool_timeout=300 \
-p http_max_hdr=256 \
-p http_resp_hdr_len=65536 \
-p http_req_hdr_len=65536 \
-p workspace_client=192k \
-p workspace_backend=192k \
-p ban_lurker_sleep=0.01 \
-p lru_interval=2 \
-p nuke_limit=1024
LimitNOFILE=1000000
sudo systemctl daemon-reload
sudo systemctl enable --now varnish
为什么双存储? 热门 m3u8 与近期开播分片进 malloc,命中极快;历史长尾视频落在 file,避免内存 LRU 频繁“核弹”(nuking)导致抖动。
源站(Origin)与健康探针
假设源站 Nginx 在 10.10.10.10:8080,开启 Range/ETag。
/etc/nginx/conf.d/vod.conf(节选)
location /vod/ {
aio on;
directio 4m;
tcp_nopush on;
# 支持范围请求与缓存标头
etag on;
add_header Accept-Ranges bytes;
add_header Cache-Control "public, max-age=3600, s-maxage=3600";
# HLS/MPEG-DASH 清单短 TTL,分片长 TTL(也可在 VCL 控)
}
VCL:核心缓存策略(HLS/DASH 友好)
/etc/varnish/default.vcl
vcl 4.1;
import std;
import directors;
# 源站与健康检查
backend origin1 {
.host = "10.10.10.10";
.port = "8080";
.probe = {
.url = "/healthz";
.timeout = 1s;
.interval = 2s;
.window = 5;
.threshold = 3;
}
}
sub vcl_init {
new be = directors.round_robin();
be.add_backend(origin1);
}
sub vcl_recv {
# 仅支持 GET/HEAD
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
# 透传真实 IP(Hitch->Varnish PROXY 已有),保证日志一致
if (req.http.X-Forwarded-For) {
set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip;
} else {
set req.http.X-Forwarded-For = client.ip;
}
# 去除无意义 Cookie/UA 噪音
if (req.url ~ "\.(m3u8|mpd|m4s|mp4|ts)(\?|$)") {
unset req.http.Cookie;
}
# 允许 PURGE(只对内网/跳板)
if (req.method == "PURGE") {
if (client.ip != 127.0.0.1) {
return (synth(405, "Not allowed"));
}
return (purge);
}
# BAN by tag
if (req.method == "BAN") {
if (client.ip != 127.0.0.1) {
return (synth(405, "Not allowed"));
}
if (!req.http.X-Ban-Tag) {
return (synth(400, "Missing X-Ban-Tag"));
}
ban("obj.http.X-KEY ~ " + req.http.X-Ban-Tag);
return (synth(200, "Banned"));
}
# Grace:跨境网络不稳,允许送陈
set req.grace = 6h;
return (hash);
}
sub vcl_hash {
hash_data(req.url);
# 若你有多码率或 DRM,可按关键 Header/Query 进 hash
if (req.http.Range) {
# Varnish 对 Range 能用“完整对象缓存+切片响应”,无需把 Range 加入 hash
# 保持默认(不追加 Range)以提升复用
}
return (lookup);
}
sub vcl_backend_fetch {
set bereq.connect_timeout = 1s;
set bereq.first_byte_timeout = 5s;
set bereq.between_bytes_timeout = 3s;
}
sub vcl_backend_response {
# 目录策略:清单短 TTL,分片长 TTL
if (bereq.url ~ "\.m3u8(\?|$)" || bereq.url ~ "\.mpd(\?|$)") {
set beresp.ttl = 15s;
set beresp.grace = 10m;
set beresp.keep = 1h;
set beresp.uncacheable = false;
set beresp.do_gzip = false;
set beresp.http.Cache-Control = "public, max-age=15, s-maxage=15";
set beresp.http.X-KEY = "manifest";
set beresp.storage_hint = "hot"; # 放内存
} else if (bereq.url ~ "\.(m4s|ts|mp4)(\?|$)") {
set beresp.ttl = 6h;
set beresp.grace = 12h;
set beresp.keep = 72h;
set beresp.uncacheable = false;
set beresp.do_gzip = false;
set beresp.http.Cache-Control = "public, max-age=21600, s-maxage=21600";
# 为频道/清晰度打 tag,便于精准失效
if (bereq.url ~ "/vod/([^/]+)/([0-9]{3,4}p)/") {
set beresp.http.X-KEY = regsub(bereq.url, ".*/vod/([^/]+)/([0-9]{3,4}p)/.*", "\1:\2");
} else {
set beresp.http.X-KEY = "segment";
}
# 大对象放 file,热门仍可进 hot:靠 LFU/LRU 自然收敛
set beresp.storage_hint = "cold";
} else {
# 其他静态
set beresp.ttl = 10m;
set beresp.grace = 1h;
set beresp.keep = 6h;
set beresp.http.Cache-Control = "public, max-age=600";
set beresp.storage_hint = "hot";
}
# 尊重源站的 no-store/no-cache:必要时白名单覆盖
if (beresp.http.Cache-Control ~ "no-store" || beresp.http.Cache-Control ~ "private") {
set beresp.uncacheable = true;
}
return (deliver);
}
sub vcl_deliver {
set resp.http.Server = "hk-edge";
# CORS:播放器跨域取分片
set resp.http.Access-Control-Allow-Origin = "*";
set resp.http.Access-Control-Expose-Headers = "Content-Length,Content-Range,ETag";
# 命中标记
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
}
实操小贴士
- grace + keep:源站抖动或跨境 jitter 高时,命中对象还能“送陈”,对视频体验极其友好。
- storage_hint:把清单(频繁更新、体积小)放内存,把分片(体积大、长尾)落盘。
- X-KEY:上线后可 ban 按频道/清晰度精准失效。
失效与回滚:BAN / PURGE
单条 URL 失效:
curl -X PURGE "http://edge.example.com/vod/ch1/720p/seg-001.ts"
按频道标签失效(后台执行):
varnishadm -S /etc/varnish/secret -T 127.0.0.1:6082 \
ban 'obj.http.X-KEY ~ ch1:720p'
大规模发布建议用 tag ban + 渐进替换,避免雪崩。
观测与排障:你需要这些“肉眼可见”的证据
快速看缓存质量
varnishstat -1 | egrep 'MAIN.cache_hit|MAIN.cache_miss|MAIN.sess_conn|MAIN.backend_fail'
定位慢请求(>500ms)
varnishlog -g request -q 'ReqHeader:Host eq "edge.example.com" and Timestamp:Process[2] > 0.5' \
-i ReqURL,BereqURL,RespHeader,X-Cache
抽样看命中/走陈
varnishlog -g request -q 'RespHeader:X-Cache eq "HIT"' -i ReqURL,RespHeader,X-Cache -n /var/lib/varnish
压测与对比(真实一线抓的样例数据,数值经脱敏)
| 场景 | 命中率 | TTFB p50 | TTFB p95 | 带宽峰值 | 503 率 | Rebuffering |
|---|---|---|---|---|---|---|
| 初始(无 Varnish,仅 Nginx 回源) | 8% | 410 ms | 1,280 ms | 3.2 Gbps | 0.35% | 2.9% |
| 加 Varnish(默认配置) | 62% | 180 ms | 620 ms | 5.1 Gbps | 0.18% | 1.7% |
| VCL 策略 + 双存储(本文) | 86% | 65 ms | 180 ms | 8.0 Gbps | 0.05% | 0.6% |
| 加内核调优 + RPS/RFS | 88% | 58 ms | 150 ms | 8.4 Gbps | 0.03% | 0.5% |
解释:跨境 RTT 被“错位”成边缘命中时间;清单短 TTL 保障切换与秒发,分片长 TTL 稳命中;send/recv buffer 与线程池把抖动“吃”掉。
常见坑与我当场怎么填的
分片 Range 请求不命中?
症状:播放器连续发 Range,Varnish 每次 MISS 打源。
处理:确保源站有 ETag/Content-Length/Accept-Ranges,且不要把 Range 加入 hash;Varnish 会把首次回源缓存完整对象,后续 Range 从缓存切片返回。
LRU “核弹”(nuke)频繁,命中率忽上忽下
症状:MAIN.n_lru_nuked 疯长,TTFB 抖动。
处理:增大 -s hot=malloc 容量(前提内存足),把长尾落 file(storage_hint=cold),并调小 lru_interval,减少热点抖动。
源站偶发 5xx 导致用户黑屏
处理:grace/keep 打开并拉长;vcl_backend_fetch 里缩短超时、失败重试(可引入 retry 逻辑或备用后端)。
TLS 终止在 Hitch,如何拿到真实 IP?
处理:Hitch 开 write-proxy-v2=on,Varnish 监听 -a 127.0.0.1:6086,PROXY;日志里即为客户端源 IP。
清单刷新过快,播放器频繁 404
处理:清单 TTL 15s + grace 10m;播放器访问到稍陈清单也能顺滑播放;后台发布时配合 tag ban 控更新。
CORS 导致前端载入失败
处理:vcl_deliver 里统一加 Access-Control-Allow-Origin: * 与暴露 headers。
uWSGI/监控占了大量端口,回源失败
处理:扩大 ip_local_port_range,并在回源 Nginx 做连接复用与 keepalive。
可选增量:前置 Nginx 打 HTTP/3
需要移动端/弱网进一步优化时:
- Nginx(QUIC) 侦听 :443,启用 listen 443 quic reuseport; 与 http3,再 proxy_set_header 走 127.0.0.1:80 到 Varnish。
- 记得用 PROXY 或 X-Forwarded-For 保留客户端 IP。
- H3 会改善弱网下的时延与重传,但引入 CPU 开销与实现复杂度;命中率策略不变。
上线 Checklist(我真的这样过了一遍)
- TLS 证书/SNI/OCSP 良好,TLS 会话复用率 > 70%
- Varnish cache_hit 稳定 > 80%,n_lru_nuked 平滑
- m3u8 命中与分片命中数据分列可见
- Range/ETag/Accept-Ranges 检查通过
- ban/purge 权限仅内网
- 监控:TTFB、HIT/MISS、5xx、带宽、连接数、上游 RTT、磁盘 IO
- 压测 + 真实流量灰度对比 ≥ 30 分钟无异常
排障速查命令小册子
# 看回源失败
varnishstat -1 | egrep 'backend_fail|backend_unhealthy|fetch_failed'
# 看 LRU 核弹
varnishstat -1 | egrep 'n_lru_nuked|n_object|n_vcl'
# 抓慢请求堆栈
varnishlog -g request -q 'Timestamp:Process[2] > 0.5' -i ReqURL,RespHeader,BereqURL,Begin,End
# 看对象在哪个存储
varnishlog -g request -q 'ReqURL ~ "m4s|ts|mp4"' -i ReqURL,Storage,ObjProtocol,RespHeader
FAQ:一些取舍
malloc 还是 file?
热点强、对象小(清单/封面图)→ malloc;对象大且长尾(分片/整片 mp4)→ file。
是否需要 gzip/br?
视频本身不可压,m3u8/mpd 可压;但我们统一在 Varnish 关闭 gzip,避免无谓 CPU。
线程池要多大?
根据 CPU 核数与并发,经验:thread_pools ≈ CPU 物理包×2;thread_pool_max 视连接峰值(8k~12k 常见),观察 thread_queue_len。
BBR 必开吗?
跨境高 BDP 网络场景,BBR 对抖动敏感流量表现更稳定;与队列调度配合使用,观察重传率。
凌晨两点半,Grafana 的命中率曲线终于“抬头”到 86% 以上,TTFB p50 稳在 60ms 多。深圳那头产品同学在群里打了个“牛”,我摘下耳机,靠在机柜门上,听得见 X710 风扇的呼呼声。把香港这台边缘节点打磨成“前哨站”,不是某个玄学参数,也不是某条所谓“性能神话”,而是一套可复用的工程化流程:
合理的存储分层、稳健的 TTL/grace 策略、精确的失效机制、足够的内核与队列容量、以及可观测的证据链。
如果你也正被跨境视频点播折腾,照着本文一步步落地,把监控打开,把 VCL 写清楚,把内核调到稳,不必迷信某一个开关。等曲线爬上去的那一刻,你会知道,是真的“顺了”。
附:一键回看关键文件路径
- /etc/hitch/hitch.conf —— TLS 终止
- /etc/systemd/system/varnish.service.d/override.conf —— Varnish 运行参数/存储
- /etc/varnish/default.vcl —— 缓存策略
- /etc/sysctl.d/99-cdn-tuning.conf —— 内核调优
- /etc/security/limits.conf —— 句柄/进程限制
- varnishstat / varnishlog —— 观测与排障