Производительность и эффективность Docker-контейнеров: как не терять ресурсы | AdminWiki

Производительность и эффективность Docker-контейнеров: как не терять ресурсы

01 сентября 2026 21 мин. чтения
Содержание статьи

Docker-контейнер обычно потребляет заметно меньше ресурсов, чем отдельная виртуальная машина: он использует ядро хоста и не запускает собственную гостевую операционную систему. В простое постоянная нагрузка на CPU и память чаще всего невелика. Основные потери появляются во время работы приложения: при интенсивном дисковом вводе-выводе, записи логов, копировании данных через overlay-файловую систему, сетевом NAT и конкуренции нескольких сервисов за один хост.

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

Рабочий порядок выглядит так: зафиксировать базовый уровень, измерить CPU, память, диск и сеть, найти узкое место, задать лимиты с запасом, повторить тест под штатной и пиковой нагрузкой. Такой подход помогает сохранить стабильность, плотность размещения и предсказуемое время ответа.

Короткий ответ: где Docker теряет ресурсы

Что контейнеризация добавляет к обычному процессу

Контейнер изолирует процессы с помощью Linux namespaces и ограничивает их через cgroups. Процессы используют общее ядро, планировщик CPU, драйверы устройств и сетевой стек хоста. Поэтому Docker не расходует память на отдельный экземпляр ядра и фоновые службы гостевой ОС.

Дополнительные расходы возникают в нескольких слоях:

  • Управление процессами. Docker daemon создает контейнер, подключает namespace, применяет ограничения cgroups и следит за жизненным циклом.
  • Файловая система. overlay2 объединяет слои образа и writable layer контейнера. При изменении существующего файла может понадобиться операция copy-up.
  • Сеть. В bridge-сети используются виртуальные Ethernet-интерфейсы, bridge, правила iptables или nftables и NAT для опубликованных портов.
  • Логирование. Docker может записывать stdout и stderr контейнера в файлы. При большом потоке сообщений это создает постоянный дисковый I/O.
  • Ограничения ресурсов. CPU quota может вызвать throttling, а memory limit при слишком малом значении приводит к OOM Killer.

Запуск idle-контейнера и работа контейнера под нагрузкой дают разные результаты. Первый обычно занимает небольшой объем метаданных и память процесса приложения. Второй может активно расходовать page cache, CPU, IOPS, сетевой bandwidth и файловые дескрипторы.

Какие ресурсы нужно контролировать

РесурсЧто измерятьКакие симптомы искать
CPUУтилизация, load average, run queue, throttled time, steal timeРост latency, очередь процессов, постоянное ограничение quota
ПамятьRSS, page cache, peak usage, swap, memory eventsOOM, перезапуски, reclaim, замедление соседних сервисов
ДискIOPS, throughput, latency, await, util, очередь устройстваМедленные fsync, высокий iowait, задержки чтения и записи
ЛогиРазмер файлов, скорость записи, число файлов ротацииПереполнение раздела, постоянный фоновой I/O
СетьПропускная способность, PPS, latency, drops, errors, retransmitsПотери пакетов, рост времени ответа, загрузка одного CPU
ПроцессыКоличество PID, файловые дескрипторы, потокиОшибки fork, исчерпание ulimit, зависание worker-процессов

Процент CPU из docker stats не описывает состояние всей системы. Контейнер может почти не использовать процессор, но ждать диск, блокировку, DNS-ответ или свободное соединение с базой данных. Диагностика должна включать метрики хоста.

Какие накладные расходы Docker возникают на практике

CPU: namespaces, cgroups и сетевой путь

Linux-планировщик видит процессы контейнера почти так же, как процессы хоста. Namespace меняет видимое окружение, но не создает отдельный процессор. Cgroups задают вес, квоту и набор доступных ядер.

В обычном состоянии Docker daemon не тратит заметную долю CPU на каждый контейнер. Его нагрузка растет при частом запуске и остановке процессов, большом количестве событий, активном healthcheck, интенсивном логировании и работе сетевых правил.

Параметр --cpu-shares или его эквивалент cpu.weight задает относительный приоритет. Он проявляется, когда несколько групп одновременно хотят получить CPU. При свободных ядрах контейнер с небольшим weight может использовать доступную мощность.

