MAATRIX / Блог / Ballooning забрал память у базы, и она ушла в своп внутри виртуалки

Ballooning забрал память у базы, и она ушла в своп внутри виртуалки

MAATRIX

Ночью графики отклика API легли на полку в три-четыре раза выше обычного, а причина не лежала на поверхности: диск был жив, сеть — тоже, CPU почти простаивал. Виновником оказался не сбой, а штатная функция гипервизора — ballooning, которая должна была тихо забирать неиспользуемую память у виртуалок, но забрала её у той, где такой памяти на самом деле не было. Разбираем, как мы это нашли, какие версии отбросили по пути и что теперь стоит в конфигурации памяти для виртуалок с базами данных.

Что сломалось

Виртуалка с PostgreSQL держала основную базу продакшена — 16 ГБ памяти, shared_buffers выставлен на 4 ГБ, остальное исторически отдавалось под page cache операционной системы, чтобы горячие индексы и часть таблиц жили в RAM без похода на диск. Всё работало ровно до одной ночи, когда на соседней виртуалке того же гипервизора запустился плановый бэкап с последующей архивацией — процесс, который резко наращивал потребление памяти хоста на время работы.

Через 15-20 минут после старта бэкапа приложение начало жаловаться на таймауты к базе. В Grafana это выглядело как рост p95 по запросам с обычных десятков миллисекунд до полутора-двух секунд, при этом счётчик активных соединений PostgreSQL не рос аномально, CPU виртуалки был загружен на 20-30%, а iostat на стороне гипервизора не показывал деградации по локальному диску. То есть все привычные подозреваемые — перегруз CPU, забитый диск, взрыв числа соединений — выглядели невиновными, а тормозило явно что-то на уровне памяти.

Что показывали логи и метрики

Первым делом посмотрели изнутри виртуалки:

free -m
              total        used        free      shared  buff/cache   available
Mem:          16384       14200         340         210        1844        820
Swap:          4096        1680        2416

Swap не был пустым — и это сразу настораживало, потому что на этой машине swap исторически почти не использовался. vmstat 1 показал ненулевые si/so (страницы, уходящие в своп и обратно) как раз в те минуты, когда росла задержка запросов:

procs -----------memory---------- ---swap-- -----io----
 r  b   swpd   free   buff  cache   si   so    bi    bo
 2  3  172032  348160 812000 1180000  412  380   96   140

Это уже не «база стала тяжелее» — это классическая картина, когда системе физически не хватает памяти и ядро вынуждено выгружать страницы. dmesg на этот момент был чист, OOM killer не срабатывал — просто устойчивое давление на память без явного падения.

Дальше посмотрели на сторону гипервизора. У нас Proxmox VE, и там на графике виртуалки есть отдельная линия — фактический размер памяти, выделенный балуном (balloon actual), в дополнение к настроенному максимуму. И вот здесь стало видно главное: линия «actual» на графике этой виртуалки за то же время резко просела с 16 ГБ до примерно 6 ГБ, а потом медленно ползла обратно. Через qm monitor можно посмотреть то же самое напрямую из QEMU monitor:

qm monitor 141
(qemu) info balloon
balloon: actual=6144

То есть гипервизор действительно уменьшил объём памяти, реально выделенной этой виртуалке, а не просто «подвинул» что-то внутри неё. С точки зрения guest-ОС это выглядит так, будто у машины физически исчезло 10 ГБ RAM за несколько минут — а рабочая нагрузка, которая только что комфортно умещалась в 16 ГБ, внезапно этого не помещается.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Какие версии отбросили

  • Деградация диска. Проверили smartctl -a на локальных NVMe хоста и статистику RAID-контроллера — ошибок, реаллокаций, роста задержки записи не было. iostat -x 1 на хосте тоже был спокоен в момент инцидента.
  • Сеть. Посмотрели mtr до базы и метрики сетевого интерфейса виртуалки — ни потерь пакетов, ни роста RTT. На проблему с сетью это не было похоже с самого начала: она объяснила бы рост задержек, но не рост использования swap.
  • Разгон autovacuum. Проверили pg_stat_activity и pg_stat_progress_vacuum — ни одного затянувшегося vacuum, никаких аномально долгих транзакций, которые могли бы раздувать использование памяти планировщиком запросов.
  • Взрыв числа соединений или блокировки. pg_locks и pg_stat_activity показывали обычное количество активных сессий, ожидающих блокировок не было. Пул соединений на стороне приложения тоже не менялся.
  • «Шумный сосед» по CPU. Проверили %steal в top внутри виртуалки — он был в пределах обычных единиц процентов, характерных для честного шедулинга гипервизора, никакого скачка. Это сразу исключило типичный сценарий шумного соседа по гипервизору, который обычно бьёт именно по CPU-времени, а не по памяти.

