Мы используем куки и предполагаем, что вы согласны, если продолжите пользоваться сайтом
Понимаю и согласен
Блог Southbridge

Как подружиться с OOM Killer

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 — у него много вариантов решения одной проблемы. Как понять, какой из них наиболее правильный? Мне кажется, тут всё субъективно: оцениваешь, исходя из текущих знаний и опыта. На момент решения кейса, у меня было меньше опыта. Сейчас я бы, наверное, поступил по-другому.