Чиним ошибку OOMKilled: настраиваем лимиты памяти Docker

```bash…

Чиним ошибку OOMKilled: настраиваем лимиты памяти Docker

$ docker run --name memory-hog -m 50m ubuntu bash -c "head -c 100M < /dev/zero | tail"

$ docker ps -a

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

8f9e10a11b12 ubuntu "bash -c 'head -c 100…" 3 seconds ago Exited (137) 2 seconds ago memory-hog

```

Перед вами классический сценарий внезапного падения контейнера. Статус `Exited (137)` однозначно указывает на то, что процесс был принудительно завершен операционной системой. В мире контейнеризации это эквивалентно аварийной остановке: ядро Linux зафиксировало превышение лимитов и натравило на контейнер механизм OOM (Out of Memory) Killer, отправив ему сигнал `SIGKILL` (9).

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

---

Анатомия ошибки 137: почему ядро Linux убивает ваш контейнер

В основе изоляции ресурсов в Docker лежит механизм ядра Linux под названием cgroups (control groups). Именно он определяет, сколько вычислительной мощности, дискового ввода-вывода и оперативной памяти может использовать конкретная группа процессов.

Когда вы запускаете контейнер, Docker создает для него отдельную cgroup. Если для этой группы задан жесткий лимит памяти, cgroup-контроллер начинает пристально следить за каждой операцией выделения страниц (page allocation). Как только процесс внутри контейнера запрашивает память, превышающую установленный лимит, и у системы не остается свободных страниц для кэширования, активируется OOM Killer ядра.

Алгоритм OOM Killer выбирает «жертву» на основе показателя `oom_score`. Этот индекс рассчитывается исходя из процента потребляемой памяти и значения `oom_score_adj` (корректировка приоритета). Контейнер, превысивший свой лимит cgroup, мгновенно получает максимальный приоритет на уничтожение. Ядро отправляет процессу с PID 1 внутри контейнера сигнал `SIGKILL`, что приводит к моментальной остановке с кодом выхода 137.

Давайте разберем, как проверить настраиваем лимиты памяти docker в вашей системе, чтобы точно локализовать проблему. Для этого используем команду `docker inspect`, которая возвращает подробный JSON-манифест состояния контейнера.

```bash

$ docker inspect memory-hog --format='{{json.State}}'

```

В выводе нас интересует конкретный блок параметров:

```json

{

"Status": "exited",

"Running": false,

"Paused": false,

"Restarting": false,

"OOMKilled": true,

"Dead": false,

"ExitCode": 137,

"Error": "",

"StartedAt": "2026-03-30T10:00:00.123456789Z",

"FinishedAt": "2026-03-30T10:00:02.987654321Z"

}

```

Если флаг `"OOMKilled"` равен `true`, а `"ExitCode"` равен `137` — диагноз подтвержден. Приложение уперлось в физический лимит памяти, выделенный контейнеру.

---

Настройка жестких и мягких лимитов: флаги --memory и --memory-reservation

По умолчанию Docker-контейнер не имеет ограничений на потребление RAM и может занять всю доступную память хост-машины. Это крайне опасная конфигурация для продакшена: один прожорливый микросервис с утечкой памяти способен уронить всю ноду вместе с системными демонами.

Для управления аппетитами контейнеров используются два основных параметра: жесткий лимит (hard limit) и мягкий лимит (soft limit).

Жесткий лимит (`--memory` или `-m`)

Этот параметр задает максимальный объем физической памяти, который контейнер может использовать. Если приложение попытается перешагнуть этот порог, и swap-память будет исчерпана (или отключена), контейнер будет убит. Минимально допустимое значение для этого флага составляет 4MB.

Пример запуска с жестким лимитом в 512 Мегабайт:

```bash

$ docker run -d --name web-app -m 512m nginx

```

Мягкий лимит (`--memory-reservation`)

Этот параметр устанавливает «гарантированный» объем памяти, который хост обязуется предоставить контейнеру. В отличие от жесткого лимита, `--memory-reservation` не останавливает контейнер при превышении указанного объема, если на хосте достаточно свободных ресурсов. Однако, когда на сервере начинается дефицит RAM, система начинает принудительно сжимать потребление памяти контейнерами именно до уровня их soft limit.

ПараметрТип лимитаПоведение при превышенииСценарий использования
`--memory` (или `-m`)Жесткий (Hard limit)Контейнер завершается с ошибкой OOMKilled (Exit Code 137).Защита хост-системы от критического исчерпания RAM.
`--memory-reservation`Мягкий (Soft limit)Контейнер продолжает работу, если на хосте есть свободная память.Резервирование гарантированного минимума ресурсов под сервис.

Настройка лимитов в Docker Compose

В современных инфраструктурных проектах ручной запуск через CLI используется редко. Для декларативного описания лимитов в файлах `docker-compose.yml` (для версии спецификации 3 и выше) используется блок `resources`:

```yaml

version: '3.8'

services:

api-service:

image: backend-node-app:latest

deploy:

resources:

limits:

memory: 1024M

reservations:

memory: 512M

restart: on-failure

```

В этой конфигурации мы гарантируем приложению 512MB RAM, но разрешаем ему разгоняться до 1024MB при наличии свободных ресурсов на сервере. При попытке выйти за гигабайт контейнер будет перезапущен благодаря политике `restart: on-failure`.

---

Управление swap-памятью и риски отключения OOM Killer

