如何在香港服务器运行 Debian 时,启用与调优 ZFS ARC,加速直播回放文件读取

那天是周六的凌晨两点,香港机房的告警把我从打盹里拎起来——用户在看直播回放时频繁缓冲。Nginx 的日志像瀑布一样刷屏,边缘节点到源站的带宽明明还有余量,磁盘 I/O 却顶到了 90% 以上。zpool iostat 显示随机读抖动严重,ARC 命中率 50% 出头。很明显:热段没被好好留在内存,回放的热点切片在机械盘上来回抽签。
那一夜我做了两件事:把 ZFS 的 ARC“胃口”放大到合适的比例,再用 NVMe 做 L2ARC 与 metadata 特殊 vdev,把热门数据和元数据“揣兜里”。第二天午后回放的 95 分位时延就下来了,命中率稳定 85%+。这篇文章,就是把我当晚的全部实操细节掏出来,不管你是新手还是老手,都能照着上手。
1. 现场环境与访问画像
1.1 物理与系统参数
| 项 | 型号/版本 | 备注 |
|---|---|---|
| 机架 | 1U Supermicro | 港区机房 |
| CPU | Intel Xeon Silver 4214(12C/24T)×1 | 支持 AES-NI |
| 内存 | 128 GB DDR4 ECC | NUMA 单路 |
| 系统盘 | SATA SSD 480 GB ×1 | Debian 系统 |
| 数据盘 | 12 TB SATA HDD ×8 | 组 RAIDZ2 |
| NVMe1 | 3.84 TB U.2 企业盘 | L2ARC / special vdev |
| NVMe2 | 3.84 TB U.2 企业盘 | L2ARC / special vdev(镜像) |
| 网卡 | 25 GbE SFP28 | 上联交换机万兆/25G |
| OS | Debian 12 (bookworm) | 内核 6.x |
| 业务 | Nginx/HLS、MP4 回放 | 每晚回放峰值 5–8k 并发 |
1.2 访问模式(决定调优方向)
- HLS 切片:2–4 MB .ts/.m4s,短时间集中被反复读取(同一热门赛事回放,重复率高)。
- MP4 进度条拖动:偏随机读,跨度几十 MB。
- 写入:回放生成阶段写入量远小于读取;我们的目标是优化读路径。
结论:
- ZFS 的 ARC 要能容纳“最近 30–60 分钟的热门切片”。
- 开启/保留 prefetch,recordsize 适合视频(大块),primarycache=all(缓存数据+元数据),secondarycache=all(启用 L2ARC)。
- 将 元数据与小文件放入 special vdev(NVMe 镜像),显著降低 HDD 寻道。
2. 在 Debian 上安装 ZFS(DKMS 方案)
如果你已经在用 ZFS,可跳到下一节“池与数据集设计”。
# 1) 确保 contrib 仓库启用
sudo sed -i 's/main$/main contrib non-free-firmware/' /etc/apt/sources.list
sudo apt update
# 2) DKMS 需要内核头文件
sudo apt install -y linux-headers-$(uname -r) build-essential
# 3) 安装 ZFS
sudo apt install -y zfs-dkms zfsutils-linux
# 4) 加载模块并校验
sudo modprobe zfs
zfs version
坑 1:DKMS 编译失败
多半是缺 linux-headers-$(uname -r),或你装了“新内核但没重启”。装头文件&重启后再装 ZFS,一次过。
3. 池与数据集:为回放读优化的磁盘布局
3.1 创建池(HDD 走 RAIDZ2,NVMe 镜像做 special)
special vdev 用来存 元数据与小文件,强烈建议用 镜像(mirror)——special 掉了,pool 直接“黑屏”。
# 假设 HDD 为 /dev/sd[b-i],两块 NVMe 为 /dev/nvme0n1 /dev/nvme1n1
# 注意:生产环境根据你的设备名调整
sudo zpool create -o ashift=12 \
-o autotrim=on \
-O compression=lz4 \
-O atime=off \
-O xattr=sa -O acltype=posixacl \
tank \
raidz2 /dev/sdb /dev/sdc /dev/sdd /dev/sde /dev/sdf /dev/sdg /dev/sdh /dev/sdi \
special mirror /dev/nvme0n1 /dev/nvme1n1
- ashift=12:对 4K 物理扇区友好。
- atime=off:避免每次读都写 atime。
- xattr=sa/acltype=posixacl:减少元数据额外开销。
- special mirror nvme:把 dnode/indirect blocks/元数据放 NVMe,回放的索引查找飞起来。
3.2 添加 L2ARC(可晚点加)
L2ARC 是 只读二级缓存(典型放 NVMe),命中率提升明显,但要先把 ARC 调好。
我通常 先跑一晚只靠 ARC,再加 L2ARC 观察命中率是否上升。
# 添加 NVMe 作为二级缓存(如果 special 已用两块 NVMe,考虑再加一对做 L2ARC)
sudo zpool add tank cache /dev/nvme2n1
# 验证
zpool status tank
4. ARC 容量规划与持久化配置
4.1 先算“能给 ARC 多大内存”
经验值(直播回放负载):
| 物理内存 | 预留给系统/进程 | ARC 可用上限(zfs_arc_max) |
|---|---|---|
| 64 GB | 12–16 GB | 36–44 GB |
| 128 GB | 20–28 GB | 80–96 GB |
| 256 GB | 40–56 GB | 180–210 GB |
我这台 128 GB,Nginx+FFmpeg 这类进程常驻 ~10–12 GB,再给监控、日志和 page cache 留冗余,把 ARC 设到 88 GB 比较稳。
4.2 临时调整(热排错用)
# 查看当前
cat /sys/module/zfs/parameters/zfs_arc_max
cat /proc/spl/kstat/zfs/arcstats | egrep "size|c_max|c_min|hits|misses|l2_hits|l2_misses"
# 临时设定(单位字节),88G:
echo $((88*1024*1024*1024)) | sudo tee /sys/module/zfs/parameters/zfs_arc_max
# 同步设个 zfs_arc_min(比如上限的 1/4~1/3)
echo $((24*1024*1024*1024)) | sudo tee /sys/module/zfs/parameters/zfs_arc_min
注意:这种写 /sys 的方式重启就没了,用于当晚观察。
4.3 持久化(开机生效)
编辑 /etc/modprobe.d/zfs.conf:
# /etc/modprobe.d/zfs.conf
options zfs zfs_arc_max=94489280512 zfs_arc_min=25769803776
# 88G / 24G
# 可按你的内存与业务进程占用调整
更新 initramfs(ZFS 模块一般随内核早期加载):
sudo update-initramfs -u -k all
# 下次重启生效;若当前要立即生效,用 4.2 的临时方法先试跑
坑 2:设了不生效
模块早已加载,modinfo zfs 可见;要么重启,要么临时写 /sys。如果是 root on ZFS 的系统,别强行 rmmod。
5. 数据集参数(让回放文件“更合胃口”)
我把用于回放的目录单独建为一个 dataset,专治视频大文件/切片:
sudo zfs create tank/replay
sudo zfs set mountpoint=/data/replay tank/replay
# 关键参数(结合访问画像)
sudo zfs set recordsize=1M tank/replay # 视频块大
sudo zfs set primarycache=all tank/replay # 数据+元数据都进 ARC
sudo zfs set secondarycache=all tank/replay # 允许进 L2ARC
sudo zfs set compression=lz4 tank/replay # 绝大多数视频已压,LZ4 主要压元数据,几乎无负担
sudo zfs set atime=off tank/replay # 减少无谓写
sudo zfs set logbias=throughput tank/replay # 偏吞吐(写入不敏感)
recordsize:HLS 切片 2–4 MB,我统一设 1 MB 比较折中。MP4 也受益于大 recordsize 的顺序读。
若你的工作集“超热但稀薄”(同一时间段只少量切片被反复读),也可试 primarycache=metadata 看看内存压力与命中率的权衡——我的场景 all 更优。
6. L2ARC 高阶调优(可选但推荐)
添加 L2ARC 后,观察默认吞吐是否过低,可适度抬高写速(别压垮业务):
# 查看 L2ARC 参数(不同版本名字可能略有差异)
grep . /sys/module/zfs/parameters/l2arc_* 2>/dev/null | sort
# 例如将 L2ARC 写入上限提到 ~64MB/s(按需)
echo $((64*1024*1024)) | sudo tee /sys/module/zfs/parameters/l2arc_write_max
# 降低 prefetch 禁忌(让顺序读也能进 L2ARC),视版本而定
# 0 = 允许预取数据进 L2ARC;1 = 禁止
echo 0 | sudo tee /sys/module/zfs/parameters/l2arc_noprefetch
持久化:同样写到 /etc/modprobe.d/zfs.conf 的 options zfs ... 里;参数名以实际系统为准。
OpenZFS 2.x 的持久化 L2ARC(重启后继续热):大多版本已默认开启,可通过 zpool get feature@persistent_l2arc tank 查看,保持 enabled/active。
7. Nginx 回放与操作系统配合
sendfile on; + ZFS 没冲突,ZFS 会自行管理页面缓存。
open_file_cache 对热点切片加速明显(减少 stat),同时 ARC/Special vdev 让元数据查询更快:
open_file_cache max=200000 inactive=60s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
不要在 Nginx 上乱开 aio threads + directio,让 ZFS 自己做预取和缓存更稳。
8. 监控与压测:我当晚用的脚本与流程
8.1 ARC 命中率快速观察脚本
#!/usr/bin/env bash
# arc_hit.sh:每秒打印一次 ARC/L2ARC 命中率
while true; do
stats=/proc/spl/kstat/zfs/arcstats
hits=$(awk '/^hits/{print $3}' $stats)
misses=$(awk '/^misses/{print $3}' $stats)
l2hits=$(awk '/^l2_hits/{print $3}' $stats)
l2miss=$(awk '/^l2_misses/{print $3}' $stats)
ah=$(awk -v h=$hits -v m=$misses 'BEGIN{printf "%.2f", (h/(h+m))*100}')
lh=$(awk -v h=$l2hits -v m=$l2miss 'BEGIN{printf "%.2f", (h/(h+m))*100}')
echo "$(date '+%H:%M:%S') ARC_hit=${ah}% L2ARC_hit=${lh}%"
sleep 1
done
8.2 回放读压测(两种)
Nginx 静态文件下载(模拟切片分发):
# 100 并发、持续 120s
wrk -t8 -c100 -d120s http://127.0.0.1/replay/hot/seg-00001.ts
fio 顺序/随机混合读(贴近 MP4 拖动):
# fio-replay-read.fio
[replay]
rw=randread
rwmixread=100
bs=1M
iodepth=32
directory=/data/replay
numjobs=4
runtime=120
time_based=1
group_reporting=1
fio fio-replay-read.fio
9. 我这台机器“前后对比”的核心指标
压测时同等并发,同一内容集,先只用 ARC,再加 L2ARC+special vdev。
| 指标 | 调优前 | 仅 ARC 调优 | + L2ARC & special |
|---|---|---|---|
| Nginx 静态下载 95p 时延(ms) | 180 | 95 | 58 |
| ARC 命中率(稳态) | 52% | 78% | 85–88% |
| L2ARC 命中率(稳态) | – | – | 20–30% |
| HDD 平均读队列 | 1.8 | 1.1 | 0.6 |
| CPU iowait | 12% | 6% | 3% |
解读:ARC 做到 70%+,已经能“救火”;加了 special vdev(元数据在 NVMe 上)和 L2ARC(冷热交界数据在 NVMe 上),尾延进一步收紧,HDD 几乎只干“冷块吞吐”。
10. 常见坑位与现场补救
L2ARC 放消费级 NVMe,长时间写入掉速
现象:晚高峰后 l2arc 写入跟不上、命中率波动。
解法:换 企业 NVMe;临时可把 l2arc_write_max 稍降但更稳定,或只先用 ARC+special。
ARC“吃太多”触发 OOM
现象:内存吃紧时 Nginx/FFmpeg 被 OOM。
解法:设置 zfs_arc_max 与 zfs_arc_min,并保守预留给业务进程;必要时将 primarycache=metadata 应用于“低复用度目录”。
special vdev 单盘
现象:单块 NVMe special 掉了,pool 直接不可用。
解法:special 至少做镜像。宁可不加,也别单盘 special。
DKMS 编译/升级踩坑
解法:升级前先 apt install linux-headers-$(uname -r);生产上一律先在灰度机验证。
prefetch 被关
现象:顺序读视频命中率异常低。
解法:检查 /sys/module/zfs/parameters/zfs_prefetch_disable 或版本相关开关,确保预取开启;必要时调大 zfetch_max_streams(不同版本名略有差异,先 grep zfetch 看看)。
11. 运维日常:观察与回滚
- 观察周期:至少跑满一个“晚高峰周期”。命中率、HDD 队列、Nginx 95/99 分位时延。
- 慢慢加码:先 ARC,后 L2ARC,再考虑调 l2arc_*。
回滚策略:
- L2ARC 可随时 zpool remove tank cache /dev/nvme2n1。
- 数据集参数改动即时生效;若异常,原值改回即可。
- special vdev 一旦上了,就当“永久部件”(所以要镜像)。
12. 我的最终配置摘录(可作模板)
# /etc/modprobe.d/zfs.conf
options zfs zfs_arc_max=94489280512 zfs_arc_min=25769803776 \
l2arc_write_max=67108864 l2arc_noprefetch=0
# 池与数据集(示意)
zpool create -o ashift=12 -o autotrim=on -O compression=lz4 -O atime=off \
-O xattr=sa -O acltype=posixacl tank \
raidz2 sdb sdc sdd sde sdf sdg sdh sdi \
special mirror nvme0n1 nvme1n1
zfs create tank/replay
zfs set mountpoint=/data/replay tank/replay
zfs set recordsize=1M primarycache=all secondarycache=all atime=off \
compression=lz4 logbias=throughput tank/replay
# 需要时添加 L2ARC
zpool add tank cache nvme2n1
13. 收尾:凌晨三点半的机房走廊
当晚三点半,机房走廊的空调风还是一阵阵地吹。ARC 命中率稳在 80% 以上,L2ARC 也接过了那部分“温热数据”,HDD 指示灯从狂闪变得从容。Nginx 的 95p 时延图曲线收紧得像拉好的弓弦。
我把 zfs.conf 的参数持久化,给监控加了几个告警阈值,然后在工单里写上这句话:“回放热点留在内存和 NVMe 上,机械盘只负责大冷块。”
第二天同事问我怎么做到的,我笑着把这套步骤发过去——就像现在写给你的一样。愿你下一个“凌晨两点”的现场,也能把问题按住。
快速清单(方便你照着做)
- 安装 zfs-dkms / zfsutils-linux,补齐内核头。
- 池:HDD 组 RAIDZ2;NVMe 镜像做 special vdev。
- 数据集:recordsize=1M,primarycache=all,secondarycache=all,atime=off。
- ARC:按照物理内存与进程占用,设 zfs_arc_max/zfs_arc_min;先临时,后持久化。
- (可选)L2ARC:添加企业 NVMe,必要时调整 l2arc_write_max/l2arc_noprefetch。
- 监控与压测:arcstats、zpool iostat、wrk/fio,至少跑一个业务周期。
- 注意坑:special 要镜像;ARC 别吃太多;DKMS 升级先灰度。