
我们部署在香港机房的短视频平台,一条“爆了”的短视频把我们边缘节点打到 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;
十二、完整上线步骤清单
- 机柜上电、链路打通(A/B 供电 + 上联 BGP)
- 部署 MinIO 8 节点(EC、桶策略、ILM)
- 边缘 Nginx 6 台(NVMe 挂载、HTTP/3、切片缓存、签名校验)
- L4 Maglev 一致性哈希 + 健康检查
- CI/CD 推送对象、生成签名 URL
- 预热热点清单(前 1MB)
- 监控阈值与告警(命中率、TTFB、5xx、NVMe 用量)
- 灰度 5% -> 25% -> 100%
- 压测与回归:seek、ABR、弱网、旧机型
- 文档与 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 组合拳,能帮你在深夜依然站得住。