Ubuntu 24.04 — OOM killer: доведіть, виправте, запобігайте повторенням

Було корисно?

Ваш сервіс працював нормально. Потім — ні. Петля перезапусків. Прогалини в метриках. Хтось каже «можливо, деплой?»
Ви переглядаєте таймлайн і не знаходите нічого — крім холодної, ефективної руки OOM killer.

В Ubuntu 24.04 процес може загинути або від ядрового OOM killer, або від користувацького менеджера OOM у systemd.
Якщо ви не доведете, хто саме це зробив (і чому), ви «виправите» не ту шар — і інцидент повернеться, як за розкладом, зазвичай о 02:17.

Що насправді означає “OOM” у Linux 2025 року

«Out of memory» звучить бінарно, ніби система вичерпала весь RAM. У продакшені це більше схоже на проблему чергування, яка перетворюється на бійку:
надходять запити на алокацію, reclaim і compaction борються, логіка OOM в ядрі чи в користувацькому просторі вирішує, що для прогресу потрібні жертви,
і ваш сервіс стає цією жертвою.

В Ubuntu 24.04 зазвичай ви маєте справу з:

  • Kernel OOM killer (класичний): спрацьовує, коли ядро не може задовольнити запити пам’яті і reclaim не вдається; обирає жертву за шкалою badness.
  • cgroup v2 memory enforcement: обмеження пам’яті можуть викликати OOM всередині cgroup, навіть якщо хост має вільний RAM; жертва обирається всередині цієї cgroup.
  • systemd-oomd: демон у userspace, який проактивно вбиває процеси або слайси при стійкому тиску за PSI, часто раніше, ніж ядро досягає жорсткого OOM.

Шість–десять фактів, що зроблять вас кращими в цьому

  1. Kernel OOM killer з’явився задовго до контейнерів; він створювався для «однієї великої системи», а потім пристосований до cgroups, де «OOM у боксі» став нормою.
  2. PSI (pressure stall information) — відносно нова річ в історії Linux і змінила правила гри: можна виміряти «час простою в очікуванні пам’яті», а не лише «вільні байти».
  3. Історія swap за замовчуванням в Ubuntu змінювалася. Деякі флоти перейшли на swapfile за замовчуванням; інші вимикали swap на хостах контейнерів, що робить OOM більш різким і швидким.
  4. Page cache теж пам’ять. «Вільної» пам’яті може бути мало, але система ще здорова, бо cache можна звільнити. Трюк — знати, коли reclaim перестає працювати.
  5. OOM — це не лише про витоки. Ви можете OOM через легітимний спайк навантаження, thundering herd або один запит, що будує величезну хеш-таблицю в пам’яті.
  6. cgroup v2 змінив семантику. У уніфікованій ієрархії memory.current, memory.max і PSI — першокласні об’єкти. Якщо ви все ще міркуєте у термінах cgroup v1, ви неправильно діагностуєте ліміти.
  7. systemd-oomd може вбити «не ту річ» з вашої точки зору, бо він орієнтується на юніти/слайси за тиском і політиками, а не за бізнес-імпактом.
  8. Overcommit — це політика, а не магія. Linux може обіцяти більше віртуальної пам’яті, ніж є; рахунок приходить пізніше, часто під час піку трафіку.

Одна цитата, яку варто приклеїти до монітора:
Надія — не стратегія. — перефразована думка, часто повторювана в колах операцій і надійності.
Сенс лишається: вам потрібні докази, а потім — контролі.

Короткий жарт №1: OOM killer схожий на сезон бюджету — усі шоковані, але жертву все одно обирають.

Швидкий план діагностики (перший/другий/третій)

Коли сервіс помирає і ви підозрюєте OOM, вам потрібна швидка певність, а не двогодинні археологічні розкопки.
Ось порядок триажу, що заощаджує час.

Перший: підтвердіть факт вбивства і ідентифікуйте вбивцю

  • Шукайте рядки kernel OOM у dmesg / журналі: “Out of memory”, “Killed process”.
  • Шукайте дії systemd-oomd: “Killed … due to memory pressure” у журналі.
  • Перевірте, чи сервіс у cgroup з встановленим memory.max: runtime контейнера, обмеження systemd unit, Kubernetes QoS тощо.

Другий: підтвердіть, що це була пам’ять, а не крах, замаскований під неї

  • Код виходу сервісу: 137 (SIGKILL) часто пов’язаний з OOM (особливо в контейнерах), але не виключно.
  • Перевірте лічильники oom_kill у memory.events cgroup.
  • Корелюйте з PSI memory «some/full» спайками тиску.

Третій: знайдіть джерело тиску

  • Чи це один процес, що виростав? (зріст RSS, витік купи, неконтрольовані потоки)
  • Чи це багато процесів? (fork bomb, thundering herd, sidecar-розростання)
  • Чи це провал reclaim? (dirty pages, IO stall, swap thrash, фрагментація пам’яті)
  • Чи це ліміт? (занадто низький ліміт контейнера, MemoryMax у юніті, невірний QoS)

Якщо ви робите лише одну річ: вирішіть, чи це був kernel OOM, cgroup OOM або
systemd-oomd. Запобіжні заходи кардинально відрізняються.

Доведіть це з логів: kernel vs systemd-oomd vs cgroup

Підписи kernel OOM killer

Kernel OOM лишає дуже впізнавані сліди: контекст алокації, процес-жертва, “oom_score_adj” і часто список задач з пам’яттєвими статистиками.
Вам не потрібні здогадки; потрібно читати правильні місця.

Підписи systemd-oomd

systemd-oomd більш тихий, але все одно аудитований. Він логуватиме рішення та юніт, на який націлився. Він діє
на підставі стійких сигналів тиску і налаштованих політик, а не через «failed allocation».

Підписи OOM через обмеження cgroup

Якщо ви в контейнерах або в сервісах systemd з обмеженнями пам’яті, вас можуть вбити, хоча хост у нормі.
Графіки хоста показують багато вільного RAM. Ваш сервіс усе одно помирає. Це історія cgroup.
Доказ у memory.events та суміжних файлах.

Практичні завдання (команди + значення виводу + рішення)

Це ті завдання, які я реально виконую під час інцидентів. Кожне містить, що означає вивід і яке рішення з нього випливає.
Запускайте як root або через sudo де потрібно.

Завдання 1: Підтвердити, що сервіс загинув від SIGKILL (часто OOM)

cr0x@server:~$ systemctl status myservice --no-pager
● myservice.service - My Service
     Loaded: loaded (/etc/systemd/system/myservice.service; enabled; preset: enabled)
     Active: failed (Result: signal) since Mon 2025-12-29 10:41:02 UTC; 3min ago
   Main PID: 21477 (code=killed, signal=KILL)
     Memory: 0B
        CPU: 2min 11.203s

Значення: Головний процес був вбитий SIGKILL. OOM — головний підозрюваний, бо ядро (і oomd) зазвичай використовують SIGKILL,
але оператор також міг виконати kill -9.

Рішення: Перейдіть до доказів у журналі. Ще нічого не «виправляйте».

Завдання 2: Пошук kernel OOM рядків у журналі навколо події

cr0x@server:~$ journalctl -k --since "2025-12-29 10:35" --until "2025-12-29 10:45" | egrep -i "out of memory|oom|killed process|oom-killer"
Dec 29 10:41:01 server kernel: Out of memory: Killed process 21477 (myservice) total-vm:3281440kB, anon-rss:1512200kB, file-rss:1200kB, shmem-rss:0kB, UID:110 pgtables:4120kB oom_score_adj:0
Dec 29 10:41:01 server kernel: oom_reaper: reaped process 21477 (myservice), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Значення: Це kernel OOM kill. Він іменує жертву і дає статистику пам’яті.

Рішення: Зосередьтеся на причинах, чому ядро досягло OOM: загальна вичерпаність пам’яті, вимкнений swap, провал reclaim або спайк.
systemd-oomd не був причиною в цій події.

Завдання 3: Якщо kernel логи тихі, шукайте дії systemd-oomd

cr0x@server:~$ journalctl --since "2025-12-29 10:35" --until "2025-12-29 10:45" -u systemd-oomd --no-pager
Dec 29 10:40:58 server systemd-oomd[783]: Memory pressure high for /system.slice/myservice.service, killing 1 process(es) in this unit.
Dec 29 10:40:58 server systemd-oomd[783]: Killed /system.slice/myservice.service (myservice), pid=21477, uid=110, total_vm=3281440kB, rss=1513408kB

Значення: systemd-oomd вчинив дію, націливши systemd-юніт через стійкий високий тиск пам’яті.

Рішення: Потрібно перевірити конфігурацію oomd і PSI, розглянути зміну захистів юніту або політик. Додавання RAM може допомогти, але політика все одно може вбити раніше.

Завдання 4: Перевірити, чи systemd-oomd увімкнений і активний

