Старый добрый Proxmox с его контейнерами и виртуалками - по-прежнему рабочая лошадка многих компаний. И если нарезать много-много мелких контейнеров, то может случиться, что память куда-то девается со временем, а контейнеры падают в OOM без очевидной причины. Причем не все. Причем иногда. И зачастую проще перезапустить и ехать дальше чем разбираться. А причина есть, и она оказалось довольно проста.
Так уж получилось, что у меня в ведении небольшая серверная ферма из пяти серверов, на которой крутится два с половиной десятка контейнеров и полтора десятка виртуалок, все вместе образующие ни много ни мало как оператора связи - с 2G и 4G сотовой связью, публичным вайфаем, проводными клиентами, биллингом, порталами и прочим обвесом. Все как у больших, просто крошечное, соразмерное острову с двадцатью тысячами населения посреди океана.
С 2018 года там используется Proxmox, сперва 6, потом 7, теперь уже 8. И все эти годы я наблюдал и без особого энтузиазма исследовал один эффект: куда-то медленно но верно утекает память. Время от времени некоторые контейнеры без видимой причины падают, одни и те же, в трудно предсказуемые моменты.
Нельзя сказать что я не искал совсем причину, но в Очень Маленькой Компании всегда задач и проблем больше, чем рук и мозгов. Поэтому решение нашлось только после апгрейда на восьмую версию, обострившего проблему.
До "восьмерки" проблема выглядела так:
На всех серверах медленно-медленно уменьшается количество свободной памяти. Процесс неспешен и неотвратим. Куда девается - не очень понятно, грешил на ZFS, и ограничение ее аппетитов действительно помогло, но количественно, а не качественно.
Избранные контейнеры иногда падают, просто падают и все. Никаких следов проблем в мониторинге, на момент падения память свободная есть и ее немало. Мистика!
Но поскольку всяко уж раз в несколько месяцев обновление серверов делается и они перегружаются - то жить это сильно не мешало. Это все же маленький, но оператор связи, все критическое зарезервировано вдвое и втрое, поэтому проще пнуть остановившийся контейнер руками, благо мониторинг на месте.
Начиная с "восьмерки" ситуация поменялась:
На самих серверах исчезла "утечка", да и вообще суммарное потребление памяти внезапно сильно уменьшилось.
Зато мониторинг контейнеров начал показывать непрерывное снижение available вплоть до нуля, ну и в итоге OOM и падение. Причем коснулось оно большего числа контейнеров, чем раньше.
А вот скорость этой потери на разных контейнерах очень разная - от почти нулевой до весьма впечатляющей.
И вот это уже что-то с чем можно работать.
Гипотеза об утечке памяти в приложениях была и раньше, и еще раз опровергнута. Используются в основном весьма "старые" приложения, устаканившиеся, без детских болезней. Идея, что freeradius или kannel могут "потечь" на смешных нагрузках - она сама по себе странная. Поэтому все пляски с top/htop оказались безрезультатны.
Тем временем стало понятно, что вся память уходит в shared. Который растет, растет, растет - пока не сожрет все свободное. И... пропустив нудный процесс дознания с пристрастием прямо переходим к обвинительному заключению, логика утечки оказалась весьма забавной:
LXC, как все вы конечно знаете, это обычный образ linux rootfs без ядра, запущенный в chroot, с отдельными неймспейсами и ограниченный по ресурсам с помощью cgroups. Соответственно и размер памяти, что мы задаем контейнеру - это просто лимит в cgroups.
А внутре у ней
неонкаобычный systemd. Который начинает с того, что монтирует tmpfs в/run
. Причем делает это с размером по умолчанию.А tmpfs, как все вы конечно знаете, размещается в памяти. И его размер по умолчанию, если не указан явно, равен половине размера физической памяти.
Ядро при создании tmpfs с размером по умолчанию не принимает во внимание никакие ограничения cgroups. И потому нарежет размер в половину физической памяти.
Таким вот образом мы получаем tmpfs размером 63GB смонтированный в
/run
. Но при этом все, что будет туда записано - должно все же помещаться в пределы, определенные в cgroups, и да, командойfree
оно показывается именно как shared.journald, как все вы конечно знаете, использует
/run/log/journal
сперва чтобы писать лог во время загрузки, а вот дальше все интереснее. По умолчанию стоит опцияStorage=auto
, что означает следующее: если каталог/var/log/journal
присутствует на диске, то пишем в него, а если нет - то продолжаем писать в/run/log/journal
.journald совсем не хочет делать нам проблемы, поэтому по умолчанию устанавливает лимит на использование
/run
в 10% от размера файловой системы. Т.е. в моем случае это примерно 6.3GB. При том что на контейнер выделяется от 1 до 2 GB, там очень экономные приложения живут.Собственно, здесь и встречаются несколько факторов, которые в зависимости от конкретного образа контейнера могут выстрелить или не выстрелить. И в худшем (моем) случае journald пишет в
/run
(читай - в память), будучи уверенным, что писать можно аж до 6GB, пока не упирается в лимиты cgroups (1..2GB), после чего OOM киллер расстреливает всех по очереди до самого systemd включительно. Приехали!
Просто звезды дефолты сошлись в неудачное сочетание, всего-то!
Когда причина найдена, возникает вопрос: почему это не было видно в мониторинге состояния контейнеров до апгрейда на восьмерку? К счастью, остался и один старый хост, проверив на котором выяснил следующее:
Команда free внутри LXC в PVE7 показывает available memory без учета shared. И по этому по мере заполнения tmpfs она не убывает. Но очевидно уходит память на самих физических серверах. И к тому же похоже что лимиты cgrouop срабатывают не всегда. Не знаю почему так и не хочу разбираться.
А вот в PVE8 available показывает с учетом, и она постепенно стремится к нулю.
Ну и когда проблема ясна, становится ясно и как с этим бороться, вариантов масса:
Можно перемонтировать
/run
на сообразный размер путем добавления в/etc/fstab
чего-то вродеnone /run none remount,size=128M 0 0
. Но при этом надо понимать, что это не единственный tmpfs в системе, и все они будут огромного размера. И непонятно что делать с остальными. И надо ли.Можно не забыть создать каталог
/var/log/journal
, и тогда journald не будет выжигать память даже при дефолтных настройках-
Можно настроить journald через опции в
/etc/systemd/journald.conf
:Storage=persistent
, и тогда tmpfs будет использован только при загрузке, а затем все будет сброшено на диск, необходимые каталоги он создаст самRuntimeMaxUse=128M
, и тем ограничить предел использования tmpfs
А можно все перечисленное сразу, это уже дело вкуса.
На этом для меня многолетняя "сказка о потерянной памяти" закончилась, все ровно и стабильно, ничего никуда не девается. А вся история в графике мониторинга выглядит примерно вот так:

Ну вот и хорошо, можно заняться и другими проблемами, которых в любом хозяйстве - не перерешать...
Комментарии (13)
Thomas_Hanniball
18.02.2025 11:14все вместе образующие ни много ни мало как оператора связи - с 2G и 4G сотовой связью, публичным вайфаем, проводными клиентами, биллингом, порталами и прочим обвесом.
А как называется оператор связи? Он самостоятельный или виртуальный оператор связи (MVNO)? Мелкие игроки вряд ли могут потянуть покупку лицензий на 4G.
pavlyuts Автор
18.02.2025 11:14Полноценный оператор с вышками, базовыми станциями, антеннами, интерконнектом и к тому же член GSMA.
Это не в России, а на далеких тропических островах, Палау называется. Там вся местная администрация отрасли связи - это огромной ширины мужчина, которому эту функцию вручили помимо всяких других обязанностей ))))) Он и конкурсы проводит, и разрешения частотные выдает, и в ITU числится главным одним за все )))))
Если представите себе поселок городского типа на 20 тыс населения вокруг которого на примерно тысячу километров - Тихий океан, то сразу придет понимание о простоте тамошних нравов )))))
Ну а так-то полноценное государство, даже член ООН.
Thomas_Hanniball
18.02.2025 11:14Это не в России, а на далеких тропических островах, Палау называется.
Понятно. Просто у вас в профиле указано "Москва, Москва и Московская обл., Россия", поэтому я удивился, что есть какой-то мелкий сотовый оператор с 4G, особенно учитывая, какое там "рубилово" было при распределении частот для 4G в России.
pavlyuts Автор
18.02.2025 11:14Времена идут, отношения остаются. Мир нынче штука глобальная невзирая на нюансы географии и момента.
Про рубилово по частотам - ну, такое. Какое рубилово, а? Всем кому нужно дать - дали. Пропорционально весу (не спрашивайте!). А про 30 МГц "цифрового дивиденда", поделенного на четыре мне потом пришлось с легким стыдом объяснять зарубежным коллегам, интересующимся что это вообще за хрень была, - что иногда вот надо поделить 30 именно на четыре. Потому что надо. Надо и все тут. Что характерно - они поняли.
Мемуары про лютое веселье на т.н. консультациях с операторами, которые демократично проводило по этому Минсвязи я было начал писать - а потом забил. Ибо не только лишь все способны оценить юмор этой ситуации. Мне и Сорокиной (Теле2) было весело, остальные делали сложные лица )))))))
turbidit
18.02.2025 11:14Это относится к контейнерам запущенным из официальных шаблонов Proxmox?
pavlyuts Автор
18.02.2025 11:14Хороший вопрос! Потому что да, у меня там весьма нестандартное для проксмокса крутится.
В части создания tmpfs с размером в половину физической памяти - да. Более того, это будет относиться и к LXC вне Proxmox. Потому что тупо нет разумного способа это предотвратить. Тикет в багзилле проксмокса в итоге был подвешен с формулировкой "может че-то со временем изменится или кто-то предложит хорошее решение"
В части как через это systemd выжирает память - надо смотреть. Если конфигурация journald в дефолте (т.е. все опции закомментированы), и нет каталога
/var/log/journal
, то да. Если journald сконфигурирован не писать бесконечно в /run - то нет.Проверять все, честно говоря, нет ни времени ни желания, извините. В любом случае зарубка на память не повредит. И мониторинг! ;)
AVX
а вот я бы с удовольствием почитал именно про этот нудный процесс, это ничуть не менее интересно готовых результатов расследования.
Вообще, если там systemd, и он рулит tmpfs (для этого сервис отдельный вроде бы есть), то очень похоже на баг в этом самом systemd.
pavlyuts Автор
Это я так замылил тот факт, что буквально случайно проверяя свободные диски командой df я зацепился взглядом за размер /run и нехилый такой объем занятого пространства на нем. Ну а описывать всю размотку с копанием док, постами на форуме проксмокса и заведением бага в их багзиллу - такое себе удовольствие что писать что читать. А там уже Фабиан из Проксмокса мне немного мозги вставил и осталось докопать детали.
systemd рулит всеми монтированиями, да, но! Все что связано с ApiVFS - /proc, /dev, /sys, /run - оно не в systemd-mount, а на самой ранней стадии старта делается и захардкожено. Потому что без /run он вообще не может работать.
А дальше дело в том, что захардкожено примерно mount -t tmpfs none /run и там нет опции size (можно посмотреть в исходниках systemd), что логично. А кернел ничего не знает про cgroup вызывающего mount процесса и отдает дефолт на полпамяти.
Это не баг, это behaviour by desighn для каждой из частей, совершенно корректное по отдельности, просто именно в случае LXC с определенной комбинацией параметров это вызывает вот такие побочки.
Кароч, просто надо держать в голове что такая шляпа возможна. И можно дунуть в любой по дефолту созданный tmpfs больше, чем ограничение cgroup, что моментально вызовет смерть контейнера по OOM.
AVX
Кажется, что если уж от захардкоженного монтирования не обойтись, то надо в каком-то сервисе перемонтировать с size, с учетом ограничений, пока оно ещë не заполнено чем-то.
pavlyuts Автор
Именно это и произойдет если добавить remount в /etc/fstab как я описал выше. В этом случае systemd-mount перемонтирует указанную точку с указанным размером.