Память простаивающих виртуальных машин – тихая утечка ресурсов гипервизора. Восемь гостей по 16 ГиБ занимают 128 ГиБ, хотя реально работают с четвертью. Вернуть излишки призван memory ballooning в лице устройства virtio-balloon, и наивная настройка умещается в две строки XML. Ловушка уже там: значение баллона в virsh показывает одно число, резидентная память процесса qemu на хосте – совсем другое. Ниже собрана настройка от XML и virsh до гостевых проверок через free, /proc/buddyinfo и sysctl, плюс главы о free page reporting, конфликте с hugepages и замене virtio-mem.
Механика inflate и deflate внутри virtio-balloon
В гостевой системе баллон виден как обычное PCI-устройство: lspci -nn показывает идентификатор 1af4:1045 для virtio 1.0 или 1af4:1002 у legacy-варианта. Драйвер живёт в файле drivers/virtio/virtio_balloon.c и общается с QEMU через virtqueue: очередь inflate передаёт хосту список страниц, очередь deflate возвращает их гостю, очередь статистики поставляет счётчики для virsh dommemstat.
Надуть баллон значит, что гость выделяет страницы и отдаёт их хосту, а тот считает эту память своей. Сдуть – забрать обратно. Поведение схемы определяют флаги, согласованные при старте устройства:
VIRTIO_BALLOON_F_STATS_VQ статистика гостя
VIRTIO_BALLOON_F_DEFLATE_ON_OOM deflate при нехватке памяти
VIRTIO_BALLOON_F_FREE_PAGE_HINT подсказки свободных страниц, 4.20
VIRTIO_BALLOON_F_PAGE_POISON заполнение при возврате
VIRTIO_BALLOON_F_REPORTING free page reporting, 5.7
Два нюанса, о которых часто забывают. Первый касается учёта: без deflate-on-oom драйвер уменьшает MemTotal гостя на объём надутых страниц, и free честно показывает освободившееся место. С включённым deflate-on-oom счётчик прежний, надутые страницы числятся занятыми, а возвращает их при нехватке OOM-нотификатор через register_oom_notifier(). Второй нюанс – миграция: с опцией сборки CONFIG_BALLOON_MIGRATION надутые страницы переезжают вместе с гостем.
Сюрпризы начинаются там, где баллон встречается с планировщиком памяти гостя и с реальным потреблением хоста.
Настройка баллона в XML libvirt и в командной строке QEMU
libvirt добавляет баллон сам с версии 0.8.3, по умолчанию модель virtio, явная запись нужна ради тонких опций. Шаблон для гостя web01 с потолком 8 ГиБ и стартовыми 4 ГиБ:
<domain type='kvm'>
<name>web01</name>
<memory unit='GiB'>8</memory>
<currentMemory unit='GiB'>4</currentMemory>
<devices>
<memballoon model='virtio'>
<autodeflate on='on'/>
<freePageReporting on='on'/>
<stats period='10'/>
</memballoon>
</devices>
</domain>
Атрибут autodeflate появился в libvirt 1.3.1 и включает флаг deflate-on-oom, freePageReporting добавлен в 6.9.0, stats period задаёт интервал опроса статистики в секундах и работает начиная с QEMU 1.5. Основные команды управления:
virsh setmem web01 2G --live сдуть до 2 ГиБ
virsh setmem web01 6G --live отдать обратно
virsh setmaxmem web01 8G --config потолок на следующий старт
virsh dominfo web01 поле Used memory
virsh dommemstat web01 --period 10 включить сбор статистики
setmaxmem не наращивает потолок работающего QEMU/KVM гостя, для роста нужен memory hotplug. С периодом dommemstat показывает swap_in, swap_out, major_fault, minor_fault, свежие ядра отдают reclaim-счётчики и hugetlb_pgalloc с hugetlb_pgfail. Вывод для сдутого до 4 ГиБ гостя:
actual 4194304
rss 8388608
available 3145728
Разница между actual и rss – центральный момент статьи. Классический inflate память хосту фактически не возвращает: QEMU помечает страницы отданными и расстаётся с ними только под давлением. available же – это MemAvailable глазами гостя, а rss показывает резидентную память процесса qemu.
Прямой доступ к монитору:
virsh qemu-monitor-command web01 --hmp "info balloon"
virsh qemu-monitor-command web01 --hmp "balloon 2048"
Команда balloon принимает мегабайты, 2048 – это 2 ГиБ. В чистом QEMU те же действия делают опции -m 8G и -device virtio-balloon-pci,deflate-on-oom=on,free-page-reporting=on. Осталось понять, что происходит внутри гостя.
Фрагментация памяти гостя после сдувания баллона
free -h покажет уменьшившийся MemTotal, но настоящая проблема видна в карте свободных блоков /proc/buddyinfo:
Node 0, zone DMA32 62 41 18 8 3 1 0 0 0 0 0
Node 0, zone Normal 415 187 62 21 9 4 2 1 0 0 0
Столбцы – свободные блоки порядка от 0 до 10, от 4 КиБ до 4 МиБ. Сдувание возвращает страницы россыпью, старшие порядки пусты, выделить два мегабайта непрерывно трудно. После агрессивного deflate проваливаются THP, а фоновый поток khugepaged жжёт CPU в попытках склейки. Признаки в /proc/vmstat: thp_collapse_alloc замирает, compact_stall растёт.
Калибровка гостя начинается с двух sysctl:
sysctl -w vm.min_free_kbytes=131072
sysctl -w vm.watermark_scale_factor=125
Резерв 128 МиБ в min_free_kbytes даёт ядру запас при большом баллоне, а watermark_scale_factor с 10 до 125 будит kswapd раньше и сглаживает page reclaim. Гостям с точечными hugepage-запросами добавляют madvise: echo madvise > /sys/kernel/mm/transparent_hugepage/enabled. Дальше решают счётчики allocstall и thp_fault_alloc.
Порядок диагностики гостя при жалобах на деградацию такой:
- Сравнить старшие порядки в /proc/buddyinfo до сдувания и после него;
- Проверить thp_collapse_alloc и compact_stall в /proc/vmstat;
- Оценить available через free -h до и после изменения баллона;
- Поднять vm.min_free_kbytes и vm.watermark_scale_factor при давлении reclaim;
- Перевести THP в режим madvise и замерить целевое приложение повторно.
Список закрывает гостевую сторону, но отдать память хосту по-настоящему схема умеет иначе.
Free page reporting вместо классического inflate
Идея free page reporting проста: драйвер периодически сообщает хосту, какие страницы в госте свободны, а QEMU сбрасывает соответствующие участки своей памяти. Резидентный размер процесса qemu уменьшается, что видно по rss. Блоки репортинга имеют порядок 5, то есть 32 страницы или 128 КиБ, а фоновый поток делает работу волнами, чтобы не мешать гостю.
Требования: ядро гостя 5.7+, QEMU 5.1+, включённый freePageReporting в XML либо свойство free-page-reporting=on. Подводный камень существенный: репортинг согласуется, только если хост знает, чем заполнены возвращённые страницы, поэтому драйвер снимает VIRTIO_BALLOON_F_REPORTING, когда в госте неактивны page poisoning и init_on_free. Проверка проста:
cat /sys/kernel/page-poisoning/enabled
Ноль означает, что нужен ядерный параметр init_on_free=on либо page_poison=on, и у выбора есть цена: обнуление освобождаемых страниц ест CPU. Для нагруженных гостей она осознанная, для лёгких – обычно оправданная.
Отдельная история – free page hinting, флаг VIRTIO_BALLOON_F_FREE_PAGE_HINT из ядра 4.20, QEMU 4.1. Во время живой миграции гость подсказывает миграционному потоку, какие страницы свободны, и они не переносятся по сети. На больших гостях экономия достигает десятков гигабайт, а под давлением памяти подсказки приостанавливает shrinker.
Деталь из документации libvirt, которая сбивает с толку: включение freePageReporting не меняет вывод virsh dommemstat. Эффект измеряется только на хосте:
ps -o rss= -p $(cat /var/run/libvirt/qemu/web01.pid)
Разница замера до и после – реально возвращённая гипервизору память. Но у арифметики есть исключение, связанное с огромными страницами.
Конфликт баллона с hugepages на хосте и THP в госте
С памятью на огромных страницах хоста у баллона отношения не складываются. Гранулярность inflate – страницы по 4 КиБ, а hugetlbfs-бэкенд не умеет отдавать часть огромной страницы. Порядок репортинга 5 с его 128 КиБ несовместим с шагом 2 МиБ, и reporting здесь бессилен. Если гость целиком живёт на зарезервированных hugepages, переразмещать нечего: память жёстко закреплена, и баллон обычно отключают совсем:
<memballoon model='none'/>
Это честность конфигурации: для hugetlb-гостей переразмещение лишено смысла, а лишнее PCI-устройство путает мониторинг.
Внутри гостя THP ломаются иначе. Сдувание возвращает память россыпью 4-килобайтных кусков, THP-выделения проваливаются, khugepaged тратит CPU впустую, производительность TLB-чувствительных приложений проседает. Лекарство гибридное: гостям, которые не планируется сдувать глубоко, оставляют madvise-режим, критичным базам данных выделяют отдельные ВМ с hugepages на хосте и отключённым баллоном.
Память всё равно остаётся внутри процесса qemu, а согласование размеров идёт через переговоры. Есть механизм, который решает вопрос насовсем.
virtio-mem и память по запросу вместо классического баллона
virtio-mem переворачивает схему: память горячо подключается и отключается блоками, отключённые блоки исчезают из гостя окончательно. Гранулярность 2 МиБ, поддержка с QEMU 5.1, ядром гостя 5.8, libvirt 7.9.0. Описание устройства:
<devices>
<memory model='virtio-mem'>
<target>
<size unit='GiB'>48</size>
<node>0</node>
<block unit='MiB'>2</block>
<requested unit='KiB'>8388608</requested>
</target>
<alias name='ua-virtiomem0'/>
</memory>
</devices>
size задаёт максимум устройства, block – гранулярность, степень двойки не меньше 1 МиБ, а 2 МиБ на x86-64 совпадает с размером THP. requested – сколько памяти просим сейчас, фактическое исполнение видно через текущий размер. Менять объём можно на лету:
virsh update-memory-device web01 --requested-size 4G --live
virsh update-memory-device web01 --alias ua-virtiomem0 --requested-size 256M --live
Гость пересматривает запрос, если отсоединению мешают несвободные страницы, поэтому память подключают в movable-зону: правило udev ACTION=="add", SUBSYSTEM=="memory", ATTR{state}="online_movable" делает отключение надёжным. С хоста прогресс виден через монитор QEMU: qom-set vm0 requested-size 1G запускает сжатие.
Ограничения стоит знать заранее. С инфляцией баллона virtio-mem не совмещается, virsh setmem действует только на стартовую память, mlock запрещён, зато free page reporting комбинируется официально. В свежих версиях включайте атрибут dynamic-memslots со значением true из libvirt 10.1.0: QEMU расходует не больше 256 слотов и не держит метаданные KVM за отключённые блоки. Бэкендам нужны reserve=off и prealloc=off, предвыделение включается на устройстве через prealloc=on. Остаётся недооценённая часть истории, переразмещение на хосте.
Переразмещение на хосте, KSM и границы безопасности
Переразмещение обязано иметь безопасные пределы, и хостовая конфигурация начинается с политики overcommit:
sysctl -w vm.overcommit_memory=2
sysctl -w vm.overcommit_ratio=80
Режим 2 разрешает выделение сверх физической памяти в пределах swap плюс проценты от RAM, и плотный массив гостей перестаёт быть лотереей. Дефолт overcommit_ratio равен 50, при активном репортинге берут 80–90 с запасом на процесс qemu.
Второй рычаг – KSM, слияние одинаковых страниц:
echo 1 > /sys/kernel/mm/ksm/run
echo 1000 > /sys/kernel/mm/ksm/pages_to_scan
echo 100 > /sys/kernel/mm/ksm/sleep_millisecs
Фоновый процесс ksmd сливает повторяющиеся страницы, тонны одинаковых нулей свежезапущенных гостей схлопываются без баллона. Обратная сторона: слитые страницы при сдувании приходится разводить заново, лишний CPU на частых изменениях размера. Такие гости помечаются в XML через <nosharepages/>.
Третий пункт – лимиты. memtune с hard_limit документация прямо отговаривает ставить: при неточной оценке ядро завершит процесс qemu вместе с гостем. Достаточно политики overcommit и контроля RSS:
virsh dommemstat web01 --period 10
watch -n 5 "ps -o rss= -p $(pgrep -f 'guest=web01')"
Последнее предостережение касается deflate-on-oom. Флаг даёт гостю право вырвать память из баллона при нехватке, но это нарушение учёта на хосте: гость выходит за отведённый объём именно тогда, когда хосту самому туго. Ставить его на каждом госте – плохая стратегия, место флага на ВМ, чья доступность важнее точности переразмещения.
Наблюдение тех, кто гоняет баллон годами: механизм из трёх virtqueue закрывает большинство задач переразмещения обычных гостей в связке с free page reporting, hugetlb-инсталляции живут с отключённым в XML устройством, а где нужна честная динамическая память с гранулярностью 2 МиБ, место баллона занимает virtio-mem. Порядок внедрения повторяет порядок разделов: XML и virsh, гостевые счётчики, репортинг, затем overcommit-политика хоста.