个人站长上ZFS:ARC、ashift与快照

为什么值得从 ext4 换到 ZFS:先看清代价

ZFS 在个人站长圈子里口碑两极。喜欢它的人说它"永远不会静默损坏数据",讨厌它的人说它"吃内存、难调、一出事就懵"。两种说法都对,区别在于你有没有在换之前想清楚自己要的是什么。

我在两台跑静态站和备份的机器上用了一年多 ZFS on Linux(现在叫 OpenZFS),也在数据库机器上试过又退回 ext4。这篇不吹也不黑,只讲清楚一件事:什么场景该用 ZFS,什么场景别用,以及用了之后必须做的几件事。

该用的场景:备份仓库、静态资源存储、需要频繁打快照又不想依赖 LVM 的机器、需要校验和发现静默损坏的归档盘、要给一堆站点做"每小时快照防误删"的情况。

不该用的场景:内存小于 4G 的 VPS(ARC 会把内存吃紧,虽然能限)、纯数据库盘(CoW 的随机写放大会让 InnoDB 性能明显下降)、跑在不能直通磁盘的虚拟化层上的环境(没有 HBA 直通就拿不到真实磁盘信息,SMART 和 scrub 的价值大打折扣)。

一、ARC 不是缓存噱头,它是最该先调的东西

ZFS 用 ARC(Adaptive Replacement Cache)做读缓存,默认会占用大量内存。很多教程上来就教你改 zfs_arc_max,但改之前先看清楚当前用了多少:

arc_summary
# 或
cat /proc/spl/kstat/zfs/arcstats | grep -E '^(size|c_max|c)'

在 Debian 上,限制 ARC 上线的推荐做法是写进 modprobe 配置,而不是临时 echo(重启就丢):

# /etc/modprobe.d/zfs.conf
options zfs zfs_arc_max=2147483648

然后 update-initramfs -u -k all 并重启。注意这个值必须是字节数,写成 2G 会静默失效。经验值:ARC 上限设为物理内存的 25%~50%,剩下的留给应用。如果机器同时跑 MySQL,把 ARC 压到 25% 以内,让 InnoDB 的 buffer pool 拿到大头,否则两边抢内存,谁都不满意。

判断 ARC 是否"够用",看命中率:

arc_summary | grep -A2 'Hit Rate'
# 命中率长期低于 80%,说明工作集大于 ARC,加内存或减少数据集

二、ashift 一旦定下就改不了,建池前必须选对

这是 ZFS 最不可逆的一个参数。ashift 表示"最小分配单元是 2 的多少次方扇区",ashift=12 即 4K 对齐。所有现代硬盘(包括 512e 的机械盘)都应该是 12;只有真正 512 字节扇区的老盘才用 9。

为什么重要:ashift 设小(比如 9)而硬盘实际是 4K,每次写都要读-改-写,写放大能到 8 倍,性能惨不忍睹且无法通过改配置挽回——只能重建池。设大(13 = 8K)不会损坏数据,但会浪费空间。宁可大不可小。

zpool create -o ashift=12 tank mirror /dev/sdb /dev/sdc

建完立刻确认:

zpool get ashift tank
zdb -C tank | grep ashift

三、recordsize 要跟文件类型匹配

ZFS 默认 recordsize=128K,对顺序读的大文件很友好,但对大量小文件(网站静态资源、日志)就偏大。按数据集分开设置是最省心的办法:

zfs create -o recordsize=1M tank/backup          # 大备份归档
zfs create -o recordsize=16K tank/www            # 网站小文件
zfs create -o recordsize=16K -o compression=lz4 tank/logs

数据库目录要单独设 recordsize=16K 并关掉 atime,MySQL 在 ZFS 上的最佳实践是 recordsize 与 InnoDB page size(16K)对齐。别再往下设成 4K,那会让元数据开销剧增。

压缩建议一律 lz4:它几乎不耗 CPU,对文本和日志通常能压到 1/3。看实际压缩比:

zfs get -o name,value compressratio tank/logs

四、快照:几乎免费,但不等于备份

ZFS 快照是瞬时、零成本、原子性的,这一点比 LVM 舒服太多。日常防误删的写法:

zfs snapshot tank/www@$(date +%Y%m%d-%H%M)
zfs list -t snapshot -o name,used,refer tank/www | tail -20

保留策略用一个简单脚本按时间分层:保留最近 24 个小时快照、7 个日快照、4 个周快照。关键在 used 这一列——快照占用的是"自它之后被改写或删除的块",如果站点每天改动很大,快照占用会长得很快。

把快照做成可浏览的目录(无需挂载),便于恢复单个文件:

ls /tank/www/.zfs/snapshot/

.zfs 目录默认不可见,但能 cd 进去。这一特性非常实用:用户误删的文章文件,直接从昨晚的快照 cp 回来即可,不用整池回滚。

但要清楚:同池快照不防磁盘损坏、不防误删池、不防勒索软件(有 root 就能删快照)。异地备份依然需要 zfs send | ssh zfs receive 或 restic 另存一份。

