如何为部署在香港服务器Ubuntu系统中的短视频平台部署多层缓存,避免国内和国外用户观看热点视频播放卡顿?
技术教程 2025-09-16 10:46 181


那天凌晨 1:40,运营在群里扔了一句:“#129876 那条视频炸了,国内卡、海外也卡。”

我盯着 Grafana 上那条“边缘回源带宽”曲线——像火箭发射一样直冲 34 Gbps;同时,Nginx 的 upstream_response_time 被 206 Range 请求拉得锯齿分明。热点视频只有 18 秒,却被拆成了几十个小分段反复拉。

我当机立断,把“多层缓存”这件事,从我脑子里的架构图搬到真实的机房里:浏览器 → 多 CDN → 区域中台缓存(香港/新加坡/东京) → 源站缓存 → 对象存储/NVMe。目标只有一个:尽量让每一层都能兜住请求,把“回源”变成最后的保底,而不是常态。

下面是我完整的部署与优化过程,细到可以直接照做;我也把踩坑与现场解法如实记录,免得你在凌晨复刻我的抓狂。

一、整体拓扑与分层思路

分层(由外到内)

  • L0 浏览器缓存:合理 Cache-Control、immutable、stale-while-revalidate;HLS/DASH 清单短 TTL、分片长 TTL。
  • L1 多 CDN:国内(阿里云/腾讯云/网宿等),海外(Cloudflare/Akamai/Fastly 等),基于调度/GeoDNS/EDNS ECS 分流。
  • L2 区域中台缓存(香港主、东京/新加坡辅):Varnish/ATS 集群,collapsed forwarding、grace、Range 合并、跨 POP 预热。
  • L3 源站(OpenResty/Nginx):proxy_cache + cache_lock、Key 规范化、HLS 索引 vs 分片差异化 TTL、签名鉴权、带宽整形。
  • L4 数据层:本地 NVMe(ZFS/EXT4 noatime)热集 + S3 兼容对象存储(阿里云 OSS/Wasabi/Backblaze B2 等)冷集,生命周期管理与后台回灌。

请求特性

  • HLS/DASH 小文件多、并发高、206 Range 常见。
  • 热点集中在 Top-N(典型 1% 内容产生 80% 请求)。
  • 国内与海外链路抖动差异大(国内跨境回源要么 CN2/GIA,要么走合规 CDN 回源专线)。

二、硬件与网络建议(我在香港的真实选型)

层级 硬件建议 关键参数 选择理由
L2 中台缓存(HK 主) AMD EPYC 7443P 单路,128GB RAM,2×U.2 NVMe 3.84TB(ZFS mirror)10GbE(可上 25GbE) IOPS 80w+,顺序读 6–7GB/s Varnish/ATS 极度吃 IO 与内存,镜像保障可用性,10G 起步
L2 辅助(SG/TYO) 同规格缩容至 64GB + 1×NVMe - 跨区兜底与预热,加速亚太各区首包
L3 源站 EPYC 7302P, 64–128GB, 2×NVMe(RAID1) + 系统盘 SSD - OpenResty + FFmpeg/打包进程,磁盘独立
网络 运营商混线 + CN2/GIA 回国优化HKIX 直连 BGP/静态都可 国内访问抖动小、海外首包更稳
存储冷层 OSS/B2/Wasabi 任一 多区域冗余 降本与扩容弹性

经验:NVMe 加散热片;fio --filename=/cache/nuster.test --size=10G --rw=randread --bs=4k 跑下稳态 IOPS,确保不会过热降频。

三、Ubuntu 基线优化(22.04 LTS 示例)

# 1) 基础系统
apt update && apt -y install build-essential git htop iotop jq curl unzip chrony \
  nginx openssl ffmpeg

# 2) 时钟同步(边缘缓存对 TTL/刷新敏感)
systemctl enable --now chrony

# 3) 内核网络栈(BBR + FQ)
cat >> /etc/sysctl.d/99-tuning.conf <<'EOF'
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
net.core.somaxconn=65535
net.ipv4.ip_local_port_range=1024 65000
net.ipv4.tcp_max_syn_backlog=262144
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_fin_timeout=15
net.core.rmem_default=1048576
net.core.wmem_default=1048576
net.core.rmem_max=16777216
net.core.wmem_max=16777216
fs.file-max=1048576
EOF
sysctl --system

# 4) 文件句柄
cat >> /etc/security/limits.conf <<'EOF'
* soft nofile 1048576
* hard nofile 1048576
EOF

四、视频打包与存储布局

ABR 梯度(示例)

240p@300k, 360p@600k, 480p@1000k, 720p@2000k, 1080p@3500k(根据你的人群与码率预算调整;国内 4G 常见 480/720,海外 Wi-Fi/5G 1080 更常见)

HLS 打包示例(fMP4 优先,便于 DASH 复用):

INPUT=source.mp4
OUT=/data/hls/vid_129876
mkdir -p "$OUT"

ffmpeg -y -i "$INPUT" \
  -filter_complex "\
    [0:v]split=5[v1][v2][v3][v4][v5]; \
    [v1]scale=-2:426[v240]; \
    [v2]scale=-2:640[v360]; \
    [v3]scale=-2:854[v480]; \
    [v4]scale=-2:1280[v720]; \
    [v5]scale=-2:1920[v1080]" \
  -map [v240] -map a:0 -c:v h264 -b:v 300k -c:a aac -b:a 64k \
  -map [v360] -map a:0 -c:v h264 -b:v 600k -c:a aac -b:a 96k \
  -map [v480] -map a:0 -c:v h264 -b:v 1000k -c:a aac -b:a 96k \
  -map [v720] -map a:0 -c:v h264 -b:v 2000k -c:a aac -b:a 128k \
  -map [v1080] -map a:0 -c:v h264 -b:v 3500k -c:a aac -b:a 128k \
  -f hls -hls_time 4 -hls_playlist_type vod \
  -hls_segment_type fmp4 -hls_flags independent_segments+append_list+omit_endlist \
  -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 v:3,a:3 v:4,a:4" \
  -master_pl_name "index.m3u8" \
  -hls_segment_filename "$OUT/v%v/seg_%06d.m4s" \
  "$OUT/v%v/stream.m3u8"

目录规范(便于 cache key 与签名参数稳定):

/hls/129/129876/        # 三级分桶
  index.m3u8
  v240/stream.m3u8
  v240/seg_000001.m4s
  ...
  v1080/stream.m3u8

小结:

  • 清单(m3u8)TTL 短(例如 30–60s),因为可能追加片段或修正;
  • 分片(m4s/ts)TTL 长(7–30 天 + immutable),几乎不变。

五、L0 浏览器缓存:响应头与 206

Nginx(OpenResty)按路径下发头部:

map $uri $hls_cache_control {
    default                              "public, max-age=60, stale-while-revalidate=60";
    ~*\.m3u8$                            "public, max-age=30, stale-while-revalidate=60";
    ~*\.m4s$                             "public, max-age=2592000, immutable, stale-while-revalidate=600";
}

# 对 Range/206 也允许缓存(后面层级处理),浏览器自然命中有限,但能减少重复请求
add_header Cache-Control $hls_cache_control;
add_header Accept-Ranges bytes;

Tips:Safari 对 HLS 细节更敏感,#EXT-X-TARGETDURATION、#EXT-X-PROGRAM-DATE-TIME 等要规范;清单短 TTL 能掩盖片段追加时的边缘差异。

六、L1 多 CDN:国内/海外分流与回源

做法要点

  1. 两个 CNAME:v.example.com(海外)→ Cloudflare/Akamai,v-cn.example.com(国内)→ 阿里云/腾讯云。
  2. App/Web 端按 IP 归属、SDK 配置或接口下发选择域名(也可 GeoDNS/ECS)。
  3. 回源统一指向L2 香港中台,而不是直打源站。
  4. 开启 CDN 的 HTTP/3(边缘到用户),边缘到回源保持 HTTP/2;Range Cache on(不同 CDN 名称不同,务必开启)。
  5. 签名/防盗链在 L3 校验,CDN 只做透传与缓存。
  6. 国内合规提示:若需在大陆区域提供加速/分发,遵循当地法规(域名备案、CDN 合规等),选择合规厂商的大陆节点与回源链路。

