香港服务器上的短视频平台:我如何用 Nginx + MinIO 分布式缓存把加载速度打到极致
技术教程 2025-09-20 10:59 177


我们部署在香港机房的短视频平台,一条“爆了”的短视频把我们边缘节点打到 90%+ CPU、出口逼近线速,APP 端反馈“转圈圈”。我在香港葵涌的机柜前,决定当场把架构改成 Nginx 前置切片缓存 + MinIO 分布式对象存储。从那一晚开始,首页首帧 TTFB 从 280ms 降到 63ms,95 线 rebuffer 率砍半。下面是我在香港机房完整可复现的实操过程、参数细节、坑与复盘。

一、目标与思路

目标:首帧加载 < 100ms(香港/大湾区)、Seek 拖动无明显卡顿、热点内容抗“缓存惊群”、单机可灰度扩缩容。

思路:

  • Nginx 作为 HTTP/3(QUIC)边缘网关 + 切片缓存(slice)+ Range 透传;
  • MinIO 作为 分布式对象源站(Erasure Coding),承接回源与多副本;
  • 四层一致性哈希(L4)把同一资源尽量固定分配到同一台边缘 Nginx,让**整柜缓存变“分布式”**但又“各司其职”;
  • 签名 URL / secure_link 控制资源有效期与防盗链;
  • 背景刷新 + stale-while-revalidate 顶住热点“惊群”。

二、现场环境与硬件拓扑

2.1 机柜与网络(香港)

项目 参数
机房 Equinix HK2(到北上广回程多线 BGP)
出口 双上联 10GbE(ISP A/B),BGP,国际带宽 5Gbps 保底,峰值 20Gbps
机柜 42U,2 路 PDU,A/B 供电
内网 Spine-Leaf,TOR 25GbE,上联 100GbE,服务网 10.30.0.0/16,存储网 10.31.0.0/16
L4 入口 两台 LVS/HAProxy(Keepalived VRRP)做 一致性哈希(Maglev),VIP:203.0.113.10

2.2 服务器规格

角色 数量 关键配置
Edge Nginx 6 AMD EPYC 7313P(16C/32T),128GB RAM,NVMe U.2 7.68TB ×2(RAID0 读密集缓存),2×25GbE
MinIO 8 Intel Xeon Silver 4410Y(12C/24T),128GB RAM,HDD 12TB ×8 + NVMe 1.92TB ×1(元数据/索引),2×25GbE
转码/打包 2 EPYC 7543(32C/64T),256GB RAM,NVMe Scratch 3.84TB,2×25GbE
监控/指标 2 轻配,Prometheus + Loki + Grafana(与业务分网)

说明:NVMe 给边缘缓存,MinIO 用 HDD 做容量、NVMe 当 metadata/缓存,成本与性能平衡。

三、软件栈与版本建议

  • OS:Rocky Linux 9.3(内核 5.14+,BBR v2 可选);如果你必须用 CentOS 7,请把内核升到 ELRepo 版 4.19+,下文参数我会给兼容说明。
  • Nginx:1.25+(原生 HTTP/3 支持),编译启用 http_slice_module、http_v2_module、http_mp4_module。
  • MinIO:RELEASE.2025-xx(>= 2024 LTS 均可),启用 Erasure Coding、WORM 视业务决定。
  • OpenSSL/Quic:系统自带(Nginx 主线已支持 HTTP/3,无需 quiche 补丁)。
  • 监控:prometheus-nginx-exporter、MinIO 内置 /minio/v2/metrics/cluster。

四、MinIO 分布式集群部署(8 节点 EC:4+2)

4.1 磁盘与挂载

# 每台 MinIO 节点,HDD 分区并挂载到 /data{1..8}
for i in {1..8}; do
  mkfs.xfs -f /dev/sd${i}
  mkdir -p /data${i}
  echo "/dev/sd${i} /data${i} xfs noatime,nodiratime 0 0" >> /etc/fstab