cr0x@server:~$ systemctl is-enabled systemd-oomd && systemctl is-active systemd-oomd
enabled
active

Значення: oomd задіяний на цьому хості.

Рішення: Коли ви бачите SIGKILL без kernel OOM логів, розглядайте oomd як першокласного підозрюваного.

Завдання 5: З’ясувати cgroup-путь для сервісу і перевірити, чи є memory limit

cr0x@server:~$ systemctl show -p ControlGroup -p MemoryMax -p MemoryHigh myservice
ControlGroup=/system.slice/myservice.service
MemoryMax=infinity
MemoryHigh=infinity

Значення: У цього юніту немає явного ліміту пам’яті. Якщо все ж відбувається cgroup OOM, це може бути ліміт батьківського слайсу,
ліміт контейнера або інша межа контролера.

Рішення: Перевірте батьківські слайси і файли cgroup v2 безпосередньо.

Завдання 6: Прочитати memory.events cgroup для суворого доказу cgroup OOM

cr0x@server:~$ cgpath=$(systemctl show -p ControlGroup --value myservice); cat /sys/fs/cgroup${cgpath}/memory.events
low 0
high 12
max 3
oom 1
oom_kill 1

Значення: oom_kill 1 — куріннявий доказ: відбулося вбивство через політику пам’яті cgroup.
max 3 вказує, що cgroup вдарялася об жорстку межу кілька разів; не завжди вбивала, але «била по стелі».

Рішення: Виправте ліміт або поведінку пам’яті. Не витрачайте час на графіки RAM хоста; це локальна справа cgroup.

Завдання 7: Перевірити поточне використання проти ліміту на рівні cgroup

cr0x@server:~$ cat /sys/fs/cgroup${cgpath}/memory.current; cat /sys/fs/cgroup${cgpath}/memory.max
1634328576
2147483648

Значення: Сервіс використовує ~1.52 GiB при капі 2 GiB. Якщо ви бачите вбивства при значеннях близько до капу, у вас реальні проблеми з запасом.

Рішення: Підніміть ліміт, зменшіть відбиток пам’яті або додайте зворотний тиск, щоб процес не біг до стіни.

Завдання 8: Перевірити PSI memory pressure, щоб зрозуміти, чи хост стоїть у блокуванні

cr0x@server:~$ cat /proc/pressure/memory
some avg10=0.48 avg60=0.92 avg300=1.22 total=39203341
full avg10=0.09 avg60=0.20 avg300=0.18 total=8123402

Значення: PSI показує, що система проводить вимірюваний час у стані очікування пам’яті. «full» означає, що задачі повністю заблоковані в очікуванні пам’яті.
Якщо «full» нетривіальне, ви втрачаєте робочий час, а не лише байти.

Рішення: Якщо PSI високий і стійкий, працюйте над системними виправленнями: поведінка reclaim, стратегія swap, формування навантаження або додавання RAM.

Завдання 9: Підтвердити стан swap і чи ви працюєте без механізму безпеки

cr0x@server:~$ swapon --show
NAME      TYPE SIZE USED PRIO
/swapfile file  8G  512M   -2

Значення: Swap існує і використовується. Це може купити час і уникнути різких OOM, але також може приховувати витоки, поки латентність не впаде.

Рішення: Якщо swap вимкнено на загального призначення хості, розгляньте увімкнення помірного swapfile. Якщо swap увімкнено і він сильно використовується,
дослідіть зростання пам’яті і ризики IO stall.

Завдання 10: Шукати в ядрі деталі вибору жертви (badness, constraints)

cr0x@server:~$ journalctl -k --since "2025-12-29 10:40" --no-pager | egrep -i "oom_score_adj|constraint|MemAvailable|Killed process" | head -n 20
Dec 29 10:41:01 server kernel: myservice invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
Dec 29 10:41:01 server kernel: Constraint: CONSTRAINT_NONE, nodemask=(null), cpuset=/, mems_allowed=0
Dec 29 10:41:01 server kernel: Killed process 21477 (myservice) total-vm:3281440kB, anon-rss:1512200kB, file-rss:1200kB, shmem-rss:0kB, UID:110 pgtables:4120kB oom_score_adj:0

Значення: CONSTRAINT_NONE свідчить про глобальний тиск пам’яті (не обмежений cpuset/mems).

Рішення: Дивіться на весь хост: основні споживачі пам’яті, reclaim, swap і IO. Якщо ви очікували обмеження cgroup, ваше припущення хибне.