七、L2 区域中台缓存(Varnish 7.x 示例)

安装与存储后端(NVMe ZFS mirror 做大缓存区):

apt -y install varnish
# 存储:文件后端,预留 2.5TB
cat > /etc/varnish/storage.conf <<'EOF'
# 生产建议使用 "file" 或 "malloc+file" 组合
# 这里用文件后端,命名为 s1,大小 2500G
s1=File,/cache/varnish_cache.bin,2500G
EOF

# 启动参数
cat > /etc/systemd/system/varnish.service.d/override.conf <<'EOF'
[Service]
Environment="VARNISH_STORAGE=file,/cache/varnish_cache.bin,2500G"
Environment="VARNISH_LISTEN_PORT=6081"
Environment="VARNISH_ADMIN_LISTEN_ADDRESS=127.0.0.1"
Environment="VARNISH_ADMIN_LISTEN_PORT=6082"
Environment="VARNISH_THREADS=200,4000"
EOF
systemctl daemon-reload && systemctl restart varnish

VCL 关键点:

  • Range 合并(把相邻或重叠的 Range 请求合并后回源)。
  • Collapsed Forwarding(同一对象首个 miss 回源,其余等待;避免“羊群效应”)。
  • grace/stale-if-error(源站异常时继续发陈旧内容)。
  • Cache Key 规范化(忽略无关 query,如 utm_*;保留 v= 版本号)。

vcl 4.1;

backend default {
  .host = "10.0.0.10"; # L3 源站 Nginx
  .port = "8080";
  .connect_timeout = 1s;
  .first_byte_timeout = 30s;
  .between_bytes_timeout = 30s;
}

sub vcl_recv {
  # 忽略跟播放无关的参数
  if (req.url.qs) {
    set req.url = querystring.remove(req.url, "utm_source|utm_medium|utm_campaign|_t|t");
  }

  # 仅允许部分方法缓存
  if (req.method != "GET" && req.method != "HEAD") {
    return (pass);
  }

  # 允许 Range,但做合并(vmod)
  if (req.http.Range) {
    set req.http.X-Range = req.http.Range;
  }

  # 粘合相同对象的并发(collapsed forwarding)
  return (hash);
}

sub vcl_backend_response {
  # m3u8 短 TTL,m4s 长 TTL
  if (bereq.url ~ "\.m3u8($|\?)") {
    set beresp.ttl = 30s;
    set beresp.grace = 60s;
  } else if (bereq.url ~ "\.(m4s|ts)($|\?)") {
    set beresp.ttl = 30d;
    set beresp.grace = 10m;
  } else {
    set beresp.ttl = 10m;
    set beresp.grace = 5m;
  }

  # 允许缓存 206
  if (beresp.status == 206) {
    set beresp.do_stream = true;
    set beresp.do_gzip = false;
  }

  # 允许 stale-if-error
  set beresp.keep = 10m;
}

sub vcl_hash {
  hash_data(req.url);
  # 用 UA 维度区分可选(一般不建议)
  # hash_data(req.http.User-Agent);
}

sub vcl_deliver {
  set resp.http.X-Cache = obj.hits > 0 ? "HIT" : "MISS";
}

如果你偏向 ATS(Apache Traffic Server),也能实现 Range 缓存与 collapsed forwarding;ATS 在大对象与磁盘 IO 上很稳,Varnish 在灵活 VCL 上更快手。

跨区预热:

热点提升时,先在 HK 节点预热完,再由脚本对 SG/TYO 发带并发限制的 GET,填满二级缓存,避免海外首次访问回 HK。

八、L3 源站(OpenResty/Nginx)缓存与鉴权

目录与缓存空间

  • proxy_temp_path /var/cache/nginx/temp;
  • proxy_cache_path /var/cache/nginx/cache levels=1:2 keys_zone=hls_cache:512m max_size=120g inactive=10m use_temp_path=off;