done
mount -a

# 元数据/索引盘(NVMe)
mkfs.xfs -f /dev/nvme0n1
mkdir -p /minio_meta
echo "/dev/nvme0n1 /minio_meta xfs noatime 0 0" >> /etc/fstab
mount -a

4.2 启动参数(systemd)

假设 8 台节点主机名:mio{01..08}.storage.hk,存储网 IP 10.31.10.{1..8}。

# /etc/systemd/system/minio.service
[Unit]
Description=MinIO
After=network.target

[Service]
User=minio
Group=minio
Environment="MINIO_ROOT_USER=media_admin"
Environment="MINIO_ROOT_PASSWORD=StrongPassw0rd!"
Environment="MINIO_PROMETHEUS_AUTH_TYPE=public"
Environment="MINIO_BROWSER=off"
ExecStart=/usr/local/bin/minio server --address 10.31.10.%i:9000 --console-address 10.31.10.%i:9001 \
  http://mio01.storage.hk/data{1...8} \
  http://mio02.storage.hk/data{1...8} \
  http://mio03.storage.hk/data{1...8} \
  http://mio04.storage.hk/data{1...8} \
  http://mio05.storage.hk/data{1...8} \
  http://mio06.storage.hk/data{1...8} \
  http://mio07.storage.hk/data{1...8} \
  http://mio08.storage.hk/data{1...8} \
  --console-dir /minio_meta

LimitNOFILE=1048576
LimitNPROC=65535
Restart=always

[Install]
WantedBy=multi-user.target

注:--address/--console-address 中 %i 用 systemd 模板也可,我在现场直接每台一份 service,简单粗暴。

4.3 初始配置与桶策略

# 客户端
mc alias set hkminio http://10.31.10.1:9000 media_admin StrongPassw0rd!
mc admin info hkminio

# 创建桶 + 生命周期
mc mb hkminio/videos
# 公开读(仅 GET),写仍需签名
mc anonymous set download hkminio/videos
# 30 天过期的临时切片
mc ilm rule add hkminio/videos --expire-days 30 --prefix "hls/"

EC 容量计算:8 盘 EC(4+2) 默认能容忍 2 盘/节点故障;我们更关注读并发而不是纯写带宽。

五、Nginx 边缘:HTTP/3 + 切片缓存 + Range 透传

5.1 内核与系统调优

/etc/sysctl.d/99-tuning.conf(Rocky 9;CentOS7 兼容则去掉 BBR v2):

fs.file-max = 2000000
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
net.ipv4.ip_local_port_range = 10000 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 10
net.ipv4.tcp_max_syn_backlog = 262144
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456
net.ipv4.tcp_rmem = 4096 87380 268435456
net.ipv4.tcp_wmem = 4096 65536 268435456
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_mtu_probing = 1

Nginx worker 绑核(16C 机器例子):

worker_processes auto;
worker_cpu_affinity auto;     # 或手动: 00000001 00000010 ... 
worker_rlimit_nofile 1048576;

NVMe 缓存挂载(XFS):

mkfs.xfs -f /dev/nvme1n1
mkdir -p /var/cache/nginx
echo "/dev/nvme1n1 /var/cache/nginx xfs noatime,nodiratime,discard 0 0" >> /etc/fstab
mount -a

5.2 L4 一致性哈希(入口层)

在两台 HAProxy/LVS 上对 $request_uri 做 Maglev 哈希,确保同一视频命中同一台边缘缓存(减少跨机重复缓存)。示例(HAProxy):

hash-type consistent
backend edge_pool
  balance maglev
  server edge1 10.30.10.11:443 check
  server edge2 10.30.10.12:443 check
  ...

5.3 Nginx 关键配置(HTTP/3、切片、缓存)

# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
pid /run/nginx.pid;

events { use epoll; worker_connections 65535; }

http {
  include       mime.types;
  default_type  application/octet-stream;

  # 基础优化
  sendfile on; aio threads; directio 4m;
  tcp_nopush on; tcp_nodelay on;
  keepalive_timeout 65;

  # HTTP/2 + HTTP/3
  ssl_session_cache shared:SSL:50m;
  ssl_session_timeout 1d;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers HIGH:!aNULL:!MD5;

  # 反向代理通用
  proxy_http_version 1.1;
  proxy_read_timeout 60s;
  proxy_send_timeout 60s;

  # 缓存路径(NVMe)
  proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=video_cache:200m
                   inactive=7d max_size=6000g
                   use_temp_path=off;

  # 加锁防惊群 & 后台刷新
  proxy_cache_lock on;
  proxy_cache_lock_timeout 10s;
  proxy_cache_background_update on;
  proxy_cache_min_uses 1;

  # 切片,每片 1MB(适合短视频 Range)
  slice 1m;

  # 上游 MinIO(走存储网)
  upstream minio_cluster {
    server 10.31.10.1:9000 max_fails=2 fail_timeout=30s;
    server 10.31.10.2:9000 max_fails=2 fail_timeout=30s;
    server 10.31.10.3:9000 max_fails=2 fail_timeout=30s;
    server 10.31.10.4:9000 max_fails=2 fail_timeout=30s;
    keepalive 128;
  }

  map $http_range $range_req { "~bytes=" 1; default 0; }

  # 防盗链签名(secure_link)
  secure_link_secret my_sign_key;

  log_format main '$remote_addr "$request" $status $body_bytes_sent '
                  '$request_time $upstream_cache_status $http3';

  access_log /var/log/nginx/access.log main;

  server {
    listen 443 ssl http2 reuseport;
    listen 443 http3 reuseport;  # HTTP/3
    server_name media.example.hk;

    ssl_certificate     /etc/nginx/tls/fullchain.pem;
    ssl_certificate_key /etc/nginx/tls/privkey.pem;

    # HLS/MP4 头
    types {
      application/vnd.apple.mpegurl m3u8;
      video/mp2t ts;
      video/mp4 mp4;
    }

    # 签名校验(?md5=...&expires=...)
    location /video/ {
      set $secret $arg_md5;
      set $expires $arg_expires;
      secure_link $arg_md5,$arg_expires;
      secure_link_md5 "$secure_link_expires$uri my_sign_key";
      if ($secure_link = "") { return 403; }
      if ($secure_link = "0") { return 410; } # 过期

      # 切片缓存 Key 把切片范围纳入
      proxy_cache_key $uri$is_args$args$slice_range;
      proxy_cache video_cache;
      proxy_ignore_headers Set-Cookie;
      proxy_hide_header Set-Cookie;

      # Range 透传 + 强制分片缓存
      proxy_set_header Range $http_range;
      proxy_force_ranges on;

      # 回源
      proxy_pass http://minio_cluster/videos$uri;

      # TTL 策略:热点短 TTL,冷门长 TTL
      proxy_cache_valid 200 206 302 1h;
      proxy_cache_valid 404 10m;
      proxy_cache_bypass $arg_nocache;

      # mp4 寻址(可选)
      mp4;
    }

    # HLS 索引(m3u8)更短 TTL,便于 ABR 切档刷新
    location ~ \.m3u8$ {
      proxy_cache_key $uri$is_args$args;
      proxy_cache video_cache;
      proxy_pass http://minio_cluster/videos$uri;
      proxy_cache_valid 200 1m;
      add_header Content-Type application/vnd.apple.mpegurl;
    }

    # 健康检查
    location = /healthz { return 200 'ok'; }
  }
}

关键点:

  • slice + proxy_cache_key 把 切片范围纳入 Key,缓存 206 Partial;
  • proxy_cache_lock 抑制“惊群”;
  • proxy_cache_background_update 后台刷新,用户直接命中旧缓存;
  • http3 双监听,移动端弱网更稳;
  • secure_link 简单可靠,生成端示例见下。

