ZFS ARC на небольшом VPS: как ограничить размер кеша
Что такое ZFS ARC, почему на VPS с 4 или 8 ГиБ он по умолчанию забирает большую часть памяти, как читать arcstats и задать zfs_arc_max на Linux и vfs.zfs.arc.max на FreeBSD.
Коротко: что делать
ZFS ARC на небольшом VPS нужно ограничить вручную, потому что потолок по умолчанию рассчитан на хранилище, а не на машину, где рядом с пулом работают веб-сервер и база данных. На Linux лимит задаётся параметром модуля zfs_arc_max в файле /etc/modprobe.d/zfs.conf, на FreeBSD параметром vfs.zfs.arc.max в /boot/loader.conf. Размер считается от нагрузки, а не по «правильной доле» памяти: сначала измеряете, сколько нужно сервисам в пике, потом отдаёте ARC то, что осталось.
Имена параметров и значения по умолчанию проверены по man zfs.4 из веток OpenZFS 2.2, 2.3 и 2.4 и по дереву sysctl FreeBSD 15.1, по состоянию на сентябрь 2026 года. Все команды ниже запускаются на вашей машине с загруженным модулем ZFS: без него файла /proc/spl/kstat/zfs/arcstats просто не существует.
Что такое ARC и чем он отличается от страничного кеша
ARC (adaptive replacement cache) это кеш чтения ZFS в оперативной памяти. В нём лежат и блоки данных, и метаданные: dnode и косвенные блоки. Внутри две очереди. MRU (most recently used) держит то, что прочитали недавно, MFU (most frequently used) держит то, что читают часто. К каждой очереди прилагается «призрачный» список (ghost list): он помнит только заголовки уже вытесненных блоков. Попадание в призрачный список означает, что ARC ошибся с балансом, и граница между MRU и MFU сдвигается. Отсюда слово adaptive в названии.
Для нашей задачи важнее другое свойство. ARC не является частью страничного кеша ядра. Обычная файловая система кладёт прочитанные страницы в page cache, и ядро знает, что их можно выбросить в любой момент: free -m показывает их в колонке buff/cache, а MemAvailable в /proc/meminfo учитывает их как доступные. ARC живёт в памяти, которую выделил модуль ZFS. Для ядра это обычная занятая память, поэтому free -m показывает её в колонке used, MemAvailable её не считает, а мониторинг рисует красную полосу «память кончилась». На FreeBSD картина похожая: ARC выделяется отдельно от буферного кеша, и top показывает его отдельной строкой ARC в заголовке.
С OpenZFS 0.7 блоки лежат в ARC в том же сжатом виде, что и на диске (compressed ARC). При compression=lz4 кеш в 2 ГиБ реально хранит больше данных, чем 2 ГиБ. Поля compressed_size и uncompressed_size в статистике показывают разницу.
В статистике ARC есть три числа, которые нельзя путать. c_max это жёсткий потолок. c это целевой размер, который ARC сам двигает между c_min и c_max: поднимает при промахах, опускает при нехватке памяти. size это сколько занято прямо сейчас. Свежезагруженная система стартует с маленьким size, и он растёт с каждым чтением, пока не упрётся в потолок. Поэтому проблема проявляется не в день установки, а через неделю, когда кеш дорос до c_max и сервисам стало тесно.
Какой потолок у ARC по умолчанию и почему его нельзя цитировать по памяти
Если zfs_arc_max равен нулю (так по умолчанию), OpenZFS вычисляет потолок от объёма памяти, которую видит ядро. Формула менялась между версиями, и это главная причина не повторять «половину памяти» из старых статей.
- Linux с OpenZFS 2.2: половина всей памяти. Так работает Ubuntu 24.04 с пакетом zfsutils-linux 2.2.2.
- FreeBSD с OpenZFS 2.2, а также любая система с OpenZFS 2.3 или 2.4: большее из двух значений, «вся память минус 1 ГиБ» и «5/8 всей памяти». Так работают FreeBSD 14.3 (ветка 2.2), Debian 13 (2.3.9), Ubuntu 26.04 (2.4.1) и FreeBSD 15.1 (2.4.2).
Версию на своей машине покажет zfs version. Таблица ниже посчитана по этим формулам. Это не измерение, а арифметика из man zfs.4; реальное значение чуть меньше, потому что ядро видит меньше памяти, чем написано в тарифе.
The data behind this chart
[
{
"label": "2 \u0413\u0438\u0411 RAM",
"linux_openzfs_2_2": 1,
"openzfs_2_3_plus_and_freebsd": 1.25
},
{
"label": "4 \u0413\u0438\u0411 RAM",
"linux_openzfs_2_2": 2,
"openzfs_2_3_plus_and_freebsd": 3
},
{
"label": "8 \u0413\u0438\u0411 RAM",
"linux_openzfs_2_2": 4,
"openzfs_2_3_plus_and_freebsd": 7
},
{
"label": "16 \u0413\u0438\u0411 RAM",
"linux_openzfs_2_2": 8,
"openzfs_2_3_plus_and_freebsd": 15
}
]На VPS с 4 ГиБ Ubuntu 24.04 отдаст кешу 2 ГиБ, а Debian 13, Ubuntu 26.04 или FreeBSD на той же машине уже 3 ГиБ. На 8 ГиБ разница ещё заметнее: 4 ГиБ против 7 ГиБ, то есть новая формула оставляет всему остальному ровно 1 ГиБ. Обновление Ubuntu 24.04 до 26.04 на той же машине молча поднимает потолок с 2 до 3 ГиБ, и сервисы, которым год хватало памяти, начинают свопить после апгрейда.
Такие значения по умолчанию разумны для хранилища, где ZFS главный жилец, а всё остальное укладывается в оставшийся гигабайт. На VPS с 4 ГиБ, где ZFS держит один пул с данными приложения, это не так.
Почему на VPS с сервисами это заканчивается свопом
Механизм такой. Ядро Linux не знает, что память ARC можно отдать без потерь. Когда процесс просит страницу, а свободных нет, ядро запускает reclaim: сбрасывает страничный кеш и зовёт зарегистрированные shrinker'ы, у ZFS есть свой. Параллельно, в соответствии с vm.swappiness, оно выгружает анонимные страницы процессов в swap. ARC через свой shrinker память отдаёт, но не всю сразу. В OpenZFS 2.2 параметр zfs_arc_shrinker_limit по умолчанию равен 10000 страниц за один вызов, и man-страница прямо пишет, что с учётом поведения ядра это около 160 МиБ на одну попытку выделения. В 2.3 и 2.4 лимит по умолчанию снят (значение 0) и применяется только к kswapd, так что новые версии уступают охотнее. Но целевой размер c снова растёт, как только чтения возобновляются. Получаются качели: часть страниц приложения ушла в swap, ARC вернулся к потолку, приложение тронуло холодную страницу и ждёт диск.
Проверить, что происходит именно это, можно тремя командами.
vmstat 1 5
awk '$1=="size"||$1=="c"||$1=="c_max"||$1=="memory_direct_count"||$1=="memory_indirect_count" {print $1, $3}' /proc/spl/kstat/zfs/arcstats
sudo dmesg -T | grep -i -E 'out of memory|killed process'Диагноз подтверждён, если в vmstat колонки si и so ненулевые в спокойное время, size в arcstats близок к c_max, а memory_direct_count растёт между двумя запусками. Этот счётчик считает, сколько раз процессы попадали в прямой reclaim через shrinker ARC, то есть сколько раз кто-то стоял и ждал, пока кеш освободит память; memory_indirect_count считает то же самое для kswapd. Третья команда покажет, если дело дошло до OOM killer.
На FreeBSD смотрите vmstat 1 5 (колонки pi и po), swapinfo -h и пару значений sysctl kstat.zfs.misc.arcstats.size kstat.zfs.misc.arcstats.c_max.
Как посмотреть, сколько ARC занимает сейчас
На Linux два источника: параметры модуля в /sys/module/zfs/parameters/ (что вы задали) и статистика в /proc/spl/kstat/zfs/arcstats (что получилось).
cat /sys/module/zfs/parameters/zfs_arc_max
cat /sys/module/zfs/parameters/zfs_arc_min
awk '$1=="size"||$1=="c"||$1=="c_min"||$1=="c_max"||$1=="hits"||$1=="misses" {print $1, $3}' /proc/spl/kstat/zfs/arcstatsНоль в первых двух файлах означает «считать автоматически». Все значения в байтах. Файл arcstats устроен как три колонки: имя, тип, значение, поэтому awk берёт третье поле.
Для наблюдения в динамике есть две утилиты из пакета zfsutils-linux. arc_summary печатает отчёт с текущим размером, целевым размером и пределами, а также долю попаданий; arc_summary -s arc ограничивает вывод одним разделом. arcstat печатает строку раз в интервал:
arc_summary -s arc
arcstat -f time,read,hit%,size,c,avail 5Поле hit% это доля попаданий в ARC, avail это сколько свободной памяти ARC видит для себя (оно может стать отрицательным, и тогда ARC снижает c). В OpenZFS 2.4.0 обе утилиты переименованы в zarcsummary и zarcstat; на Ubuntu 26.04 они лежат в /usr/bin под новыми именами, на Ubuntu 24.04 и Debian 13 работают старые.
На FreeBSD та же статистика доступна через sysctl, а параметры лежат в ветке vfs.zfs.arc:
zfs version
sysctl vfs.zfs.arc.max vfs.zfs.arc.min
sysctl kstat.zfs.misc.arcstats.size kstat.zfs.misc.arcstats.c kstat.zfs.misc.arcstats.c_max
sysctl kstat.zfs.misc.arcstats.hits kstat.zfs.misc.arcstats.missesУтилиты arcstat и arc_summary в базовую систему FreeBSD не входят. Их роль выполняет порт sysutils/zfs-stats: pkg install zfs-stats, затем zfs-stats -A для сводки по ARC и zfs-stats -E для доли попаданий.
Как задать zfs_arc_max и zfs_arc_min на Linux
Сначала проверьте значение на живой системе, не трогая файлы. Параметр принимает только байты, без суффиксов вроде 2G, так что число проще посчитать в шелле.
echo $((2 * 1024 * 1024 * 1024))
echo 2147483648 | sudo tee /sys/module/zfs/parameters/zfs_arc_max
awk '$1=="size"||$1=="c_max" {print $1, $3}' /proc/spl/kstat/zfs/arcstatsc_max должен стать равным записанному числу. Если он не изменился, значение отвергнуто молча: zfs_arc_max должен быть не меньше 64 МиБ (67108864 байт), не меньше текущего c_min и не больше объёма памяти. Файл в /sys при этом покажет то, что вы записали, поэтому сверять нужно именно c_max. Обратно в 0 на работающей системе параметр не возвращается. Ещё одна оговорка из документации: уменьшение потолка ниже текущего size не обязано освободить память немедленно, если на память никто не давит. Подождите минуту и посмотрите size ещё раз. Если он не опустился к новому c_max, надёжный путь один: прописать значение постоянно и перезагрузиться.
Постоянная настройка живёт в /etc/modprobe.d/. Минимум оставьте по умолчанию или задайте небольшим: он нужен, чтобы под давлением ARC не схлопнулся до нуля и не потерял метаданные.
printf 'options zfs zfs_arc_max=2147483648\noptions zfs zfs_arc_min=268435456\n' | sudo tee /etc/modprobe.d/zfs.conf
sudo update-initramfs -uВторой шаг обязателен, когда корень на ZFS. Модуль zfs тогда загружается из initramfs до того, как смонтирован настоящий корень, и читает копию modprobe.d, упакованную внутрь образа. Без пересборки новый файл на диске никто не прочитает, а модуль, уже загруженный со старыми параметрами, не перечитает его никогда. Если корень на ext4, а ZFS держит только пул с данными, модуль загружается позже и читает /etc/modprobe.d напрямую, но пересборка ничего не портит и снимает вопрос. На Rocky и Alma та же команда называется sudo dracut -f.
После перезагрузки сверьте оба источника:
cat /sys/module/zfs/parameters/zfs_arc_max
awk '$1=="c_max" {print $3}' /proc/spl/kstat/zfs/arcstatsОба числа должны совпасть с тем, что записано в zfs.conf.
Как задать vfs.zfs.arc.max на FreeBSD
Параметр называется vfs.zfs.arc.max начиная с FreeBSD 13, когда базовая система перешла на OpenZFS. Старое имя vfs.zfs.arc_max тоже работает, но в описании sysctl оно помечено как LEGACY, и в новых конфигурациях его лучше не использовать. Проверка на живой системе:
sysctl vfs.zfs.arc.max=2147483648
sysctl kstat.zfs.misc.arcstats.c_maxПостоянно значение задаётся в /boot/loader.conf, который загрузчик читает до старта ядра. Никаких образов пересобирать не нужно.
vfs.zfs.arc.max="2147483648"
vfs.zfs.arc.min="268435456"Параметры помечены как RWTUN, поэтому строка в /etc/sysctl.conf тоже сработает. Но sysctl.conf применяется уже после загрузки ядра, а loader.conf до неё, когда ARC ещё пуст, и оговорка про «не обязан уменьшиться» вас не касается. Проверка после перезагрузки: sysctl vfs.zfs.arc.max kstat.zfs.misc.arcstats.c_max.
Метод расчёта: сколько отдать ARC
Магического числа нет. Есть порядок действий, который занимает один рабочий день и один вечер.
- Узнайте, сколько памяти видит ядро: колонка
totalвfree -mна Linux,sysctl hw.physmemна FreeBSD. Считайте от этого числа, а не от цифры в тарифе. - Временно прижмите ARC, например до 512 МиБ (536870912 байт) через
/sysилиsysctl, чтобы кеш не искажал измерение. - Прогоните нагрузку хотя бы сутки, включая ночные задания: резервные копии, обновления пакетов, ротацию логов и cron приложения.
- Снимите пик потребления сервисов. На Linux с cgroup v2 это одна строка:
cat /sys/fs/cgroup/system.slice/memory.peak(файл есть в ядрах с 5.19, на Ubuntu 24.04 и Debian 13 он на месте). Отдельную службу покажетsystemctl show -p MemoryPeak nginx.service. На FreeBSD пик снимайте вручную:ps -axo rss,command | sort -rn | headв момент нагрузки илиtop -o res. - Сложите базовое потребление системы (то, что
usedпоказывает на машине без ваших сервисов), пик из шага 4 и запас 20 % на всплески. Это резерв. В запас входит и scrub: он сортирует ввод-вывод в памяти, и под эту очередь может уйти до 1/20 RAM (параметрzfs_scan_mem_lim_fact, по умолчанию 20), на 4 ГиБ это до 200 МиБ поверх ARC. - ARC получает разницу: память из шага 1 минус резерв. Округлите вниз до целых МиБ, переведите в байты и запишите.
- Верните нагрузку и неделю смотрите
arcstat -f time,read,hit%,size,c,avail 5илиzfs-stats -E. Еслиhit%стабильно высокий, задача решена.
Пример для VPS с 4 ГиБ, где free -m показывает 3920 МиБ. Базовая система 350 МиБ, пик nginx с PostgreSQL и приложением 1400 МиБ, запас 20 % от их суммы 350 МиБ. Резерв 2100 МиБ, ARC получает 1820 МиБ, округляем вниз до 1792 МиБ, это 1879048192 байт.
Два граничных случая. Если под ARC осталось меньше 1 ГиБ, ZFS будет работать, но кеш станет в основном хранилищем метаданных, и чтения данных пойдут с диска; решайте, нужен ли на этой машине ZFS или нужен тариф побольше. Если разница отрицательная, VPS мал для нагрузки независимо от файловой системы, и никакой параметр это не исправит. Обзор того, что ZFS даёт и чего требует на обеих системах, есть в статье про ZFS на FreeBSD и Linux, а выбор самой платформы разобран в сравнении Linux и FreeBSD как серверной ОС.
Про zfs_arc_min. Значение по умолчанию это большее из 32 МиБ и 1/32 памяти, на 4 ГиБ это 128 МиБ. Задавать zfs_arc_min равным zfs_arc_max, как советуют для домашних NAS, на VPS не надо: так вы запрещаете ARC уступать даже при всплеске, и вместо кеша под нож пойдёт приложение.
Что ещё помогает на маленькой машине
Не кешировать дважды. База данных со своим буферным пулом (PostgreSQL с shared_buffers, MySQL с innodb_buffer_pool_size) держит горячие страницы сама, и ARC хранит их второй раз. Свойство primarycache=metadata на датасете базы оставляет в ARC только метаданные: zfs set primarycache=metadata tank/pgdata. Это не бесплатно. PostgreSQL рассчитывает на кеш ОС для части чтений, поэтому меняйте и смотрите на hit% и на задержки запросов, а не на теорию.
Заставить ARC уступать раньше. На Linux параметр zfs_arc_sys_free задаёт, сколько свободной памяти ARC старается оставить системе; по умолчанию это большее из 512 КиБ и 1/64 памяти, на 4 ГиБ всего 64 МиБ. Строка options zfs zfs_arc_sys_free=536870912 в том же zfs.conf поднимает порог до 512 МиБ, и ARC начинает ужиматься до того, как ядро полезет в swap. Это дополнение к жёсткому потолку, а не замена.
Swap оставить, но небольшой. Потолок ARC убирает качели, а swap остаётся страховкой от единичного всплеска. Сколько его нужно и нужен ли он вообще, разобрано в статье нужен ли VPS swap. Крутить vm.swappiness в надежде вылечить ARC бесполезно: этот параметр управляет выбором между страничным кешем и анонимной памятью, а ARC ни то и ни другое.
Ограничить и сервисы тоже. Когда у кеша есть потолок, логично дать потолок и приложениям, иначе один утёкший процесс съест долю ARC и всё вернётся. Для служб systemd это MemoryMax= в юните, подробно в статье про ограничение памяти и CPU процесса через systemd; для контейнеров те же лимиты задаются в compose-файле, см. лимиты памяти в Docker Compose.
Scrub в тихое окно. Плановая проверка пула читает весь пул и держит очередь сортировки в памяти, о которой сказано выше, поэтому на машине с 4 ГиБ её лучше не совмещать с ночным бэкапом. Расписание и параметры для небольших пулов разобраны в статье про расписание scrub для небольших пулов ZFS.
FAQ
Почему free показывает память занятой, хотя процессы используют мало?
Потому что ARC не входит в страничный кеш ядра. Он выделен модулем ZFS, и для ядра это обычная занятая память: free -m показывает её в used, MemAvailable её не учитывает. Сложите size из /proc/spl/kstat/zfs/arcstats с суммой RSS процессов, и цифра сойдётся. Мониторинг, который считает «занято минус кеш», на машине с ZFS нужно научить вычитать ещё и size ARC.
Я записал zfs_arc_max в /sys, а c_max не изменился. Почему?
Значение отвергнуто молча. zfs_arc_max принимается, только если он не меньше 64 МиБ (67108864 байт), не меньше текущего c_min и не больше объёма памяти. Чаще всего ловят второе условие: задали потолок в 100 МиБ на машине, где c_min по умолчанию 128 МиБ. Файл в /sys при этом честно показывает записанное число, поэтому проверять надо c_max в arcstats. Ноль на работающей системе тоже не принимается.
Нужно ли пересобирать initramfs, если корень не на ZFS?
Строго говоря, нет: модуль загружается после монтирования корня и читает /etc/modprobe.d/zfs.conf с диска. Но sudo update-initramfs -u занимает секунды и снимает вопрос о том, откуда именно загрузился модуль на этой машине, поэтому запускайте его всегда. На Rocky и Alma это sudo dracut -f. После перезагрузки сверьте c_max в arcstats с числом в файле.
Какой минимальный размер ARC ещё имеет смысл?
Ниже примерно 1 ГиБ кеш превращается в хранилище метаданных: dnode и косвенные блоки ещё помещаются, данные уже нет, и каждое чтение файла идёт на диск. Работать будет, но сравнивать такую машину с ext4 по скорости чтения бессмысленно. Абсолютный минимум, который примет модуль, 64 МиБ, и он существует для тестов, а не для сервера. Если расчёт по методу выше даёт меньше гигабайта, честный ответ: нагрузке нужен тариф побольше или файловая система попроще.
На FreeBSD параметр называется vfs.zfs.arc_max или vfs.zfs.arc.max?
Оба имени существуют. vfs.zfs.arc.max это актуальное имя с FreeBSD 13, когда базовая система перешла на OpenZFS, и именно его стоит писать в /boot/loader.conf. vfs.zfs.arc_max сохранён для совместимости и в описании sysctl помечен как LEGACY. Значение в обоих случаях в байтах, а проверять результат нужно по sysctl kstat.zfs.misc.arcstats.c_max.