Завдання 11: Визначити топ-споживачів пам’яті у момент інциденту (або зараз, якщо ще живі)

cr0x@server:~$ ps -eo pid,ppid,comm,rss,vsz,oom_score_adj --sort=-rss | head -n 12
  PID  PPID COMMAND           RSS    VSZ OOM_SCORE_ADJ
30102     1 java         4123456 7258120             0
21477     1 myservice    1512200 3281440             0
 9821     1 postgres      812344 1623340             0
 1350     1 prometheus    402112  912440             0

Значення: RSS — реальна resident пам’ять. VSZ — віртуальний адресний простір (часто вводить в оману). Якщо щось значно більше за ваш сервіс,
жертвою міг стати «нещасний», а не «найбільший».

Рішення: Вирішіть, чи обмежити або перемістити важкий процес, чи захистити ваш сервіс через oom_score_adj і політики юніту.

Завдання 12: Перевірити налаштування overcommit (політика, що змінює поведінку при OOM)

cr0x@server:~$ sysctl vm.overcommit_memory vm.overcommit_ratio
vm.overcommit_memory = 0
vm.overcommit_ratio = 50

Значення: Режим overcommit 0 — евристичний. Він може дозволяти алокації, що пізніше спричинять OOM під навантаженням.

Рішення: Для деяких класів систем (бази даних, чутливі до резервування пам’яті) розгляньте суворішу політику (mode 2).
Для багатьох застосунків mode 0 прийнятний; спочатку зверніть увагу на ліміти і витоки.

Завдання 13: Перевірити проблеми reclaim/IO, що роблять “available memory” оманливою

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0 524288 112344  30240 8123448    0    4   120   320  912 2100 11  4 83  2  0
 3  1 524288  80320  30188 8051120    0   64   128  4096 1102 2600 18  6 62 14  0
 4  2 524288  60220  30152 7983340    0  256   140  8192 1200 2901 22  7 48 23  0
 2  1 524288  93210  30160 8041200    0   32   110  1024  980 2400 14  5 74  7  0
 1  0 524288 104220  30180 8100100    0    0   100   512  900 2200 10  4 84  2  0

Значення: Високий so (swap out) плюс високий wa (IO wait) можуть сигналізувати про swap thrash або контенцію сховища.
Це може штовхнути систему в пам’ятні затримки, а потім до вбивств процесів.

Рішення: Якщо IO wait стрибає, розглядайте сховище як частину інциденту пам’яті. Виправте насичення IO, поведінку dirty page, і розгляньте швидший swap backing або іншу стратегію swap.

Завдання 14: Перевірити захисти systemd unit, що впливають на вибір для вбивства

cr0x@server:~$ systemctl show myservice -p OOMScoreAdjust -p ManagedOOMMemoryPressure -p ManagedOOMMemoryPressureLimit
OOMScoreAdjust=0
ManagedOOMMemoryPressure=auto
ManagedOOMMemoryPressureLimit=0

Значення: З ManagedOOMMemoryPressure=auto systemd може увімкнути управління в залежності від дефолтів і версії.

Рішення: Для критичних сервісів явно оберіть поведінку: або захищайте їх (відключіть управління oomd для юніту),
або застосуйте слайс-політи, що вбиватимуть менш важливі задачі першими.

Завдання 15: Перевірити, чи ваш сервіс працює всередині контейнера з власними лімітами

cr0x@server:~$ cat /proc/21477/cgroup
0::/system.slice/myservice.service

Значення: Цей приклад показує systemd-сервіс безпосередньо на хості. У Docker/Kubernetes ви побачите шляхи, що вказують на контейнерні scope/slice.

Рішення: Якщо шлях вказує на container scopes, зайдіть у cgroup контейнера і читайте memory.max і memory.events там.

Завдання 16: Якщо у вас критичний бізнес-сервіс, встановіть свідоме OOM score відкоригування

cr0x@server:~$ sudo systemctl edit myservice
# (editor opens)
# Add:
# [Service]
# OOMScoreAdjust=-500
cr0x@server:~$ sudo systemctl daemon-reload && sudo systemctl restart myservice

Значення: Більш низькі (від’ємні) значення роблять процес менш ймовірною мішенню ядра при OOM.
Це не запобігає OOM; це змінює, кого буде вбито.

Рішення: Використовуйте це лише коли маєте план, що має померти натомість (батч-джоби, кеші, best-effort воркери).
Інакше ви просто перемістите зону ураження кудись ще.

Чому це сталося: надійні сценарії відмов

