Почему производительность Kubernetes требует постоянного контроля
Кластер Kubernetes не имеет одного узкого места. На скорость приложений одновременно влияют API-сервер, etcd, kubelet, планировщик ресурсов, cgroups, сеть и хранилище. Рост числа подов увеличивает нагрузку на API-сервер: контроллеры, планировщик и каждый kubelet постоянно обращаются к нему за обновлениями. Когда latency API-сервера растёт, замедляются планировщик, контроллеры и Horizontal Pod Autoscaler.
Деградация почти всегда проявляется одинаково: приложение отвечает медленнее, поды перезапускаются, растёт очередь запросов. Причины при этом разные. Заниженный CPU-лимит даёт throttling, и сервис тормозит, хотя процессоры в кластере простаивают. Отсутствие requests превращает планирование в лотерею и перегружает отдельные ноды. Завышенные limits резервируют ресурсы, которые могли бы использовать другие сервисы. Резкий рост числа подключений к базе данных валит PostgreSQL раньше, чем заканчивается CPU.
В 2026 году большинство production-кластеров работает на cgroups v2. Поддержка cgroup v2 в Kubernetes доведена до статуса general availability (GA) в версии 1.25, что позволяет kubelet использовать новейшие возможности управления ресурсами контейнеров (Kubernetes 1.25: cgroup v2 graduates to GA). Многие свежие релизы Linux-дистрибутивов перешли на cgroup v2 по умолчанию, поэтому важно, чтобы Kubernetes продолжал корректно работать на них. Это меняет и набор метрик, и поведение планировщика. Тюнинг без метрик и базовой линии превращается в угадывание: одно изменение улучшает сценарий, другое ломает. Разбор реальной загрузки нод и подов с примерами расчётов есть в отдельном материале о загрузке нод и подов Kubernetes.
Ключевые метрики для оценки производительности кластера
Для диагностики хватает двух источников. metrics-server даёт быстрые команды kubectl, Prometheus хранит историю и позволяет строить алерты. Разовый просмотр не показывает, был пик единичным или повторяется каждый час.
kubectl top nodes kubectl top pods --containers --all-namespaces kubectl describe node node-1
kubectl top nodes выводит текущее потребление CPU и памяти по нодам. kubectl top pods --containers разбивает расход по контейнерам внутри пода. kubectl describe node показывает блок Allocated resources: сколько CPU и памяти уже зарезервировано через requests и сколько осталось. Разница между фактическим потреблением и зарезервированными requests показывает, где кластер держит ресурсы впустую.
Основной набор метрик для контроля производительности:
- apiserver_request_duration_seconds: задержка запросов к API-серверу;
- kubelet_http_requests_duration_seconds: время ответа kubelet на HTTP-запросы;
- container_cpu_usage_seconds_total: потребление CPU контейнерами;
- container_memory_working_set_bytes: рабочая память контейнера, именно по ней работает OOM-киллер;
- container_cpu_cfs_throttled_seconds_total: время, проведённое в throttling;
- container_memory_failcnt: сколько раз контейнер упирался в лимит памяти;
- node_disk_io_time_seconds_total: время, которое диск занят операциями ввода-вывода.
Примеры PromQL для поиска проблем:
rate(container_cpu_cfs_throttled_seconds_total[5m]) > 0 rate(apiserver_request_duration_seconds_sum[5m]) / rate(apiserver_request_duration_seconds_count[5m]) rate(node_disk_io_time_seconds_total[5m]) > 0.8
Первый запрос выводит контейнеры, которые тормозятся лимитом CPU. Второй считает среднюю задержку запросов к API-серверу. Третий ловит диски, загруженные более чем на 80 процентов времени. Готовые запросы и порядок поиска корневой причины аварии собраны в руководстве по диагностике подов и узлов по метрикам.
Метрики API-сервера и etcd
apiserver_request_duration_seconds хранит распределение задержки запросов с разбивкой по verb, resource и коду ответа. Рост этой метрики замедляет все контроллеры: они работают через watch к API-серверу. Стоит отдельно следить за долей кодов 429 и 5xx по метрике apiserver_request_total.
etcd влияет на API-сервер напрямую. В актуальных релизах практичнее смотреть на дисковые операции: etcd_disk_wal_fsync_duration_seconds (задержки fsync, вызываемого WAL) и etcd_disk_backend_commit_duration_seconds (задержки коммитов, вызываемых серверной частью). Большие задержки при работе с диском часто указывают на проблемы с диском и могут привести к большой задержке обработки запросов или сделать кластер нестабильным. В документации etcd дисковые метрики описываются с префиксом etcd_disk_ (Metrics | etcd).
Практические ориентиры для etcd: держать его на быстрых SSD или NVMe с предсказуемым fsync, регулярно выполнять compaction и defrag, следить за сменой лидера через etcd_server_leader_changes_seen_total. Большая база и медленный диск дают пики latency, которые затем отражаются на всех компонентах кластера.
Метрики kubelet и узлов
kubelet_http_requests_duration_seconds показывает, как быстро kubelet отвечает на запросы. Если он загружен, нода может получить статус NotReady, а поды на ней перестанут обновлять статус. Kubernetes позволяет kubelet собирать Linux kernel Pressure Stall Information (PSI) для CPU, памяти и I/O; это стабильная функция, которую нельзя отключить (Understand Pressure Stall Information (PSI) Metrics | Kubernetes). PSI-метрики предоставляются для трёх ресурсов и делятся на два типа давления: some (часть задач простаивает на ресурсе) и full (все не-idle задачи простаивают одновременно). Кумулятивное время ожидания memory PSI отражает метрика node_pressure_memory_waiting_seconds_total.
Kubelet выставляет ноде условия, по которым видно состояние узла:
- MemoryPressure: свободной памяти меньше порога, начинаются вытеснения подов;
- DiskPressure: растёт давление на корневой и на образный разделы;
- PIDPressure: заканчиваются доступные PID, новые процессы не создаются.
Проверить состояние kubelet и его логи:
systemctl status kubelet journalctl -u kubelet --since "1 hour ago" kubectl describe node node-1 | grep -A3 Conditions
Частая причина торможения kubelet - слишком частые обращения к API-серверу и большой объём локальных данных. Регулярно забитый образный раздел и частая сборка мусора тоже замедляют работу узла.
Метрики подов: CPU, память, throttling
container_cpu_cfs_throttled_seconds_total считает время, которое контейнер провёл в очереди планировщика CFS, потому что выбрал свой CPU-лимит. Throttling добавляет задержку к каждому запросу, и это одна из самых частых причин просадок в Kubernetes. Приложение может потреблять заметно меньше процессора в среднем, но упираться в лимит на коротких пиках.
container_memory_working_set_bytes отражает набор страниц памяти, который ядро считает рабочим. Именно по этой метрике OOM-киллер выбирает, кого убить. container_memory_failcnt показывает, сколько раз контейнер пытался превысить лимит памяти. Если счётчик растёт, приложение в опасной зоне. Отметим, что источники расходятся в типе этой метрики: cAdvisor описывает её как Counter с формулировкой «Number of memory usage hits limits» (cadvisor/docs/storage/prometheus.md), а документация VK WorkSpace — как Gauge «количество превышений лимита памяти» (Метрики контейнеров в VK WorkSpace). На практике важнее сам факт роста значения, чем формальный тип.
Для быстрой проверки самых прожорливых контейнеров:
kubectl top pods --containers --sort-by=memory kubectl top pods --containers --sort-by=cpu
Если в Prometheus видно, что rate(container_cpu_cfs_throttled_seconds_total[5m]) держится выше нуля, лимит CPU стоит поднимать. Сначала сравнивают throttling с фактической утилизацией: если ядра на ноде свободны, а контейнер тормозится, лимит занижен.
Настройка requests и limits: как избежать OOMKill и throttling
requests определяет, сколько ресурсов планировщик гарантирует поду при размещении. limits задаёт потолок потребления. requests влияют на решение о том, на какую ноду попадёт под, limits влияют на то, что произойдёт при скачке нагрузки. Заниженные limits дают OOMKill и throttling, завышенные резервируют ресурсы без пользы.
Пример манифеста с рабочими значениями:
apiVersion: v1
kind: Pod
metadata:
name: api
spec:
containers:
- name: api
image: example/api:1.0
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "768Mi"
Правило простое: requests ставят по типичному потреблению, limits по пиковому с запасом. Ставить limits равными requests без анализа не стоит: разница между Guaranteed и Burstable важна для поведения при нехватке ресурсов. Границы по ресурсам на уровне namespace задают LimitRange (значения по умолчанию и минимум/максимум) и ResourceQuota (суммарный предел на namespace).
QoS-классы: Guaranteed, Burstable, BestEffort
Kubernetes присваивает поду класс качества обслуживания автоматически, по заданным ресурсам.
- Guaranteed: у всех контейнеров requests равны limits по CPU и памяти. Такие поды вытесняются последними.
- Burstable: хотя бы у одного контейнера requests меньше limits или задан только requests. Основная масса рабочих нагрузок.
- BestEffort: ни requests, ни limits не заданы. Такие поды вытесняются первыми.
Когда на узле заканчиваются ресурсы, Kubernetes сначала вытесняет поды BestEffort, затем Burstable и в последнюю очередь Guaranteed. При вытеснении из-за давления на ресурсы кандидатами являются только поды, превышающие свои resource requests (Pod Quality of Service Classes | Kubernetes). Критичные сервисы логично держать в Guaranteed, остальные в Burstable с честными requests. Класс напрямую влияет на eviction ranking, поэтому он определяет, кто пострадает при перегрузке ноды.
Инструменты для автоматического подбора ресурсов
Vertical Pod Autoscaler (VPA) анализирует фактическое потребление и выдаёт рекомендации по requests и limits. Режимы: Off (только рекомендации), Initial (выставляет значения один раз при создании пода), Auto (меняет ресурсы у работающих подов, что приводит к их перезапуску). Для продакшена безопаснее начинать с Off и вручную переносить рекомендации в манифесты.
Goldilocks собирает рекомендации VPA и показывает их в дашборде, что ускоряет просмотр по всем namespace. В режиме Auto VPA перезапускает поды при изменении ресурсов, поэтому его не включают для сервисов, чувствительных к рестартам.
Horizontal Pod Autoscaler: тонкая настройка для стабильности
HPA меняет число реплик по метрикам. Наиболее распространённый вариант - масштабирование по утилизации CPU. Важное условие: HPA считает утилизацию как отношение текущего потребления к requests. Если requests не заданы, контроллер не сможет вычислить процент и не будет масштабировать.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
averageUtilization: 70 означает, что HPA держит среднее потребление CPU на уровне 70 процентов от requests. Значение слишком близко к 100 приводит к постоянному масштабированию на любом всплеске.
Масштабирование на основе кастомных метрик
Когда CPU плохо отражает нагрузку, HPA переводят на метрики приложений: число запросов в секунду, длину очереди, latency. Метрики передают в HPA через API custom.metrics.k8s.io, обычно с помощью Prometheus Adapter. Адаптер преобразует запросы Prometheus в формат, который понимает HPA.
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "100"
Учитывайте задержку сбора метрик: Prometheus забирает данные с интервалом, а HPA пересчитывает состояние не чаще заданного периода. Оба фактора замедляют реакцию на резкий рост нагрузки, поэтому для критичных сервисов кастомные метрики дополняют запасом реплик и прогревом.
Предотвращение колебаний реплик
Без ограничений HPA может постоянно увеличивать и уменьшать число подов. Это явление называют flapping: частые перезапуски вредят базе данных и внешним сервисам. Блок behavior управляет скоростью масштабирования.
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
stabilizationWindowSeconds: 300 для scaleDown означает, что HPA смотрит на рекомендации за последние 300 секунд и берёт минимальную, что сглаживает пики. Политика не более 1 пода в минуту при снижении защищает от резкого сброса нагрузки. Для scaleUp окно обычно оставляют нулевым, чтобы быстро реагировать на рост.
Профилирование приложений внутри подов
Метрики кластера показывают, что приложение тормозит, но не отвечают, почему. Ответ даёт профилирование внутри пода. Для Go и ряда других языков используют pprof: в приложение встраивают HTTP-эндпоинт, а профиль снимают через kubectl exec или port-forward.
kubectl exec -it api-0 -- wget -O - http://localhost:6060/debug/pprof/profile?seconds=30
Для процессов на C, C++ и Rust применяют perf, который показывает, какие функции съедают процессорное время. Технология eBPF позволяет трассировать приложение без изменения кода: перехватывать системные вызовы, сетевые операции и вызовы функций. Инструменты continuous profiling (Parca, Pyroscope) собирают профили постоянно и позволяют сравнивать их между версиями.
Профилирование добавляет нагрузку на процессор и память, поэтому его проводят в тестовой среде или в продакшене с осторожностью, ограничивая длительность сбора. Порядок поиска узкого места между CPU, памятью, диском и сетью разобран в статье о том, как найти узкое место в системе.
Влияние CNI на сетевую производительность
Сетевой плагин (CNI) определяет, как поды общаются между собой и с внешними сервисами. От него зависят задержка, пропускная способность и накладные расходы на обработку пакетов.
- Calico: зрелое решение с гибкими сетевыми политиками, поддерживает несколько режимов маршрутизации;
- Cilium: использует eBPF, обрабатывает пакеты в ядре и часто даёт меньшую задержку и меньше переключений контекста;
- Flannel: прост в настройке, подходит для небольших кластеров, но уступает по гибкости и производительности.
Неправильный MTU ломает сеть незаметно: пакеты фрагментируются или отбрасываются, и пропускная способность падает при формально рабочем соединении. Значение MTU подбирают под сеть: при инкапсуляции VXLAN его уменьшают, чтобы избежать фрагментации. Реальные потери пакетов, джиттер и задержки разобраны в статье о том, как сеть влияет на производительность.
Проверить пропускную способность между подами можно с помощью iperf3: запускают сервер в одном поде и клиент в другом, замеряют throughput и задержку. Так сравнивают поведение до и после смены CNI или правки MTU.
Оптимизация подключений к PostgreSQL через PgBouncer в Kubernetes
Большое число подов быстро создаёт лавину подключений к PostgreSQL. Флот из 500 подов легко попросит около 20 000 серверных процессов при max_connections в несколько сотен. Развитие событий типичное: ночью проходит выкат, все поды разом открывают свои пулы, PostgreSQL отвечает FATAL: too many connections, а новые поды зависают в CrashLoopBackOff. Разбор этого сценария и настройки пула приведён в материале о PgBouncer в Kubernetes.
Решение - PgBouncer между приложениями и базой. Режим pool_mode определяет, как выдаются серверные соединения. В transaction соединение выдаётся на одну транзакцию и забирается обратно, что позволяет сотням клиентов обойтись десятками серверных процессов. Режим session держит соединение до отключения клиента, а statement выдаёт его на один оператор и ломает любую транзакцию из двух операторов, поэтому его не используют.
Ключевые параметры и их значения по умолчанию:
- default_pool_size равен 20 и считается на пару «пользователь и база»;
- max_db_connections по умолчанию 0, то есть без потолка;
- query_wait_timeout равен 120 секундам: клиент, которому не досталось серверного соединения, отключается, а не ждёт бесконечно.
Потолок, который важен на практике, считают как instances умножить на max_db_connections, и он должен быть ниже max_connections PostgreSQL с запасом на репликацию и живой psql. Пример: две реплики PgBouncer с max_db_connections 40 дают не больше 80 серверных соединений, а при max_connections 200 запас ещё остаётся. Поднимать max_connections в PostgreSQL для обхода ошибки не стоит: это убирает симптом и портит пропускную способность из-за роста памяти и переключений контекста. Когда активных серверных соединений становится заметно больше двух на ядро, лишние обычно добавляют задержку.
Состояние пула смотрят командой SHOW POOLS. Метрика cl_waiting показывает клиентов в очереди, cl_active - клиентов, которые держат серверное соединение. Очередь cl_waiting, которая не рассасывается, говорит о маленьком пуле или медленном запросе. В режиме transaction есть ловушка: с PgBouncer 1.7 server_reset_query не выполняется, поэтому состояние сессии (SET без LOCAL, pg_advisory_lock, временные таблицы, LISTEN) остаётся на серверном соединении и всплывает у следующего клиента.
Практические шаги по тюнингу кластера
Порядок действий снижает риск сломать рабочую среду. Меняйте по одному параметру за раз и фиксируйте результат.
- Соберите метрики за несколько дней: latency API-сервера, дисковые задержки etcd, throttling и потребление памяти по подам.
- Сравните фактическое потребление с requests через kubectl describe node и kubectl top pods. Найдите поды без requests и поды с завышенными limits.
- Поставьте requests и limits по данным VPA в режиме Off. Для критичных сервисов рассмотрите QoS Guaranteed.
- Настройте HPA на CPU, добавьте behavior с окном стабилизации, при необходимости подключите кастомные метрики через Prometheus Adapter.
- Снимите профиль проблемного приложения через pprof, perf или eBPF и устраните узкое место в коде или конфигурации.
- Проверьте CNI и MTU, замерьте throughput через iperf3 между подами.
- При работе с PostgreSQL добавьте PgBouncer в режиме transaction и рассчитайте потолок соединений.
- После каждого изменения смотрите метрики и сравнивайте с базовой линией.
Все изменения сначала обкатывают на staging. Как производительность связана с отказоустойчивостью и почему запас по ресурсам влияет на поведение при сбоях, разобрано в статье о связи производительности и отказоустойчивости.
Типичные ошибки и как их избежать
- Поды без requests и limits. Планировщик размещает их вслепую, при нехватке памяти они вытесняются первыми. Решение: задать requests и включить LimitRange для namespace.
- limits выше реальных потребностей в разы. Ресурсы резервируются и простаивают. Решение: подбирать limits по пиковому потреблению, а не на глаз.
- Неверный QoS-класс для критичного сервиса. BestEffort-поды вытесняются первыми. Решение: для критичных нагрузок держать Guaranteed или Burstable с честными requests.
- HPA без requests. Утилизация не вычисляется, масштабирование не работает. Решение: задать requests до включения HPA.
- Игнорирование throttling. Приложение отвечает медленно при свободных ядрах. Решение: следить за container_cpu_cfs_throttled_seconds_total и поднимать лимит CPU.
- Неправильный MTU в CNI. Пакеты теряются, throughput падает. Решение: подобрать MTU под инкапсуляцию и проверить iperf3.
- Отсутствие пула соединений к PostgreSQL. Всплеск подов даёт FATAL: too many connections и CrashLoopBackOff. Решение: PgBouncer в режиме transaction и расчёт потолка соединений.
Каждую правку проверяйте на staging, а в продакшене вводите постепенно, следя за метриками. Такой подход экономит время и не превращает тюнинг в источник новых аварий.