Код завершения 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.
Аллокатор системной библиотеки заводит отдельные области по числу видимых процессоров, а видит он процессоры хоста. Квота его при этом не ограничивает — областей столько, сколько процессоров у хоста. Каждый поток приносит с собой свой стек. С квотой потребление мимо кучи связано по другой линии: рантайм, который считает от неё число потоков, вместе с каждым потоком приносит его стек. И то и другое после спада нагрузки обратно не возвращается.
Предел памяти может быть выставлен не на самом контейнере, а на родительской группе — на поде целиком или на срезе управляющей системы. Тогда убийство происходит при непревышенном пределе контейнера. Несколько процессов в одном контейнере, надзиратель и рабочие, делят один предел на всех.
Предел контейнера и потолок памяти рантайма меняются вместе, одним изменением. Поднятый предел при неизменном потолке рантайма не даёт куче ничего, а поднятый потолок рантайма при неизменном пределе приближает убийство.
Запас на не-кучу считается разницей между счётом контрольной группы и потолком, заданным рантайму, под ровной нагрузкой. Это и есть ответ, сколько памяти остаётся на стеки потоков, нативные аллокации, отображённые файлы и разделяемую память.
Отличить тесный предел от утечки помогают три признака: возвращается ли нижняя точка потребления после спада нагрузки, сокращается ли время от старта до убийства при неизменном пределе, привязан ли рост к трафику или ко времени работы. Подробнее эта развилка разобрана в материале Память течёт.
Позиция редакции
Предел контейнера — не настройка приложения, а граница, за которой ядро убивает без предупреждения. Поэтому потолок рантайма задаётся явно и ниже этой границы, с запасом на всё, что кучей не является. Потолок не уменьшает потребность сервиса в памяти и не заменяет разбор — он переносит решение с ядра на рантайм, чтобы тот освобождал раньше, а не был убит.
Позиция неверна, если процесс продолжают убивать при неизменном потолке рантайма и растущем счёте контрольной группы. Тогда память уходит мимо кучи — в нативные аллокации, стеки потоков, отображённые файлы или разделяемую память. Сюда же случай, когда предел выставлен на родительской группе, а не на контейнере, и потолок рантайма к делу вообще не относится.