Все эти гипотезы отпадали по одной простой причине: ни одна из них не объясняла одновременный рост si/so в vmstat и падение available в free -m без видимого роста нагрузки внутри самой базы. Проблема была не «внутри» PostgreSQL, а снаружи — на уровне того, сколько физической памяти виртуалке вообще выделено.

Как нашли настоящую причину

Как только заметили просадку balloon actual на графике Proxmox, дальше было делом техники сопоставить время. Бэкап на соседней виртуалке стартовал в 02:00 по расписанию cron, а первые жалобы приложения на таймауты начали приходить примерно в 02:15-02:20 — с задержкой, за которую гипервизор успел «отжать» память у нескольких виртуалок, включая нашу с базой.

Дальше разобрались, как вообще работает баллон-драйвер. У виртуалки в настройках Proxmox было два значения памяти: максимум (16 ГБ) и «Minimum memory» — по сути нижняя граница, до которой гипервизор может стягивать баллон при нехватке общей памяти хоста. Эта виртуалка была создана из общего шаблона, где минимум по умолчанию стоял на уровне 4 ГБ — параметр, который имел смысл для веб-воркеров и очередей, но никто отдельно не пересматривал его для машины с базой данных.

Когда хост почувствовал давление от соседской виртуалки с бэкапом, планировщик ballooning в Proxmox/QEMU начал по очереди инфлировать баллоны у виртуалок с наименьшим приоритетом — по механике virtio-balloon это выглядит как запрос guest-ОС «отдай мне N страниц». Внутри guest-ядро сначала честно отдаёт то, что может: чистый page cache, буферы, неиспользуемые анонимные страницы. Но когда запрос на возврат памяти продолжает расти, а свободных «дешёвых» страниц уже не осталось, ядро вынуждено начинает вытеснять активно используемые страницы — в том числе те, что относятся к процессам PostgreSQL — через обычный механизм подкачки. То есть страница физически остаётся «занятой» с точки зрения приложения, но чтобы освободить место под баллон, ядро пишет её в swap.

Ключевая ошибка была в предположении, что у базы «наверняка есть немного свободной памяти под page cache, которую не жалко забрать». На практике под конкретную нагрузку у этой виртуалки почти вся память была занята полезной работой: shared_buffers PostgreSQL плюс горячий page cache под индексы, которые сама база активно использовала для чтения. Свободного «жира» для безопасного изъятия почти не оставалось, поэтому баллон, добираясь до нижней границы в 4 ГБ, неизбежно упирался в рабочий набор базы, а не в реально простаивающие страницы.

Почему это произошло

Если собрать причину в одно предложение: конфигурация ballooning была скопирована из общего шаблона виртуалок и не учитывала, что стейтфул-нагрузка вроде СУБД не может безопасно отдать половину своей памяти по первому требованию гипервизора. У этого есть три конкретных составляющих:

  1. Overcommit памяти на хосте без явного резерва под критичные виртуалки. Хост был сконфигурирован так, что сумма максимальных объёмов памяти виртуалок превышала физическую RAM — штатная и разумная практика для смеси нагрузок, но она предполагает, что у каждой виртуалки корректно выставлена нижняя граница «неприкосновенного» минимума. Подробнее о том, где разумный предел такого overcommit и чем он отличается для CPU и памяти, разбирали в статье про overcommit CPU и памяти.
  2. Единый шаблон для разных классов нагрузки. Минимум памяти в 4 ГБ был нормален для стейтлесс-сервисов, которые действительно держат в памяти немного «мягких» данных, но неверен для базы, чья полезная работа почти линейно зависит от объёма RAM.
  3. Мониторинг не следил за самим фактом инфляции баллона как за сигналом. Метрика balloon actual собиралась в Grafana, но алерт был настроен только на использование CPU и диска — просадку доступной памяти виртуалки никто не отслеживал как триггер тревоги, поэтому инцидент заметили по жалобам пользователей, а не по графику.