Nginx 关键配置(/etc/nginx/conf.d/hls.conf):

proxy_cache_path /var/cache/nginx/cache levels=1:2 keys_zone=hls_cache:512m
                 max_size=120g inactive=10m use_temp_path=off;

map $request_uri $cache_key {
    default $request_uri;
}

map $uri $cache_ttl {
    default          10m;
    "~*\.m3u8$"      30s;
    "~*\.m4s$"       30d;
}

server {
    listen 8080 http2 reuseport;
    server_name origin-hls;

    # 鉴权(示例:签名 ?token=xxx&exp=...,Lua 校验)
    set $authorized 0;
    access_by_lua_block {
        local args = ngx.req.get_uri_args()
        -- 这里校验 token、过期时间、path 绑定等,失败则 ngx.exit(403)
        if args.token and args.exp and tonumber(args.exp) > ngx.time() then
            ngx.var.authorized = 1
        else
            return ngx.exit(403)
        end
    }

    # 缓存与锁,避免击穿
    proxy_cache          hls_cache;
    proxy_cache_key      $cache_key;
    proxy_cache_lock     on;
    proxy_cache_lock_timeout 10s;
    proxy_cache_valid    206 200 301 302 $cache_ttl;
    proxy_ignore_headers Set-Cookie Expires;
    add_header X-Cache-Status $upstream_cache_status always;

    location /hls/ {
        root /data;
        # 静态命中:直接本地文件
        try_files $uri @miss;
    }

    location @miss {
        # 回对象存储(示例:S3 兼容网关或 SDK 代理服务)
        proxy_set_header Host s3-gw.internal;
        proxy_pass http://s3-gw.internal$request_uri;
    }
}

对象存储回源

  • 通过内网 S3 网关或直接公网 S3(配专线更稳)。
  • 分片文件一旦落库,尽量不可变,避免多点缓存出入不一致。

九、预热(热点上架时)与自动降温

预热脚本(Python/asyncio 简化版)

读取 index.m3u8,批量 GET 前 20 个片段,命中 L2/L1:

# prewarm.py
import asyncio, aiohttp, re

async def fetch(session, url):
    async with session.get(url, timeout=10) as r:
        return await r.text() if url.endswith('.m3u8') else await r.read()

async def main(base):
    async with aiohttp.ClientSession() as s:
        m3u8 = await fetch(s, f"{base}/index.m3u8")
        variants = re.findall(r'(v\d+/stream\.m3u8)', m3u8)
        tasks = []
        for v in variants:
            sub = await fetch(s, f"{base}/{v}")
            segs = re.findall(r'(seg_\d+\.m4s)', sub)[:20]
            for seg in segs:
                tasks.append(fetch(s, f"{base}/{v.replace('stream.m3u8', seg)}"))
        await asyncio.gather(*tasks)

if __name__ == "__main__":
    import sys
    asyncio.run(main(sys.argv[1]))

使用:

python3 prewarm.py https://v.example.com/hls/129/129876
python3 prewarm.py https://edge-hk.example.com/hls/129/129876  # L2 直预热

自动降温

  • 播放日志上报到 Kafka → Flink 聚合出 topN → 写入 Redis ZSET(HOT:score)。
  • N 小时未访问的内容,从 L2/L3 缓存淘汰(inactive),仅留 L4 冷层保底;定期 varnishadm ban 逐步扫陈旧。

十、缓存清理与版本化策略

强烈建议:URL 中携带版本号或内容哈希,如:

/hls/129/129876?v=1700000100(或把版本编码在路径),上线新版本直接换 URL,避免全网 PURGE。

确需清理(比如下架/侵权):

CDN:各家提供 API,一次性 Purge 精准路径前缀。

Varnish:

varnishadm ban "req.url ~ /hls/129/129876"

源站:删除本地与对象存储;为兼容边缘 TTL,返回 410/Cache-Control: no-store 至多 1 小时。

十一、监控与告警(我线上用的指标)