五、scrub 与 SMART:让 ZFS 真正发挥"防静默损坏"的作用

ZFS 的校验和只有在读的时候才校验。镜像盘里某一块坏了但你从没读到过,它就一直不知道。scrub 就是主动把整池读一遍、校验、并用副本修复:

# 每月一次,建议写进 systemd timer
zpool scrub tank
zpool status -v tank

单盘池(无冗余)scrub 只能"发现"不能"修复",会明确告诉你哪个文件损坏——这本身也有价值,至少你知道备份要重点恢复哪个文件。有镜像或 raidz 才能自动修复。

scrub 期间 IO 会明显变慢,安排到凌晨。同时装 smartd 盯盘:

smartctl -a /dev/sdb | grep -E 'Reallocated|Pending|Uncorrect'

这三个数只要非零并且在增长,就是换盘的信号,别等 ZFS 报 DEGRADED。

六、几个容易翻车的地方

别把 ZFS 放在不支持直通的环境。某些云主机的虚拟磁盘对 ZFS 不友好,写放大严重,性能可能还不如 ext4。上线前用 fio 跑一轮随机写对比。

swap 不要放在 zvol 上。会死锁。需要 swap 就用独立分区,或者干脆不加 swap(同时把 vm.swappiness 调低)。

内存不足时的 OOM 表现很迷惑。ARC 会把内存吃满,free 看起来"没有可用内存",但这是正常的——ARC 会主动归还。别看到 free 少就 panic,看 arc_summary 的实际命中率和 /proc/pressure/memory。

升级内核前先看 OpenZFS 兼容性。DKMS 模块编译失败会导致系统起不来。Debian 上的做法是把 zfs-dkms 加入 apt 的 hold,或者在内核升级后先确认 modinfo zfs 正常再重启。

池满到 95% 以上性能会断崖式下跌。CoW 需要空闲块来写新数据,池子太满时分配器退化,写入速度可能掉到几 MB/s。设一条 80% 告警线,别等它满。

不要用 zpool upgrade 图新鲜。升级后老版本 OpenZFS 就无法导入该池,回滚系统版本会把数据锁死。除非要用新特性,否则不动。

七、给个人站长的取舍建议

如果只是一台跑 WordPress 的 2 核 4G 小机器,ext4 + 定时 rsync + 异地对象存储,已经足够了,没必要引入 ZFS 的复杂度。ZFS 的收益在多盘、大容量、需要频繁快照、对数据完整性有硬要求时才明显。

我目前的分工是:静态资源和备份仓库用 ZFS(快照 + 压缩 + 校验,收益最大),业务数据库留在 ext4(随机写性能优先),日志盘单独一块 SSD。这不是标准答案,但一年下来没出过一次数据事故,也没因为调参熬过夜——对个人站长来说,能睡好觉的架构就是好架构。

八、日常运维命令:五个一眼看懂的检查

ZFS 的运维门槛不在建池,而在于出事时能不能快速判断局势。我把它压缩成五条命令,写在 ~/zfs-check.sh 里,每天手动跑一次,或者挂到 cron 里把输出发到邮箱。

第一,池子健康状态。zpool status -x 只在有问题时才输出内容,健康时返回 "all pools are healthy"。这一条最适合塞进监控脚本,用退出码判断。

第二,容量与增长率。zpool list -o name,size,alloc,free,cap,health。我特别关注 cap 这一列的变化速度而不是绝对值。日志盘从 60% 涨到 85% 用了一个月,那是正常增长;一夜之间从 60% 到 90%,一定是某个任务失控了。

第三,快照占用排行。zfs list -t snapshot -o name,used -s used | tail -10 按占用倒序列出,第一名通常是"一个月前有人手动建了快照忘了删"。清理时用 zfs destroy 逐个删,别用通配符一次删光——zfs destroy tank/www@%recursive 这类写法一旦写错池名就是灾难。

第四,压缩比与实际节省。zfs get -r -o name,value compressratio tank。如果某个数据集的压缩比是 1.00x,说明它装的是已压缩格式(图片、视频、zip),再给它开压缩只是白耗 CPU。这类数据集应该单独建、单独关压缩。

第五,新建数据的写放大。这一条稍微进阶,用 zpool iostat -v 5 观察每个 vdev 的实际写入量,与业务口径的写入量对比。如果 ZFS 层面写入是应用层的三倍,多半是 recordsize 设错或者开了过于激进的压缩。

#!/bin/bash
# ~/zfs-check.sh  —— 每日巡检
zpool status -x || echo "池子有异常,去看 zpool status"
zpool list -o name,cap,health
zfs list -t snapshot -o name,used -s used | tail -5
df -h /tank | tail -1

这套检查全部跑完不到两秒。它的价值不在于"查出问题",而在于让你对池子的运行节奏有直觉——等真出事时,你能立刻判断出"这和上周比不正常"。

Last modification:October 28th, 2026 at 07:24 pm

Leave a Comment