5.4 签名 URL 示例(Python)

import hashlib, time, urllib.parse

def gen_signed_url(base, path, key, ttl=600):
    expires = int(time.time()) + ttl
    sign_str = f"{expires}{path} {key}"
    md5 = hashlib.md5(sign_str.encode()).hexdigest()
    return f"{base}{path}?expires={expires}&md5={md5}"

print(gen_signed_url("https://media.example.hk", "/video/abc/123.mp4", "my_sign_key"))

六、转码与对象布局

存储布局:videos/{vid}/index.m3u8、videos/{vid}/seg_00001.ts 或 videos/{vid}.mp4

首帧优化:MP4 moov 前移(faststart)

ffmpeg -i in.mp4 -movflags +faststart -c copy out.mp4

HLS 切片(6s 一片,短视频我给 2s)

ffmpeg -i in.mp4 -codec: copy -start_number 0 -hls_time 2 -hls_list_size 0 \
  -hls_segment_filename seg_%05d.ts index.m3u8

上传:CI/CD 用 mc mirror 推到 hkminio/videos/

七、预热、灰度与回滚

7.1 热点预热脚本(并发 HEAD/GET)

# urls.txt 中每行一个完整签名 URL
xargs -P 64 -n 1 -I{} curl -s --http3 --range 0-1048575 -o /dev/null "{}"

只拉取前 1MB,刺激切片缓存,成本小、收益大。

7.2 灰度发布

新的边缘节点先 接收 5% 流量,观察 $upstream_cache_status 命中率;

MinIO 节点扩容:直接新加节点进集群,EC 自动重平衡;期间限制写入速率,避免迁移抖动。

八、监控告警(关键指标)

Nginx:$upstream_cache_status(HIT/MISS/BYPASS/STALE/UPDATING)、request_time、status、body_bytes_sent、$http3。

MinIO:minio_http_requests_total、minio_s3_requests_4xx/5xx_total、minio_node_disk_used_bytes、minio_heal_*。

业务:首帧 TTFB、播放起播时间、seek 复位时间、rebuffer ratio、ABR 码率分布。

九、真实数据(某晚 22:00-23:00 A/B)

指标 优化前 优化后(Nginx+MinIO)
首帧 TTFB(P50) 280 ms 63 ms
起播时间(P95) 1.27 s 0.58 s
Rebuffer 率(P95) 3.2% 1.4%
边缘缓存命中率 58% 89%
回源带宽峰值 4.1 Gbps 1.2 Gbps
入口 CPU(单机) 78% 41%

采集窗口 1 小时,APP 端 + 网关联合埋点统计。

十、我踩过的坑(以及我是怎么在机柜里解决的)

缓存“惊群”导致瞬时回源风暴

现象:某 30s 热点视频刚过 TTL,N 台边缘同时 MISS;

解决:开启 proxy_cache_lock on;、proxy_cache_background_update on;,只有首个请求回源,后续等待/用旧;并把 TTL 切细(m3u8 1 分钟,ts/mp4 1 小时)。

Range/206 没命中缓存

现象:移动端频繁 Range 拖动,缓存未生效;

解决:slice 1m; + proxy_cache_key $uri$is_args$args$slice_range; + proxy_force_ranges on;,把切片范围写进 Key。

MinIO ETag 与分块上传

现象:多段上传导致 ETag 与 MD5 不一致,客户端校验失败;

解决:统一转码/上传流程,HLS/MP4 整文件写入;若必须分块,前端不要以 ETag=MD5 假设做校验。

HTTP/3 在部分安卓机型握手慢

解决:保留 HTTP/2,双栈;并在少数机型 UA 回退到 H2(map UA 白/黑名单)。

NVMe 写放大导致缓存盘寿命预估偏短

