Устранение неполадок медленных контейнеров Docker: пошаговое руководство по производительности
Узнайте, почему контейнеры Docker работают медленно, проверив загрузку ЦП, память, дисковый ввод-вывод, сеть, лимиты, монтирования и ведение журнала.
Устранение неполадок медленных контейнеров Docker: пошаговое руководство по производительности
Когда контейнер Docker работает медленно, не начинайте с пересборки образа или изменения случайных флагов времени выполнения. Сначала определите, что значит «медленно». Высокое время ответа API? Отставание воркера? Медленный запуск? Медленная сборка? Перегружен хост? Каждый случай указывает на разное решение.
Контейнер — это не магическая изоляция от физики. Он всё ещё использует ЦП хоста, память хоста, хранилище хоста, сеть хоста и код приложения, который вы загрузили. Docker добавляет элементы управления и пространства имён вокруг этих ресурсов, но он не делает медленный запрос быстрым или насыщенный диск простаивающим.
Начните с быстрой прямой проверки:
docker stats
Наблюдайте за контейнером, пока воспроизводите замедление. Один снимок менее полезен, чем наблюдение за изменениями под нагрузкой. Если ЦП подскакивает и остаётся высоким, у вас проблема с ЦП. Если память растёт, пока контейнер не умирает, следуйте по пути памяти. Если BLOCK I/O активно перемещается, пока запросы зависают, стоит обратить внимание на хранилище. Если контейнер выглядит спокойным, но пользователи всё ещё видят задержки, смотрите на приложение, сетевые вызовы, базу данных или вышестоящие сервисы.
Сначала сравните состояние контейнера и хоста
Медленный контейнер может просто находиться на медленном хосте. Проверьте оба уровня.
docker stats <container>
top
free -h
df -h
В Linux iostat -xz 1 полезен, если доступен. Высокая загрузка диска или длительное время ожидания могут объяснить медленные базы данных, установку пакетов и сервисы с интенсивным ведением журнала. В Docker Desktop также проверьте ЦП и память, назначенные виртуальной машине Docker. Mac с большим объёмом памяти всё равно может голодать контейнеры, если Docker Desktop ограничен слишком низко.
Если все контейнеры медленные, подозрение падает на хост. Если один контейнер медленный, а соседние в порядке, сосредоточьтесь на этой рабочей нагрузке, её лимитах, монтированиях и зависимостях.
Узкие места ЦП
В docker stats ЦП может превышать 100%, потому что Docker сообщает об использовании по всем ядрам. Контейнер, использующий 200%, примерно использует два ядра. Важный вопрос — ожидаемо ли это для данной рабочей нагрузки.
Проверьте лимиты времени выполнения:
docker inspect <container> --format 'NanoCPUs={{.HostConfig.NanoCpus}} CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} Cpuset={{.HostConfig.CpusetCpus}}'
Если сервис был запущен с --cpus=0.5, он может быть ограничен при нормальном трафике. В Kubernetes или Compose та же проблема может скрываться в лимитах ЦП. Воркер, который быстро обрабатывал задачи на ноутбуке, может ползти в CI, потому что получает только половину ядра.
Для профилирования ЦП на уровне приложения профилируйте процесс, а не гадайте. Для Node используйте встроенное профилирование ЦП или инструменты типа clinic. Для Python используйте py-spy, где разрешено. Для Java используйте JFR или async-profiler. Если вы не можете установить инструменты внутри рабочего образа, запустите тот же образ в среде staging или используйте шаблон отладочного контейнера.
Распространённые причины проблем с ЦП включают плотные циклы опроса, дорогую сериализацию JSON, регрессивный поиск по регулярным выражениям, обработку изображений, сжатие и слишком много потоков воркеров, борющихся за слишком мало ядер. Увеличение ЦП помогает только если приложение может его использовать и у хоста есть ёмкость.
Давление памяти и OOM-убийства
Проблемы с памятью проявляются как растущее использование памяти, частая сборка мусора, активность подкачки на хосте или внезапные завершения. Подтвердите статус OOM:
docker inspect <container> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} memory={{.HostConfig.Memory}}'
Если OOMKilled=true, контейнер превысил лимит памяти. Это может быть явный лимит --memory, лимит виртуальной машины Docker Desktop или общее давление на хост.
Используйте docker stats при отправке реалистичного трафика. Если память растёт без выравнивания, подозревайте утечку, неограниченный кеш, накопление очереди или рабочую нагрузку, которая загружает слишком много данных за раз. Если память скачет во время запуска, а затем стабилизируется, лимит может быть просто слишком низким для среды выполнения.
Настройки языка имеют значение. Java, Node и некоторые серверы приложений могут резервировать или использовать память по-разному внутри контейнеров в зависимости от версии и конфигурации. Установите явные параметры кучи или памяти, когда требуется предсказуемое поведение. Например, сервису Java могут потребоваться проценты кучи с учётом контейнера; сервису Node может понадобиться --max-old-space-size; базе данных нужны настройки кеша, которые оставляют место для процесса и файловой системы.
Не устанавливайте лимиты памяти настолько жёсткими, чтобы приложение всё время тратило на сборку мусора. Контейнер, который никогда не падает, но постоянно приостанавливается, всё равно неисправен.
Дисковый ввод-вывод и медленные bind-монтирования
Проблемы с хранилищем легко пропустить, потому что графики ЦП и памяти выглядят нормально. В Docker замедление диска часто происходит из-за одного из четырёх мест: интенсивный ввод-вывод приложения, чрезмерное ведение журнала, драйвер хранилища или bind-монтирования на Docker Desktop.
Проверьте представление Docker:
docker stats <container>
docker logs --tail 20 <container>
Если журналы очень шумные, драйверу ведения журнала есть над чем работать. Журналы JSON-file могут быстро расти, если не настроена ротация. В занятом сервисе регистрация каждого тела запроса или отладочной строки может стать реальной проблемой производительности.
Проверьте настройки ведения журнала:
docker inspect <container> --format '{{json .HostConfig.LogConfig}}'
Для локальных и небольших серверных установок рассмотрите ротацию журналов в конфигурации демона или файле Compose. Для производственных платформ отправляйте журналы в систему ведения журнала платформы и держите объём журнала приложения намеренным.
Bind-монтирования заслуживают особого внимания на macOS и Windows. Дерево исходников, смонтированное с хоста в контейнер Linux, пересекает уровень виртуализации. Это удобно для разработки, но может быть намного медленнее именованного тома для папок зависимостей, баз данных или каталогов с интенсивной записью.
Например, контейнер разработки Node может быть медленным, если node_modules находится на bind-монтировании. Лучший шаблон — смонтировать исходный код через bind, но хранить зависимости в именованном томе:
services:
app:
volumes:
- .:/app
- node_modules:/app/node_modules
volumes:
node_modules:
Для баз данных предпочитайте именованные тома bind-монтированиям, если у вас нет специального рабочего процесса резервного копирования или проверки, требующего путей хоста.
Сетевая задержка и замедление зависимостей
Контейнер может быть «медленным», потому что он ждёт другой сервис. Локальный процесс может быть здоровым, в то время как DNS, база данных, Redis, API или прокси медленные.
Проверьте изнутри контейнера:
docker exec -it <container> sh
curl -w '
lookup:%{time_namelookup} connect:%{time_connect} start:%{time_starttransfer} total:%{time_total}
' -o /dev/null -s http://service:8080/health
Вывод curl -w разделяет DNS-запрос, TCP-подключение, первый байт и общее время. Если DNS-запрос медленный, проверьте /etc/resolv.conf и настройки DNS демона Docker. Если подключение медленное или не удаётся, проверьте сети, брандмауэры и привязку сервиса. Если время до первого байта медленное, вышестоящий сервис принял соединение, но ответил с задержкой.
Для трафика между контейнерами используйте пользовательскую мостовую сеть, чтобы контейнеры могли разрешать друг друга по имени:
docker network create appnet
docker run -d --name api --network appnet my-api
docker run --rm --network appnet curlimages/curl http://api:8080/health
Не проводите бенчмаркинг через опубликованные порты хоста, когда реальный трафик идёт между контейнерами. Тестируйте путь, который используется в production.
Производительность запуска — отдельная проблема
Медленный запуск часто возникает из-за времени вытягивания образа, установки зависимостей при запуске контейнера, миграций базы данных или прогрева приложения.
Контейнер не должен устанавливать пакеты каждый раз при запуске. Если ваша точка входа запускает npm install, pip install, apt-get или загружает бинарники при каждой загрузке, перенесите эту работу в сборку образа, если нет веской причины не делать этого.
Проверьте журналы запуска с метками времени, если ваше приложение их предоставляет. Если нет, добавьте простые метки времени вокруг шагов точки входа при отладке:
date; echo 'starting migrations'
# migration command
date; echo 'starting server'
# server command
Для образов, вытягиваемых по сети, размер образа имеет значение. Многоэтапные сборки, .dockerignore и меньшие базовые образы времени выполнения улучшают скорость холодного запуска и развёртывания. Но как только образ уже присутствует и контейнер запущен, размер образа обычно менее важен, чем ЦП, память, ввод-вывод и поведение приложения.
Производительность сборки — это не производительность выполнения
Медленные сборки Docker раздражают, но это другой класс проблем. Если изменения кода заставляют устанавливать зависимости при каждой сборке, исправьте порядок слоёв:
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
Не копируйте весь репозиторий перед установкой зависимостей, если не хотите, чтобы каждое изменение исходников аннулировало слой зависимостей.
Также держите контекст сборки небольшим:
.git
node_modules
coverage
dist
*.log
Кеш-монтирования BuildKit могут помочь с повторными загрузками зависимостей, но сначала убедитесь, что Dockerfile упорядочен правильно. Кеш-монтирование не может полностью спасти Dockerfile, который аннулирует кеш слишком рано.
Лимиты ресурсов могут защитить хост и навредить приложению
Лимиты ЦП и памяти полезны, потому что один контейнер не должен обрушить хост. Они также могут создавать искусственное замедление, если скопированы из примера без измерения рабочей нагрузки.
Проверьте лимиты:
docker inspect <container> --format '{{json .HostConfig}}' | jq '{Memory, NanoCpus, CpuQuota, CpuPeriod, BlkioWeight}'
Если jq недоступен, проверьте контейнер обычным способом и найдите HostConfig.
Для Compose проверьте фактическую отрисованную конфигурацию:
docker compose config
Это ловит лимиты, унаследованные от файлов переопределения или переменных окружения. Частый сюрприз — файл переопределения разработки, который устанавливает низкие лимиты и случайно используется в тестовой среде.
Практический поток диагностики
Используйте этот поток, когда жалоба просто «контейнер медленный»:
- Воспроизведите медленное поведение и запустите
docker statsво время воспроизведения. - Проверьте ЦП, память, диск и лимиты виртуальной машины Docker Desktop на хосте.
- Проверьте лимиты ЦП и памяти контейнера.
- Прочитайте журналы на предмет повторных попыток, тайм-аутов соединения, миграций, отладочного ведения журнала или подсказок OOM.
- Проверьте зависимости изнутри контейнера с помощью
curl,digили специально созданного отладочного образа. - Проверьте монтирования: переместите пути с интенсивной записью на именованные тома, где это уместно.
- Профилируйте приложение, если графики ресурсов указывают на код.
Лучшие исправления, как правило, конкретны: поднять слишком низкий лимит памяти, прекратить регистрацию больших полезных нагрузок, переместить данные базы данных с bind-монтирования, исправить медленный путь DNS, изменить порядок слоёв Dockerfile или настроить среду выполнения приложения. Общий совет «оптимизировать Docker» менее полезен, чем доказательство того, какой ресурс на самом деле медленный.