32GB内存只剩2G,ZFS说这是为你好

32GB内存只剩2G,ZFS说这是为你好

FreeBSDZFSARC内存调试

数据源:HN + crocidb.com · HN

你打开 htop,显示屏上的内存条一片通红。

4.49 GB / 5.69 GB——使用率接近 80%。你往上翻进程列表,没有看到任何进程在疯狂吃内存。你退出来,换 btop 再看一眼:可用内存 5.27 GB,使用率 7%。

两台显示器,同一台机器,完全相反的结论。

这是 Bruno Croci 在 2026 年 7 月经历的真实场景。他刚把博客服务器从 Ubuntu 16.04 迁移到 FreeBSD,就陷入了长达一个月的内存调试之旅。结果你猜怎么着?内存没有消失。偷走它的,是文件系统本身。

被吃掉的不是内存,是你没读过的磁盘数据

先说结论:**ZFS ARC 缓存偷了你的内存。**但它不是真正的坏人——它在给你打工。

ZFS 的 ARC(Adaptive Replacement Cache,自适应替换缓存)是 ZFS 文件系统内置的一套内存缓存机制。它的工作很简单:凡是磁盘上读过的数据,尽量留在内存里。下次再读同一块数据,直接从内存取,不需要碰磁盘。

这听起来像常识——操作系统用空闲内存做缓存,Linux 也这么干。但 ZFS 的 ARC 做得更激进。它不像传统的 buffer cache 只缓存元数据和最近用过的文件块,ARC 的设计目标是把几乎所有空闲内存都吃满,然后根据使用频率动态调整。

它内部维护了两条关键队列:MRU(Most Recently Used,最近使用)和 MFU(Most Frequently Used,频繁使用)。两条队列的容量不是固定的——ARC 会根据实际命中率动态倾斜。如果最近使用的数据命中率更高,MRU 队列就吞掉更多空间;如果频繁访问的数据更重要,MFU 就扩张。这就是”自适应”的含义。

听起来很智能。问题在于,默认情况下,ARC 不知道什么叫”留给别人一点”。

从 top 到源码:一条完整的排查链

回到 Bruno 的 ThinkPad X230。5.69 GB 物理内存,htop 显示 4.49 GB 已用。他走了一遍标准排查:

第一步:看 top。

FreeBSD 的 top 把内存分成五个队列:

  • active:用户进程正在使用的页面
  • inactive:最近没被访问、但随时可以回收的页面
  • laundry:即将被换出(swap out)的页面
  • wired:内核和驱动锁定的内存——ARC 缓存就在这里
  • free:真正的空闲内存

注意 wired。这个名字很直白——“焊死的”。但被 ARC 占用的那部分 wired 其实并不”死”,它可以在系统需要更多内存时被 ARC 主动释放。只是因为 ARC 绕过了 FreeBSD 的 VM 页面管理系统,直接向内核申请 wired 内存,所以在 top 里它被归入 wired 而不是 cache。

也正因为此,ARC 缓存对大多数监控工具来说是隐形的

第二步:查 ARC。

$ sysctl -n kstat.zfs.misc.arcstats.size | gnumfmt --to=iec
3.1G

3.1 GB——这几乎就是”消失”的那部分内存。再看 ARC 的最大值:

$ sysctl -n kstat.zfs.misc.arcstats.c_max

在 Bruno 的机器上,c_max 几乎和总物理内存一样大。这并不是某个配置错误。**ZFS 的默认行为就是让 ARC 最大可用内存等于物理内存减去 1 GB 左右的内核预留空间。**在 16 GB、32 GB 甚至 128 GB 的服务器上,你都逃不掉这个默认值。

第三步:比较不同工具的报告。

这时候,Bruno 发现了更有趣的事情。htopbtopfastfetch 对同一台机器的内存使用率给出了三种不同答案。不是因为某一款工具坏了,而是它们各自选了不同的启发式算法来定义”已用内存”:

  • fastfetch:已用 = 总量 − (free + inactive + cache)
  • btop:已用 = active + wired
  • htop:已用 = active + wired + laundry

btop 的算法最激进——它把所有 inactive 页面都算作”可用”,加上它使用的是 u_int(32 位无符号整数)来存储内存计数,当 active + wired 超过 4 GiB 时就会发生整数回绕。Bruno 机器上实际已用 4.42 GiB,但 btop 因为溢出只显示了 422 MiB。这就是为什么同一台机器出现了 7% 和 80% 两种使用率。

fastfetch 的 cache 统计也有问题。它去读了 vm.stats.vm.v_cache_count——这个 sysctl 的值从 FreeBSD 12.0 开始就永远是 0,因为它的描述清楚地写着:

“Dummy for compatibility”

一个为了兼容性而存在的假接口。但 fastfetchbtop 都在用它。