1) Ілюзія «хост має пам’ять» (OOM через cgroup)

Хост може мати гігабайти вільних, а ваш сервіс усе одно буде вбитий, якщо його cgroup досягне memory.max.
Це поширено на вузлах Kubernetes, Docker-хостах і systemd-сервісах, де хтось встановив MemoryMax місяці тому і забув.

Доказ не в free -h. Він у memory.events для тієї cgroup.

2) Політика «нема swap через latency» (різкий глобальний OOM)

Вимкнення swap може бути виправданим для деяких latency-sensitive середовищ, але це міняє поступове падіння на раптову смерть.
Якщо ви це робите, вам потрібні суворі ліміти пам’яті, admission control і відмінне планування ємності. Багато команд цього не мають.

3) Витік, що проявляється лише під реальним трафіком

Класика. Кеш без обмежень. Вибух міток у метриках. Шлях запиту, що тримає посилання.
Усе виглядає добре в staging, бо staging не зазнає реальної різноманітності користувачів.

4) Reclaim не працює, бо сховище — прихована вузька пляшка

Reclaim пам’яті часто залежить від IO: запис dirty pages, свапінг, читання назад. Коли сховище насичене,
тиск пам’яті перетворюється на затримки, а потім на вбивства.
Правило SRE: якщо інциденти пам’яті корелюють з IO wait, це не просто «проблема пам’яті». Це системна проблема.

5) systemd-oomd робить те, що ви попросили (або що «auto» вирішив)

oomd спроектований так, щоб вбивати раніше, ніж ядро, щоб запобігти повному падінню системи.
Це добре. Але це також дивно, коли воно вбиває юніт, який ви вважали захищеним.
Якщо ви запускаєте multi-tenant хости, oomd може бути вашим другом. Якщо ви маєте вузли для однієї мети, це може створювати шум, якщо не налаштований.

Короткий жарт №2: Вимкнути swap, щоб «зменшити шум від латентності» — як зняти пожежну тривогу, щоб «менше шуміло».

Три корпоративні міні-історії (анонімізовано, правдоподібно, технічно точно)

Міні-історія 1: Інцидент через хибне припущення

Середній SaaS запускал reporting API на пулі Ubuntu-хостів. On-call бачив хвилю рестартів і зробив звичну річ:
перевірив графіки пам’яті хоста. Все виглядало добре — багато вільного RAM, без використання swap, без явної причини. Команда звинуватила
«випадкові крашi» і відкотила нешкідливе оновлення бібліотеки.

Рестарти продовжувалися. Старший інженер нарешті подивився memory.events для systemd-юніта і знайшов
інкременти oom_kill. Сервіс мав MemoryMax, успадкований від батьківського слайсу для «не критичних аплікацій».
Ніхто не пам’ятав про це, бо це налаштували під час спринту з економії, і воно жило в інфраструктурному репозиторії, який команда API не читала.

Хибне припущення було просте: «Якщо хост має вільний RAM, то не може бути OOM». У світі cgroup це припущення мертве.
Хост був в порядку; сервіс був у коробці.

Виправлення не було драматичним: підняли ліміт юніту до реального пікового використання, додали алерт на memory.events для high/max до oom_kill,
і документували політику слайсу. Постмортем не звинуватив ядро — звинуватив відсутність власника і невидимі обмеження.

Міні-історія 2: Оптимізація, що підвела

Платформа платежів хотіла нижчий p99 latency. Хтось запропонував вимкнути swap на вузлах, бо «swap повільний».
Також налаштували JVM на більший heap, щоб зменшити частоту GC. Перший тиждень виглядав чудово — чисті дашборди і трохи кращі хвостові латентності.

Потім трапилася маркетингова кампанія. Використання пам’яті зросло, кеші нагрілися і одночасність зросла. Без swap не було буфера для транзиторних спайків.
Ядро швидко досягло OOM і вбив випадкові Java-воркери. Деякі вузли вижили, деякі — ні, що створило нерівномірне навантаження і шторми повторних спроб.

Команда спробувала виправити це, ще більше піднявши heap, що зробило ситуацію гіршою: heap забрав файловий кеш і зменшив варіанти reclaim.
Коли система потрапляла в біду, їй було куди іти менше.

Остаточне рішення було нудним, але ефективним: знову ввімкнули помірний swapfile, зменшили heap до безпечної частки RAM і ввели пер-воркер ліміти пам’яті через systemd slices,
щоб один воркер не міг з’їсти весь вузол. Вони зберегли виграш по латентності, керуючи одночасністю замість того, щоб прибрати систему безпеки.

Міні-історія 3: Нудна, але правильна практика, що врятувала день

Сервіс інгесту великих файлів обробляв великі клієнтські файли. Нічого гламурного: стримінговий IO, декомпресія, парсинг.
Платформна команда мала сувору політику: кожен сервісний юніт повинен оголошувати очікування пам’яті через MemoryHigh і MemoryMax,
і вимірювати «current RSS» як метрику. Команди скаржилися, що це бюрократія.

Одного вечора новий клієнт прислав зіпсований файл, що спричинив патологічну поведінку в парсері.
RSS росло поступово. На хості без лімітів це потягло б усе вниз.

Натомість MemoryHigh спричинив ранні сигнали throttling (reclaim pressure), продуктивність погіршилася, але сервіс лишався працездатним,
а MemoryMax запобіг тотальному виснаженню вузла. Інжест-воркер був вбитий у межах своєї cgroup,
не знісши бази даних чи node exporter’ів.

On-call побачив алерт: memory.events high піднімався. Вони змогли корелювати це з однією роботою клієнта, ізолювати її і випустити виправлення парсера наступного дня.
Нудна політика перетворила інцидент вузла на одну проваляну задачу. Ніхто не святкував. У цьому й суть.

Запобігання, що працює: ліміти, swap, налаштування та дисципліна рантайму

Вирішіть, який шар має падати першим

Ви не запобігаєте вичерпанню пам’яті, сподіваючись, що ядро «зрулить це». Ви запобігаєте, проєктуючи порядок відмов:
які навантаження будуть стискатися, які відбудуться вбивства, а які — захищені.

  • Захищати: бази даних, coordination сервіси, агенти вузла, що зберігають керованість хоста.
  • Best-effort: батч-джоби, кеші, що можуть відбудуватися, асинхронні воркери з повторними спробами.
  • Ніколи без ліміту: все, що може розростися за входом користувача (regex, декомпресія, парсинг JSON, кешуючі шари).

Використовуйте системні обмеження systemd свідомо (MemoryHigh + MemoryMax)

MemoryMax — жорстка стіна. Корисна, але жорстока: при її досягненні ви можете отримати вбивство. MemoryHigh — поріг тиску:
він викликає reclaim і throttling і дає шанс відновитися до смерті.

Прагматичний шаблон для довготривалих сервісів:

  • Встановіть MemoryHigh на рівні, де деградація продуктивності прийнятна, але тривоги відчутні.
  • Встановіть MemoryMax достатньо високо для відомих піків, але достатньо низько, щоб захистити хост.
cr0x@server:~$ sudo systemctl edit myservice
# Add:
# [Service]
# MemoryHigh=3G
# MemoryMax=4G
cr0x@server:~$ sudo systemctl daemon-reload && sudo systemctl restart myservice

Налаштуйте oomd замість того, щоб робити вигляд, що його немає

Якщо systemd-oomd увімкнений, вам потрібна явна політика. «auto» — це не політика; це дефолт, що зрештою здивує вас у найгірший час.

Типові підходи:

  • Multi-tenant вузли: тримайте oomd, використовуйте слайси, розміщуйте best-effort навантаження в слайсі, який oomd може вбити першим.
  • Single-purpose вузли: розгляньте відключення управління oomd для критичного юніту, якщо він вбивається передчасно, але тримайте kernel OOM як останню інстанцію.

Swap: оберіть стратегію і керуйте нею

Swap не «поганий». Неналаштований swap — поганий. Якщо ви вмикаєте swap, моніторьте швидкості swap-in/out та IO wait. Якщо вимикаєте swap,
прийміть, що OOM будуть раптовими і частими, якщо у вас немає суворих лімітів і передбачуваних навантажень.

Зупиніть спайки пам’яті на межі застосунку

Найшвидший спосіб запобігти OOM — перестати приймати роботу, що призводить до необмеженого росту пам’яті.
Практики для продакшену:

  • Обмежуйте кеші (розмір + TTL) і вимірюйте rate витіснень.
  • Обмежуйте одночасність. Більшість сервісів не потребують «максимальної кількості потоків», їм потрібна «кількість, яку можна утримати в кеші й RAM».
  • Обмежуйте розмір payload і використовуйте стримінговий парсинг.
  • Впроваджуйте backpressure: черги з лімітами, load shedding, circuit breakers.

Розумійте різницю між RSS і «виглядає великим»

Інженери люблять звинувачувати «витоки пам’яті», спираючись на VSZ. Так ви «виправляєте» mmap-резерви, що ніколи не були resident.
Використовуйте RSS, PSS (якщо можете) і cgroup memory.current. І корелюйте з тиском (PSI), а не лише з байтами.

Примітка інженеру зі сховища: інциденти пам’яті часто — це інциденти IO в іншому обличчі

Якщо ваша система робить reclaim, свапить або записує dirty pages під тиском, латентність сховища стає частиною історії пам’яті.
Повільний або насичений диск може перетворити «відновлюваний тиск» на «вбити щось зараз».

Якщо ви бачите високий IO wait під час тиску, виправте маршрут сховища: queue depth, галасливі сусідні томи, throttling, writeback налаштування і swap backing.

Поширені помилки: симптом → корінь → виправлення

1) Симптом: «Сервіс отримав SIGKILL, але в dmesg немає OOM»

Корінь: systemd-oomd вбив його на підставі PSI, або вбивство сталося всередині контейнера/cgroup і ви дивитесь не в ту область журналу.

Виправлення: Перевірте journalctl -u systemd-oomd і memory.events юніта. Підтвердіть шлях cgroup і читайте правильні файли.

2) Симптом: «Хост показує 30% вільного RAM, але контейнер постійно OOM’иться»

Корінь: memory.max cgroup занадто низький (ліміт контейнера), або спайки пам’яті перевищують запас.

Виправлення: Перевірте memory.max і memory.events у cgroup контейнера. Підніміть ліміт або зменшіть пікове використання.

3) Симптом: «Усе повільніє хвилини, потім один процес вмирає»

Корінь: Reclaim і swap thrash; IO wait робить пам’ятні затримки нестерпними.

Виправлення: Перевірте vmstat на предмет swap/wa. Зменшіть dirty writeback pressure, покращіть продуктивність сховища, розгляньте адекватну стратегію swap і обмежте найгірших споживачів.

4) Симптом: «OOM вбиває малий процес, а не великого хрюна»

Корінь: OOM badness scoring, різниці в oom_score_adj, shared memory або те, що хрюна захищено політикою.

Виправлення: Перевірте логи на предмет oom_score_adj; встановіть свідомий OOMScoreAdjust для критичних сервісів і обмежте best-effort роботу cgroup лімітами, щоб ядру було легше обирати жертву.

5) Симптом: «OOM відбувається одразу після деплою, але не завжди»

Корінь: Cold caches + збільшена одночасність + нові алокації. Також поширено: новий код шляху алокує на підставі вхідних даних.

Виправлення: Додайте canary навантаження, що імітує cold start, обмежте кеші, додайте ліміти розміру запитів і встановіть MemoryHigh для раннього виявлення тиску.

6) Симптом: «Ми додали RAM, OOM все одно відбувається»

Корінь: Ліміт стоїть у cgroup, а не на хості. Або витік масштабується і заповнює скільки б ви не купили.

Виправлення: Доведіть, де саме ліміт (memory.max). Потім виміряйте швидкість росту, щоб підтвердити витік або навантаження.

Чеклісти / покроковий план

Чекліст інциденту (15 хвилин, без героїчних вчинків)

  1. Отримайте точний час смерті і сигнал: systemctl status або статус runtime контейнера.
  2. Перевірте kernel log навколо того часу на “Killed process”.
  3. Якщо kernel тихий, перевірте журнал systemd-oomd.
  4. Прочитайте memory.events для cgroup сервісу і для батьківського слайсу.
  5. Захопіть PSI memory: /proc/pressure/memory (хост) і cgroup PSI, якщо релевантно.
  6. Захопіть топ RSS споживачів і їх oom_score_adj.
  7. Перевірте стан swap і vmstat на предмет взаємодії swap/IO wait.
  8. Запишіть: kernel OOM vs oomd vs cgroup limit OOM. Не залишайте неоднозначності.

План стабілізації (той же день)

  1. Якщо cgroup OOM: підніміть MemoryMax (або ліміт контейнера), щоб зупинити кровотечу.
  2. Якщо глобальний kernel OOM: додайте swap, якщо доречно, зменшіть одночасність і тимчасово обітніть не критичне навантаження.
  3. Якщо oomd вбив: відрегулюйте ManagedOOM політику або перемістіть best-effort навантаження в killable slice.
  4. Налаштуйте алерти на memory.events «high» і PSI «full», щоб виявляти тиск до вбивств.
  5. Захистіть критичні юніти через OOMScoreAdjust, але лише після того, як забезпечите безпечний клас жертв.

План запобігання (цього тижня)

  1. Визначте бюджети пам’яті для сервісів: очікуваний steady-state RSS і гірший кейс піка.
  2. Встановіть MemoryHigh і MemoryMax відповідно; документуйте відповідальність.
  3. Додайте захисні механізми на рівні застосунку: ліміти розмірів запитів, обмежені кеші, обмеження одночасності.
  4. Інструментуйте пам’ять: RSS-гіджі, heap-метрики там, де потрібно, і періодичні перевірки витоків.
  5. Запустіть керований навантажувальний тест, що імітує cold start + пік одночасності.
  6. Перегляньте swap і IO шлях: переконайтеся, що reclaim має куди йти і це не вб’є сховище.

Поширені питання

1) Як швидко відрізнити kernel OOM від systemd-oomd?

Kernel OOM показує «Out of memory» і «Killed process» у journalctl -k. systemd-oomd показує рішення про вбивство в
journalctl -u systemd-oomd. Якщо обидва тихі, перевірте cgroup memory.events на предмет oom_kill.

2) Що означає код виходу 137?

Це зазвичай означає, що процес отримав SIGKILL (128 + 9). OOM — поширена причина, особливо в контейнерах, але оператор або watchdog також можуть SIGKILL.
Завжди корелуйте з логами і подіями cgroup.

3) Чому OOM killer обрав мій важливий сервіс?

Ядро обирає за шкалою badness і обмеженнями. Якщо все критичне і без лімітів, щось критичне помре.
Використовуйте OOMScoreAdjust для захисту ключових юнітів, але також створіть класи, що можуть помирати (батч, кеш) з відповідними лімітами.

4) Чи можна просто відключити systemd-oomd?

Можна, але не робіть це рефлекторно. На multi-tenant вузлах oomd може запобігти тотальному колапсу, діючи раніше.
Якщо вимикаєте його, будьте впевнені, що ваші cgroup-ліміти і контроль навантаження запобігають глобальному тиску.

5) У чому різниця між MemoryHigh і MemoryMax?

MemoryHigh викликає reclaim і throttling при перевищенні — раннє попередження і м’який контроль.
MemoryMax — жорсткий ліміт; його перевищення може спричинити OOM kills усередині cgroup.

6) Чому «free» виглядає низьким, навіть коли система в порядку?

Linux використовує RAM для page cache. Низьке «free» не обов’язково погано. Дивіться на «available» пам’ять і, краще, на PSI memory pressure,
щоб знати, чи система стоїть у затримках.

7) Чи завжди рекомендований swap на серверах Ubuntu 24.04?

Не завжди. Swap може зменшити частоту жорстких OOM kills, але під тиском може збільшити латентність.
Для загального призначення помірний swapfile часто дає перевагу. Для суворих low-latency навантажень ви можете відключити swap —
але тоді повинні нав’язати суворі ліміти пам’яті і admission control.

8) Як довести OOM контейнера проти OOM хоста?

Перевірте cgroup контейнера: memory.events з oom_kill вказує на cgroup OOM.
Host OOM покаже kernel «Killed process» записи і часто зачепить кілька сервісів.

9) Яка найкраща рання метрика попередження?

Memory PSI (/proc/pressure/memory) плюс зростання memory.events high у cgroup. Байти показують «скільки».
Тиск показує «наскільки погано».

Наступні кроки (що зробити до наступної сторінки)

Не сприймайте OOM як погоду. Це інженерія. Негайний виграш — довести, хто вбив: kernel OOM, cgroup OOM або systemd-oomd.
Коли ви це знаєте, виправлення перестануть бути забобонами.

Зробіть ці три речі цього тижня:

  1. Додайте алерт на memory.events (high і oom_kill) для ваших топ-сервісів.
  2. Встановіть явні бюджети MemoryHigh/MemoryMax для важливих сервісів і помістіть best-effort роботу в killable slice.
  3. Визначте стратегію swap і моніторьте її — бо «ми одного разу вимкнули swap» — це не план, а чутка.

Наступного разу, коли сервіс зникне, ви маєте вміти відповісти «хто його вбив?» менше ніж за п’ять хвилин.
Тоді ви витратите решту часу на запобігання повтору, а не на суперечки знімком дашборда.

← Попередня
Помилки читання ZFS: коли винен диск, кабель або контролер
Наступна →
Неприємності з рідким металом: апгрейд, що перетворюється на рахунок за ремонт

Залишити коментар