ZFS 在 FreeBSD 和 Linux 上如何权衡内存
ZFS 提供校验和、快照、send/receive 复制和压缩,但 ARC 默认占用大量 RAM。本指南说明如何在 2 GB 或 4 GB VPS 上评估 ZFS。
ZFS 能提供什么,以及它需要什么
FreeBSD 和 Linux 上的 ZFS 现在使用同一套代码库 OpenZFS,因此两种系统上的功能相同。运行 ZFS 的服务器可以获得带校验和的数据、在数据发生变化前不占用额外空间的快照、使用 zfs send 进行复制,以及只需设置一个属性即可启用的压缩。它的代价是内存:ARC(自适应替换缓存)默认会占用大量 RAM;而在 2 GB 或 4 GB 的 VPS(虚拟专用服务器)上,这部分内存正是应用程序需要的资源。
本指南从租用的 VPS 角度评估 ZFS。该 VPS 只有一个或两个虚拟磁盘,而不是拥有 40 个硬盘槽位的存储服务器。迁移到这种环境后仍然有用的功能,才值得投入时间。无法适应该环境的部分,应在创建存储池前了解清楚。
FreeBSD 和 Linux 上的 OpenZFS:同一套代码,两种打包方式
自 FreeBSD 7.0 于 2008 年发布以来,FreeBSD 一直将 ZFS 纳入基本系统,最初以实验性功能提供。自 OpenZFS 2.0 于 2020 年 12 月发布以来,FreeBSD 和 Linux 从同一源代码树构建,因此 zfs 和 zpool 在两者上的行为相同,在一方创建的存储池也可以在另一方导入。
ZFS 在 Linux 上以软件包形式提供,而在 FreeBSD 上属于基本系统,原因在于许可证。OpenZFS 使用 CDDL(通用开发与分发许可证)授权。Linux 内核使用 GPL(通用公共许可证)第 2 版授权。Linux 内核项目认为这两种许可证不兼容,因此 ZFS 代码不会合并到主线 Linux 中,各个发行版自行决定如何提供它。FreeBSD 不存在这一冲突,因此 ZFS 直接包含在系统中。实际情况就是这样:只有打包方式不同,不需要在两者之间作出选择。
SSD Nodes 不提供 FreeBSD 镜像,因此在此租用的服务器上,应使用本指南中适用于 Linux 的部分。如果你在其他地方运行 FreeBSD,FreeBSD 服务器无需构建模块,也无需应对内核升级,即可使用 ZFS。
安装 ZFS 并创建存储池
在 Ubuntu 上,模块包含在内核软件包中,因此只需安装命令行工具。
sudo apt update
sudo apt install -y zfsutils-linux
zfs versionzfs version 会输出两行,分别表示用户空间版本和内核模块版本。只输出一行表示模块未加载。软件包位于 universe 组件中,Ubuntu Server 镜像默认启用该组件;如果 apt 找不到软件包,请先运行 sudo add-apt-repository universe。
在 Debian 上,软件包位于 contrib 组件中,模块由 DKMS(动态内核模块支持)在本机编译。将 contrib 添加到 /etc/apt/sources.list.d/debian.sources 中的 Components: 行,然后运行 sudo apt update,再执行:
sudo apt install -y linux-headers-$(dpkg --print-architecture) zfs-dkms zfsutils-linux安装过程会编译模块并输出 Building initial module for 6.12.0-...,这需要几分钟。请注意,这意味着每次升级内核时都要重新编译模块;如果编译失败,存储池将无法导入,直到修复该问题。
在 FreeBSD 上,无需安装任何内容。启用并启动该服务。
sysrc zfs_enable=YES
service zfs start现在创建存储池。先查看稳定的设备路径,因为 /dev/vdb 按检测顺序分配设备名;连接其他卷后,设备名可能发生变化。
ls -l /dev/disk/by-id/
sudo zpool create -o ashift=12 tank /dev/disk/by-id/virtio-abc123def456
zpool status tankzpool status 应输出 state: ONLINE,并在 tank 下列出你的设备。ashift=12 将存储池的最小块大小固定为 4 KiB,这与当前的 SSD 匹配,创建后无法更改。
大多数租用的镜像从 ext4 根文件系统启动,因此这里的 ZFS 是第二个卷上的数据存储池,而不是根文件系统。创建存储池前,请确认设备确实是你认为的设备,因为 确认你购买的 NVMe 磁盘 只需一分钟,而重建需要一个下午。
校验和只有在存储池具备冗余时才能修复数据
ZFS 写入的每个块都带有校验和,每次读取都会验证校验和。检测始终有效。修复需要第二份副本。
在单磁盘存储池中,ZFS 会报告事实,但无法继续修复。zpool status -v 的报告如下:
status: One or more devices has experienced an error resulting in data
corruption.
action: Restore the file in question if possible. Otherwise restore the
entire pool from backup.
errors: Permanent errors have been detected in the following files:
/tank/data/archive.tar报告会指出损坏的文件名。ext4 会直接返回这些字节而不作提示,因此这已经很有价值。但 ZFS 仍然无法修复它,因为存储池中没有第二份副本可用于修复。
使用镜像时,读取会从正常的一侧提供,损坏的块会被重写,事件会显示在 zpool status 的 CKSUM 列中。这就是自修复,但需要两个设备。
sudo zpool create -o ashift=12 tank mirror /dev/disk/by-id/DISK1 /dev/disk/by-id/DISK2在 VPS 中,主机存储通常已经具备冗余,常见形式是 虚拟机管理程序下的 RAID 10。这可以防止磁盘损坏,但无法告诉你某个块返回的数据是否错误,因为阵列无法判断哪个副本正确。ZFS 可以做到这一点,因为它会将数据与自身写入的校验和进行比较。
如果你只有一块虚拟磁盘,但希望具备一定的修复能力,sudo zfs set copies=2 tank/important 会在同一磁盘上为该数据集的每个块存储两份副本。这样会使该数据集占用的空间翻倍,可以应对损坏的块,但无法应对整个卷消失的情况。
scrub 会读取存储池中的所有数据并验证其完整性。
sudo zpool scrub tank
zpool status tank健康的存储池最后会显示类似 scan: scrub repaired 0B in 00:04:11 with 0 errors 的一行。请将其设置为定期执行;对于小型存储池,每月执行一次即可。
systemctl list-unit-files 'zfs-scrub*'
sudo systemctl enable --now zfs-scrub-monthly@tank.timer数据集是策略的基本单位
数据集是池中的一个文件系统,创建成本很低,因此每项作业使用一个数据集。属性会从池向下继承,因此只需设置一次默认值,再在需要的位置覆盖。在 FreeBSD 上,jail 通常就是这样运行的:每个 jail 使用一个数据集,这样可以单独对某个 jail 创建快照并回滚。这也是jail 与 Docker 容器的区别之一。
sudo zfs create tank/data
sudo zfs create tank/pg
sudo zfs set compression=lz4 tank
sudo zfs set atime=off tank
sudo zfs set quota=20G tank/data
sudo zfs set recordsize=16K tank/pg
zfs get -r compression,compressratio,quota tank压缩是人们出于谨慎而经常不启用的属性,但这种做法并不合理。lz4只增加少量 CPU 开销,并减少必须写入磁盘的字节数,因此对于可压缩数据,通常可以加快读写速度。zstd会使用更多 CPU 来实现更高的压缩率,适合很少回读的日志和归档文件。使用 zfs get compressratio tank 检查实际获得的压缩效果,并记住,该比率只统计属性设置后写入的数据。
recordsize是数据集写入的最大块大小,默认值为 128K。数据库将 8 KiB 页面写入 128 KiB 记录时,一次小写入会变成读取整个记录、修改记录,然后将其写回。应在加载数据前对数据库数据集设置 recordsize=16K,因为该属性只适用于新写入的块。
quota用于防止单个数据集占满池。ZFS 池接近 100% 使用率时会变慢,也更难清理,因此应主动预留空间。
快照在数据发生变化前不占用空间
ZFS 从不覆盖正在使用的块。它会写入新块并更新指针,这就是写时复制。快照相当于记录“保留此数据集当前指向的块”,因此创建快照会立即完成,且不占用空间。
sudo zfs snapshot tank/data@2026-08-11
zfs list -t snapshot -o name,used,refer -r tank/data快照的 USED 列表示仅由该快照保留的空间。该值最初接近 0;当您修改或删除数据时,它会逐渐增加,因为旧块无法再释放。
恢复文件不需要执行还原步骤。
ls /tank/data/.zfs/snapshot/
cp /tank/data/.zfs/snapshot/2026-08-11/notes.txt /tank/data/notes.txt在运行 sudo zfs set snapdir=visible tank/data 之前,即使是 ls -a 也看不到 .zfs 目录。应在需要快照之前创建它,因为没有快照时,一次误操作 rm -rf 会使您进入ext4 恢复流程;该流程首先要求卸载磁盘,之后会更加复杂。
回滚会丢弃快照创建后写入的所有内容。
sudo zfs rollback tank/data@2026-08-11存在更新的快照时,回滚会被拒绝;-r 会销毁这些更新的快照以继续执行。按下 Enter 前,请仔细核对两次数据集名称。
快照不是备份。 快照位于同一个池、同一个卷和同一台服务器上。卷故障或一次 zpool destroy 会使快照与数据一起丢失。快照可以防止您自己的 rm 和升级失败,这已经覆盖了许多实际事故;但对于池本身发生的问题,快照无法提供保护。完整说明请参阅:为什么 VPS 快照不是备份。
发送和接收:一条命令完成复制
zfs send 将快照转换为标准输出上的字节流,zfs receive 再将该字节流转换回数据集。第一次复制是完整发送。
sudo zfs snapshot tank/data@daily-2026-08-11
sudo zfs send tank/data@daily-2026-08-11 | ssh backup.example.com "sudo zfs recv -F backup/data"之后,只发送两个快照之间发生变化的内容。
sudo zfs snapshot tank/data@daily-2026-08-12
sudo zfs send -i tank/data@daily-2026-08-11 tank/data@daily-2026-08-12 | ssh backup.example.com "sudo zfs recv backup/data"接收端仍必须保留要发送的源快照。如果没有该快照,接收会因 cannot receive incremental stream: most recent snapshot of backup/data does not match incremental source 而停止,因为 ZFS 没有可用于应用差异的基线。请从双方都保留的快照发送,或重新执行完整发送。
请在目标端授予权限,不要使用远程 root:sudo zfs allow -u backupuser create,mount,receive backup/data。
这是真正的异地备份,但有一个条件。远端必须是 ZFS 池,因为对象存储无法接收数据流。如果目标是兼容 S3 的存储或普通 Linux 主机,请使用支持该目标的工具;从 VPS 使用 restic 备份介绍了这条路径。
ZFS 为什么会使用这么多 RAM?ARC
ARC(自适应替换缓存)是 ZFS 的读取缓存。它位于内核内存中,而不是普通的 Linux 页面缓存中,因此 free -h 不会在 buff/cache 下报告它。它会显示为已用内存。看起来内存几乎已用尽的 ZFS 主机,通常只是缓存已预热,大多数“ZFS 吃掉了我的 RAM”报告都由此产生。
默认上限设置得较高,这是有意为之。OpenZFS 2.3 会将 ARC 最大大小设置为 RAM 减去 1 GiB 与 RAM 的 5/8 两者中的较大值。OpenZFS 2.2 及更早版本在 Linux 上使用 RAM 的一半,而 FreeBSD 已经使用较新的规则。运行 zfs version,查看哪条规则适用于您的系统。
The data behind this chart
[
{
"label": "2 GB VPS",
"openzfs_2_2_linux_gib": 1,
"openzfs_2_3_gib": 1.25
},
{
"label": "4 GB VPS",
"openzfs_2_2_linux_gib": 2,
"openzfs_2_3_gib": 3
},
{
"label": "8 GB VPS",
"openzfs_2_2_linux_gib": 4,
"openzfs_2_3_gib": 7
},
{
"label": "16 GB VPS",
"openzfs_2_2_linux_gib": 8,
"openzfs_2_3_gib": 15
}
]这些数值是将文档中的默认规则应用于常见实例规格后得到的结果,不是运行中主机的实测值。对于 4 GB 实例,2.3 规则允许 ARC 达到 3 GiB。同一主机在 2.2 上的上限为 2 GiB。对于 2 GB 实例,2.3 规则仍允许 ARC 达到 1.25 GiB。应用只能使用剩余内存。
请直接从您自己的服务器读取实际数值,不要依赖该表:
grep -E '^(size|c_max) ' /proc/spl/kstat/zfs/arcstats
arc_summary | head -n 20第三列的单位是字节。c_max 是当前生效的上限,size 是 ARC 当前持有的内存量。
ARC 确实会释放内存。内核发出内存压力信号后,ARC 会缩小。问题在于时序:ARC 的缩小由内存压力驱动,因此某个进程一次申请几百 MiB 时,可能会遇到 OOM(内存不足)killer,而 ARC 仍在释放内存。在运行数据库和 Web 服务器的 2 GB 主机上,这并不罕见。OpenZFS 手册对手动修改也作了同样说明:降低上限“不会在没有内存压力触发收缩时使 ARC 缩小”。
如何在小型 VPS 上限制 ARC
先确定工作负载需要多少内存。汇总数据库和应用所需的内存,给操作系统留出余量,再将剩余内存分配给 ARC。在运行 Postgres 和一个 Web 应用的 4 GB 实例上,512 MiB 到 1 GiB 的 ARC 是合理的起始值。
以字节为单位立即设置。下面的值为 1 GiB。
echo 1073741824 | sudo tee /sys/module/zfs/parameters/zfs_arc_max让设置在重启后保持有效。
echo 'options zfs zfs_arc_max=1073741824' | sudo tee /etc/modprobe.d/zfs.conf
sudo update-initramfs -uinitramfs 步骤很重要,因为模块可能会在挂载根文件系统之前从 initramfs 加载。这样,它就不会读取你刚写入的文件。重启后,使用 arcstats 中的 c_max 行进行确认。
手册本身还说明了两个注意事项。系统运行时不能将该值重新设置为 0,因此要撤销此设置,必须编辑文件并重启。降低该数值也不会立即缩小已经很大的 ARC。
在 FreeBSD 上,相同的限制通过 vfs.zfs.arc 下的 sysctl 设置。运行 sysctl vfs.zfs.arc 查看当前值以及你的版本使用的确切名称,然后将最大值写入 /boot/loader.conf。
对于小型服务器,还需遵循两条内存规则。关闭去重功能,因为去重表驻留在内存中;通常公布的经验值是每 TB 唯一数据需要 1 到 3 GB RAM。不要将 swap 放在 zvol 上(即从存储池中划出的块设备),因为通过正在尝试释放内存的文件系统进行交换可能导致机器死锁。请将 swap 放在普通分区上,或放在存储池之外的 swap 文件中。
ext4 或 XFS 加 restic 是更合适的方案
如果服务器有闲置内存和第二个卷,ZFS 才能发挥价值。除此之外,普通文件系统加上真正的备份工具通常更合适。以下情况应选择 ext4 或 XFS:
- 实例只有 2 GB 或 4 GB RAM,而工作负载需要使用全部内存。
- 只有一个虚拟磁盘,没有第二份副本,因此 ZFS 只能检测问题,无法修复。
- 备份目标是对象存储或普通 Linux 主机,目标端无法接收
zfs send流。 - 在 Debian 上使用 DKMS,无法承担内核升级后模块未能构建的风险。
- 需要将 ZFS 用于根文件系统,但服务提供商提供的镜像仅支持 ext4。
如果有独立的数据卷、充足的 RAM(8 GB 及以上使用起来更宽裕),并且备份方案会实际使用快照和 zfs send,而不只是启用这些功能,就应保留 ZFS。其他情况下,使用 ext4 和 restic 将加密、去重后的备份写入服务器无法控制的存储,通常可以覆盖大部分相同需求,而且不占用额外内存。
故障模式及其对应的输出信息
重启后存储池消失。 zpool status 输出 no pools available。导入服务读取 /etc/zfs/zpool.cache,因此未出现在该文件中的存储池不会在启动时自动导入。sudo zpool import 列出可导入的存储池,sudo zpool import tank 将其导入,sudo zpool set cachefile=/etc/zfs/zpool.cache tank 使设置持久生效。从其他系统未正常导出的存储池会报告 cannot import 'tank': pool may be in use from other system;确认没有其他主机使用该存储池后,sudo zpool import -f tank 可覆盖此限制。
内核升级后,Debian 上出现 modprobe: FATAL: Module zfs not found in directory /lib/modules/6.12.0-...。 DKMS 没有针对新内核完成构建,通常是因为未安装匹配的内核头文件。dkms status 显示已为哪些内核完成构建。然后运行 sudo apt install -y linux-headers-$(uname -r),再通过 sudo dkms autoinstall 重新构建,最后使用 sudo zpool import tank 恢复存储池。
存储池已满,但你已经删除了文件。 只要快照仍引用已删除的数据,这些数据就会继续占用磁盘空间,因此 du 和 df 的结果会不一致。zfs list -o space -r tank 将使用量拆分为 USEDDS 和 USEDSNAP;USEDSNAP 较大就是原因所在。使用 sudo zfs destroy tank/data@2026-06-01 删除旧快照,空间就会释放。
zpool status 中的 CKSUM 计数持续增加。 ZFS 下层的某个组件返回了错误数据。在镜像存储池中,这表示警告,且数据块已被修复。在单磁盘存储池中,文件已经损坏,zpool status -v 会指出该文件;请从不位于此存储池中的备份恢复该文件。
服务器运行缓慢并开始使用交换空间。 按上文所述限制 ARC,然后运行 arc_summary 并查看命中率。如果 ARC 太小,无法容纳工作集,每次读取都必须访问磁盘;在这种情况下,使用页面缓存的普通文件系统反而更适合。
FAQ
ZFS 在 VPS 上需要多少 RAM?
ZFS 可以运行在 2 GB 实例上。真正需要关注的是应用还能使用多少内存。在未调优的情况下,OpenZFS 2.3 会将 ARC 增长到 RAM 减去 1 GiB 和 RAM 的 5/8 两者中较大的值,因此 4 GB 服务器可将 3 GiB RAM 交给缓存。将 zfs_arc_max 设置为工作负载可以分配的数值,然后从 /proc/spl/kstat/zfs/arcstats 中读取 c_max 行进行确认。
ZFS 快照是备份吗?
不是。快照与数据位于同一个池中。它可以在发生 rm 或升级失败后保留,但池或实例丢失时也会随之丢失。使用 zfs send 将快照发送到另一台机器,或运行将数据写入本服务器无法控制的存储的备份工具,才能将其转换为备份。
ZFS 在 FreeBSD 和 Linux 上的工作方式相同吗?
自 OpenZFS 2.0 于 2020 年 12 月发布以来,两者使用相同的代码库、相同的命令和相同的磁盘格式,存储池也可以在两者之间迁移。区别在于打包方式。FreeBSD 将 ZFS 包含在基本系统中。在 Linux 上,各发行版自行决定实现方式:Ubuntu 将模块构建到内核软件包中,而 Debian 使用 DKMS 在本机上构建模块。因此,内核升级后,在模块重新构建成功之前,系统可能暂时没有模块。
在只有一块磁盘的 VPS 上,ZFS 能修复损坏吗?
它可以检测损坏并指出文件,但无法修复,因为修复需要块的第二份副本。在数据集上启用 zfs set copies=2 可提供这份第二副本,但空间占用会翻倍。它可以处理坏块,但无法处理卷丢失。跨两个卷创建镜像才是真正能够修复数据的方案。
压缩会降低服务器速度吗?
lz4 通常会让服务器更快。压缩块意味着需要写入和读取的字节更少,而每个块的 CPU 开销相对于节省的磁盘 I/O 很小。在池根上设置 compression=lz4,这样每个数据集都会继承该设置;写入实际数据后,再检查一次 zfs get compressratio tank。