Webrium
Подписаться

Контейнер убили по памяти, а куча чистая

Как найти настоящую причину убийства контейнера по памяти, если куча чистая, а метрики врут

Превышение лимита памяти вызывает убийство процессаЛимит памятиУбит сигналом

Код завершения 137 не доказывает нехватку памяти

Код завершения 137 складывается из 128 и номера сигнала KILL (9). Это не диагноз, а симптом: процесс получил SIGKILL и завершился немедленно. Тем же кодом заканчиваются истёкший срок мягкой остановки после SIGTERM, перезапуск по отказавшей пробе живости, снятие контейнера при разгрузке узла и ручное убийство — причина по коду не определяется. Доказательства, что убила именно нехватка памяти, лежат в другом месте: в состоянии контейнера и в файле событий памяти контрольной группы. Первое выставляется только при убийстве главного процесса, второе фиксирует и убийство дочернего.

Контейнер делит ядро с хостом и ограничен снаружи контрольной группой. Устройство контейнера разобрано в материале Docker простыми словами, здесь оно берётся как данность. Превышение квоты процессора не убивает никогда: процесс ждёт следующего окна и продолжает работать. С памятью иначе, и устроена она сложнее: в новой схеме контрольных групп у неё мягкий порог и жёсткий предел. Мягкий порог только заставляет группу освобождать память и не убивает никогда, жёсткий предел убивает без предупреждения. Оркестратор по умолчанию выставляет только жёсткий предел, а мягкий порог, если он нужен, задаётся отдельно. Без него и торможение, и убийство приходят от одной ручки — от приближения к жёсткому пределу. При этом подход к пределу памяти выглядит как торможение: ядро уходит в освобождение памяти в самом аллоцирующем процессе, поэтому по симптому «стоит колом» память отбрасывать нельзя. Общий разбор «стоит колом» есть в материале Сервер тормозит; здесь он не отменяется, а уточняется: память из подозреваемых не выпадает.

Что делают первым и почему это не помогает

Первое, что делают с потолком памяти рантайма, — выставляют его равным пределу контейнера: раз память контейнеру выделена, пусть используется вся. После этого убийства становятся неизбежными. Потолок ограничивает кучу, а в счёт контрольной группы, кроме неё, идут стеки потоков, нативные аллокации, отображённые файлы и разделяемая память. Рантайм видит незанятый остаток под своим потолком и разрешает очередную аллокацию ровно в тот момент, когда ядро уже принимает решение убивать. Запас на всё, что кучей не является, при таком потолке равен нулю, и предел контейнера перестаёт быть границей с полем для манёвра.

Второй шаг — посмотреть профиль кучи. Если он чистый, расследование часто закрывают. Это ошибка: память уходит не только в кучу. Стеки потоков, нативные аллокации, отображённые файлы и разделяемая память тоже входят в счёт контрольной группы и могут довести его до предела при безупречной куче.

Два счёта памяти: почему они не сходятся

Внутри контейнера системные файлы состояния показывают память и процессоры хоста, а не предел контейнера. Предел и текущее потребление доступны только в файлах контрольной группы. При этом память, освобождённая сборщиком мусора, не обязана возвращаться ядру: рантайм держит страницы за собой под будущие аллокации, и для ядра они остаются занятыми. Поэтому чистый профиль кучи и счёт контрольной группы у предела — не противоречие, а норма.

В счёт памяти контрольной группы входят не только куча приложения, но и стеки потоков, нативные аллокации, отображённые файлы и кеш страниц. Ядро убивает не тогда, когда счёт дошёл до предела, а тогда, когда очередная попытка освободить память не покрыла запрос. Файловый кеш под давлением вытесняется, поэтому счёт у предела сам по себе ещё не улика. Метрика контейнера в оркестраторе показывает не весь счёт, а рабочий набор — счёт за вычетом неактивного файлового кеша. Активный кеш вытесняется тоже, но в рабочий набор входит, поэтому и эта панель не доказательство.

Без подкачки анонимная память — куча, стеки потоков, нативные буферы — не вытесняется вообще. Весь манёвр ядра перед убийством ограничен страницами, за которыми стоит файл на диске, — кешем страниц и отображёнными файлами. Чистые ядро просто отбрасывает, грязные сначала записывает на диск, поэтому освобождение идёт медленнее, но вытесняются и они. Запас до убийства равен объёму вытесняемого: больше отнимать ядру нечего. Пока ядру есть что вытеснять, оно освобождает память прямо в аллоцирующем процессе, и контейнер заметно тормозит. Как только вытеснять становится нечего, приходит убийство — промежуточной ступени между торможением и убийством нет.

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

Кто убил и какой след остался

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

Предупредить процесс о будущем убийстве нельзя: SIGKILL не доставляется заранее и не перехватывается. Наблюдать за подходом к пределу снаружи можно: контрольная группа публикует показатель давления по памяти и файл событий со счётчиками — сколько раз группа упиралась в предел и сколько раз в ней убивали. Счётчик убийств — естественное место для оповещения: он, в отличие от кода завершения, не путается со штатными остановками и не теряется, когда убит дочерний процесс.

Запрос памяти в оркестраторе, в отличие от запроса процессора, не становится для ядра ни пределом, ни резервированием. Предел контрольной группы от него не зависит, и момент убийства по превышению предела он не двигает. Одно он всё же задаёт — поправку к оценке жертвы, по которой ядро выбирает, кого убивать при нехватке памяти на узле, и место пода в очереди на вытеснение. Чем больше запрошено, тем позже до него доходит очередь. Запрос процессора при этом становится весом и начинает действовать, когда процессора на узле не хватает.

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

Как проверить и что исправить

Часть своих параметров рантайм берёт от предела контрольной группы, часть — от параметров машины, и у разных рантаймов это разные параметры. Размер кучи, число потоков сборщика мусора и размеры пулов потоков проверяются по отдельности — командой самого рантайма, которая печатает действующие настройки, запущенной внутри контейнера. Вторая половина картины — то, от чего считает ядро: в новой схеме контрольных групп предел и текущий счёт лежат рядом.

cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.current

В старой схеме те же величины называются memory.limit_in_bytes и memory.usage_in_bytes и лежат в подкаталоге memory.

Аллокатор системной библиотеки заводит отдельные области по числу видимых процессоров, а видит он процессоры хоста. Квота его при этом не ограничивает — областей столько, сколько процессоров у хоста. Каждый поток приносит с собой свой стек. С квотой потребление мимо кучи связано по другой линии: рантайм, который считает от неё число потоков, вместе с каждым потоком приносит его стек. И то и другое после спада нагрузки обратно не возвращается.

Предел памяти может быть выставлен не на самом контейнере, а на родительской группе — на поде целиком или на срезе управляющей системы. Тогда убийство происходит при непревышенном пределе контейнера. Несколько процессов в одном контейнере, надзиратель и рабочие, делят один предел на всех.

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

Запас на не-кучу считается разницей между счётом контрольной группы и потолком, заданным рантайму, под ровной нагрузкой. Это и есть ответ, сколько памяти остаётся на стеки потоков, нативные аллокации, отображённые файлы и разделяемую память.

Отличить тесный предел от утечки помогают три признака: возвращается ли нижняя точка потребления после спада нагрузки, сокращается ли время от старта до убийства при неизменном пределе, привязан ли рост к трафику или ко времени работы. Подробнее эта развилка разобрана в материале Память течёт.

Позиция редакции

Предел контейнера — не настройка приложения, а граница, за которой ядро убивает без предупреждения. Поэтому потолок рантайма задаётся явно и ниже этой границы, с запасом на всё, что кучей не является. Потолок не уменьшает потребность сервиса в памяти и не заменяет разбор — он переносит решение с ядра на рантайм, чтобы тот освобождал раньше, а не был убит.

Позиция неверна, если процесс продолжают убивать при неизменном потолке рантайма и растущем счёте контрольной группы. Тогда память уходит мимо кучи — в нативные аллокации, стеки потоков, отображённые файлы или разделяемую память. Сюда же случай, когда предел выставлен на родительской группе, а не на контейнере, и потолок рантайма к делу вообще не относится.

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Потребление растёт и не выходит на полкурост без полки
Углубление

Память течёт: как найти и чем мерить

Как диагностировать утечки памяти в контейнерах: от метрик до типовых причин и инструментов для анализа.

Четыре ключевых ресурса сервера: один из показателей зашкаливаетНехватка ресурса
Практика

Сервер тормозит: команды первой минуты

Узнайте, как за минуту диагностировать нехватку CPU, памяти, диска или сети и сохранить данные до перезагрузки.

Вложенные слои: программа, зависимости и настройки упакованы в контейнер DockerПрограмма в Docker
Предыстория

Docker простыми словами: что он решает и чего не решает

Как Docker упаковывает программы, чтобы они работали везде — и почему иногда этого недостаточно.

Три датчика дискового пространства: блоки в норме, записи о файлах зашкаливаютЗаписи о файлах
Усиление

Место на диске кончилось, а удалять нечего

Если контейнер упал по памяти, а диск полон — проверьте не только блоки, но и метаданные, скрытые дескрипторы и кэши служб, которые могут блокировать освобождение места даже после удаления файлов.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

↑↓ выбрать · Enter открыть · Esc закрыть