Старый добрый Proxmox с его контейнерами и виртуалками - по-прежнему рабочая лошадка многих компаний. И если нарезать много-много мелких контейнеров, то может случиться, что память куда-то девается со временем, а контейнеры падают в OOM без очевидной причины. Причем не все. Причем иногда. И зачастую проще перезапустить и ехать дальше чем разбираться. А причина есть, и она оказалось довольно проста.

Так уж получилось, что у меня в ведении небольшая серверная ферма из пяти серверов, на которой крутится два с половиной десятка контейнеров и полтора десятка виртуалок, все вместе образующие ни много ни мало как оператора связи - с 2G и 4G сотовой связью, публичным вайфаем, проводными клиентами, биллингом, порталами и прочим обвесом. Все как у больших, просто крошечное, соразмерное острову с двадцатью тысячами населения посреди океана.

С 2018 года там используется Proxmox, сперва 6, потом 7, теперь уже 8. И все эти годы я наблюдал и без особого энтузиазма исследовал один эффект: куда-то медленно но верно утекает память. Время от времени некоторые контейнеры без видимой причины падают, одни и те же, в трудно предсказуемые моменты.

Нельзя сказать что я не искал совсем причину, но в Очень Маленькой Компании всегда задач и проблем больше, чем рук и мозгов. Поэтому решение нашлось только после апгрейда на восьмую версию, обострившего проблему.

До "восьмерки" проблема выглядела так:

  • На всех серверах медленно-медленно уменьшается количество свободной памяти. Процесс неспешен и неотвратим. Куда девается - не очень понятно, грешил на ZFS, и ограничение ее аппетитов действительно помогло, но количественно, а не качественно.

  • Избранные контейнеры иногда падают, просто падают и все. Никаких следов проблем в мониторинге, на момент падения память свободная есть и ее немало. Мистика!

Но поскольку всяко уж раз в несколько месяцев обновление серверов делается и они перегружаются - то жить это сильно не мешало. Это все же маленький, но оператор связи, все критическое зарезервировано вдвое и втрое, поэтому проще пнуть остановившийся контейнер руками, благо мониторинг на месте.

Начиная с "восьмерки" ситуация поменялась:

  • На самих серверах исчезла "утечка", да и вообще суммарное потребление памяти внезапно сильно уменьшилось.

  • Зато мониторинг контейнеров начал показывать непрерывное снижение available вплоть до нуля, ну и в итоге OOM и падение. Причем коснулось оно большего числа контейнеров, чем раньше.

  • А вот скорость этой потери на разных контейнерах очень разная - от почти нулевой до весьма впечатляющей.

И вот это уже что-то с чем можно работать.

Гипотеза об утечке памяти в приложениях была и раньше, и еще раз опровергнута. Используются в основном весьма "старые" приложения, устаканившиеся, без детских болезней. Идея, что freeradius или kannel могут "потечь" на смешных нагрузках - она сама по себе странная. Поэтому все пляски с top/htop оказались безрезультатны.

