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

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

发布人:Minchunlin 发布时间:2025-09-07 11:08 阅读量:632


那天是个潮湿的香港夜,机场快线还没收车,我提着随身包进了机房。客户在深圳,用户分布在华南与华东,晚高峰视频点播卡顿、缓冲转圈。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 —— 观测与排障
目录结构
全文