Одно из моих приложений (это скрипт, который автоматизирует некоторые действия на некотором сайте, имитируя действия человека, использует Puppeteer для управления браузером, сохраняет сессию через cookies и работает в фоновом режиме (headless)) падал по OOM-killer. Скрипт работает на сервере с 1 CPU и 1 Gb RAM со всеми вытекающими.
...
Apr 09 10:49:54 v2542187.example.ru systemd[1]: myapp.service: Failed with result 'oom-kill'.
Apr 09 10:49:54 v2542187.example.ru systemd[1]: myapp.service: Consumed 15min 14.743s CPU time, 796.7M memory peak.
Apr 09 10:50:04 v2542187.example.ru systemd[1]: myapp.service: Scheduled restart job, restart counter is at 101.
...
Судя по логам, система убила сервис myapp из-за нехватки памяти (OOM Kill). Причина — кто-то сожрал всю доступную оперативную память.
Failed with result 'oom-kill' — ядро принудительно завершило процесс.
restart counter is at 101 — сервис постоянно перезапускается, но снова падает по OOM.
Процесс успел потребить ~800 MB памяти (796.7M memory peak) и 15 минут CPU.
Для начала можно вспомнить, «Как работает OOM Killer«.
Что здесь можно сделать, как точно узнать причину OOM:
- Посмотреть системный лог OOM Killer
sudo dmesg | grep -i "out of memory" -A 50
или
sudo grep -i "killed process" /var/log/kern.log
В выводе увидите, какой процесс и сколько памяти запрашивал, какая была нагрузка на систему.
- Посмотреть логи самого приложения перед смертью
sudo journalctl -u myapp --since "10:45:00" --until "10:50:00"
Ищите ошибки типа FATAL, heap out of memory, JavaScript heap out of memory.
- Мониторить память в реальном времени (при следующем запуске)
sudo systemctl stop myapp
sudo systemctl start myapp
watch -n 1 'ps aux --sort=-%mem | head -20'
Выполняю
dmesg | grep -i "out of memory" -A 30
и вижу множество примерно таких записей:
[6602255.399645] Out of memory: Killed process 1235755 (chrome) total-vm:1459804800kB, anon-rss:219936kB, file-rss:152kB, shmem-rss:0kB, UID:0 pgtables:2136kB oom_score_adj:300 [6603500.141304] unix_chkpwd invoked oom-killer: gfp_mask=0x140dca(GFP_HIGHUSER_MOVABLE|GFP_COMP|GFP_ZERO), order=0, oom_score_adj=0 [6603500.141332] CPU: 0 UID: 0 PID: 1237965 Comm: unix_chkpwd Not tainted 6.12.0-63.el10.x86_64 #1 [6603500.141336] Hardware name: Red Hat KVM, BIOS 1.11.0-2.el7 04/01/2014 [6603500.141338] Call Trace: [6603500.141342] [6603500.141347] dump_stack_lvl+0x4e/0x70 [6603500.141356] dump_header+0x44/0x190 [6603500.141362] oom_kill_process.cold+0x8/0x88 [6603500.141365] out_of_memory+0xf3/0x290 [6603500.141371] alloc_pages_slowpath.constprop.0+0x594/0xb70 [6603500.141377] __alloc_pages_noprof+0x31b/0x330 [6603500.141381] alloc_pages_mpol_noprof+0xd7/0x1c0 [6603500.141384] ? count_memcg_events+0x87/0x120 [6603500.141389] folio_alloc_mpol_noprof+0x14/0x30 [6603500.141391] vma_alloc_folio_noprof+0x69/0xb0 [6603500.141393] ? __mod_memcg_lruvec_state+0xe2/0x190 [6603500.141396] alloc_anon_folio+0x1a2/0x3a0 [6603500.141401] do_anonymous_page+0x64/0x5f0 [6603500.141404] __handle_mm_fault+0x30f/0x740 [6603500.141409] handle_mm_fault+0x109/0x350 [6603500.141412] do_user_addr_fault+0x20c/0x640 [6603500.141419] exc_page_fault+0x73/0x160 [6603500.141424] asm_exc_page_fault+0x26/0x30 [6603500.141428] RIP: 0033:0x7fb3fb7ccfda [6603500.141449] Code: Unable to access opcode bytes at 0x7fb3fb7ccfb0. [6603500.141450] RSP: 002b:00007ffcd0e37070 EFLAGS: 00010206 [6603500.141453] RAX: 00000000000004e0 RBX: 00007fb3fb5a607c RCX: 0000000000000e60 [6603500.141454] RDX: 00000ff000000ff0 RSI: 00007fb3fb5a5000 RDI: 00007fb3fb5a7000 [6603500.141456] RBP: 00007fb3fadfd000 R08: 00007fb3fb5a5000 R09: 00007fb3fa852000 [6603500.141457] R10: 00007fb3fadfc000 R11: 0000000000000100 R12: 00007fb3fb5a7000
Теперь понятно, что виновник — лавина процессов chrome. OOM-killer убивает не сервис, а процессы Chrome. Каждая запись Killed process ... (chrome) — это прибитый процесс.
myapp.service получает oom-kill не первым, а когда система уже в критическом состоянии. В логе systemd видно Failed with result 'oom-kill', но настоящая причина в том, что системе не хватает памяти из-за Chrome.
Chrome-процессы потребляют ~1.46 TB виртуальной памяти (total-vm:1459792072kB — это 1.4 ТБ!) и ~200-250 MB физической RAM каждый. И их десятки.
Мой Node.js сервис (myapp) запускает Headless Chrome через Puppeteer и не контролирует их количество. Каждый экземпляр Chrome gотребляет ~200 MB RAM, создает кучу потоков (видно libuv-worker, DelayedTaskSche) и имеет огромное виртуальное адресное пространство (это нормально для Chrome, но сбивает с толку).
Сервис падает, systemd его перезапускает (счетчик уже 101), при каждом запуске создаются новые Chrome-процессы, старые не убиваются и приходит OOM.
Первым делом что делаю:
- Останавливаю бесконечный цикл перезапусков:
sudo systemctl stop myapp
sudo systemctl disable myapp # пока не починю
- Прибиваю все процессы Chrome прямо сейчас:
sudo pkill -9 chrome
sudo pkill -9 chromium
Проверяю, что осталось:
ps aux | grep -E "chrome|node|myapp" | wc -l
- Проверяю свободную память:
free -h
Если память все еще забита, то придется ребутнуться:
sudo reboot
Дальше долго читаю разные документации, пытаюсь понять, что происходит и, наконец, нахожу причину: оказывается, что мой код создает новый браузер при каждом запуске, но при любых ошибках не закрывает его. А так как сервис перезапускается каждые несколько минут по крону (counter 101), процессы Chrome накапливаются.
let browser = await puppeteer.launch({...});
// ... много кода ...
await browser.close();
Если где-то выше browser.close() возникает какое-нибудь исключение (например, networkidle0 таймаут, пропал интернет, изменилась структура сайта или любая другая ошибка), await browser.close(); не выполняется и браузер остается висеть в памяти навсегда.
Исправил код.
Юнит в systemd править не стал, туда можно было бы впилить что-то типа такого:
[Service]
# Жесткий лимит памяти (если превысит — перезапустится)
MemoryMax=768M
MemoryHigh=512M
# Ограничение на количество процессов (предотвращает утечку)
TasksMax=15
# Убивать сервис, если он не закрылся за 30 секунд
TimeoutStopSec=30
# Перезапускать не чаще раза в минуту (предотвращает лавину)
Restart=on-failure
RestartSec=60
# Ограничения для Node.js
Environment=NODE_OPTIONS="--max-old-space-size=384"
# Очищать /tmp от мусора Chrome
PrivateTmp=yes
Выполнил
sudo systemctl start myapp
проверил логи:
journalctl -u myapp -f
последил за памятью:
watch -n 2 'free -h && echo "---" && ps aux --sort=-%mem | grep -E "chrome|node" | head -10'
в логах теперь чисто и все работает так, как и ожидалось.
Запускаю приложение в свободный полет:
sudo systemctl enable myapp