Тем временем стало понятно, что вся память уходит в shared. Который растет, растет, растет - пока не сожрет все свободное. И... пропустив нудный процесс дознания с пристрастием прямо переходим к обвинительному заключению, логика утечки оказалась весьма забавной:

  1. LXC, как все вы конечно знаете, это обычный образ linux rootfs без ядра, запущенный в chroot, с отдельными неймспейсами и ограниченный по ресурсам с помощью cgroups. Соответственно и размер памяти, что мы задаем контейнеру - это просто лимит в cgroups.

  2. А внутре у ней неонка обычный systemd. Который начинает с того, что монтирует tmpfs в /run. Причем делает это с размером по умолчанию.

  3. А tmpfs, как все вы конечно знаете, размещается в памяти. И его размер по умолчанию, если не указан явно, равен половине размера физической памяти.

  4. Ядро при создании tmpfs с размером по умолчанию не принимает во внимание никакие ограничения cgroups. И потому нарежет размер в половину физической памяти.

  5. Таким вот образом мы получаем tmpfs размером 63GB смонтированный в /run. Но при этом все, что будет туда записано - должно все же помещаться в пределы, определенные в cgroups, и да, командой free оно показывается именно как shared.

  6. journald, как все вы конечно знаете, использует /run/log/journal сперва чтобы писать лог во время загрузки, а вот дальше все интереснее. По умолчанию стоит опция Storage=auto, что означает следующее: если каталог /var/log/journal присутствует на диске, то пишем в него, а если нет - то продолжаем писать в /run/log/journal.

  7. journald совсем не хочет делать нам проблемы, поэтому по умолчанию устанавливает лимит на использование /run в 10% от размера файловой системы. Т.е. в моем случае это примерно 6.3GB. При том что на контейнер выделяется от 1 до 2 GB, там очень экономные приложения живут.

  8. Собственно, здесь и встречаются несколько факторов, которые в зависимости от конкретного образа контейнера могут выстрелить или не выстрелить. И в худшем (моем) случае 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)


  1. AVX
    18.02.2025 11:14

    И... пропустив нудный процесс дознания с пристрастием

    а вот я бы с удовольствием почитал именно про этот нудный процесс, это ничуть не менее интересно готовых результатов расследования.

    Вообще, если там systemd, и он рулит tmpfs (для этого сервис отдельный вроде бы есть), то очень похоже на баг в этом самом systemd.


    1. pavlyuts Автор
      18.02.2025 11:14

      Это я так замылил тот факт, что буквально случайно проверяя свободные диски командой 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.


      1. AVX
        18.02.2025 11:14

        Кажется, что если уж от захардкоженного монтирования не обойтись, то надо в каком-то сервисе перемонтировать с size, с учетом ограничений, пока оно ещë не заполнено чем-то.


        1. pavlyuts Автор
          18.02.2025 11:14

          Именно это и произойдет если добавить remount в /etc/fstab как я описал выше. В этом случае systemd-mount перемонтирует указанную точку с указанным размером.


  1. Thomas_Hanniball
    18.02.2025 11:14

    все вместе образующие ни много ни мало как оператора связи - с 2G и 4G сотовой связью, публичным вайфаем, проводными клиентами, биллингом, порталами и прочим обвесом.

    А как называется оператор связи? Он самостоятельный или виртуальный оператор связи (MVNO)? Мелкие игроки вряд ли могут потянуть покупку лицензий на 4G. 


    1. pavlyuts Автор
      18.02.2025 11:14

      Полноценный оператор с вышками, базовыми станциями, антеннами, интерконнектом и к тому же член GSMA.

      Это не в России, а на далеких тропических островах, Палау называется. Там вся местная администрация отрасли связи - это огромной ширины мужчина, которому эту функцию вручили помимо всяких других обязанностей ))))) Он и конкурсы проводит, и разрешения частотные выдает, и в ITU числится главным одним за все )))))

      Если представите себе поселок городского типа на 20 тыс населения вокруг которого на примерно тысячу километров - Тихий океан, то сразу придет понимание о простоте тамошних нравов )))))

      Ну а так-то полноценное государство, даже член ООН.


      1. Thomas_Hanniball
        18.02.2025 11:14

        Это не в России, а на далеких тропических островах, Палау называется.

        Понятно. Просто у вас в профиле указано "Москва, Москва и Московская обл., Россия", поэтому я удивился, что есть какой-то мелкий сотовый оператор с 4G, особенно учитывая, какое там "рубилово" было при распределении частот для 4G в России.


        1. pavlyuts Автор
          18.02.2025 11:14

          Времена идут, отношения остаются. Мир нынче штука глобальная невзирая на нюансы географии и момента.

          Про рубилово по частотам - ну, такое. Какое рубилово, а? Всем кому нужно дать - дали. Пропорционально весу (не спрашивайте!). А про 30 МГц "цифрового дивиденда", поделенного на четыре мне потом пришлось с легким стыдом объяснять зарубежным коллегам, интересующимся что это вообще за хрень была, - что иногда вот надо поделить 30 именно на четыре. Потому что надо. Надо и все тут. Что характерно - они поняли.

          Мемуары про лютое веселье на т.н. консультациях с операторами, которые демократично проводило по этому Минсвязи я было начал писать - а потом забил. Ибо не только лишь все способны оценить юмор этой ситуации. Мне и Сорокиной (Теле2) было весело, остальные делали сложные лица )))))))


  1. ant_perch
    18.02.2025 11:14

    Интересный пост. Наконец-то раскрыта тайна почему контейнеры в LXC падали.


  1. turbidit
    18.02.2025 11:14

    Это относится к контейнерам запущенным из официальных шаблонов Proxmox?


    1. pavlyuts Автор
      18.02.2025 11:14

      Хороший вопрос! Потому что да, у меня там весьма нестандартное для проксмокса крутится.

      В части создания tmpfs с размером в половину физической памяти - да. Более того, это будет относиться и к LXC вне Proxmox. Потому что тупо нет разумного способа это предотвратить. Тикет в багзилле проксмокса в итоге был подвешен с формулировкой "может че-то со временем изменится или кто-то предложит хорошее решение"

      В части как через это systemd выжирает память - надо смотреть. Если конфигурация journald в дефолте (т.е. все опции закомментированы), и нет каталога /var/log/journal, то да. Если journald сконфигурирован не писать бесконечно в /run - то нет.

      Проверять все, честно говоря, нет ни времени ни желания, извините. В любом случае зарубка на память не повредит. И мониторинг! ;)


  1. DeathRAMCC
    18.02.2025 11:14

    Отличный слог. Спасибо.


    1. pavlyuts Автор
      18.02.2025 11:14

      Спасибо! Хотя бывало и лучше ;)