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

При всплеске нагрузки контейнерное приложение чаще всего ломается не из-за найденной CVE, а из-за исчерпания памяти, PID, файловых дескрипторов, сетевых соединений или места под логи. Для разбора возьмём Docker Engine 27.x на Linux с cgroups v2 и OCI-образами: под Kubernetes те же проблемы проявляются на уровне контейнера, Pod и узла, но распределяются между ними иначе.
Почему сканирование образа не спасает от аварии
Сканер проверяет содержимое образа: пакеты, библиотеки, версии и известные уязвимости. Он не знает, сколько памяти потребуется процессу после открытия тысячи соединений, насколько быстро растут логи и что произойдёт, когда пятьдесят экземпляров одновременно начнут скачивать большой образ.
У безопасности контейнеров есть два разных контура:
- безопасность образа — базовый образ, зависимости, секреты, права файлов и происхождение слоёв;
- безопасность запуска — пользователь внутри контейнера, Linux capabilities, seccomp, AppArmor, сетевые правила, лимиты CPU и памяти, доступ к хосту.
Под нагрузкой эти контуры встречаются. Например, процесс с лишними правами может не просто упасть при переполнении буфера, а получить доступ к большему числу ресурсов. Контейнер без лимита памяти может вытеснить соседние сервисы и превратить локальную ошибку в аварию узла.
Лимит ресурса — это не только защита от случайного потребления. Это граница, за которой отказ одного контейнера не должен становиться отказом всей машины.
Сноска. Cgroups — механизм ядра Linux, который ограничивает и учитывает потребление ресурсов группой процессов. Контейнер использует cgroups, но сам по себе не получает отдельное ядро и не становится виртуальной машиной.
Порядок отказа под нагрузкой
Универсального сценария нет: он зависит от приложения, размера узла, лимитов и типа трафика. Но на практике полезно проверять цепочку ниже. Она показывает не «самую опасную» проблему вообще, а наиболее вероятное место первого отказа.
1. Память и OOM
Память заканчивается первой, если приложение хранит очереди в RAM, создаёт много рабочих процессов или получает слишком большой кэш. При лимите cgroup ядро может завершить процесс внутри контейнера. Без лимита под ударом окажутся другие контейнеры и системные службы.
Смотрите не только на общий процент памяти. Нужны текущий и пиковый расход контейнера, события OOM, рестарты, размер heap, page cache и время ответа до отказа. В Docker это сопоставляют с данными docker stats, состоянием cgroup и метриками приложения. Рост рестартов без роста HTTP-ошибок на балансировщике часто означает, что экземпляр умирает между проверками здоровья.
2. PID и файловые дескрипторы
Сервис может выглядеть здоровым по CPU и RAM, но перестать создавать потоки или открывать сокеты. Причины — утечка процессов, зависшие дочерние процессы, незакрытые файлы и слишком низкие лимиты ядра.
Симптомы зависят от рантайма: «cannot fork», «too many open files», ошибки создания соединения, обрыв ответов при почти нормальной загрузке CPU. Проверяйте число процессов и потоков, занятые файловые дескрипторы, лимит процесса, длину очереди соединений и состояние сокетов. Один показатель «количество контейнеров» здесь ничего не объясняет: тысяча короткоживущих процессов опаснее одного долгоживущего воркера.
3. Сеть, conntrack и внешние зависимости
Следом часто заканчиваются не адреса, а состояние соединений на узле: таблица conntrack, порты, очереди сокетов или лимит соединений у базы данных. Сервис начинает отвечать медленнее, тайм-ауты провоцируют повторы, а повторы создают ещё больше соединений.
Так появляется положительная обратная связь: база отвечает дольше, приложение держит больше сокетов, балансировщик считает экземпляр нездоровым, трафик уходит на оставшиеся экземпляры и ускоряет их отказ. Для диагностики нужны число активных и ожидающих соединений, доля повторов, время установления TCP-соединения и ошибки DNS. На уровне приложения отдельно измеряйте пул соединений к каждой зависимости, а не только общий RPS.
4. Логи заполняют диск
Стек ошибок под нагрузкой может оказаться опаснее самой ошибки. Если каждый неуспешный запрос пишет несколько строк, а ротация не настроена, заканчивается место на файловой системе узла или на разделе, куда Docker складывает журналы.
После этого ломаются не только контейнеры: база может не записать журнал транзакций, агент мониторинга — не сохранить метрики, а обновление образа — не распаковать слои. Безопасная схема требует ограничения размера и количества локальных лог-файлов, доставки важных событий во внешнюю систему и отдельного алерта на свободное место. Логи нельзя считать бесконечным диском для отладки.
5. Холодный старт и реестр образов
При масштабировании после аварии узлу может понадобиться скачать образ. Если образ большой, содержит лишние инструменты и состоит из неудачно собранных слоёв, восстановление занимает больше времени. Когда десятки узлов делают это одновременно, нагрузка переезжает с приложения на реестр, сеть и дисковую подсистему.
Проверяйте время pull отдельно от времени запуска процесса. Фиксируйте образы по digest, а не только по изменяемому тегу: одинаковое имя не гарантирует одинаковое содержимое. Это одновременно снижает риск неожиданной поставки и помогает повторить инцидент.
| Что заканчивается | Первый симптом | Что измерять | Защита |
|---|---|---|---|
| Память | OOM, рестарты, резкий рост задержки | RSS, лимит cgroup, события OOM, heap | Лимит, корректный sizing, контроль очередей |
| PID и потоки | cannot fork, зависшие воркеры | Процессы, потоки, PID-лимит | Лимит PID, разбор утечек, backpressure |
| Дескрипторы и сокеты | too many open files, тайм-ауты | FD, listen queue, активные соединения | Лимиты, закрытие ресурсов, пул соединений |
| Диск | ошибки записи, остановка логирования | Свободное место, inode, объём логов | Ротация, квоты, внешнее хранение |
| Реестр и сеть | медленный pull, длинный recovery | Размер образа, pull latency, ошибки сети | Минимальный образ, кэш, digest, прогрев |
Какие настройки безопасности усиливают отказ
Жёсткая изоляция обычно полезна, но она тоже должна быть проверена под рабочим профилем нагрузки. Запуск от непривилегированного пользователя уменьшает последствия компрометации, однако приложение может неожиданно потерять доступ к каталогу или порту. Отключение лишних capabilities сокращает поверхность атаки, но отдельным сетевым или файловым операциям потребуется явное разрешение.
Опаснее всего режимы, которые незаметно расширяют границы контейнера:
- запуск с
--privilegedбез документированной причины; - монтирование
/var/run/docker.sockвнутрь приложения; - запись в чувствительные каталоги хоста;
- секреты в переменных окружения или слоях образа;
- общий network namespace без понимания, какие процессы его используют;
- отключение seccomp или AppArmor ради обхода ошибки запуска.
Последний пункт особенно коварен. Если профиль безопасности мешает запуску, отключение защиты скрывает несовместимость и расширяет последствия будущей уязвимости. Сначала выясните конкретный syscall или доступ к файлу, затем разрешайте только необходимое и повторяйте нагрузочный тест.
Как проводить проверку, не выдумывая цифры
Тест должен повторять не абстрактное «много запросов», а профиль системы: размер ответа, долю ошибок, число одновременных соединений, частоту обращений к базе и длительность теста. Сравнивайте минимум три состояния: нормальная нагрузка, пиковая и восстановление после принудительного завершения экземпляра.
- Зафиксируйте исходные условия. Запишите версию Docker Engine, ядра Linux, cgroups, размер узла, лимиты контейнеров, размер образа и способ доставки логов.
- Определите границы отказа. Задайте допустимые p95 и p99 задержки, долю ошибок, время восстановления и максимальный объём логов. Если критерии не определены, «система выдержала» проверить нельзя.
- Нагрузите один экземпляр. Наблюдайте CPU, память, PID, FD, сеть, диск и метрики зависимостей. Так вы отличите дефект приложения от эффекта масштабирования.
- Повторите тест на нескольких экземплярах. Проверьте холодный старт, pull образа, распределение трафика и суммарное потребление узла.
- Проверьте отказ. Удалите или остановите один экземпляр штатным способом тестового окружения и измерьте, сколько времени занимает восстановление без ручного вмешательства.
Результатом должна быть не одна цифра производительности, а карта пределов: при каком числе соединений растёт p99, при каком объёме памяти начинается OOM, когда исчерпываются дескрипторы и сколько занимает повторный запуск. Без одинаковых условий «до» и «после» оптимизация превращается в впечатление.
Где подход ломается
Лимиты контейнера не исправляют утечку памяти, медленную базу или бесконечную очередь. Они только ограничивают ущерб. Сканирование образа не обнаружит секрет, который появляется во время запуска, и не проверит опасное монтирование хоста. Read-only root filesystem не спасёт, если приложение должно писать в каталог, который не вынесен в отдельный том.
Ещё одна граница — узел. Контейнер может иметь корректный memory limit, но десятки таких лимитов вместе превышают физическую память. Аналогично, ограничения внутри контейнеров не отменяют системные пределы conntrack, inode, диска и PID. Поэтому проверяйте не только каждый workload, но и суммарный бюджет узла.
Что делать дальше
Начните с инвентаризации: какие контейнеры запускаются от root, какие имеют лишние capabilities, какие образы используют изменяемые теги и куда пишутся логи. Затем добавьте дашборд с памятью cgroup, OOM-событиями, PID, файловыми дескрипторами, сокетами, свободным местом и временем pull.
Следующий практический шаг — повторить нагрузочный тест после изменения только одного ограничения: например, лимита памяти или ротации логов. Так вы увидите причинную связь, а не просто получите новый набор графиков. Для Kubernetes отдельно проверьте requests и limits, probes, PDB и поведение автоскейлинга: это уже слой оркестратора, а не свойство Docker-образа.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Docker Docs: Resource constraints, Docker Docs: Building best practices
Частые вопросы
Нужно ли ограничивать память каждому контейнеру?
Почему контейнер перезапускается, хотя CPU не загружен?
Достаточно ли просканировать Docker-образ перед публикацией?
Можно ли отключить seccomp или AppArmor, если приложение не стартует?
Что будет, если логи заполнят весь диск?
Нужно ли фиксировать образ по digest?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.