OOM Killer — защитный механизм ядра Linux, призванный решать проблемы с нехваткой памяти. При исчерпании доступной памяти он принудительно «убивает» наиболее подходящий по приоритетам процесс, отправляя ему сигнал KILL. Сообщение об этом отображается в /var/log/syslog (Debian/Ubuntu) или /var/log/messages (Centos/Rhel).

Иногда OOM Killer может затрагивать важные процессы, нарушая работу проекта. Как исправить это, узнали у Сергея Юдина, инженера Southbridge. Ниже подробный кейс с примерами кода. 

Проблема

К нам пришёл клиент с проблемой нехватки памяти на сервере, где много разных сервисов: Nginx, PHP-FPM, Redis и MongoDB. 

PHP-FPM, обслуживая запросы, плодил дочерние процессы — столько, сколько ему разрешалось, т.е. был лимитирован. Лимиты на количество дочерних процессов рассчитывались из того, что скрипты php используют определенный размер оперативной памяти. Это возможная ошибка при настройке PHP-FPM, так как разработчики постоянно требуют всё больше и больше оперативной памяти.

В один прекрасный момент прилетела довольно высокая нагрузка, и память оказалась исчерпана. А дальше OOM Killer, как это часто бывает, нашёл самый «плохой», по его мнению, процесс — MongoDB. И убил его. 

Запрос клиента — сделать так, чтобы OOM Killer не убивал MongoDB.

Что сделали сначала

Самый «простой» способ решить проблему — посчитать количество нужной оперативной памяти для всех служб и процессов на сервере и, определив нужный размер для PHP-FPM, выставить ему лимиты: 

pm.max_children если у вас pm = static

Но так как скрипты достаточно часто меняются, такой расчёт в ближайшем будущем станет ошибочным и приведёт к повторной аварии.

Мы установили приоритет для процесса MongoDB, используя параметры systemd unit:

OOMScoreAdjust=-1000

Прошла неделя и появилась новая проблема — в момент увеличения нагрузки OOM Killer убил Redis для высвобождения оперативной памяти. Мы сообщили об этом клиенту, и он попросил для Redis тоже сделать OOMScoreAdjust=-1000.

Мы всё установили, но спустя ещё неделю пришло уведомление, что произошла авария, и сервер недоступен. Попытались разобраться, в чём причина, но подключиться к серверу не удалось. Других вариантов не было, и мы попросили клиента перезагрузить сервер. После перезагрузки посмотрели по логам и обнаружили, что из-за нехватки оперативной памяти OOM Killer начал искать претендента для убийства. MongoDB, которая занимала 50% оперативной памяти, убить нельзя, потому что она не входит в приоритет. Redis тоже. В итоге OOM Killer начал убивать дочерние процессы PHP FPM, причём выглядело это так: OOM Killer убивал один процесс, и PHP FPM тут же порождал ещё два. Операционная система не могла с этим справиться.  

Как правило, в документациях многих приложений рекомендуют отключить swap, что и было сделано на этом проекте. Возможно, если бы swap был, то подключиться к серверу получилось без перезагрузки. Но о swap чуть позже.

Мы предлагали ограничить количество дочерних процессов для PHP-FPM ещё до того, как лимитировали MongoDB. Но клиент не хотел делать это по двум причинам:

  • Увеличение времени ожидания. Ограничение дочерних процессов PHP FPM решило бы проблему нехватки оперативной памяти, но тогда пришло бы слишком много запросов. Разберём на примере: пришло 200 запросов, а у нас всего 100 процессов, которые могут обрабатывать данные. Первые запросы обработаются быстро, а следующие будут в очереди, из-за чего timeout возрастёт.

  • Непостоянное количество оперативной памяти, которую могут запрашивать скрипты. Один и тот же скрипт сегодня может попросить 200 Мб, завтра — 500 Мб. А после того, как разработчик внесет в него какие-то изменения, — и 1000 Мб. Этот лимит достаточно сложно вычислить, когда у тебя много процессов. 

Как решали проблему дальше

Мы пришли к тому, что все процессы, которые есть на сервере, нужно ограничивать физически. Распределили оперативную память на все службы, оговорили, кто и сколько должен поедать, и для каждой службы установили два лимита. Ещё подключили swap, при этом запретив MongoDB и Redis использовать его. 