解决:inactive=7d、max_size 控制,外加 只缓存 200/206/302,过滤非必要写入;启用 discard。

L4 不一致导致“跨机取片”

现象:LB 变更后,同 URL 跳到另一台边缘,缓存命中骤降;

解决:Maglev + 固定权重;变更前导出哈希表对比;同时在边缘间不开共享缓存(避免复杂度),靠 L4 保持粘性。

CentOS 7 老内核吞吐不稳

解决:上 ELRepo 4.19+ 或直接上 Rocky 9;BBR 与 RPS/RFS 打开,NIC 驱动升级。

十一、运维跑批与日常操作

批量清缓存(只清路径前缀)

# 通过改名触发更新:上传新版对象后刷新索引
curl -X PURGE "https://media.example.hk/video/abc/*"  # 需要自行实现 purge 接口(例如 Nginx 扩展或自定义 API)

更推荐 短 TTL + 背景刷新,少做硬清。

MinIO 扩容:加节点 -> mc admin info 观察 re-balance -> 业务限流窗口(凌晨 3-5 点)。

回源熔断:MinIO 5xx 连续错误则 proxy_next_upstream error timeout http_500 http_502 http_503 http_504;

十二、完整上线步骤清单

  1.  机柜上电、链路打通(A/B 供电 + 上联 BGP)
  2.  部署 MinIO 8 节点(EC、桶策略、ILM)
  3.  边缘 Nginx 6 台(NVMe 挂载、HTTP/3、切片缓存、签名校验)
  4.  L4 Maglev 一致性哈希 + 健康检查
  5.  CI/CD 推送对象、生成签名 URL
  6.  预热热点清单(前 1MB)
  7.  监控阈值与告警(命中率、TTFB、5xx、NVMe 用量)
  8.  灰度 5% -> 25% -> 100%
  9.  压测与回归:seek、ABR、弱网、旧机型
  10.  文档与 Runbook 更新

十三、配置片段(便于复制)

Nginx:日志带命中态

log_format play '$time_iso8601 $remote_addr $request $status $request_time '
               '$upstream_response_time $upstream_cache_status $bytes_sent $http3';
access_log /var/log/nginx/play.log play;

Nginx:只缓存 200/206/302

proxy_cache_valid 200 206 302 1h;
proxy_ignore_headers Set-Cookie;

MinIO:Prometheus(public)

export MINIO_PROMETHEUS_AUTH_TYPE=public
# Prom 抓取 http://mio01:9000/minio/v2/metrics/cluster

十四、成本与收益(我的粗算)

项目 月成本(HKD) 备注
机柜 + 电力 12,000 双路供电
国际带宽 5G 保底 30,000 峰值 20G
边缘缓存 6 台 36,000 含 NVMe
MinIO 8 台 48,000 HDD 大容量
合计 126,000 仅供参考
带宽回源节省 ≈ -30% 热点高时更明显
QOE 提升 TTFB -77%rebuf ->50% 直接降低流失

风停了,机房的空调声重新成了主旋律。新的 Nginx+MinIO 架构上线后一小时,命中率爬到了 89%,回源低到我有点不放心,特地在走廊里又绕了两圈确认监控没“抽风”。第二天早上产品同学说:“首页视频像是提前放在手机里一样,点开就走。”我笑着把那杯彻夜的冷咖啡倒进了废液桶——这套在香港落地的分布式缓存方案,值。

最后给同行的三条建议

  • 让缓存“靠得住”:slice + cache_lock + 背景刷新,是对抗热点的“三板斧”。
  • 让流量“粘得住”:L4 一致性哈希,别让同一个 URL 在不同边缘来回“漂”。
  • 让运维“跑得动”:监控做前、灰度走稳、预热有清单,避免拍脑袋回滚。

如果你也在香港、也被短视频的“热度曲线”追着跑,这套可落地、可维护的 Nginx + MinIO 组合拳,能帮你在深夜依然站得住。