Поведение контейнера при достижении лимита памяти во многом зависит от настроек swap (файла или раздела подкачки). За это отвечает параметр `--memory-swap`.

Важно понимать формулу расчета: параметр `--memory-swap` указывает суммарный объем физической памяти (RAM) и swap-пространства, доступный контейнеру. Это не размер отдельного swap-файла, а общий лимит `RAM + Swap`.

Рассмотрим три сценария настройки этого параметра:

1. `--memory-swap` равен удвоенному значению `--memory` (поведение по умолчанию).

Если вы запустили контейнер с флагом `-m 500m` и не указали настройки swap, контейнер сможет использовать 500MB физической памяти и 500MB swap (всего 1GB).

2. `--memory-swap` равен значению `--memory`.

В этом случае swap для контейнера полностью заблокирован. Контейнер сможет использовать только выделенный объем RAM. При его превышении сразу сработает OOM Killer.

```bash

$ docker run -d -m 500m --memory-swap 500m my-app

```

3. `--memory-swap` равен `-1`.

Контейнеру разрешено использовать неограниченный объем swap-памяти хоста (если swap включен на уровне ОС).

Использование swap позволяет предотвратить моментальное падение приложения при кратковременных пиках нагрузки, однако за это приходится платить производительностью. Скорость чтения/записи на диск (даже на быстрые NVMe-накопители) на несколько порядков ниже скорости работы с оперативной памятью. При активном использовании swap приложение может войти в состояние «thrashing» (постоянная перезапись страниц памяти на диск и обратно), из-за чего его производительность упадет практически до нуля.

Флаг `--oom-kill-disable`

В Docker существует параметр, позволяющий отключить OOM Killer для конкретного контейнера:

```bash

$ docker run -d -m 500m --oom-kill-disable my-app

```

Отключение OOM Killer через `--oom-kill-disable` без жесткого ограничения физической памяти (`-m`) — это верный способ повесить всю хост-систему. Если ядро Linux не сможет убить прожорливый контейнер, оно начнет завершать другие критически важные процессы операционной системы, включая сам демон Docker. Используйте этот параметр только тогда, когда у вас настроен внешний мониторинг и жесткие лимиты на уровне cgroups.

---

Диагностика и мониторинг: как отследить потребление ресурсов в реальном времени

Когда мы исследуем, как проверить настраиваем лимиты памяти docker, информационные технологии, ии-инструменты и saas-решения предоставляют нам массу готовых дашбордов (Prometheus, Grafana, Datadog), но базовый `docker stats` остается самым быстрым способом локальной диагностики.

Команда `docker stats` выводит динамический поток системных метрик для всех запущенных контейнеров:

```bash

$ docker stats

```

Вывод команды выглядит следующим образом:

```text

CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS

8f9e10a11b12 web-app 0.15% 256.4MiB / 512MiB 50.08% 1.2kB / 0B 0B / 12.3MB 18

a9c8d7e6f5a4 db-service 1.42% 1.2GiB / 4GiB 30.00% 45.1kB / 120kB 105MB / 1.1GB 42

```

Здесь колонка `MEM USAGE / LIMIT` наглядно показывает текущее потребление физической памяти и установленный жесткий лимит. Если процентное соотношение в колонке `MEM %` приближается к 90-95%, это верный сигнал к тому, что контейнер находится в зоне риска и требует оптимизации или расширения лимитов.

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

Если вам нужно получить метрики в неинтерактивном режиме (например, для отправки в систему сбора логов или скрипт автоматизации), используйте флаг `--no-stream`:

```bash

$ docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"

```

---

Поиск утечек памяти вместо бесконечного масштабирования лимитов

Увеличение лимитов памяти в конфигурационных файлах — это временное решение (workaround), которое лишь оттягивает момент падения, если в коде приложения присутствует утечка памяти (memory leak). Если график потребления ресурсов контейнером представляет собой непрерывно растущую прямую без фазы плато, никакое увеличение RAM не спасет систему от очередного OOMKilled.

Для выявления причин утечек памяти рекомендуется следовать методичному алгоритму:

1. Профилирование приложения (Profiling).

Запустите профилировщик для вашего стека технологий.

* Для Node.js используйте встроенный инспектор (`node --inspect`) и снимите снимки кучи (heap snapshots) через Chrome DevTools. Сравните снимки, сделанные сразу после запуска и после имитации нагрузки.

* Для Go используйте пакет `net/http/pprof` для анализа профиля выделения памяти (heap profile).

* Для Java (JVM) настройте параметры логирования сборщика мусора `-XX:+PrintGCDetails` и используйте утилиты типа `jmap` или `VisualVM` для анализа утечек в Heap.

2. Анализ конфигурации сборщика мусора (Garbage Collector).

В некоторых средах исполнения (например, Java) виртуальная машина не знает о том, что она запущена внутри контейнера с ограничениями cgroups. JVM может ориентироваться на общий объем RAM хост-системы и выделять кучу (Heap), превышающую лимит контейнера. Для решения этой проблемы в Java 10+ по умолчанию включена поддержка cgroups, но параметры `-XX:MaxRAMPercentage` и `-XX:InitialRAMPercentage` все равно требуют ручной калибровки.

3. Проверка утечек ресурсов на уровне ОС.

Убедитесь, что приложение корректно закрывает файловые дескрипторы, сетевые соединения и соединения с базами данных. Незакрытый сокет или зависший файловый дескриптор в Linux резервирует под себя область системной памяти, которая освобождается только после перезапуска процесса.

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