Почему приняли решение устанавливать два вида лимитов: MongoDB не может жёстко устанавливать количество оперативной памяти, используемой для своей службы. Только устанавливать лимиты для cash size — максимальный размер внутреннего кэша. Поэтому мы установили лимит для движка WiredTider cash size в 50 Гб. И дополнительно установили жёсткий лимит через systemd unit в 80 Гб. Если MongoDB превысит 80 Гб, придёт OOM Killer и точно её убьет.

wiredTiger:
  engineConfig:
    cacheSizeGB: 50

Где споткнулись

Выставление лимита storage.wiredTiger.engineConfig.cacheSizeGB

не обязывает MongoDB не использовать свободную оперативную память и кэш файловой системы.

Вопрос, как Linux работает с памятью, достаточно сложный. Если интересно, мы можем раскрыть его в следующей статье:) А пока оговоримся примерно так:

Все данные, которые процессы желают записать на диски, в Linux сначала записываются в оперативную память, а уже потом записываются на диски.

Почему прошлый раз упал сервер

  • По всем показателям MongoDB использовала то количество оперативной памяти, которое мы выдали. И по нашим расчётам должно было остаться ещё около 40 Гб оперативной памяти, но по факту их не было — память была закэширована. Сначала MongoDB читала и писала в оперативную память, потом скидывала всё на диск. Но когда произошла авария, MongoDB не успела всё сбросить на диск из своего кэша. И не всегда память, которая используется в Linux для записи на диск, высвобождается моментально — это происходит по мере высвобождения ресурса. 

  • Был отключен swap. Если бы swap работал, система бы очень долго откликалась, но всё же была доступна. Мы смогли бы подключиться в ssh и оперативно что-то сделать. Но для этого нужно было сделать так, чтобы MongoDB не использовала swap. 

Мы поняли, что для MongoDB, Redis и PHP-FPM нужно установить «не использовать swap». Чтобы это сделать нужно установить определенное количество оперативной памяти для данных служб в cgroups. Мы задали это в systemd unit для MongoDB, Redis и PHP-FPM.  В версиях systemd имеются различия установки тех или иных параметров, мы сделали так:

Для MongoDB:

[Service]
MemoryLimit=80G
ExecStartPre=/bin/bash -c "echo 80G > /sys/fs/cgroup/memory/system.slice/mongod.service/memory.memsw.limit_in_bytes"
ExecStartPre=/bin/bash -c "echo 0 > /sys/fs/cgroup/memory/system.slice/mongod.service/memory.swappiness"

Для PHP-FPM:

[Service] 
OOMScoreAdjust=1000
Restart=always
ExecStartPost=/bin/bash -c "echo 20G > /sys/fs/cgroup/memory/system.slice/php-fpm.service/memory.memsw.limit_in_bytes"
ExecStartPost=/bin/bash -c "echo 0 > /sys/fs/cgroup/memory/system.slice/php-fpm.service/memory.swappiness"
MemoryLimit=20G

Для Redis:

[Service]
MemoryLimit=3G
PermissionsStartOnly=true
ExecStartPost=/bin/bash -c "echo 3G > /sys/fs/cgroup/memory/system.slice/redis.service/memory.memsw.limit_in_bytes"
ExecStartPost=/bin/bash -c "echo 0 > /sys/fs/cgroup/memory/system.slice/redis.service/memory.swappiness"

Каких результатов добились

Мы установили лимит в 20 Гб оперативной памяти для PHP-FPM и настроили systemd.unit так, что если процессы, порожденные PHP-FPM, занимают больше 20 Гб памяти, то PHP-FPM и все его дочерние процессы будут убиты, а после перезапущены. В течение нескольких месяцев PHP-FPM убивался и стартовал. Мы мониторили через Zabbix, когда происходят аварии, почему и сколько секунд недоступен какой-то сайт, делали выводы и сообщали клиенту. Спустя время лимиты для сервисов остались прежними, но позволили понять, в какой момент, что и где идёт не так. Мониторинг уведомлял нас, мы сообщали клиенту, клиент шёл и разбирался, в чём проблема. 

Через несколько месяцев проблема была решена — скрипты, которые, пожирали много памяти были устранены и оптимизированы клиентом.  

Немного рассуждений напоследок: всё это «костыли»

Всякий раз, когда мы ограничивали MongoDB или Redis, мы говорили клиенту, что так делать не стоит. Нужно искать причину и решать основную проблему, а не лечить последствия. 

То, что мы сделали, — это не best practices. Это просто кейс, основанный на работе с определенным клиентом. Теоретически всё можно было бы сделать по-другому. В этом и плюс, и минус Linux — у него много вариантов решения одной проблемы. Как понять, какой из них наиболее правильный? Мне кажется, тут всё субъективно: оцениваешь, исходя из текущих знаний и опыта. На момент решения кейса, у меня было меньше опыта. Сейчас я бы, наверное, поступил по-другому. 

Для тех, кто хочет углубить знания в Linux и изучить best practices

28 июля стартует продвинутый курс «Администрирование Linux Мега» с практикой и траблшутингом от инженера Southbridge Платона Платонова. Даже в базовых темах мы будем разбирать best practices и смотреть в глубину работы с Linux. Вы узнаете про установку Linux с помощью чёрной магии, приёмы ускорения работы в консоли, создание и применение bash-скриптов и многое другое.

Курс в целом по Linux, а не по конкретному дистрибутиву. Он поможет углубить ваши знания в работе с ОС. Всё что мы разберём во время обучения, вы сможете сразу применять на практике.

Посмотреть программу и записаться: https://slurm.club/3R8ACXv

Комментарии (11)


  1. 13werwolf13
    04.07.2022 14:49
    +4

    чёт помоему у systemd есть более удобный способ нарезать слайсы с ограничением по ресурсам..


    1. x1shn1k
      04.07.2022 22:17
      +1

      Какой?


      1. event1
        05.07.2022 14:58

        Тут пишут, что MemoryLimit как раз и контролирует limit_in_bytes. Но, мопед не мой...


  1. n_bogdanov
    05.07.2022 00:32

    В php-fpm есть замечательный access log, в котором можно включить отображение использованной памяти. Это на будущее, чтобы месяцами не искать скрипты с высоким запросом памяти.

    А если лог сделать в json формате и выводить в loki - то время решения задачи наверное сократилось бы до дней.


  1. rhangelxs
    05.07.2022 02:09

    Пытаются подружиться с OOM, счастливые))

    А в мире Kubernetes пытаются понять как с ним (OOMKiller) выживать. Хотя что докер, что кубер хорошо позволяют «нарезать» ресурсы, но в контейнерах нет нормальной работы со swap.

    При таком типичном профиле нагрузки в Linux не стоит отключать swap, тогда и проблемы будут другие. Не в плане доступности, а в плане задержек и скорости…


  1. edo1h
    05.07.2022 05:36

    Ещё подключили swap, при этом запретив MongoDB и Redis использовать его.

    Мы поняли, что для MongoDB, Redis и PHP-FPM нужно установить «не использовать swap»

    a. какой смысл включать своп, если всем потенциальным потребителям вы запрещаете свопиться?
    b. как именно вы поняли, что для этих приложений нужно запретить использование свопа? притом php-fpm не было в первоначальном списке, почему решили его внести? ИМХО лучше бы пусть php-fpm свопился, чем падал.


    1. ZimniY
      05.07.2022 12:30

      a) Сама система сможет работать и останется доступна.


      1. edo1h
        05.07.2022 12:34

        за счёт чего? что свопиться-то будет? всем активным потребителям памяти запретили использовать своп.


        1. ZimniY
          05.07.2022 15:27

          Вот кроме них никто своп не использует. Ну и swappiness мы знаем, да


  1. shalamberidze
    05.07.2022 09:56

    Ну да ну да. А потом оракловский админ забудет выставить huge_pages и 20% памяти уйдет в никуда хотя все лимиты будут в порядке


  1. wolfer
    05.07.2022 14:13
    +2

    не знаю ландшафта, но судя по тому, что монге разрешалось жрать 50Гб с потолком в 80, в ресурсах заказчик не стеснен. Почему бы не вынести монгу на отдельный сервер, можно вместе с редисом, а можно и вообще отдельно и забыть о проблемах с дракой за ресурсы? Тем более что судя по всему речь о достаточно важном проекте, если вы секунды недоступности меряли, а значит пора думать о резервировании на случай отказов, а тут без БД-кластера никак