Отдельно стоит сказать про механику самого virtio-balloon: это не баг, а штатное поведение — драйвер не различает «холодный» page cache и «горячие» страницы приложения, если явно не настроена более умная политика на уровне guest-ядра. С точки зрения гипервизора любая страница, которую можно вытеснить через своп, формально «доступна» для баллона, даже если её вытеснение стоит приложению секунд задержки на каждый промах. Общий принцип того, как гипервизор забирает и возвращает память, подробно разбирали в отдельном материале про ballooning памяти — эта статья хороший фон для понимания, почему тут вообще возможна такая ситуация.

Что изменили после

Правки разделили на немедленные и системные.

Немедленно — убрали ballooning как риск для конкретно этой виртуалки. В Proxmox для машины с базой выставили «Minimum memory» равным максимуму (16 ГБ = 16 ГБ), что фактически отключает возможность гипервизора инфлировать баллон ниже полного объёма:

qm set 141 --balloon 16384

Если баллон полностью не нужен и хочется явно зафиксировать выделенную память, можно и вовсе снять галку «Ballooning Device» в настройках виртуалки — тогда гипервизор резервирует объём сразу и не трогает его динамически.

Системно пересмотрели три вещи:

  • Классификацию виртуалок. Завели два профиля памяти в шаблонах: «эластичный» (веб, воркеры, очереди — баллон разрешён, минимум 40-50% от максимума) и «фиксированный» (базы данных, брокеры сообщений, кэши с персистентностью — баллон выключен или минимум равен максимуму). Дальше это же различие поможет и на других виртуалках, где раньше просто копировали шаблон не глядя.
  • Мониторинг баллона как отдельного сигнала. Добавили алерт на резкое падение balloon actual относительно настроенного максимума и отдельный алерт на рост si/so внутри критичных гостевых ОС через node_exporter. Это тот случай, когда правильная метрика существовала и раньше, просто не была включена в контур тревог — общий разбор того, какие метрики гипервизора вообще стоит смотреть, есть в статье про мониторинг гипервизора.
  • Расписание тяжёлых фоновых задач. Бэкап с соседней виртуалки, который и запустил цепочку, перенесли на отдельный узел кластера, где нет других требовательных к памяти сервисов, и развели по времени с остальными плановыми задачами, чтобы не создавать одновременный пик спроса на память хоста.

Отдельно как защиту на будущее внутри самой гостевой ОС снизили vm.swappiness для базы:

sysctl -w vm.swappiness=1

Это не решает корневую причину — если баллон всё же заберёт память, своп всё равно случится, — но снижает охоту ядра уходить в подкачку по менее критичным поводам и оставляет своп действительно последним средством, а не первой реакцией на любое давление. Похожий сценарий, когда система уходит в своп вместо того, чтобы честно упасть с ошибкой нехватки памяти, разбирали в статье «антипаттерн: своп вместо памяти» — там больше про то, почему полагаться на своп как на «резерв» для боевой нагрузки обычно плохая идея.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Ballooning вообще стоит использовать для продакшена?

Да, для эластичных нагрузок это разумный способ повысить утилизацию хоста без постоянной переподписки на уровне резервирования — проблема не в самой функции, а в применении её без разбора к стейтфул-сервисам с плотным рабочим набором в памяти.

Как быстро понять, что причина именно в баллоне, а не в утечке памяти внутри приложения?

Сравните метрику фактически выделенной памяти виртуалки (в Proxmox — balloon actual, в libvirt — virsh dommemstat) с настроенным максимумом. Если фактический объём заметно ниже максимума именно в момент проблемы — это баллон, а не рост потребления внутри гостя.

Можно ли сочетать hugepages и ballooning?

На практике нет: при использовании hugepages для backing памяти виртуалки баллон-драйвер обычно не может динамически изменять выделение постранично, и ballooning фактически перестаёт работать. Если для базы важна предсказуемая задержка доступа к памяти, стоит выбирать между hugepages с фиксированным объёмом или обычными страницами с явно ограниченным ballooning — не пытаться совместить оба преимущества сразу.

Что делать, если overcommit по памяти на хосте необходим и от него нельзя отказаться?

Держите фиксированный минимум памяти именно у стейтфул-виртуалок (баз, брокеров, кэшей с персистентностью) и разрешайте overcommit только за счёт эластичных сервисов — тогда даже при пиковом давлении на хост гипервизор будет ужимать тех, кому это действительно не критично.

Как понять заранее, что виртуалке не хватает «жира» для безопасного ballooning?

Посмотрите на available из free -m в спокойное время — если это значение уже близко к нулю при нормальной нагрузке, у виртуалки нет резерва, который можно безопасно забрать, и минимум ballooning для неё стоит поднимать до полного объёма памяти.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →