Производительность Docker и Kubernetes: диагностика узких мест и настройка | AdminWiki

Производительность Docker и Kubernetes: диагностика узких мест и настройка

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

Docker и Kubernetes сами по себе не ускоряют приложение. Сначала найдите bottleneck: CPU-bound обработку, нехватку памяти, медленную базу данных, внешнее API, дисковый ввод-вывод, сетевую задержку или перегруженный узел. После этого меняйте размещение, requests и limits, число реплик либо параметры сети и хранения.

Реплики и балансировка дают измеримый эффект, когда запросы можно параллельно обслуживать на нескольких сопоставимых экземплярах, а проблема связана с перегрузкой отдельных узлов или неравномерным трафиком. Балансировщик исключает неготовые экземпляры из пула, но не ускоряет медленный SQL-запрос, единственный primary-сервер базы данных или CPU-bound обработку внутри каждого экземпляра.

Рабочая последовательность выглядит так: зафиксировать baseline, определить слой проблемы, изменить одну настройку, повторить нагрузочный тест и сравнить latency, p95, p99, throughput, error rate и ресурсные показатели. Средний отклик 150 мс может сочетаться с p99 около 3 секунд, если часть запросов попадает в очередь или ждет зависшую зависимость.

Что действительно влияет на производительность Docker и Kubernetes

Производительность контейнерного приложения определяется всей цепочкой обработки: кодом, зависимостями, контейнерным runtime, cgroups, узлом, сетью, хранилищем и способом планирования подов. Docker обычно запускает контейнер непосредственно на выбранном хосте. Kubernetes добавляет scheduler, Pod, Service, проверки готовности, сетевой маршрут и правила распределения ресурсов.

При диагностике полезно составить карту запроса: клиент, балансировщик, Service, Pod, приложение, база данных или внешнее API, PersistentVolume и физическая система хранения. На каждом участке нужно измерить время ожидания. Высокий CPU на узле не доказывает, что приложение упирается в вычисления, а свободная память не подтверждает отсутствие memory pressure.

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

Когда реплики и балансировка дают измеримый эффект

Горизонтальное масштабирование помогает stateless-сервисам, которые обрабатывают независимые запросы. Например, четыре экземпляра HTTP-приложения получают равномерный поток, каждый из них выдерживает 100 запросов в секунду, а база данных и сеть имеют запас. При перегрузке одного экземпляра балансировщик распределяет новые запросы между тремя свободными и одной восстановленной репликой. Throughput растет, а p95 и p99 снижаются.

Условия такого эффекта нужно проверить заранее:

  • экземпляры имеют сопоставимые CPU, memory и настройки;
  • запросы не зависят от локального состояния конкретного контейнера;
  • у базы данных, очереди и внешних API остается свободная пропускная способность;
  • узлы Kubernetes располагают allocatable-ресурсами для новых подов;
  • readiness probe исключает экземпляр из трафика до завершения прогрева;
  • пул соединений и лимиты внешних систем рассчитаны на увеличившееся число клиентов.

Readiness probe отвечает за готовность принимать трафик, а liveness probe помогает перезапустить зависший процесс. Эти проверки решают разные задачи. Если под еще загружает кэш, но уже проходит liveness probe, балансировщик может направить ему запросы слишком рано, и p99 вырастет даже при свободном CPU.

Балансировка не меняет скорость одного запроса к базе данных. Если каждый экземпляр приложения ждет один и тот же primary-сервер, добавление реплик увеличит количество одновременных соединений и длину очереди на базе. Сначала измерьте время ожидания зависимости, число активных соединений, блокировки и частоту retries.

Когда перенос в контейнеры не ускоряет приложение

Docker и Kubernetes меняют размещение и управление ресурсами. Они не меняют алгоритм приложения, скорость диска и пропускную способность единственного сервера базы данных. CPU-bound задача, которая занимает 800 мс на выделенном сервере, сохранит близкое время и в контейнере, если получает тот же процессор без throttling.

Перенос не устраняет такие причины:

  • медленный SQL-запрос или блокировка в базе данных;
  • исчерпание пула соединений;
  • ожидание внешнего API с rate limit;
  • синхронная запись на медленный диск;
  • очередь сообщений с ограниченной скоростью обработки;
  • один primary-сервер, через который проходят все операции записи;
  • непараллелизуемая CPU-bound обработка;
  • частые ретраи, вызванные сетевыми потерями или таймаутами.

Сравнивать Docker и Kubernetes нужно при одинаковой нагрузке, версии приложения, размере данных, профиле запросов и параметрах хоста. Если в Kubernetes добавились Service, overlay-сеть, сетевой PersistentVolume и конкурирующие поды, различие в latency отражает условия среды, а не только overhead оркестратора.

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

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

Минимальный набор показателей включает latency, p50, p95, p99, throughput и error rate. К ним добавляются CPU utilization, CPU throttling, RSS, memory working set, memory pressure, сетевые ошибки, активные соединения, длина очередей и дисковая latency. Одно значение CPU или RAM не описывает поведение системы.

Почему p95 и p99 важнее средней задержки

Среднее значение скрывает хвост распределения. При среднем отклике 150 мс половина запросов может завершаться быстро, а небольшая группа будет ждать 3 секунды из-за блокировки, GC-паузы, дискового I/O, сетевого ретрая или очереди на базе данных.

p50 показывает типичный запрос, p95 описывает задержку для 95 процентов запросов, а p99 помогает увидеть редкие, но критичные задержки. Для пользовательского API p99 часто лучше отражает инцидент, чем среднее значение. Если после изменения p50 не изменился, p95 снизился с 900 до 300 мс, а p99 остался на уровне 3 секунд, проблема в хвосте еще не устранена.

Сравнивайте одинаковые интервалы. Тест на 1000 запросов и тест на 1 миллион запросов дают разную статистическую надежность. Фиксируйте warm-up, длительность измерения и число ошибок. Рост throughput при одновременном росте p99 и error rate не подтверждает улучшение.

На p95 и p99 влияют:

  • длинные очереди перед приложением или базой;
  • CPU throttling в отдельных периодах;
  • вытеснение страниц и активный swap;
  • задержка fsync;
  • TCP retransmissions и DNS-задержки;
  • GC-паузы и холодный старт реплик;
  • исчерпание соединений и повторные запросы.

Что собрать на уровне приложения, контейнера и узла

На уровне приложения измеряйте время обработки внутри процесса и время ожидания каждой зависимости. Полезны количество активных запросов, длина внутренних очередей, размер пула соединений, число retries, коды ошибок и время ответа базы данных. Разница между полным latency и временем CPU часто сразу показывает внешнее ожидание.

Для Docker начните с команды:

docker stats --no-stream

Она показывает текущее потребление CPU, памяти, сети и блокового I/O контейнерами. Для устойчивого вывода нужны временные ряды, поскольку единичный снимок не показывает throttling и краткие пики. Проверяйте ограничения контейнера через docker inspect, а состояние хоста сопоставляйте с нагрузкой процесса.

В Kubernetes используйте:

kubectl top pods -A
kubectl top nodes
kubectl describe pod <pod-name> -n <namespace>
kubectl get events -A --sort-by=.lastTimestamp

kubectl top требует работающего metrics-server и показывает текущие значения, но не заменяет историю. Prometheus и Grafana помогают сопоставить latency приложения с CPU throttling, memory pressure, рестартами, сетевыми drops и I/O.

На node проверяйте allocatable, реальное потребление, давление памяти, load average, PSI, очередь диска, TCP retransmissions и число conntrack-записей. Средняя загрузка node может быть умеренной, пока один контейнер получает CPU quota, конкретный диск держит очередь, а один под обрабатывает весь трафик.

Для контейнеров особенно полезны метрики container_cpu_cfs_throttled_seconds_total, container_cpu_usage_seconds_total, RSS и memory working set. Рост throttled time вместе с p99 указывает на ограничение CPU, даже если общий CPU node не достиг 100 процентов.

Оценить реальную загрузку Kubernetes-кластера, качество requests и limits, работу HPA и распределение ресурсов можно в практическом материале о загрузке нод и подов.

Почему одно приложение ведет себя по-разному в Docker и Kubernetes

Одинаковый образ не гарантирует одинаковую производительность. На результат влияют cgroups, соседние нагрузки, лимиты CPU и memory, политика планирования, сетевой маршрут, DNS, Service, storage driver и тип PersistentVolume.

Docker-контейнер на свободном хосте может получать процессорные циклы без заметной конкуренции. В Kubernetes тот же процесс окажется на node с несколькими workload, ограничением CPU quota и сетевым доступом через Service. Разница проявится в хвосте latency, запуске фоновых задач и времени ответа зависимостей.

Контейнер изолирует процессы через namespace и ограничивает ресурсы через cgroups. Pod объединяет один или несколько контейнеров, которые делят сетевой namespace и могут использовать общие тома. Scheduler размещает Pod с учетом requests, taints, affinity и доступной емкости node. Service добавляет стабильную точку доступа и правила маршрутизации к ready-подам.

Различия в ресурсных ограничениях и соседстве

CPU request участвует в планировании и помогает scheduler выбрать node с подходящей емкостью. CPU limit задает верхнюю границу, после которой cgroup может ограничить выполнение. При коротких burst-операциях низкий limit способен вызвать throttling и поднять p95, даже если среднее потребление CPU невелико.

Memory limit работает жестче. Когда процесс или группа контейнеров превышает доступный лимит, ядро может завершить процесс внутри cgroup. Kubernetes покажет состояние OOMKilled. Если памяти не хватает всей node, kubelet может запустить eviction подов, а ядро Linux может применить OOM killer на уровне узла.

Соседняя нагрузка влияет на кэш CPU, очередь планировщика, память, сетевой интерфейс и диск. Поэтому сравнивайте:

  • фактическую мощность node и значение allocatable;
  • requests и limits конкретного Pod;
  • потребление соседних workload;
  • CPU throttling и периоды ожидания;
  • memory pressure, eviction и рестарты;
  • тип диска и путь к данным.

В Kubernetes качество эксперимента падает, если новый Pod случайно попадает на перегруженную node. Укажите имя node, распределение реплик и состояние ресурсов в момент теста. Свободный CPU в среднем по кластеру не означает доступный CPU на выбранном узле.

Различия в сетевом маршруте и хранении

Прямой вызов по Pod IP и запрос через Service проходят разными путями. Во втором варианте участвуют DNS, таблицы маршрутизации и механизм kube-proxy, который может использовать iptables или IPVS. При overlay-сети добавляется обработка encapsulation, а неправильный MTU способен вызвать фрагментацию, drops или повторную передачу пакетов.

Сравнивайте три маршрута:

  1. клиент к Pod IP;
  2. клиент к Service DNS и ClusterIP;
  3. Pod к внешней зависимости.

Для хранения сравнение должно учитывать storage driver Docker, тип Kubernetes PersistentVolume, CSI-плагин, файловую систему, параметры монтирования и backend. Локальный NVMe, сетевой NFS и удаленный блочный том могут иметь совершенно разные IOPS, throughput и fsync latency.

Практические тесты Docker, Kubernetes и LXC/Incus с измерением CPU, памяти, сети и времени запуска собраны в сравнении контейнерных технологий за 2026 год. Используйте его как ориентир по методике, но собственные результаты проверяйте на своем workload.

Requests и limits CPU и memory Kubernetes

Requests и limits задают разные правила. Request сообщает scheduler, сколько ресурса нужно Pod для размещения. Limit ограничивает расход контейнера во время работы. Эти значения влияют на плотность размещения, QoS-класс, вероятность конкуренции и реакцию node на давление ресурсов.

Завышенный request может оставить свободные CPU и memory недоступными для новых подов, хотя текущий процесс почти не использует заявленный объем. Заниженный request позволяет плотнее разместить workload, но повышает конкуренцию и риск деградации под нагрузкой. Случайные значения из чужого манифеста не дают надежного результата.

CPU: конкуренция, quota и CPU throttling

CPU request задает ожидание ресурса при планировании и влияет на относительный вес контейнера при конкуренции. CPU limit задает квоту. Когда контейнер исчерпывает квоту в периоде CFS, его выполнение приостанавливается до следующего периода. Приложение видит это как рост времени ответа, хотя процессор node может оставаться частично свободным в другие моменты.

Ищите одновременно четыре сигнала: фактическое потребление CPU, throttled periods, throttled time и p95/p99. Высокий throttled time при росте p99 подтверждает связь между limit и задержкой. Высокое потребление без throttling может указывать на реальную CPU-bound обработку, которую стоит профилировать или распараллелить.

Для коротких burst-задач слишком жесткий CPU limit часто вреднее, чем умеренно высокий предел при наличии емкости на node. Изменяйте его постепенно. После каждого изменения проверяйте throughput, latency, число активных запросов и потребление соседних подов.

Пример диагностики:

  • CPU usage контейнера держится на 0,6 ядра при limit 1 CPU;
  • throttled time растет в часы пик;
  • p50 остается 80 мс, p99 увеличивается с 400 мс до 2 секунд;
  • после повышения limit до 2 CPU throttling исчезает, p99 возвращается к 450 мс.

Такой результат подтверждает ресурсное ограничение. Если p99 не изменился, ищите очередь, сеть, базу данных или диск.

Memory: limit, рабочий набор и OOMKilled

Memory limit ограничивает объем памяти, который cgroup может использовать. RSS показывает страницы процессов, working set помогает оценить активную память, а page cache используется ядром для ускорения доступа к файлам. Эти показатели нельзя смешивать в один вывод о дефиците RAM.

При превышении memory limit контейнер получает OOMKilled, после чего Deployment или другой контроллер может запустить его заново. Пользователь увидит ошибки, обрывы соединений, холодный кэш и повторные пики нагрузки. При memory pressure на node Kubernetes может удалить Pod через eviction, даже если конкретный контейнер еще не пересек свой limit.

Проверяйте:

  • состояние контейнера и причину последнего завершения;
  • events Pod и сообщения kubelet;
  • RSS, working set и скорость роста памяти;
  • cgroup memory events;
  • available memory и PSI memory pressure на node;
  • активность swap и задержки диска;
  • распределение memory requests между соседними подами.

Memory leak обычно проявляется устойчивым ростом RSS. Проблема с рабочим набором может проявиться скачками page faults и reclaim. Превышение лимита из-за большого кэша требует отдельного анализа: увеличивать limit без проверки поведения кэша рискованно.

Как выбирать значения requests и limits

Соберите наблюдения на штатной и пиковой нагрузке. Зафиксируйте p95 и p99, максимальный RSS, CPU usage, throttling, размер очередей, GC-паузы и фоновые задачи. Для memory оставьте запас на burst, запуск воркеров, изменение размера входных данных и восстановление после сбоя.

Пример начальной настройки для API с типичным потреблением 300 м CPU и 420 MiB working set:

resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: 1500m
    memory: 768Mi

Эти значения не универсальны. Request CPU выше обычного потребления дает scheduler более реалистичную картину, limit допускает краткий burst, а memory limit оставляет запас для рабочего набора. После нагрузки проверьте, не ухудшилась ли плотность размещения и не появились ли eviction у соседних workload.

Для критичного сервиса полезно стремиться к предсказуемому QoS-классу, но его выбор не заменяет измерения. Под с низкими requests может быть вытеснен раньше при memory pressure. Под с завышенными requests может долго ждать подходящую node.

Фиксируйте версию Kubernetes, container runtime и конфигурацию node. Механика cgroups, поведение runtime и настройки kubelet влияют на результат. Меняйте CPU request, CPU limit, memory request и memory limit по одному, чтобы связать изменение с наблюдаемым эффектом.

Планирование подов, балансировка и масштабирование

Scheduler принимает решение на основе requests и доступной allocatable-емкости node. Он не знает полный профиль будущего burst, если этот профиль не отражен в настройках и метриках. После размещения трафик распределяется между ready-подами, но scheduler не балансирует HTTP-запросы в реальном времени.

Нужно проверять две разные картины: где расположены реплики и как между ними фактически распределяется трафик. Четыре пода на одной node могут давать худший результат, чем два пода на двух узлах с запасом CPU и memory.

Как не собрать все реплики на одной node

Topology spread constraints помогают распределять поды по зонам, node или другим доменам. Pod anti-affinity запрещает или ограничивает соседство реплик. Node affinity выбирает узлы с нужными характеристиками, а taints и tolerations отделяют специальные node для конкретных workload.

Проверяйте фактическое размещение:

kubectl get pods -A -o wide
kubectl get nodes -o custom-columns=NAME:.metadata.name,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory

После размещения сопоставьте число реплик, node, requests, limits и реальное потребление. Жесткое anti-affinity может оставить Pod в состоянии Pending, если в кластере мало узлов. Мягкое правило сохраняет возможность размещения, но не гарантирует равномерность.

Taints и tolerations часто применяют для node с быстрым диском, GPU или выделенной сетевой нагрузкой. Ошибка в toleration может отправить обычный workload на специальный узел, а слишком строгий taint способен уменьшить доступную емкость для критичного сервиса.

Когда HPA повышает пропускную способность

HPA дает результат для stateless-сервиса, когда новые реплики действительно получают запросы и имеют доступ к достаточным ресурсам. Приложение должно быстро стартовать, проходить readiness probe, не хранить критичное состояние локально и не упираться в общую зависимость.

Сопоставляйте изменение числа реплик с throughput, p95, p99, error rate, активными соединениями и нагрузкой на базу данных. Если HPA увеличил число подов с 4 до 12, а throughput остался на уровне 400 запросов в секунду, проверяйте базу, очередь, внешний API, пул соединений и сетевой лимит.

Медленный startup снижает пользу HPA: новые Pod еще не готовы, а старые продолжают принимать весь поток. Слишком низкий target CPU может вызвать частое масштабирование при коротких всплесках. Масштабирование по CPU не всегда подходит сервису, который ждет сеть или диск, поэтому для него полезнее метрика очереди, активных запросов или latency.

Балансировщик не исправляет медленную зависимость

Представьте четыре реплики API, которые отправляют записи на один primary-сервер базы данных. При росте числа подов приложение начинает создавать больше соединений, запросы дольше ждут блокировки, а p99 растет. Балансировщик продолжает распределять входной трафик корректно, но общий bottleneck остается прежним.

Перед увеличением реплик измерьте:

  • время ожидания базы и внешнего API;
  • число активных и ожидающих соединений;
  • размер очереди и скорость ее обработки;
  • частоту retries и таймаутов;
  • лимиты запросов внешней системы;
  • CPU, memory и I/O primary-сервера.

В некоторых системах масштабирование приложения нужно сочетать с кэшированием, read replica, разделением очередей, ограничением concurrency или изменением SQL. Выбор зависит от причины, которую подтвердили метрики.

Связь между производительностью и отказоустойчивостью, включая балансировку, retries, concurrency и репликацию, разобрана в материале о производительности и отказоустойчивости вычислительных систем.

Сетевой слой как источник задержек

Сеть может добавлять latency при свободных CPU и memory. Проверяйте маршрут client-Service-Pod, обмен Pod-Pod и соединение Pod-external service отдельно. Иначе задержка DNS, kube-proxy, NAT или внешнего канала окажется приписана к коду приложения.

Полезно записать для каждого запроса время DNS, TCP connect, TLS handshake, ожидание первого байта и полную обработку. Рост только одного этапа сразу сужает область поиска.

Проверка DNS, Service и маршрута до пода

Сравните прямой запрос к Pod IP с запросом через Service. Если Pod IP отвечает за 20 мс, а Service за 60 мс, проверьте kube-proxy, правила iptables или IPVS, endpoints, DNS и распределение трафика. Прямой Pod IP удобен для изоляции проблемы, но не заменяет рабочий путь клиента.

Проверьте endpoints и готовность:

kubectl get svc,endpointslice -n <namespace>
kubectl get pods -n <namespace> -o wide
kubectl describe svc <service-name> -n <namespace>

Пустые или часто меняющиеся EndpointSlice указывают на проблемы readiness, рестарты или неверные selectors. Неравномерный трафик может возникать из-за долгих соединений, session affinity или различий в готовности реплик.

DNS-задержку измеряйте отдельно от соединения. Частые повторные резолвинги, маленький TTL или перегруженный CoreDNS способны поднять p99 при создании новых соединений. Долгоживущие соединения могут скрывать проблему DNS, пока приложение не перезапустится или не начнет масштабирование.

MTU, conntrack и сетевые политики

Overlay-сеть уменьшает доступный размер полезного пакета из-за служебных заголовков. Если MTU интерфейсов не согласована, крупные пакеты могут фрагментироваться или теряться. Симптомы включают таймауты только для больших ответов, retransmissions и резкое ухудшение p99 при росте payload.

Conntrack хранит состояние соединений. Заполненная таблица или высокая скорость создания коротких TCP-соединений приводит к drops и ошибкам установки соединения. Проверяйте размер таблицы, число записей, ошибки ядра и связь событий с ростом error rate.

NetworkPolicy добавляет правила фильтрации. Некорректная или слишком сложная политика может увеличить количество проверок и заблокировать нужный маршрут. Сопоставляйте изменения политики с TCP retransmissions, drops, временем connect и latency.

Проверяйте:

  • CNI и используемый режим сети;
  • MTU на node, интерфейсах и overlay;
  • iptables или IPVS и время обработки правил;
  • DNS и состояние CoreDNS;
  • conntrack drops и лимиты;
  • NAT и пропускную способность сетевого интерфейса;
  • TCP retransmissions, packet loss и сетевые ошибки;
  • NetworkPolicy и доступ к внешним сервисам.

Не объединяйте сетевое изменение с изменением CPU limit или StorageClass. Один эксперимент должен проверять одну гипотезу.

Хранилище и ввод-вывод в контейнерах

Приложение может ждать диск при загрузке CPU ниже 20 процентов. Это часто происходит при синхронной записи, fsync, большой очереди запросов или конкуренции нескольких подов за один backend. Низкая загрузка CPU не означает, что storage работает быстро.

В контейнерных средах нужно различать ephemeral storage, локальный том и PersistentVolume. Ephemeral storage подходит для временных данных и кэшей, но его потеря связана с удалением Pod. PersistentVolume сохраняет данные согласно политике backend и подключается через CSI, однако его задержка зависит от конкретной системы хранения и сети.

Локальный диск, сетевой том и PersistentVolume

Локальный NVMe обычно дает низкую latency и высокие IOPS, но привязывает workload к конкретной node. Сетевой NFS или другой remote backend упрощает перемещение Pod, зато каждый I/O проходит через сеть и зависит от сервера хранения. Блочный CSI-том может показать другой профиль очередей, fsync и пропускной способности.

При сравнении зафиксируйте:

  • StorageClass и CSI-драйвер;
  • тип backend и его репликацию;
  • файловую систему и параметры монтирования;
  • локальность тома и topology;
  • IOPS, throughput и latency backend;
  • долю синхронных операций;
  • конкуренцию нескольких Pod за один том или пул;
  • состояние page cache и прогретость данных.

Одинаковый размер тома не означает одинаковую производительность. У провиженированного тома могут быть отдельные ограничения IOPS или throughput. Несколько приложений, использующих один storage pool, способны увеличить queue depth и await.

Как подтвердить, что приложение упирается в I/O

На node смотрите iostat, await, utilization, queue depth, IOPS и throughput. На уровне приложения сравнивайте время CPU с временем ожидания файла, базы или fsync. Если процесс почти не тратит CPU, но запрос долго блокируется на записи, проверяйте диск и backend.

iostat -xz 1 10

Высокий await при умеренных IOPS указывает на задержку устройства или backend. Высокий utilization и растущая queue depth говорят о насыщении. Высокий throughput сам по себе не доказывает проблему: последовательное чтение может использовать канал эффективно, а мелкие синхронные записи будут медленными при небольшой полосе.

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

Если приложение медленно пишет в PersistentVolume, сначала измерьте fsync latency и очередь backend, затем проверьте CSI и сетевой маршрут. Увеличение числа реплик без проверки тома может увеличить конкуренцию и поднять задержку каждой записи.

Память Linux и контейнерная деградация

Linux использует RAM для процессов, page cache, структур ядра и буферов ввода-вывода. Большой объем занятой памяти не доказывает дефицит. Часть page cache можно освободить при необходимости, поэтому показатель free memory нужно читать вместе с available memory, RSS и признаками давления.

Производительность снижается, когда система постоянно сканирует и вытесняет страницы, выполняет активный swap-in и swap-out, ждет диск или сокращает рабочие наборы процессов. OOM killer обычно срабатывает позже, когда reclaim и swap уже не удовлетворяют запросы на память.

Как отличить полезный page cache от дефицита памяти

Смотрите available memory, RSS процессов, working set, major page faults, reclaim, PSI memory pressure и I/O wait. Полезный page cache ускоряет повторное чтение. Он становится частью проблемы, когда системе приходится постоянно вытеснять нужные страницы, а приложение снова читает их с диска.

Признаки memory pressure:

  • растет PSI memory some или full;
  • увеличивается reclaim и число major page faults;
  • появляется активный swap-in и swap-out;
  • растет I/O wait и latency диска;
  • сокращается working set процессов;
  • p95 и p99 растут вместе с задержкой памяти;
  • kubelet фиксирует давление памяти или eviction.

Очистка page cache без подтвержденной причины временно меняет распределение памяти и может убрать полезные данные из кэша. Это не устраняет leak, слишком большой рабочий набор или медленный storage. Отключение swap без диагностики сокращает запас до OOM и способно превратить медленную деградацию в резкое завершение процессов.

Swap, reclaim и OOM killer

Swap-in переносит страницы с диска в RAM, swap-out выгружает их из RAM. При активном обмене памятью процессы конкурируют с I/O и получают более высокий latency. Проверяйте скорость операций, задержку устройства и связь swap с p95, а не только наличие настроенного swap-раздела.

Для контейнера различайте три случая:

  1. процесс превышает memory limit и получает OOMKilled;
  2. node испытывает memory pressure и kubelet удаляет подходящий Pod;
  3. ядро Linux завершает процесс при общем дефиците памяти.

Проверяйте состояние Pod:

kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}'
kubectl describe pod <pod-name> -n <namespace>

Затем сопоставьте результат с memory limit, RSS, working set, cgroup events, логами kubelet и сообщениями ядра. Если после рестарта память снова растет с тем же наклоном, ищите утечку или неограниченный кэш. Если рост появляется только при большом payload, проверьте размер буферов и concurrency.

Пошаговый алгоритм поиска узкого места

Алгоритм ниже подходит для инцидента, приемочного теста и проверки перед изменением конфигурации. Он сокращает число параллельных гипотез и помогает сохранить связь между действием и результатом.

Проверка приложения и внешних зависимостей

Сначала опишите симптом и SLO: например, p99 должен быть ниже 500 мс, throughput должен составлять 800 запросов в секунду, error rate не должен превышать 1 процента. Зафиксируйте окно деградации, тип запроса и нагрузку.

Измерьте:

  • время обработки внутри приложения;
  • ожидание SQL и блокировки;
  • время ответа внешнего API;
  • размер очередей и число активных задач;
  • состояние пула соединений;
  • число retries, таймаутов и ошибок;
  • время CPU отдельно от времени ожидания.

Если полное время запроса 2 секунды, а приложение использует CPU 30 мс и 1,8 секунды ждет базу, повышение CPU limit не решит проблему. Следующим шагом станет анализ SQL, блокировок, соединений и диска базы.

Проверка контейнера и Kubernetes node

Сопоставьте потребление контейнера с ресурсами node. Используйте docker stats для одиночного Docker-хоста, kubectl top pods для Pod и kubectl top nodes для узлов. Затем изучите describe pod, events, requests, limits и причину рестартов.

Ищите такие сигналы:

  • CPU throttling при росте p95 или p99;
  • OOMKilled и повторные рестарты;
  • memory pressure и eviction;
  • высокий RSS при постоянном росте;
  • перегруженная node и дефицит allocatable;
  • несколько тяжелых workload на одном узле;
  • расхождение между requests и фактическим профилем.

Если container usage низкий, а latency растет, проверяйте сеть, storage и зависимости. Если node перегружена, сравните перенос Pod на свободный узел с изменением limits. Такой эксперимент помогает отделить проблему соседства от проблемы приложения.

Проверка сети и хранения

После проверки приложения, CPU и memory сравните прямой Pod IP с Service. Измерьте DNS, TCP connect, TLS, время первого байта, retransmissions, drops и conntrack. Затем отдельно проверьте Pod-Pod и Pod-external service.

Для хранения измерьте IOPS, throughput, await, queue depth и fsync latency. Сопоставьте эти значения с временем ожидания в приложении и профилем PersistentVolume. Если сеть стабильна, но await и fsync растут вместе с p99, меняйте storage backend или профиль операций, а не количество реплик.

Не проводите сетевой и дисковый тест одновременно. Сначала изолируйте маршрут, затем том. Иначе один эксперимент даст несколько возможных причин и не позволит выбрать безопасное исправление.

Валидация исправления и критерии завершения

Измените одну переменную: CPU limit, memory limit, requests, число реплик, topology spread, Service-маршрут или StorageClass. Повторите тот же нагрузочный сценарий с тем же payload, concurrency, временем прогрева и длительностью.

Сравните:

  • p50, p95 и p99;
  • throughput и error rate;
  • CPU usage и throttling;
  • RSS, working set и memory pressure;
  • число OOMKilled, eviction и рестартов;
  • активные соединения и длину очередей;
  • сетевые drops и retransmissions;
  • IOPS, await, queue depth и fsync latency.

Изменение можно считать подтвержденным, если SLO улучшился на повторяемом тесте, ресурсные показатели не создали новую проблему, а зависимость не приблизилась к своему пределу. Заранее определите условие отката, период наблюдения и владельца решения.

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

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

Минимальный набор вопросов перед настройкой

  • Какие версии Kubernetes, container runtime, CNI, CSI и приложения используются?
  • Какой тип нагрузки: чтение, запись, смешанный профиль, короткие запросы или долгие соединения?
  • Какой SLO задан для latency, throughput и error rate?
  • Как выглядит baseline на штатной и пиковой нагрузке?
  • Какие requests и limits назначены CPU и memory?
  • На каких node расположены Pod и есть ли свободная allocatable-емкость?
  • Проходит ли трафик через Service, NetworkPolicy, NAT или overlay?
  • Какой StorageClass, CSI backend и тип файловой системы используется?
  • Не ограничивают ли базовая данных, очередь или внешнее API скорость обработки?
  • Как выполняется откат и сколько длится наблюдение после изменения?

Зафиксируйте версии перед сравнением Docker и Kubernetes. Разные CNI, runtime или storage backend могут полностью изменить результат теста, даже если образ приложения не менялся.

Для тестового Kubernetes-кластера с изменяемыми ресурсами можно использовать облачную инфраструктуру с серверами и Kubernetes. Перед размещением рабочего сервиса проверьте регион, тип диска, сетевую задержку, лимиты IOPS и условия масштабирования.

Контрольный список метрик после изменения

СлойМетрикиСигнал проблемы
Приложениеlatency, p95, p99, throughput, error rateрастет хвост задержек или ошибки при неизменном среднем отклике
CPUusage, request, limit, throttled periods, throttled timethrottling совпадает по времени с ростом p99
MemoryRSS, working set, available memory, PSI, swapreclaim, swap-in, OOMKilled или eviction
СетьDNS, connect time, drops, retransmissions, conntrack, MTUтаймауты, потеря пакетов или задержка Service выше Pod IP
ХранилищеIOPS, throughput, await, queue depth, fsync latencyочередь и await растут вместе со временем записи
Планированиечисло реплик, node, allocatable, affinity, readinessреплики сосредоточены на одной node или не получают трафик

p99 вырос при нормальном среднем отклике. Проверьте очереди, GC, throttling, DNS, retransmissions, дисковый await и зависшие запросы. Сравните конкретные медленные операции с обычными.

Контейнер получает OOMKilled. Сопоставьте RSS и working set с memory limit, посмотрите cgroup events и нагрузку на node. Увеличение limit допустимо после проверки утечки, размера кэша и доступной памяти узла.

Добавление реплик не увеличивает throughput. Измерьте время ожидания базы, очереди, внешнего API, пул соединений и rate limit. Если все реплики ждут одну зависимость, уменьшайте очередь или меняйте зависимость.

Docker работает быстрее Kubernetes. Сравните CPU quota, соседние workload, Pod placement, Service и DNS, CNI, MTU, storage driver и PersistentVolume. Проведите тест на одной node со свободной емкостью, затем добавляйте сетевые и дисковые компоненты по одному.

Поды распределены неравномерно. Проверьте requests, allocatable, topology spread constraints, anti-affinity, node affinity, taints и tolerations. Убедитесь, что жесткие правила не оставляют Pod в Pending.

Приложение медленно пишет в PersistentVolume. Измерьте fsync latency, await, IOPS, queue depth и время ожидания в приложении. Сравните локальный диск и сетевой backend на одинаковом профиле записи. Увеличение CPU или HPA не устранит насыщенный storage.

Контрольная процедура после любого изменения проста: повторить одинаковый тест, сравнить p95 и p99, проверить throughput и error rate, посмотреть CPU, memory, сеть, I/O и рестарты, затем оставить настройку под наблюдением. Если улучшился один показатель, но выросли ошибки, очередь или нагрузка на зависимость, изменение нельзя считать успешным.

Производительность Docker и Kubernetes становится предсказуемой, когда измерения связывают пользовательскую latency с конкретным ресурсом или зависимостью. Найдите bottleneck, проверьте гипотезу на одном изменении и сохраните условия воспроизводимого теста. Такой порядок снижает риск случайной настройки и помогает отличить реальное ускорение от временного исчезновения симптома.

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