--cpus=2 задает средний верхний предел, равный двум ядрам. Тот же результат можно получить через пару --cpu-period и --cpu-quota. При слишком низкой квоте процесс получает меньше времени, чем требует рабочая нагрузка, и начинает ждать следующий период. Это называется CPU throttling.

Сетевой путь способен добавить CPU-расход на обработку большого числа пакетов, особенно при малом размере пакета и высокой PPS-нагрузке. Для обычного HTTP-сервиса через bridge разница часто не определяет результат. Для прокси, телеметрии, балансировщика или сервиса с миллионами мелких пакетов ее нужно измерить.

Высокая загрузка CPU внутри контейнера сама по себе не доказывает проблему Docker. Причиной могут быть сериализация JSON, шифрование TLS, сборка мусора runtime, синхронные блокировки или неэффективный запрос к базе данных.

Память: RSS, page cache и OOM

RSS показывает страницы памяти, которые процессы держат в оперативной памяти. Page cache хранит недавно прочитанные и записанные данные файловой системы. Для базы данных, очереди или файлового сервиса кэш может повышать скорость, но он тоже учитывается при контроле доступного запаса памяти.

Отображение cache в docker stats зависит от версии Docker и способа учета cgroups. Поэтому при спорных значениях нужно сверять статистику контейнера с данными cgroup и хоста. Смотрите не одну текущую цифру, а пик, динамику и события давления на память.

Жесткий memory limit защищает хост от бесконтрольного роста контейнера. Когда процессу требуется больше доступной памяти и система не может освободить страницы, cgroup может вызвать OOM Killer. В результате контейнер внезапно завершается или перезапускается по заданной политике.

Слишком малый лимит проявляется несколькими признаками:

  • процесс завершается без понятной ошибки в собственном логе;
  • растет число рестартов;
  • увеличивается p95 latency перед аварией;
  • появляются ошибки выделения heap или отказа в создании потока;
  • соседние сервисы теряют память из-за давления на общий хост.

Swap может сгладить короткий пик, но медленный swap превращает нехватку памяти в рост latency. Для базы данных и сервисов с жесткими требованиями к времени ответа обычно задают понятную политику swap и оставляют запас для ядра, Docker и фоновых задач.

Диск: overlay2, volumes и логи

Образ Docker состоит из read-only слоев. Контейнер получает writable layer, куда попадают изменения, если приложение пишет в путь без отдельного тома. При изменении файла из нижнего слоя overlay2 сначала копирует его в верхний слой, после чего применяет запись. Для больших файлов или частых изменений это создает лишнюю работу.

Временные файлы, кэш приложения и базы данных не следует складывать в writable layer без причины. volume или bind mount выносит рабочие данные из слоя контейнера и упрощает резервное копирование. Фактическая скорость зависит от файловой системы хоста, режима монтирования, диска и самого приложения.

Тип храненияПодходящий сценарийРиск
Writable layerНебольшие временные файлы и служебные измененияСложнее управлять объемом, возможен copy-up
Docker volumeПостоянные данные сервиса, которыми управляет DockerНужно явно знать путь и порядок резервного копирования
Bind mountКонтроль каталога на хосте, конфигурации, данные NASПрава доступа и поведение зависят от хостовой файловой системы

Базы данных, очереди задач и сервисы с частыми случайными операциями чувствительны к latency и fsync. Высокий throughput не компенсирует задержку каждого подтверждения записи. Удаленный NAS или сетевой mount может стать главным узким местом даже при свободном CPU.

Логи Docker часто пишутся в JSON-файлы. Если приложение выводит тысячи строк в секунду, каталог Docker быстро растет, а запись журналов начинает конкурировать с рабочими данными. Для каждого сервиса задайте ротацию и контролируйте свободное место.

Сеть: bridge, NAT и публикация портов

В стандартной bridge-сети контейнер получает виртуальный интерфейс veth и подключается к bridge на хосте. Публикация порта добавляет правила фильтрации и NAT. Внутренний DNS Docker помогает контейнерам находить друг друга по именам сетей.

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

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

Ошибки и drops на физическом интерфейсе, перегрузка виртуального bridge, retransmits TCP и медленный DNS часто выглядят как медленная работа приложения. Проверяйте сеть отдельно от CPU и диска.

Для сравнения разных вариантов сети пригодится практическое сравнение Docker, Kubernetes и LXC/Incus, где разобраны latency, throughput и влияние сетевого слоя.

Как измерить производительность Docker-контейнеров

Быстрый обзор через docker stats и inspect

docker stats подходит для первичного поиска контейнера, который расходует больше ресурсов. Команда показывает CPU, память, сетевой трафик, block I/O и число процессов. Снимок нужно сопоставлять с периодом нагрузки и метриками хоста.

docker stats --no-stream

docker stats --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.BlockIO}}\t{{.NetIO}}\t{{.PIDs}}'

docker inspect api

docker inspect -f '{{.Name}} mounts={{json .Mounts}} networks={{json .NetworkSettings.Networks}}' api

docker inspect -f '{{.HostConfig.NanoCpus}} {{.HostConfig.Memory}} {{.HostConfig.MemorySwap}} {{.HostConfig.PidsLimit}}' api

В inspect проверьте лимиты, mounts, сетевые подключения, restart policy, healthcheck, entrypoint и команду запуска. Значение 0 для некоторых полей означает отсутствие соответствующего ограничения, поэтому его нельзя принимать за безопасную настройку.

Для повседневной отладки процессов, логов, рестартов и очистки диска используйте отдельную шпаргалку по управлению запущенными контейнерами Docker.

Проверка CPU и памяти на уровне Linux

Сначала определите, занят ли CPU вычислениями или ожиданием. vmstat показывает run queue, свободную память, swap и iowait. pidstat помогает связать процесс с конкретным контейнером. top полезен для быстрого обзора, но не заменяет временной ряд.

uptime
vmstat 1
pidstat -u -r -d 1
ps -eo pid,ppid,stat,pcpu,pmem,rss,comm --sort=-pcpu | head -n 20
free -h
cat /proc/pressure/cpu
cat /proc/pressure/memory

В cgroups v2 проверьте файлы cpu.stat, memory.current, memory.events и memory.max. В cpu.stat ищите nr_throttled и throttled_usec. В memory.events важны счетчики high, oom и oom_kill. Пути к cgroup зависят от дистрибутива и режима запуска, поэтому сначала найдите каталог по идентификатору контейнера.

container_id=$(docker inspect -f '{{.Id}}' api)
find /sys/fs/cgroup -type f \( -name cpu.stat -o -name memory.events \) 2>/dev/null | head
cat /sys/fs/cgroup/.../cpu.stat
cat /sys/fs/cgroup/.../memory.events

Низкий процент CPU при высоком времени ответа часто указывает на ожидание диска, сети или блокировки. Высокий load average при свободном CPU может возникать из-за процессов в состоянии uninterruptible sleep, обычно при дисковом I/O.

Диагностика диска и сети

Для диска важны не только мегабайты в секунду. Смотрите latency, await, util, queue size и долю операций чтения и записи. Устройство с почти полной загрузкой и растущей очередью не справляется с текущим профилем операций.

iostat -xz 1
pidstat -d 1
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS
sudo du -xhd1 /var/lib/docker 2>/dev/null | sort -h
docker system df -v

Путь /var/lib/docker может отличаться, а команда du на большой системе создает дополнительное чтение. Запускайте ее вне пикового окна. Не удаляйте каталоги вручную: для очистки используйте команды Docker и заранее проверьте volumes.

Сеть проверяйте с трех сторон: интерфейс хоста, контейнерная сеть и прикладной запрос.

ip -s link
ss -s
sar -n DEV 1
etstat -s 2>/dev/null | head -n 40
iperf3 -c HOST -t 30
curl -sS -o /dev/null -w 'code=%{http_code} time=%{time_total}\n' http://127.0.0.1:8080/health

Команда iperf3 нужна для отдельной проверки пропускной способности, а curl показывает прикладную задержку. Не делайте вывод о качестве сети по одному успешному запросу. Для сервисов с требованиями к времени ответа фиксируйте p95 и p99.

Если контейнер использует NAS, измерьте путь к удаленному хранилищу отдельно. Сравните latency локального диска, сетевого mount и volume. Проверьте протокол, параметры mount, права доступа, блокировки и поведение при временной потере связи.

Контрольный тест нагрузки

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

  1. Зафиксируйте baseline в простое в течение 5-10 минут.
  2. Запустите штатный сценарий на одинаковом наборе запросов или данных.
  3. Запишите throughput, p95 и p99 latency, средний и пиковый CPU, throttled time, memory peak, OOM events, iowait, latency диска и ошибки сети.
  4. Повторите тот же тест после одного изменения конфигурации.
  5. Сравните не средние значения, а хвост задержек и число ошибок.
СостояниеМинимальная длительностьЧто фиксировать
Простой5-10 минутФоновый CPU, память, логи, свободное место
Штатная нагрузка15-30 минутLatency, throughput, I/O, сеть, рестарты
Пик10-15 минутThrottle, OOM, очередь диска, drops, запас хоста

Единичный вывод docker stats не показывает, как сервис ведет себя после прогрева кэша, заполнения очереди или ротации логов. Нагрузка должна длиться достаточно долго, чтобы проявились фоновые операции и накопление данных.

Docker лимиты ресурсов: CPU, память и I/O

CPU quota, shares и привязка к ядрам

У CPU-настроек три разных назначения:

  • Quota. Ограничивает максимальное процессорное время контейнера.
  • Shares или weight. Определяет относительный приоритет при конкуренции.
  • Cpuset. Ограничивает контейнер конкретным набором логических CPU.

Пример запуска сервиса с лимитом двух CPU:

docker run -d \
  --name api \
  --cpus=2 \
  --cpu-shares=1024 \
  --memory=1g \
  --memory-reservation=768m \
  --memory-swap=1g \
  --pids-limit=512 \
  example/api:stable

При значении --memory-swap=1g и --memory=1g дополнительное использование swap для контейнера обычно запрещено, если хост и версия cgroups поддерживают такой режим. Поведение нужно проверять в конкретной системе.

--cpus=2 защищает соседей от бесконечного роста CPU, но не гарантирует, что контейнер постоянно получит два свободных ядра. Если сервис регулярно упирается в quota и растет throttled_usec, его latency может ухудшиться даже при формально доступной мощности хоста.

--cpuset-cpus=2,3 полезен для чувствительного к кэшу сервиса, сетевого обработчика или изоляции фоновых задач. При слишком узком наборе CPU балансировка ухудшается, а одно ядро может стать перегруженным. Pinning применяйте после измерения.

Memory limit и memory-swap

Начинайте с наблюдаемого рабочего набора. Если сервис в пике использует 700 МБ, лимит 700 МБ оставляет слишком мало места для page cache, кратковременного роста heap, потоков и служебных структур. Запас выбирайте по тесту, а не по круглому числу.

memory-reservation задает мягкий ориентир для давления на память. memory задает жесткую границу. memory-swap определяет общую сумму RAM и swap, доступную контейнеру, но точные эффекты зависят от cgroups, наличия swap на хосте и версии Docker.

Для JVM, Node.js, Python и других runtime учитывайте внутренние лимиты heap. Контейнер с memory limit в 1 ГБ не всегда должен отдавать весь этот объем heap-процессу. Оставьте место для native memory, библиотек, потоков и page cache.

После задания лимита проверьте:

  • пиковое значение памяти во время прогрева и нагрузки;
  • события oom и oom_kill;
  • частоту swap in и swap out на хосте;
  • latency при заполнении кэша;
  • поведение после нескольких рестартов и длительной работы.

Ограничения процессов, диска и сети

--pids-limit защищает хост от бесконтрольного создания процессов и потоков. Для веб-сервиса с фиксированным числом worker-процессов можно задать несколько сотен PID. Для очереди задач, которая запускает дочерние процессы, значение подбирают по реальному пику.

Параметры ulimits регулируют файловые дескрипторы, размер core dump и другие лимиты процесса. Низкий nofile выглядит как сетевые ошибки или невозможность открыть файл, хотя CPU и память остаются свободными.

docker run -d \
  --name proxy \
  --pids-limit=256 \
  --ulimit nofile=65536:65536 \
  example/proxy:stable

Ограничения блочного I/O зависят от драйвера, ядра и файловой системы. Параметры blkio.weight или аналогичные настройки могут задавать приоритет, но не создают отдельный физический диск. Для критичных баз данных надежнее разделить устройства или классы хранения.

Docker не заменяет полноценную сетевую политику. Bandwidth, rate limit, QoS и контроль трафика на уровне хоста могут требовать настройки Linux traffic control, оркестратора или внешнего сетевого оборудования. Проверяйте, поддерживает ли выбранная среда нужный параметр.

Дополнительные примеры cgroups, volumes, сетевых драйверов и ограничений собраны в гайде по продвинутому Docker.

Как проверить, что лимит не стал новым узким местом

Лимит считается удачным, когда он защищает соседей и сохраняет целевые показатели сервиса. После настройки сравните тест до и после.

ПроверкаПлохой признакДействие
CPU quotaПостоянный рост throttled timeПроверить quota, параллелизм и необходимость большего CPU
ПамятьOOM или резкие пики reclaimУвеличить запас, уменьшить heap или найти утечку
ДискРастущая очередь и p99 latencyРазделить I/O, вынести данные из writable layer
PIDОшибки fork или создание workerПроверить pids-limit и модель процессов
СетьDrops, retransmits, недостаточная скоростьПроверить интерфейс, MTU, NAT и пропускную способность

В качестве критериев приемки задайте конкретные значения: допустимый p95, отсутствие OOM, приемлемую долю throttled time, контролируемую дисковую очередь и резерв CPU с памятью на хосте. Формулировка должна зависеть от SLA сервиса.

Конкуренция контейнеров и эффект noisy neighbor

CPU-bound и memory-bound сервисы

CPU-bound сервис тратит время на вычисления: обработку запросов, кодирование, компиляцию, шифрование или анализ данных. Memory-bound сервис упирается в объем RAM, задержки доступа к памяти и работу сборщика мусора. Одинаковое количество контейнеров не означает одинаковую потребность в ресурсах.

Например, четыре небольших HTTP-сервиса могут спокойно работать на восьми ядрах, пока фоновый контейнер не начнет кодировать видео. Без quota он заберет свободное процессорное время и увеличит latency API. С quota API сохранит долю CPU, но слишком низкий предел ухудшит его собственное время ответа.

Кэш или runtime с большим heap может вытеснить page cache базы данных. База продолжит работать, но станет чаще читать данные с диска. Симптомом будет рост I/O wait и latency при умеренной загрузке CPU.

Конкуренция за диск и файловый кэш

База данных, контейнер логирования и резервное копирование могут одновременно создавать очередь на одном устройстве. Backup обычно формирует длинный последовательный поток, а база чувствительна к мелким случайным операциям и fsync. Суммарная пропускная способность диска может выглядеть достаточной, но задержка базы вырастет.

Разделяйте рабочие данные, логи и временные файлы, когда это оправдано архитектурой. Ограничьте размер журналов, запускайте тяжелые резервные копии вне пикового окна и контролируйте await, util и очередь устройства.

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

Как изолировать критичные сервисы

Изоляция нужна при подтвержденной конкуренции. Используйте подходящий набор мер:

  • задайте CPU quota и относительный вес для критичного сервиса;
  • зарезервируйте память и оставьте запас для хоста;
  • вынесите базу данных на отдельный диск или класс хранения;
  • ограничьте логирование шумных контейнеров;
  • разделите фоновые и пользовательские задачи по хостам;
  • примените cpuset только после проверки влияния на latency;
  • задайте приоритеты для резервного копирования и фоновых worker-процессов.

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

Как сделать Docker-контейнеры эффективнее без лишней сложности

Уменьшение размера образов и времени запуска

Размер образа влияет на место в registry и на время pull. Он не равен потреблению CPU или памяти после запуска: большой read-only слой может почти не влиять на runtime, если приложение использует малую часть файлов.

Практичные меры для Dockerfile:

  • используйте multi-stage build, чтобы не переносить компиляторы в финальный образ;
  • добавьте точный .dockerignore и исключите исходные архивы, кэши и каталоги сборки;
  • очищайте кэш пакетного менеджера в том же слое, где устанавливаете пакеты;
  • фиксируйте версии базовых компонентов и регулярно пересобирайте образ;
  • запускайте один основной процесс контейнера, а фоновые роли разделяйте на сервисы;
  • удаляйте отладочные инструменты из production-образа, если они не нужны для диагностики.
FROM golang:1.24 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/api ./cmd/api

FROM alpine:3.22
RUN adduser -D -u 10001 app
COPY --from=build /out/api /usr/local/bin/api
USER app
ENTRYPOINT ['/usr/local/bin/api']

Минимальный образ снижает объем хранения и поверхность обновлений, но не заменяет профилирование приложения. После изменения Dockerfile проверьте время pull, старта и прохождения healthcheck.

Правильная работа с данными и томами

Путь записи должен быть явным. Временный кэш можно направить в tmpfs, если он небольшой и не нужен после рестарта. Постоянные данные храните в volume или bind mount с понятным владельцем и заранее проверенным резервным копированием.

docker volume create app-data

docker run -d \
  --name db \
  -v app-data:/var/lib/postgresql/data \
  -v /srv/db-backup:/backup:rw \
  example/database:stable

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

Логирование и ротация

Ограничьте объем локальных логов на уровне Compose или Docker daemon. Для одного сервиса пример выглядит так:

services:
  api:
    image: example/api:stable
    logging:
      driver: json-file
      options:
        max-size: 50m
        max-file: '5'

Такой пример ограничивает локальное хранение примерно пятью файлами по 50 МБ, но точный объем зависит от служебных данных и момента ротации. Значения выбирайте с учетом скорости логирования и окна, необходимого для расследования инцидента.

Снизьте уровень логирования приложения, если debug-сообщения не нужны постоянно. Для большого production-окружения отправляйте журналы в централизованную систему, но измеряйте сетевой трафик, очередь доставки и локальный буфер. Централизация не устраняет стоимость генерации и передачи логов.

Проверяйте свободное место, размер Docker-каталога и скорость роста журналов отдельными метриками. Очистка образов не решает проблему контейнерных логов.

Процессы, healthcheck и корректное завершение

Главный процесс контейнера получает роль PID 1. Он отвечает за обработку сигналов и сбор завершившихся дочерних процессов. Если приложение плохо работает как PID 1, используйте init-процесс, например параметр --init, либо корректно настройте образ.

Graceful shutdown позволяет закрыть соединения, завершить транзакции и сохранить данные до остановки контейнера. Настройте stop timeout под реальное время завершения, особенно для базы данных и очереди задач.

services:
  worker:
    image: example/worker:stable
    init: true
    stop_grace_period: 30s
    healthcheck:
      test: ['CMD', '/usr/local/bin/healthcheck']
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    restart: on-failure

Healthcheck проверяет состояние приложения, но сам по себе не исправляет причину сбоя. restart: always не заменяет анализ логов, exit code, доступности томов и зависимостей. Бесконечный restart loop может создавать дополнительный CPU, I/O и сетевой трафик.

Оптимизация сетевого пути

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

Сокращение лишних proxy-слоев может уменьшить latency и CPU-расход. Режим host стоит применять только при подтвержденной сетевой потребности и понятных последствиях для безопасности. Bridge дает изоляцию и предсказуемую модель портов, поэтому для большинства приложений его достаточно.

Проверьте MTU, DNS, retransmits и drops. Ошибки на физическом интерфейсе или перегруженный reverse proxy нельзя исправить изменением CPU-лимита контейнера.

Типичные ошибки при запуске сервисов в Docker

Сервис запускается и сразу завершается

Сначала определите факт завершения и код выхода:

docker ps -a --no-trunc
docker logs --tail 200 api
docker inspect -f 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}' api
docker inspect -f 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}}' api

Проверьте entrypoint, command, переменные окружения, рабочий каталог и права доступа. Процесс может завершиться штатно, если команда выполнила одно действие и вышла. В другом случае причиной бывает отсутствие конфигурации или недоступный volume.

Поле OOMKilled=true меняет направление поиска: нужно проверить memory limit, peak usage и события cgroup. Не принимайте убийство процесса ядром за ошибку entrypoint.

Постоянные перезапуски и медленный старт

Restart loop часто возникает из-за неверного healthcheck, недоступной базы, неготового DNS, миграции схемы или задержки монтирования хранилища. Начните с времени первого запуска и последующих кодов выхода.

docker inspect -f 'restart={{json .HostConfig.RestartPolicy}} health={{json .State.Health}}' api
docker events --filter container=api --since 10m
docker compose ps
docker compose logs --tail 200 api

Разделяйте состояние процесса и готовность сервиса. Процесс может запуститься за секунду, а база данных будет готова через минуту. Увеличьте start_period, если проверка слишком рано объявляет контейнер нездоровым, но сначала убедитесь, что приложение действительно умеет отвечать после старта.

Меняйте одну настройку за раз. Одновременная смена лимитов, healthcheck и restart policy скрывает причину и усложняет сравнение.

Данные пропадают или storage внезапно заполняется

Данные внутри writable layer относятся к конкретному контейнеру. После удаления контейнера они исчезнут, если путь не подключен к volume или bind mount.

docker inspect -f '{{json .Mounts}}' db
docker system df -v
docker ps -s
find /srv -xdev -type f -size +1G -printf '%s %p\n' 2>/dev/null | sort -n | tail

Проверьте фактический путь записи приложения. Конфигурация может монтировать /var/lib/app, а сервис по ошибке писать в /data. Проверьте владельца каталога, режим доступа, временные файлы, логи и старые образы.

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

Подробный порядок очистки образов, слоев и неиспользуемых объектов приведен в руководстве по управлению Docker-образами в production.

Практический план настройки контейнерной среды

Минимальный чек-лист перед запуском

  1. Запишите версии Docker Engine, Docker Compose, ядра и базовой ОС.
  2. Проверьте число CPU, доступную память, swap и запас ресурсов для операционной системы.
  3. Проверьте свободное место, inode, состояние файловой системы и latency диска.
  4. Определите каталоги для постоянных данных, временных файлов, конфигурации и логов.
  5. Подключите volumes или bind mounts до первого запуска приложения с данными.
  6. Задайте CPU, memory, pids и ulimit для сервисов, которые могут расти без контроля.
  7. Настройте ротацию логов и проверьте, что она действительно работает.
  8. Добавьте healthcheck с разумными interval, timeout, retries и start period.
  9. Определите graceful shutdown и stop timeout.
  10. Проверьте резервное копирование и восстановление хотя бы одного тестового volume.

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

Проверка под штатной и пиковой нагрузкой

Составьте таблицу baseline. Для каждого сервиса укажите нормальный throughput, p95 latency, пиковую память, CPU quota, число процессов, объем логов и тип хранения.

МетрикаШтатный сценарийПиковый сценарийКритерий готовности
p95 latencyФактическое значениеФактическое значениеНиже установленного SLA
CPU throttlingНизкая доляКонтролируемая доляНет постоянного упора в quota
Memory peakРабочий наборРабочий набор с запасомНет OOM и опасного swap
Disk awaitОбычная задержкаПиковая задержкаОчередь не растет бесконечно
Network errorsНоль или фоновой уровеньИзмеренное значениеНет drops и retransmits, влияющих на SLA

Нагрузочный тест проводите на данных и запросах, близких к рабочим. Для базы учитывайте размер индексов и кэша. Для очереди учитывайте скорость поступления, время обработки и накопленный backlog. Для веб-сервиса проверяйте холодный старт, прогретый процесс и длительную работу.

После каждого изменения фиксируйте результат вместе с конфигурацией. Уменьшение размера образа, перенос базы на volume и изменение CPU quota влияют на разные части системы, поэтому их нельзя оценивать одной метрикой.

Когда контейнеры лучше разнести

Разносите сервисы по хостам или отдельным классам хранения, если измерения показывают устойчивую конкуренцию:

  • база данных регулярно сталкивается с очередью резервного копирования;
  • фоновые CPU-bound задачи вызывают постоянный throttling API;
  • кэш вытесняет память критичного сервиса и приводит к swap или OOM;
  • один контейнер заполняет диск логами;
  • сетевой поток одного сервиса создает drops или повышает latency других;
  • лимиты сохраняют соседей, но нарушают SLA самого приложения.

Плотность размещения должна учитывать резерв на отказ, обновление, всплеск трафика и фоновые операции. Хост, постоянно работающий на 100% CPU и почти полном объеме RAM, плохо переносит рестарт, миграцию или кратковременный пик.

Для VPS, тестовых стендов и сервисов, которым нужен быстрый запас CPU, памяти, диска или Kubernetes, можно использовать облачную инфраструктуру Timeweb Cloud. Выбор площадки проверяйте нагрузочным тестом: виртуальное число CPU не гарантирует одинаковую производительность диска и сети.

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

После проверки сохраните Compose-файл, значения лимитов, результаты тестов и порядок диагностики в базе знаний команды. Через несколько месяцев эти записи помогут понять, изменилась ли производительность из-за нового образа, ядра, файловой системы, нагрузки или соседнего сервиса.

Поделиться:
Сохранить гайд? В закладки браузера