Память в Linux никогда не простаивает: пока приложения считают, ядро набивает свободные страницы кэшем файлов, а когда места становится мало, начинает вытеснять их на диск. Этот процесс называется reclaim, и от его настройки зависит, будет ли сервер работать ровно или начнет замирать на глазах у пользователей. Симптомы косвенные и обманчивые: внезапные паузы по 200-300 мс, растущий load average при спокойном процессоре и диск, который шуршит без видимой причины. За всем этим почти всегда стоит одно и то же: ядро поздно замечает нехватку памяти и загоняет рабочие процессы в прямое вытеснение прямо на пути выделения страниц. Ниже разберемся, как устроен механизм вытеснения изнутри, какие параметры управляют его поведением и как измерить результат вместо слепого перебора настроек.
Водяные знаки памяти и поток ядра kswapd
Ядро делит физическую память на зоны: DMA, DMA32 и Normal на типичной машине x86-64. Для каждой зоны оно поддерживает три порога свободных страниц: min, low и high, вместе их называют водяными знаками. Пока свободных страниц больше high, фоновый поток ядра kswapd спит. Запас упал ниже low, и kswapd просыпается: чистые страницы кэша файлов он просто отпускает, грязные сбрасывает на диск, а анонимную память процессов отправляет в swap. Дойдя до high, поток снова засыпает. Если память кончается быстрее и свободные страницы проваливаются ниже min, в дело вступает прямое вытеснение: само приложение, запрашивающее память, вместо полезной работы сканирует списки страниц и освобождает их вручную. Именно прямые вытеснения рождают те самые непредсказуемые паузы в 100-300 мс, которые так сложно поймать в профилировщике.
Живые пороги удобно смотреть в /proc/zoneinfo:
grep -A 5 "zone Normal" /proc/zoneinfo | head -6
Вывод для одной зоны:
Node 0, zone Normal
pages free 397642
min 4021
low 5026
high 6031
Здесь порог min равен 4021 страницам, то есть около 16 МБ. Расстояние от low до high, примерно тысяча страниц, это рабочая зона потока kswapd, и шириной этого зазора управляет следующий параметр.
Базовый резерв min_free_kbytes и цена ошибки в нем
Параметр vm.min_free_kbytes задает суммарный резерв, из которого ядро вычисляет порог min для каждой зоны. При загрузке система считает его сама, отталкиваясь от объема памяти: резерв получается как четыре квадратных корня из объема в килобайтах, то есть 4 * sqrt(lowmem_kbytes). Для машины с 16 ГБ это около 16384 КБ, чуть больше 16 МБ из примера выше. Занижать значение опасно: ядро прямо предупреждает, что ниже 1024 КБ система становится хрупкой и склонной к взаимоблокировкам под высокой нагрузкой, потому что резерв не покрывает срочные запросы на выделение страниц. Завышать тоже нельзя: зарезервированные страницы недоступны обычным выделениям, и щедрый резерв оборачивается мгновенным исчерпанием памяти с принудительным завершением процессов механизмом OOM. Рабочий диапазон для типичных серверов лежит в пределах от 16 до 256 МБ, точное значение подбирают по нагрузке. Проверить текущее число и поднять резерв на ходу можно так:
cat /proc/sys/vm/min_free_kbytes
sysctl -w vm.min_free_kbytes=65536
Пороги min, low и high во всех зонах пересчитываются сразу, перезапуск не нужен.
Ширина рабочей зоны kswapd через watermark_scale_factor
Если kswapd просыпается слишком поздно или слишком рано, виноват обычно не резерв, а расстояние между водяными знаками. За него отвечает vm.watermark_scale_factor. Единица измерения здесь доли от десяти тысяч: при значении по умолчанию 10 зазор между порогами составляет 0,1% управляемой памяти, максимум 3000 означает 30%. Расчет простой: зазор в страницах равен числу управляемых страниц, умноженному на значение фактора и деленному на десять тысяч. Возьмем зону на 4 000 000 страниц, это около 15,6 ГБ. При стандартном факторе 10 зазор составит примерно 4000 страниц, или 16 МБ: kswapd на активном сервере просыпается каждые несколько секунд и снова засыпает, так и не успев накопить запас. При факторе 125 зазор вырастет до 50 000 страниц, около 195 МБ: поток проснется реже, зато за один заход подготовит заметный буфер свободных страниц. Ориентироваться помогает пара счетчиков из /proc/vmstat:
grep -E "allocstall|kswapd_low_wmark_hit_quickly" /proc/vmstat
Ненулевой и растущий allocstall означает, что процессы попадают в прямое вытеснение: зазор мал, поток не успевает. Частые события kswapd_low_wmark_hit_quickly, когда kswapd засыпает, едва коснувшись нижнего порога, говорят о той же проблеме. Рядом живет vm.watermark_boost_factor со значением по умолчанию 15000: при фрагментации памяти ядро дополнительно вытесняет до 150% верхнего порога, чтобы собрать непрерывные блоки под прозрачные и огромные страницы; ноль отключает этот механизм. В свежих ядрах серии 6.x вместо классических списков LRU может работать алгоритм MGLRU, но водяные знаки и перечисленные параметры сохраняют силу и там.
Выбор между памятью процессов и кэшем файлов задает swappiness
Ища кандидатов на вытеснение, kswapd стоит перед развилкой: отправить в swap анонимные страницы приложений или освободить страницы кэша файлов, которые в любой момент перечитаются с диска. Весами на этой развилке служит vm.swappiness, диапазон от 0 до 200, по умолчанию 60. Логика параметра не процентная, а стоимостная: значение 100 означает, что ядро считает обращение к swap и к файловой системе равноценным по вводу-выводу и давит на оба вида страниц одинаково. Меньшие значения говорят, что swap дороже, большие, что дешевле. При нуле ядро не трогает swap, пока суммарный запас свободных и файловых страниц не провалится ниже верхнего водяного знака. Типовые решения выглядят так. Базам данных с собственным кэшем на машине с медленным HDD ставят около 10. Десктопу со swap на SSD и zswap разумно дают 100-150. Системам с zram, где swap лежит в сжатой памяти и не пишет на диск вовсе, разработчики ядра разрешают значения выше 100 и приводят расчет: если случайный ввод-вывод на устройство swap в среднем вдвое быстрее файловой системы, из уравнения x + 2x = 200 выходит значение 133. Соседние ручки тоже приводят в порядок. vm.page-cluster со значением по умолчанию 3 управляет упреждающим чтением из swap по 8 страниц подряд и помогает при последовательном доступе, а при случайном его снижают до 0 или 1. vm.vfs_cache_pressure со значением по умолчанию 100 задает склонность ядра вытеснять кэш каталогов и индексов: почтовым и файловым серверам с миллионами мелких файлов ставят 150-200, а ноль опасен, потому что кэш перестанет освобождаться вовсе и машина дойдет до OOM. Посмотреть все три сразу:
sysctl vm.swappiness vm.vfs_cache_pressure vm.page-cluster
Давление памяти по PSI и раннее оповещение о трэшинге
Тюнинг вслепую превращается в гадание, поэтому измерение налаживают в первую очередь. Путеводной нитью служит PSI, информация о простое из-за нехватки ресурсов. Файл /proc/pressure/memory показывает две строки. Строка some отражает долю времени, когда хотя бы часть задач ждала память, а строка full, когда простаивали все готовые к работе задачи сразу: это уже трэшинг, полезная работа остановилась. Каждая строка содержит средние avg10, avg60 и avg300 за окна 10, 60 и 300 секунд и суммарное время ожидания total в микросекундах. В cgroup v2 те же данные лежат в файле memory.pressure каждой группы, так что давление видно и по отдельному сервису. Чтение тривиально:
cat /proc/pressure/memory
some avg10=24.31 avg60=6.02 avg300=1.84 total=421357094
full avg10=3.17 avg60=0.44 avg300=0.11 total=58702133
Такой снимок сообщает, что прямо сейчас четверть времени какая-то из задач ждала память, а все задачи одновременно простаивали 3% последней десятисекундки. Ориентиры на практике такие: вспышки some avg10 до 20-30% терпимы, а full avg10, устойчиво держащийся выше 5-10%, означает предельное состояние, когда настройки уже не спасают и память нужно освобождать или добавлять. Ядро умеет и активное оповещение: в открытый дескриптор /proc/pressure/memory записывают порог ожидания и окно в микросекундах, скажем 150 мс в окне 1 секунды, после чего вызов poll() проснется, как только суммарное ожидание его превысит. Окна допускаются от 500 мс до 10 секунд, а непривилегированным пользователям достаются окна, кратные 2 секундам. На такой триггер удобно повесить скрипт, который сам вызовет memory.reclaim в cgroup и разгрузит систему до того, как пользователи заметят тормоза.
Лимиты cgroup v2 и принудительное вытеснение через memory.high и memory.reclaim
Системные параметры задают поведение узла целиком, а в cgroup v2 управление вытеснением спускается до отдельного сервиса. Главный инструмент здесь memory.high: как только использование группы превышает лимит, ядро притормаживает ее процессы и отправляет их вытеснять собственные страницы прямо в момент выделения памяти. OOM не наступает, у администратора остается время отреагировать, поэтому memory.high считается основным механизмом контроля в cgroup v2. Счетчик high в файле memory.events показывает, сколько раз группа упиралась в лимит, и по нему сразу видно, что лимит занижен. Ручной инструмент memory.reclaim устроен как write-only файл: запись в него просит ядро вытеснить требуемый объем из группы прямо сейчас. Ядро может вытеснить больше или меньше запрошенного, а если получилось меньше, запись вернет ошибку EAGAIN. В той же строке допустимо передать собственную swappiness для этого вытеснения, в диапазоне от 0 до 200, а ключевое слово max означает, что вытеснять нужно исключительно анонимную память. Любопытная деталь: проактивное вытеснение не считается давлением памяти, поэтому балансировка буферов сетевых сокетов при нем не включается, и сетевой стек не подстроится. Типовая сессия для сервиса выглядит так:
cd /sys/fs/cgroup/app.slice/worker.service
echo "2G" > memory.high
echo "512M" > memory.reclaim
echo "512M swappiness=150" > memory.reclaim
Параллельно файл memory.stat раскрывает, чем группа платит за память. Ключи pgscan_kswapd, pgscan_direct и pgscan_proactive показывают, сколько страниц просканировал фоновый поток, сколько вытянули процессы в прямом вытеснении и сколько пришлось на проактивные записи в memory.reclaim. Ключи workingset_refault_anon и workingset_refault_file считают повторные промахи по страницам, вытесненным незадолго до этого: если refault растет после смены настроек, ядро начало выбрасывать горячие страницы, и лимит пора ослабить.
Схема внедрения и проверка результата по счетчикам
Порядок действий при внедрении разумно зафиксировать заранее:
- собрать базовую линию, сохранив вывод cat /proc/pressure/memory и ключевые счетчики /proc/vmstat до любых изменений;
- занести постоянные значения vm.min_free_kbytes, vm.watermark_scale_factor и vm.swappiness в файл /etc/sysctl.d/99-reclaim.conf;
- выбрать swappiness по типу хранилища swap и заодно проверить page-cluster и vfs_cache_pressure;
- разнести сервисы по группам cgroup v2 и назначить каждому memory.high с запасом 10-15% от рабочего пика;
- прогнать характерную нагрузку и сравнить avg300 и allocstall с базовой линией;
- поднимать watermark_scale_factor шагами по 25-50, пока частота kswapd_low_wmark_hit_quickly не перестанет расти;
- следить за refault после каждой итерации, чтобы не вытеснять горячие страницы.
Файл с настройками выглядит скромно:
vm.min_free_kbytes = 65536
vm.watermark_scale_factor = 125
vm.swappiness = 100
vm.vfs_cache_pressure = 100
Применяется он одной командой, живую картину дальше показывает vmstat:
sysctl --system
vmstat 5 3
В колонке si видно подкачку страниц в память, в so наружу, в wa долю процессорного времени, съеденную ожиданием ввода-вывода. Ровный фон so при спокойном wa означает здоровое фоновое вытеснение, а взлетающий wa вместе с замираниями приложений сообщает, что система опять докатилась до прямой вытеснения. Отдельный соблазн, сброс кэшей записью в /proc/sys/vm/drop_caches, лучше оставить лаборатории: он не лечит причину, а лишь стирает кэш, который тут же придется читать с диска заново.
Настройка вытеснения страниц не сводится к одной волшебной цифре, а работает связкой: резерв min_free_kbytes держит последний рубеж, водяные знаки с разумным зазором позволяют потоку kswapd трудиться в фоне, swappiness расставляет приоритеты по стоимости ввода-вывода, лимиты cgroup v2 изолируют сервисы друг от друга, а PSI превращает ощущения в числа. Машина, у которой эти ручки выставлены под собственные нагрузки, а не под заводские умолчания, переживает пики памяти ровно и предсказуемо.