Bruno 最后给三个项目都提交了补丁——htop(已合并)、btop(待审核)、fastfetch(被关闭,但维护者用类似思路在更大范围内修了)。花了一个月安装十几次 FreeBSD,就为了让别人打开终端时看到的不是幻觉。

HN 评论区:每个 FreeBSD 用户都经历过

帖子上了 Hacker News 后,评论区有一种”过来人”的默契。

有用户直接点破本质:“ZFS cache. The end.” 但这句看似不耐烦的评论下面,另一位用户追评道:“确实,但有些工具确实需要修。”

有评论说:“每个 FreeBSD 用户都经历过这个阶段。“还有人补了一句:“不是所有工具都知道 ZFS ARC 的存在。这恰恰是这篇文章要说的。”

一位自称 vermaden 的用户甩了一句精辟的调侃——“No ZFS no problem”——既是玩笑,也是真相。

更有意思的是关于 Linux 和 FreeBSD 内存哲学的讨论。一位用户回忆,FreeBSD 历史上不支持内存过量提交(overcommit),每个匿名页必须有对应的 swap 页,所以 swap 分区必须设为内存两倍。这种保守策略和 ZFS ARC 的激进缓存看起来矛盾,但本质上出自同一个设计哲学:系统应该精确管理每一页内存,而不是靠事后补救。

Linux 用户:你不也是这么过来的吗?

如果你用 Linux,看到这里可能在想:这跟 Linux 的 page cache 不是一回事吗?

确实如此。linuxatemyram.com 这个网站已经存在了十几年,就为了解决同一个困惑:为什么 htop 显示内存快满了,但系统一点也不卡?

区别在于 Linux 的 free -h 命令后来增加了一个关键列——available。它告诉用户:不是所有”已用”内存都被锁死了,其中有很大一部分是磁盘缓存,随时可以回收。这个 UX 改进让 Linux 用户很少再被 page cache 吓到。

而 FreeBSD 没有这个传统。top 里没有 available 列。htop 的彩色条虽然直观,但内存分类的逻辑藏在源码里。ZFS ARC 又把缓存藏在了 wired 里,对大多数工具来说是黑箱。

换句话说:**同样的机制,Linux 用一行 “available” 消解了用户的恐慌;FreeBSD 让用户在源码里自己找答案。**这不算 bug,但默认值对新手极度不友好。

怎么管住 ARC:一条 sysctl 就够了

如果你用的是 FreeBSD + ZFS,并且真的需要给应用程序留出更多内存:

  1. 看当前 ARC 大小:

    sysctl -n kstat.zfs.misc.arcstats.size | gnumfmt --to=iec
  2. 查看 ARC 的最小/最大值:

    sysctl -n kstat.zfs.misc.arcstats.c_min
    sysctl -n kstat.zfs.misc.arcstats.c_max
  3. 限制 ARC 最大值——在 /boot/loader.conf 中加入:

    vfs.zfs.arc_max="4G"

    替换 4G 为你想要的任意值。重启生效。

或者用 sysctl 临时设置(重启失效):

sysctl vfs.zfs.arc_max=4294967296

注意:**不要把 ARC 设得太小。**ARC 存在的意义是减少磁盘 I/O。如果你的工作负载是数据库或文件服务器,太小反而降低性能。笔者的建议是:先在 loader.conf 里设一个保守值,跑一周,用 arc_summary 看命中率,再逐步调整。

这不是 bug,是 feature——但默认值需要说明书

ZFS ARC 的默认行为——吃掉几乎所有空闲内存——在技术上完全合理。它是经过大量生产环境验证的缓存策略,远比传统的 LRU(最近最少使用)聪明。IBM 的研究员在 2003 年发表 ARC 论文时,证明了它在多种负载下的命中率同时逼近 LRU 和 LFU 的最优解。

问题不在于机制本身,而在于它被默认启用时没有附带一个足够显眼的说明:“您的内存显示为已用,但其中大部分是缓存,会在需要时自动释放。”

Bruno 的一个月调试之旅之所以在 HN 上引发共鸣,不是因为 FreeBSD 做错了什么,而是因为它做对了太多事却没有告诉用户。当一个操作系统让你同时看到 80% 和 7% 两种内存使用率时,问题一定出在信息传递上,而非内核逻辑上。

当然,笔者没有在生产环境跑过 petabyte 级别的 ZFS 存储池。本文的观点基于 HN 讨论、Bruno Croci 的原创文章和个人对 FreeBSD VM 文档的理解。如果你在调优 ARC 时遇到了不同的行为,或者有更好的实践方法,欢迎用实际数据纠正。

毕竟,关于内存的故事,从来不是”空间够不够”这么简单。它是关于谁在定义”已用”、谁在决定什么是”空闲”——以及写监控工具的人有没有读过内核源码。

参考链接: