cgroups и OOM-killer: кого убьют, почему в логе пусто и что на самом деле значит 137
Контейнер исчез, в логе приложения — ни строчки, в отчёте оркестратора — код 137. Это не загадка, а нормальная работа двух механизмов: учёт памяти по группе процессов и убийство сигналом, который нельзя перехватить. Урок разбирает оба и показывает прогоном, что потребление дошло до лимита и не превысило его: убивают не за превышение, а за то, что освобождать внутри группы стало нечего.
Полное техническое изложение
TL;DR
Лимит памяти ставится группе процессов, а не процессу. Учёт тоже идёт по группе, и когда ей становится некуда расти, ядро сначала освобождает внутри неё что может, а если освобождать нечего — прекращает один из её процессов. Убивают за общее потребление группы, поэтому «мой процесс маленький» не защищает.
Отсюда главное следствие: программа не видит ни ошибки, ни исключения.
Убивает её SIGKILL, а его нельзя перехватить: измерено — код выхода −9,
снаружи 137, stdout пуст, stderr пуст. Пустой лог здесь не потеря
сообщений, а признак способа смерти. И потребление при этом не «раздувается
сверх лимита»: в прогоне с лимитом 64 МиБ программа просила 1600 МиБ, а пик
группы остановился на 64,0 МиБ.
Дальше — то, что отличает знающего от читавшего. Попадание в лимит —
обычное событие, а не смерть: в том же прогоне счётчик failcnt дошёл до 36, и
до последнего из этих тридцати шести раз группа продолжала работать, потому что
ядро освобождало что могло. Жертву выбирают по объёму занятой памяти, а не по
очерёдности: в группе с лимитом 160 МиБ выжил маленький процесс, убит большой —
ядро считает оценку вредности, и её основа — the
amount of memory used by the process
(объём памяти, используемый процессом). Код 137 не значит «OOM», он значит
«убит SIGKILL»: отличает одно от другого только строка ядра
oom-kill:constraint=CONSTRAINT_MEMCG с именем группы. Тот же код с тем же
запросом при лимите 2048 МиБ отработал нормально — failcnt ноль, код выхода
0; а файловый кеш хоть и считается группе, но возвращается: файл в 256 МиБ,
прочитанный через группу с лимитом 64 МиБ, дал пик 63,2 МиБ, failcnt 95 и код
выхода 0. И про пик, дошедший ровно до лимита: это результат нашего прогона, а
не гарантия — memory.max описан в документации cgroup v2 как предел, у
которого потребление при некоторых обстоятельствах может временно оказаться
выше.
- работающая программа занимает память, и её можно измерить;
- контейнер запускается на общей машине, рядом с другими такими же;
- память машины конечна: когда её не хватает, кто-то должен уступить.
- что такое cgroup,
memory.limit_in_bytes,failcnt,memory.max; - оценка вредности,
oom_score_adj,CONSTRAINT_MEMCG; - чем анонимная память отличается от файлового кеша.
Что здесь на самом деле спрашивают
Лестница обычно такая, и первые две ступени решают больше, чем кажется:
- «Что такое cgroups?» — разминка: группировка, учёт, лимит.
- «Что произойдёт, если процесс упрётся в лимит памяти?» — здесь
отсеиваются те, кто ждёт
MemoryError. - «Почему в логе приложения ничего нет?» — проверяют, связали ли вы это с
SIGKILLиз прошлого урока. - «Кого убьют, если в контейнере несколько процессов?» — знаете ли вы про оценку вредности и её основу.
- «Как отличить OOM от остановки оркестратором, если код один и тот же?» — вопрос про улики, а не про догадки.
- «Как выбирать лимит?» — понимаете ли вы, что
failcntи пик потребления говорят разные вещи.
Числа ниже получены прогоном bench/cgroups/oom.py и
bench/cgroups/practice.py на cgroup v1. Во второй версии те же величины
называются иначе (memory.max, memory.events, memory.peak) — механизм тот
же, имена другие.
База: у контейнера есть бюджет памяти, и считает его не приложение
Прежде чем разбирать, кого и за что убивают, стоит назвать обычными словами три вещи.
Контрольная группа (cgroup) — это механизм ядра Linux, который считает ресурсы группы процессов и ограничивает их. Не одного процесса, а именно группы: ядро ведёт для неё счётчики — сколько памяти сейчас занято, сколько раз группа упиралась в потолок — и хранит число, которым это потребление ограничено.
Контейнер — это и есть такая группа. Когда в его настройках пишут «памяти 512 мегабайт», это число попадает в файл его группы, и дальше исполняет его ядро — не приложение, не среда исполнения и не библиотека.
Процессы внутри делят один бюджет. Их может быть один, а может быть несколько: главный, вспомогательный, случайно запущенный дочерний. Память все они берут из общего числа, и поэтому «мой процесс маленький» ничего не гарантирует — считают не его, а всех вместе.
Отсюда вопрос, на который отвечает остальной урок: что происходит, когда процессам группы нужно больше памяти, чем ей разрешено? Ответ короткий и неуютный: ядро не отказывает, а освобождает или убивает. Сначала оно пытается забрать внутри этой же группы то, что можно забрать, и всё-таки выдать память; когда забирать становится нечего, оно выбирает один из процессов группы и прекращает его. Не сообщает, не просит закрыться, не даёт последнего слова — прекращает.
Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Дальше — про то, кого именно выбирают, почему после этого в логе приложения пусто и по какой улике такую смерть отличают от обычной остановки контейнера.
Механизм 1: лимит принадлежит группе, а не процессу
Теперь то же самое словами документации. Контрольная группа — это набор
процессов плюс учёт того, что они потребляют, и лимиты на потребление. Про
память cgroups(7) говорит коротко:
The memory controller supports reporting and limiting of process memory, kernel
memory, and swap used by cgroups.
Контроллер памяти умеет учитывать и ограничивать память процессов, память ядра и подкачку, используемые контрольными группами.
Ключевое слово — cgroups, множественное число: и учёт, и лимит относятся к группе. Отсюда сразу два следствия, о которых спрашивают отдельно.
Первое: если в контейнере несколько процессов, они тратят общий лимит. Вспомогательный процесс, который «занимает всего сто мегабайт», отнимает эти сто мегабайт у основного.
Второе: убийство — это событие группы, а не процесса. Ядро обнаруживает, что группе некуда расти, и выбирает, кого из её членов убить. Тот, кого выбрали, мог не сделать ничего плохого.
Устроено это так: у группы есть файл с лимитом, файл с пиком потребления и
счётчик попаданий в лимит (failcnt). Скрипт создаёт группу, ставит лимит в 64 МиБ и запускает в
ней программу, которая честно просит 1600 МиБ, касаясь каждой страницы:
1. THE KILL: the limit is 64 MiB, the program asks for 1600
-----------------------------------------------------------
limit (memory.limit_in_bytes) 64 MiB
exit code from waitpid -9
as the shell reports it 137
stdout of the program ''
stderr of the program ''
peak usage before the kill 64.0 MiB
times the limit was hit (failcnt) 36
Дальше урок разбирает эти семь строк по одной, потому что каждая отвечает на свой вопрос лестницы.
Механизм 2: потребление упирается в лимит, а не перерастает его
Самая частая неверная картинка: процесс «раздулся сверх лимита и лопнул». Посмотрите на пик: 64,0 МиБ при лимите 64 МиБ. В этом прогоне потребление дошло до лимита и не превысило его.
Границу этого наблюдения стоит назвать сразу, потому что дальше на него
опирается практика. «Пик равен лимиту» — то, что мы измерили, а не обещание
ядра: memory.max описан в документации cgroup v2 как предел, у которого
потребление при некоторых обстоятельствах может временно оказаться выше.
Поэтому правило пишется не от точности числа, а от причины: смерть наступает
не после того, как процесс перерос лимит, а потому, что внутри группы стало
нечего освобождать.
Программа в прогоне не просто просит память — она касается каждой страницы, и до счётчиков группы доходит именно занятое, а не запрошенное: просили 1600 МиБ, пик группы — 64,0 МиБ. Когда очередная страница не помещается в лимит, ядро её просто не выдаёт. Дальше есть два пути. Обычный — освободить что-нибудь в этой же группе и всё-таки выдать память. Крайний — если освобождать нечего, убить.
Первый путь и считает failcnt — та самая строка из блока выше:
times the limit was hit (failcnt) 36
Тридцать шесть раз группа упиралась в лимит — и до последнего из них
продолжала работать. Программа
об этом не знала: с её точки зрения память просто выдавалась. Отсюда
практический вывод, который стоит произносить на собеседовании: ненулевой
failcnt — это не отчёт об аварии, а признак того, что группа живёт на
границе. Она ещё работает, но каждый следующий запрос обслуживается через
освобождение, то есть медленнее.
А теперь две пустые строки в том же блоке:
stdout of the program ''
stderr of the program ''
Ни MemoryError, ни трассировки, ни последнего сообщения в лог. Причина
известна из прошлого урока и записана в signal(7): SIGKILL нельзя
перехватить, заблокировать или проигнорировать. Программу не просили
остановиться — её убрали.
Отсюда ответ на третий вопрос лестницы: пустой лог — это не потеря
сообщений, а признак способа смерти. Если бы кончилась память внутри
процесса, было бы исключение и трассировка; если бы пришёл SIGTERM — была бы
запись обработчика. Пусто бывает после SIGKILL.
Механизм 3: жертву выбирают по занятой памяти
Теперь вопрос, ради которого лестница и строилась: если в группе несколько
процессов, кого убьют? Ответ записан в proc(5), и он не про очерёдность:
The badness heuristic assigns a value to each candidate task ranging from 0
(never kill) to 1000 (always kill) to determine which process is targeted. The
units are roughly a proportion along that range of allowed memory the process
may allocate from, based on an estimation of its current memory and swap use.
For example, if a task is using all allowed memory, its badness score will be
1000.
Эвристика вредности присваивает каждому кандидату значение от 0 («никогда не убивать») до 1000 («убивать всегда»), чтобы определить цель. Единицы — примерно доля от того объёма памяти, который процессу разрешено занять, по оценке его текущего потребления памяти и подкачки. Например, если задача использует всю разрешённую память, её оценка вредности будет 1000.
И отдельно — что считать «разрешённой памятью» в случае контейнера:
If it is due to a memory limit (or swap limit) being reached, the allowed memory
is that configured limit.
Если причина — достижение лимита памяти (или лимита подкачки), то разрешённой памятью считается этот настроенный лимит.
То есть оценка — это доля лимита группы, которую процесс занимает. Проверяем: в группе с лимитом 160 МиБ работают двое. Маленький занял 40 МиБ и держит их; большой просит 600.
3. WHO GETS KILLED: the biggest, not the last one
-------------------------------------------------
small process (40 MiB, holding it) alive
big process (asked for 600 MiB) killed
victim big
Это хорошая новость и плохая одновременно. Хорошая: сосед, который ведёт себя прилично, обычно переживает чужую ошибку. Плохая: «обычно» — не «всегда». Оценка считается по доле занятого, и если два процесса заняли поровну, исход решают мелочи.
Подкрутить выбор можно вручную — oom_score_adj прибавляется к оценке, и
диапазон известен:
The lowest possible value, -1000, is equivalent to disabling OOM-killing
entirely for that task, since it will always report a badness score of 0.
Наименьшее возможное значение, −1000, равносильно полному отключению OOM-killer для этой задачи, поскольку её оценка вредности всегда будет нулевой.
Пользоваться этим стоит осторожно: защищённый процесс не перестаёт потреблять память — он перестаёт быть кандидатом. Память всё равно кончится, и убьют кого-то другого, возможно, того, кто нужнее.
Механизм 4: улика, по которой OOM отличают от остановки
Код 137 одинаков у процесса, убитого OOM-killer, и у процесса, которого добил
чужой SIGKILL, — например, тот, что приходит в конце льготного срока при
остановке контейнера из прошлого урока. В обоих случаях это 128 + 9.
Различает их не код, а запись ядра — второй блок прогона:
2. THE KERNEL SAYS IT, THE APPLICATION DOES NOT
-----------------------------------------------
constraint reported by the kernel CONSTRAINT_MEMCG
cgroup that ran out /de_oom_kill
victim named by the kernel python3
Поле constraint и говорит, где кончилась память. В прогоне ядро назвало
группу: CONSTRAINT_MEMCG — значит, дело в её лимите. Если бы в строке была
названа не группа, лечилось бы это совсем другим: не лимитом контейнера, а
общим объёмом памяти машины.
Отсюда способ разобраться, которым стоит отвечать: у OOM есть свидетель, и
это ядро. Три вопроса подряд — есть ли строка oom-kill:constraint= рядом по
времени; какая группа в ней названа; и совпадает ли время с событием
оркестратора. Если dmesg вы прочитали, а строки в нём
нет, то и оснований называть это OOM нет: искать надо среди тех, кто имеет
право посылать сигналы.
Глубже: тот же код, другой лимит — другой исход
Четвёртый блок прогона — тот же код, та же машина и тот же запрос на 1600 МиБ, но лимит 2048 МиБ:
4. THE SAME PROGRAM AND THE SAME REQUEST, A LIMIT THAT FITS
-----------------------------------------------------------
limit 2048 MiB
exit code 0
stdout of the program allocated 1600 MiB
times the limit was hit (failcnt) 0
Ноль в failcnt и ноль в коде выхода. Ничего в программе не поменялось —
поменялось разрешение.
Пятый блок отвечает на вопрос, который отсюда и возникает: любая ли память одинаково опасна? Через группу с лимитом 64 МиБ прочитан файл в 256 МиБ — вчетверо больше лимита:
5. PAGE CACHE COUNTS TOO, BUT IT CAN BE GIVEN BACK
--------------------------------------------------
limit 64 MiB
page cache dropped before the read yes
file read through the group 256 MiB
exit code 0
stdout of the program read 256 MiB
peak usage 63.2 MiB
times the limit was hit (failcnt) 95
Девяносто пять попаданий в лимит, пик у самого потолка — и код выхода ноль. Страницы файлового кеша считаются группе, но их можно вернуть, и ядро их вернуло. Не возвращается анонимная память — та, из-за которой погиб процесс в первом блоке.
Отсюда практическое различение, которого обычно не делают: большой failcnt у
программы, много читающей с диска, чаще означает потерю скорости, а не
приближение к смерти. Смотреть надо на анонимную память.
Из этого следует практический способ ставить лимиты, который и спрашивают под видом «как выбрать лимит». Смотреть надо на две разные величины:
- пик потребления говорит, сколько группе нужно в худшем виденном случае;
failcntговорит, сколько раз она упиралась в текущий лимит.
Пик близко к лимиту и failcnt растёт — запас исчерпан, следующий всплеск
кончится убийством. failcnt ноль и пик втрое меньше лимита — лимит завышен, и
за этот запас кто-то платит: на плотно упакованной машине он отнят у соседей.
Как отвечать на собеседовании
Короткий ответ: лимит памяти ставится группе процессов, и при упирании в него
ядро сначала пытается освободить память внутри группы, а если не выходит —
убивает одного из её процессов сигналом SIGKILL. Поэтому в логе приложения
пусто, а снаружи видно 137 — это 128 + 9. Потребление при этом за лимит не
улетает: в измеренном прогоне пик группы дошёл ровно до лимита и остановился.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
Хороший ответ отличают три детали. Первая — вы говорите, что упирание в лимит
не равно смерти: счётчик failcnt растёт и у совершенно здоровой группы,
это признак жизни на границе. Вторая — вы называете основу выбора жертвы: доля
занятой памяти от лимита, а не очерёдность запросов. Третья — вы знаете улику,
которая отличает OOM от остановки оркестратором: строка ядра с
constraint=CONSTRAINT_MEMCG и именем группы; кода выхода для этого
недостаточно.
И одна вещь, на которой легко переусердствовать. Сказать «процесс не превышает
лимит ни на байт» — значит выдать замер за гарантию. Пик у нас действительно
дошёл до лимита и не превысил его, но memory.max описан в документации
cgroup v2 как предел, у которого потребление при некоторых обстоятельствах
может временно оказаться выше. Точная формулировка звучит так: убивают не за
превышение, а за то, что освобождать внутри группы стало нечего — и
проверяется это не арифметикой, а пиком и failcnt в файлах группы.
Дальше спросят
Почему приложение не может поймать нехватку памяти и освободить кеш само?
Потому что его никто не спрашивает. Ядро не сообщает процессу «у твоей группы
кончается память» — оно либо освобождает страницы само, либо убивает.
SIGKILL не перехватывается, поэтому обработчика «на последний вздох» не
существует.
Узнать о приближении к границе можно только заранее и самостоятельно: читать
потребление группы и failcnt — те самые файлы, из которых сняты числа этого
урока, — и освобождать своё по собственному порогу. Это опрос, а не исключение
внутри вашего кода: момент, когда «уже поздно», ядро с программой не
обсуждает.
Если увеличить лимит, проблема исчезнет?
Исчезнет ровно в одном случае: программе действительно нужно было больше, чем
разрешили, и её пик конечен. Тогда новый лимит выше пика — и failcnt
остаётся нулём, что и видно в четвёртом блоке прогона.
Если же потребление растёт со временем — утечка, неограниченный кеш, накапливающийся буфер, — то лимит только отодвигает убийство. Отличить одно от другого можно, не читая код: посмотрите, выходит ли пик на полку. Растущий без предела пик лимитом не лечится.
Учитывается ли в лимите файловый кеш?
Да, и это измерено пятым блоком прогона: файл в 256 МиБ, прочитанный через группу с лимитом 64 МиБ, дал 95 попаданий в лимит и пик у самого потолка — при коде выхода ноль.
Разница с анонимной памятью в том, что кеш можно вернуть. Поэтому большой
failcnt у программы, много читающей с диска, чаще означает постоянный сброс
кеша — то есть потерю скорости, — а не приближение к смерти. Смотреть надо на
анонимную память: её вернуть нельзя.
Чем отличается лимит от запроса ресурсов в оркестраторе?
Ядро знает ровно одну величину — лимит группы: он записан в файле группы и
проводится в жизнь так, как показано выше. Числа, по которым задачи
расставляют по машинам, живут вне ядра, и в /sys/fs/cgroup их нет.
Отсюда типичная поломка: задачу разместили по одному числу, а убивают её по другому. Ядро исполнит лимит независимо от того, из каких соображений задача сюда попала, — и сойдётся ли сумма лимитов на машине, оно не проверяет.
Частые заблуждения
процесс превысил лимит памяти, поэтому его убили
Убивают не за превышение: учёт идёт по заселённым страницам, и страницу, которая в лимит не помещается, ядро просто не выдаёт. Измерено: лимит 64 МиБ, пик потребления группы 64,0 МиБ при запросе на 1600. Гарантией «ни на байт сверх» это делать не стоит — memory.max описан в документации cgroup v2 как предел, у которого потребление при некоторых обстоятельствах может временно оказаться выше. Причина смерти в другом: внутри группы стало нечего освобождать.
при нехватке памяти программа получит MemoryError и сможет что-то сделать
Не получит: измеренные stdout и stderr убитого процесса пусты. MemoryError возникает, когда память кончается внутри процесса — например, упёрлись в адресное пространство. Здесь память кончается у группы, и разговаривать с программой ядру не о чем: оно не спрашивает, оно убивает, а SIGKILL нельзя перехватить.
ненулевой failcnt означает, что что-то сломалось
Означает, что группа живёт на границе. Измерено: 36 попаданий в лимит за один прогон, и все, кроме последнего, кончились освобождением памяти, а не смертью. Это сигнал «запаса нет и работа идёт медленнее», а не отчёт об аварии.
убьют того, кто в момент нехватки попросил память
Выбирают по доле занятой памяти от лимита: if a task is using all allowed memory, its badness score will be 1000
(если задача использует всю разрешённую память, её оценка вредности будет 1000). Измерено: в группе с лимитом 160 МиБ выжил маленький процесс, занявший 40 МиБ и стартовавший первым, а убит был большой. Просящий и жертва — разные роли, и совпадают они не всегда.
код 137 означает нехватку памяти
137 — это 128 + 9, то есть «убит SIGKILL»; ровно тот же код будет у процесса, добитого оркестратором после льготного срока. Отличает OOM только запись ядра: oom-kill:constraint=CONSTRAINT_MEMCG и имя группы. Нет строки — нет оснований называть это OOM.
oom_score_adj = -1000 защищает приложение от OOM
Защищает от выбора, а не от нехватки: задача перестаёт быть кандидатом, но потреблять память не перестаёт. Память всё равно кончится, и ядро убьёт кого-то другого в той же группе — возможно, того, без кого защищённый процесс бесполезен.
раз лимит на группу, значит важен только суммарный размер контейнера
Важно и то, из скольких процессов он состоит: лимит общий, и вспомогательный процесс отнимает память у основного. По той же причине «мой процесс маленький» ничего не гарантирует — убивают в группе, а не в процессе.
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
group = make_group("de_practice_kill", 64)
done = subprocess.run(
[sys.executable, "-c", EATER, group, "1600"], capture_output=True, text=True
)
print(128 - done.returncode)
print(done.stderr.strip() or "none")
print(victim_of(make_group("de_practice_victim", 160)))Практика · оцените
Проверка знаний
Контейнеру поставили лимит 512 МиБ. Программа в цикле выделяет и трогает память. Что покажет пик потребления группы в момент смерти?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Лимит памяти ставится группе процессов, а не процессу. Учёт тоже идёт по группе, и когда ей становится некуда расти, ядро сначала освобождает внутри неё что может, а если освобождать нечего — прекращает один из её процессов. Убивают за общее потребление группы, поэтому «мой процесс маленький» не защищает.
- Отсюда главное следствие: программа не видит ни ошибки, ни исключения. Убивает её
SIGKILL, а его нельзя перехватить: измерено — код выхода −9, снаружи 137,stdoutпуст,stderrпуст. Пустой лог здесь не потеря сообщений, а признак способа смерти. И потребление при этом не «раздувается сверх лимита»: в прогоне с лимитом 64 МиБ программа просила 1600 МиБ, а пик группы остановился на 64,0 МиБ. - Дальше — то, что отличает знающего от читавшего. Попадание в лимит — обычное событие, а не смерть: в том же прогоне счётчик
failcntдошёл до 36, и до последнего из этих тридцати шести раз группа продолжала работать, потому что ядро освобождало что могло. Жертву выбирают по объёму занятой памяти, а не по очерёдности: в группе с лимитом 160 МиБ выжил маленький процесс, убит большой — ядро считает оценку вредности, и её основа — the amount of memory used by the process. Код 137 не значит «OOM», он значит «убитSIGKILL»: отличает одно от другого только строка ядраoom-kill:constraint=CONSTRAINT_MEMCGс именем группы. Тот же код с тем же запросом при лимите 2048 МиБ отработал нормально —failcntноль, код выхода 0; а файловый кеш хоть и считается группе, но возвращается: файл в 256 МиБ, прочитанный через группу с лимитом 64 МиБ, дал пик 63,2 МиБ,failcnt95 и код выхода 0. И про пик, дошедший ровно до лимита: это результат нашего прогона, а не гарантия —memory.maxописан в документации cgroup v2 как предел, у которого потребление при некоторых обстоятельствах может временно оказаться выше.
На самом деле
- Убивают не за превышение: учёт идёт по заселённым страницам, и страницу, которая в лимит не помещается, ядро просто не выдаёт. Измерено: лимит 64 МиБ, пик потребления группы 64,0 МиБ при запросе на 1600. Гарантией «ни на байт сверх» это делать не стоит —
memory.maxописан в документации cgroup v2 как предел, у которого потребление при некоторых обстоятельствах может временно оказаться выше. Причина смерти в другом: внутри группы стало нечего освобождать. - Не получит: измеренные
stdoutиstderrубитого процесса пусты.MemoryErrorвозникает, когда память кончается внутри процесса — например, упёрлись в адресное пространство. Здесь память кончается у группы, и разговаривать с программой ядру не о чем: оно не спрашивает, оно убивает, аSIGKILLнельзя перехватить. - Означает, что группа живёт на границе. Измерено: 36 попаданий в лимит за один прогон, и все, кроме последнего, кончились освобождением памяти, а не смертью. Это сигнал «запаса нет и работа идёт медленнее», а не отчёт об аварии.
- Выбирают по доле занятой памяти от лимита: if a task is using all allowed memory, its badness score will be 1000. Измерено: в группе с лимитом 160 МиБ выжил маленький процесс, занявший 40 МиБ и стартовавший первым, а убит был большой. Просящий и жертва — разные роли, и совпадают они не всегда.
- 137 — это
128 + 9, то есть «убитSIGKILL»; ровно тот же код будет у процесса, добитого оркестратором после льготного срока. Отличает OOM только запись ядра:oom-kill:constraint=CONSTRAINT_MEMCGи имя группы. Нет строки — нет оснований называть это OOM. - Защищает от выбора, а не от нехватки: задача перестаёт быть кандидатом, но потреблять память не перестаёт. Память всё равно кончится, и ядро убьёт кого-то другого в той же группе — возможно, того, без кого защищённый процесс бесполезен.
- Важно и то, из скольких процессов он состоит: лимит общий, и вспомогательный процесс отнимает память у основного. По той же причине «мой процесс маленький» ничего не гарантирует — убивают в группе, а не в процессе.
Что разобрано
- Что здесь на самом деле спрашивают
- База: у контейнера есть бюджет памяти, и считает его не приложение
- Механизм 1: лимит принадлежит группе, а не процессу
- Механизм 2: потребление упирается в лимит, а не перерастает его
- Механизм 3: жертву выбирают по занятой памяти
- Механизм 4: улика, по которой OOM отличают от остановки
- Глубже: тот же код, другой лимит — другой исход
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
Источники и что читать дальше
5 ИСТОЧНИКОВ
- cgroups(7), Linux man-pages 6.7Официальная документация. Что такое контроллер памяти и на что он способен: «The memory controller supports reporting and limiting of process memory, kernel memory, and swap used by cgroups» (Контроллер памяти умеет учитывать и ограничивать память процессов, память ядра и подкачку, используемые контрольными группами). Отсюда же следует главное для урока свойство: лимит и учёт относятся к ГРУППЕ, а не к отдельному процессу.https://man7.org/linux/man-pages/man7/cgroups.7.html
- proc(5): /proc/pid/oom_score_adj, Linux man-pages 6.7Официальная документация. Как ядро выбирает жертву: «The badness heuristic assigns a value to each candidate task ranging from 0 (never kill) to 1000 (always kill) to determine which process is targeted. The units are roughly a proportion along that range of allowed memory the process may allocate from, based on an estimation of its current memory and swap use. For example, if a task is using all allowed memory, its badness score will be 1000» (Эвристика вредности присваивает каждому кандидату значение от 0 („никогда не убивать“) до 1000 („убивать всегда“), чтобы определить цель. Единицы — примерно доля от того объёма памяти, который процессу разрешено занять, по оценке его текущего потребления памяти и подкачки. Например, если задача использует всю разрешённую память, её оценка вредности будет 1000). И то, что делает эту оценку применимой к контейнеру: «If it is due to a memory limit (or swap limit) being reached, the allowed memory is that configured limit» (Если причина — достижение лимита памяти (или лимита подкачки), то разрешённой памятью считается этот настроенный лимит). Плюс граница ручной подстройки: «Acceptable values range from -1000 (OOM_SCORE_ADJ_MIN) to +1000 (OOM_SCORE_ADJ_MAX)» (Допустимые значения — от −1000 (OOM_SCORE_ADJ_MIN) до +1000 (OOM_SCORE_ADJ_MAX)), и что даёт нижнее: «The lowest possible value, -1000, is equivalent to disabling OOM-killing entirely for that task, since it will always report a badness score of 0» (Наименьшее возможное значение, −1000, равносильно полному отключению OOM-killer для этой задачи, поскольку её оценка вредности всегда будет нулевой).https://man7.org/linux/man-pages/man5/proc_pid_oom_score_adj.5.html
- proc(5): /proc/pid/oom_score, Linux man-pages 6.7Официальная документация. Чем оценка вредности считается в первую очередь: «The basis for this score is the amount of memory used by the process» (Основа этой оценки — объём памяти, используемый процессом). Это и есть ответ на вопрос «кого убьют»: не того, кто попросил последним, а того, кто держит больше.https://man7.org/linux/man-pages/man5/proc_pid_oom_score.5.html
- Bash Reference Manual, раздел Exit StatusОфициальная документация. Откуда 137 и 143: «When a command terminates on a fatal signal N, bash uses the value of 128+N as the exit status» (Когда команда завершается по фатальному сигналу N, bash использует в качестве кода выхода значение 128+N). Это соглашение того, кто печатает результат, а не ядра: ядро отдаёт номер сигнала отдельно от кода выхода.https://www.gnu.org/software/bash/manual/bash.html#Exit-Status
- signal(7), Linux man-pages 6.7Официальная документация. Почему после убийства в логе приложения ничего нет: «SIGKILL and SIGSTOP cannot be caught, blocked, or ignored» (SIGKILL и SIGSTOP нельзя перехватить, заблокировать или проигнорировать). Ни обработчика, ни последней записи в лог, ни сброса буферов.https://man7.org/linux/man-pages/man7/signal.7.html