类别 指标 说明/阈值
命中率 L1/L2/L3 Hit Ratio L2 ≥ 92%,L3 ≥ 75% 为良好
回源带宽 to L3/L4 热点期曲线平稳无锯齿
首包时间 TTFB p95 国内 < 300ms,海外 < 500ms(视 CDN 区域)
状态码 5xx/4xx 比例 5xx < 0.1%,403 明显增高排查签名
Nginx upstream_response_time, waiting 锯齿=击穿/合并不佳
Varnish MAIN.n_object, MAIN.threads, n_lru_nuked LRU 淘汰过快=缓存过小或 TTL 过短
磁盘 NVMe 温度/延时 温度 > 70℃ 降频,预防

日志统一到 Loki/ELK,图表用 Grafana;异常波动设置告警(飞书/钉钉/Slack)。

十二、关键参数备忘(表)

推荐值
HLS 片段时长 4s(3–6s 之间权衡)
m3u8 TTL 30–60s
m4s/ts TTL 7–30 天 + immutable
Varnish grace m3u8: 60s;m4s: 10m
Nginx proxy_cache_lock_timeout 10s
打开文件数 ≥ 1,000,000
NVMe 缓存区 ≥ 热门内容峰值的 2–3 倍
CDN 边缘 Range 缓存 必须开启

十三、那些坑与我当场的解法

206 Range 不缓存:

好几家 CDN 默认不缓存 206;打开后回源立刻降 40%。

L2/L3 要明确允许 206 缓存,并对 Range 做合并。

清单与分片 TTL 搞反:

把 m3u8 设太长导致客户端等不到新分片;我把 m3u8 降到 30s,问题消失。

羊群效应:

热点瞬时涌入,L3 未开 proxy_cache_lock,同一对象并发回源打爆对象存储。

立刻开启锁 + L2 collapsed forwarding,曲线平滑。

签名鉴权与缓存互斥:

早期把 token 放在 Path,导致每个 token 都是不同 URL,命中率崩溃。

改为 Query 的 token 仅用于鉴权,Cache-Key 排除它,或用版本号 v= 作为唯一影响缓存的参数。

NVMe 过热降频:

连夜加了风道与散热片,I/O 抖动消失;长期方案是把热数据均衡到多台 L2。

海外首包慢:

开了东京/新加坡 L2,做跨区预热;海外用户 TTFB 下降 35%。

对象存储限流:

热点期回源 QPS 激增触发限流;在 L3 源站做“回源速率限制+指数退避”,并提升 L2 TTL 与 grace,用户无感知。

十四、上线步骤(我当晚的实际节奏)

  • 先在 L3 源站 开 proxy_cache + lock + TTL,保护对象存储。
  • 再上 L2 Varnish,验证 Range 与 collapsed forwarding;先 30% 流量,命中率稳定后全量。
  • 最后切 多 CDN 到 L2,开启各家的 Range 缓存与 HTTP/3。
  • 打通 预热脚本,联动运营的“热点列表”;
  • 配置 清理/版本化策略,降低全网 PURGE 频率。
  • 补充 监控与告警,值班表更新。

十五、复盘:前后指标对比

指标 优化前 优化后
L2 命中率 0%(未部署) 92.4%
L3 命中率 18.7% 77.8%
总回源带宽峰值 34 Gbps 6.1 Gbps
海外 p95 TTFB 820 ms 420 ms
国内 p95 TTFB 560 ms 240 ms
5xx 比例 0.6% 0.05%

注:数据来自当晚到次日午间的实际观测,设备与地域分布会影响你的结果,但量级的趋势具备参考意义。

第二天中午,我在机房走了一圈

风扇的噪声从“呼呼”回到了“嗡嗡”,边缘命中率在 99% 左右轻轻跳动。
运营发来“兄弟稳了”的时候,我正把一张便利贴贴到机柜门上:“先缓存,后计算;先就近,后回源;先可回滚,再全量。”
如果你也在香港给短视频平台扛热点,把上面这套多层缓存打法照着搭起来——不用等到凌晨,也能有底气面对每一次“突然爆了”的 18 秒