Диагностика ресурсов Kubernetes
Профессиональная пошаговая диагностика и устранение неполадок ресурсов Kubernetes с готовыми kubectl-командами и исправлениями.
Пример запуска
Сгенерировано Claude Opus 5.5, 23 сентября 2026 г.
Значения переменных
- Тип ресурса
- Deployment (и связанные Pod'ы)
- Имя и namespace
- payment-api, namespace billing
- Симптомы
- Поды постоянно уходят в CrashLoopBackOff с интервалом ~2-5 минут, счётчик RESTARTS растёт (уже 47 за последние 3 часа). В момент падения в логах приложения (Java/Spring Boot) видна java.lang.OutOfMemoryError: Java heap space. При этом kubectl top показывает, что использование памяти подом резко подскакивает перед рестартом. Проблема началась сразу после вчерашнего релиза v2.14.0, в котором увеличили batch-размер обработки платёжных транзакций с 100 до 1000.
- Версия kubernetes
- v1.28.6
- Окружение
- Продакшен, self-managed кластер на bare-metal через kubeadm, 3 master-ноды + 8 worker-нод, Ubuntu 22.04, CNI Calico v3.27, CSI Longhorn v1.6, containerd 1.7
- Манифесты и вывод команд
- $ kubectl get pods -n billing -l app=payment-api NAME READY STATUS RESTARTS AGE payment-api-7f9d8c6b45-2kx9p 0/1 CrashLoopBackOff 47 (2m ago) 3h12m payment-api-7f9d8c6b45-9mvqt 0/1 CrashLoopBackOff 45 (3m ago) 3h12m payment-api-7f9d8c6b45-jr4wz 1/1 Running 43 (7m ago) 3h12m $ kubectl describe pod payment-api-7f9d8c6b45-2kx9p -n billing ... Containers: payment-api: Container ID: containerd://a1b2c3d4e5f6... Image: registry.internal.example.com/billing/payment-api:v2.14.0 Port: 8080/TCP State: Waiting Reason: CrashLoopBackOff Last State: Terminated Reason: OOMKilled Exit Code: 137 Started: Tue, 23 Sep 2026 10:41:02 +0300 Finished: Tue, 23 Sep 2026 10:43:47 +0300 Ready: False Restart Count: 47 Limits: cpu: 500m memory: 256Mi Requests: cpu: 250m memory: 256Mi Liveness: http-get http://:8080/actuator/health delay=30s timeout=1s period=10s #success=1 #failure=3 Readiness: http-get http://:8080/actuator/health delay=15s timeout=1s period=5s #success=1 #failure=3 Environment: JAVA_OPTS: -Xms256m -Xmx256m BATCH_SIZE: 1000 Conditions: Type Status Initialized True Ready False ContainersReady False PodScheduled True Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 3h12m default-scheduler Successfully assigned billing/payment-api-7f9d8c6b45-2kx9p to worker-node-04 Normal Pulled 3h12m kubelet Container image already present on machine Normal Created 3h12m kubelet Created container payment-api Normal Started 3h12m kubelet Started container payment-api Warning BackOff 28s (x39 over 3h10m) kubelet Back-off restarting failed container payment-api in pod payment-api-7f9d8c6b45-2kx9p_billing $ kubectl logs payment-api-7f9d8c6b45-2kx9p -n billing --previous --tail=30 2026-09-23 10:43:41.221 INFO 1 --- [io-8080-exec-9] c.e.billing.service.TxnBatchProcessor : Processing batch of 1000 transactions, batchId=88213 2026-09-23 10:43:42.884 WARN 1 --- [io-8080-exec-9] o.s.o.jpa.JpaSystemException : could not extract ResultSet 2026-09-23 10:43:43.019 ERROR 1 --- [io-8080-exec-9] c.e.billing.service.TxnBatchProcessor : Exception in batch processing java.lang.OutOfMemoryError: Java heap space at com.example.billing.service.TxnBatchProcessor.loadTransactionEntities(TxnBatchProcessor.java:142) at com.example.billing.service.TxnBatchProcessor.process(TxnBatchProcessor.java:88) at java.base/java.lang.Thread.run(Thread.java:840) 2026-09-23 10:43:47.331 INFO 1 --- [Thread-3] o.s.b.a.ApplicationAvailabilityBean : Application availability state LivenessState changed to BROKEN $ kubectl top pod -n billing -l app=payment-api NAME CPU(cores) MEMORY(bytes) payment-api-7f9d8c6b45-2kx9p 480m 248Mi payment-api-7f9d8c6b45-9mvqt 475m 251Mi payment-api-7f9d8c6b45-jr4wz 310m 230Mi Фрагмент манифеста Deployment (текущая версия, spec.template.spec.containers[0]): - name: payment-api image: registry.internal.example.com/billing/payment-api:v2.14.0 ports: - containerPort: 8080 env: - name: JAVA_OPTS value: "-Xms256m -Xmx256m" - name: BATCH_SIZE value: "1000" resources: requests: cpu: 250m memory: 256Mi limits: cpu: 500m memory: 256Mi livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 15 periodSeconds: 5 failureThreshold: 3
Диагностика: payment-api (namespace billing)
Диагноз
Корневая причина — неверный расчёт памяти JVM, который вскрылся после увеличения batch-размера. -Xmx256m равен limits.memory: 256Mi, поэтому у JVM нет запаса под non-heap память. В v2.14.0 BATCH_SIZE вырос с 100 до 1000, и рабочий набор heap при загрузке JPA-сущностей (loadTransactionEntities) превысил допустимое. В результате происходят сразу два отказа:
- внутри JVM:
OutOfMemoryError: Java heap space; - на уровне cgroup: kernel OOM killer завершает контейнер (
OOMKilled, exit 137).
Причина не в kubelet, планировщике, сети или хранилище. Это ошибка конфигурации ресурсов приложения, а триггер — изменение в коде и конфигурации релиза.
Обоснование
Тип завершения. Last State: Terminated, Reason: OOMKilled, Exit Code: 137 означает SIGKILL от OOM killer по лимиту memory cgroup контейнера. Liveness probe и само приложение контейнер не завершали.
Хронология одного цикла (под 2kx9p):
| Время | Событие |
|---|---|
| 10:41:02 | Старт контейнера |
| 10:43:41 | Processing batch of 1000 transactions, batchId=88213 |
| 10:43:42 | JpaSystemException: could not extract ResultSet. Вероятно, OOM возник уже при материализации ResultSet |
| 10:43:43 | OutOfMemoryError: Java heap space в TxnBatchProcessor.loadTransactionEntities:142 |
| 10:43:47 | Spring переводит LivenessState в BROKEN. В ту же секунду Finished: ядро убивает процесс |
Время жизни контейнера около 2 мин 45 с, что совпадает с наблюдаемым интервалом 2–5 минут. Liveness probe (3 × 10 с) не успела бы сработать за 4 секунды. Значит, убил именно cgroup OOM.
Бюджет памяти не сходится даже теоретически. В 256Mi лимита нужно уместить:
- heap до 256 MiB;
- Metaspace (Spring Boot + Hibernate обычно 80–150 MiB);
- CodeCache (20–50 MiB);
- стеки потоков Tomcat (до 200 потоков);
- служебные структуры GC, direct/NIO буферы, память glibc malloc.
Реальный RSS JVM с таким -Xmx составляет 400–500 MiB. Пока batch был 100, heap не заполнялся целиком и RSS оставался чуть ниже лимита. При batch 1000 heap заполняется, GC касается всех страниц, и RSS упирается в лимит.
Метрики. kubectl top показывает 248–251Mi при лимите 256Mi у падающих подов. Под jr4wz (230Mi, 43 рестарта) находится в том же цикле и просто пока не дошёл до своего batch.
Корреляция с релизом. Проблема началась сразу после v2.14.0 (BATCH_SIZE: 1000, в 10 раз больше). Стек указывает на загрузку сущностей всего batch в память одной выборкой.
Все реплики падают одинаково. Это типично для «ядовитого» batch: batch 88213 не фиксируется как обработанный, и после рестарта его подхватывает та же или соседняя реплика. Для платёжного сервиса нужно проверить идемпотентность, см. раздел о недостающих данных.
Сопутствующие проблемы. Они не являются корневой причиной, но усугубляют картину:
limits.cpu: 500m, фактическое потребление 475–480m, то есть CPU throttling. JVM видитActiveProcessorCount=1. При 1 CPU и менее 1792 MB памяти JDK автоматически выбирает SerialGC, а не G1: GC-паузы длинные, старт медленный.- Liveness и readiness используют один эндпоинт
/actuator/healthсtimeout=1s. При GC-паузах или недоступности БД это может вызывать ложные рестарты и каскадные отказы.startupProbeотсутствует. - QoS класс Burstable: memory request равен limit, но CPU request меньше limit.
- Нет
-XX:+ExitOnOutOfMemoryError. После heap OOM JVM остаётся в неопределённом состоянии, пока её не добьёт ядро или probe.
Исключено по данным: Pending/scheduling (PodScheduled=True), ImagePull (image already present), PVC, RBAC, CNI/DNS. Событий о проблемах с томами, сетью или eviction нет.
Команды для подтверждения
NS=billing; POD=payment-api-7f9d8c6b45-2kx9p
# 1. Причина завершения по всем подам (OOMKilled vs Error/liveness)
kubectl get pods -n $NS -l app=payment-api -o custom-columns=\
NAME:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount,\
REASON:.status.containerStatuses[0].lastState.terminated.reason,\
EXIT:.status.containerStatuses[0].lastState.terminated.exitCode,\
NODE:.spec.nodeName
# 2. События по времени: нет ли Unhealthy (liveness), Evicted, MemoryPressure
kubectl get events -n $NS --sort-by=.lastTimestamp \
--field-selector involvedObject.name=$POD
kubectl get events -n $NS --sort-by=.lastTimestamp | grep -Ei 'unhealthy|oom|evict|killing'
# 3. Логи предыдущего запуска: какой batch валит поды, повторяется ли batchId
kubectl logs $POD -n $NS --previous | grep -E 'batchId|OutOfMemory|LivenessState'
for p in $(kubectl get pods -n $NS -l app=payment-api -o name); do
echo "== $p"; kubectl logs $p -n $NS --previous | grep -oE 'batchId=[0-9]+' | tail -3
done
# 4. Реально ли JVM применяет JAVA_OPTS и какие флаги выбраны
# (выполнять в окне ~2 мин, пока контейнер жив)
kubectl exec -n $NS $POD -- sh -c 'cat /proc/1/cmdline | tr "\0" " "; echo'
kubectl exec -n $NS $POD -- jcmd 1 VM.flags # MaxHeapSize, UseSerialGC/UseG1GC
kubectl exec -n $NS $POD -- jcmd 1 GC.heap_info
kubectl exec -n $NS $POD -- sh -c 'cat /sys/fs/cgroup/memory.max; cat /sys/fs/cgroup/memory.current; grep -E "anon|file " /sys/fs/cgroup/memory.stat'
kubectl exec -n $NS $POD -- sh -c 'grep -E "nr_throttled|throttled_usec" /sys/fs/cgroup/cpu.stat'
# 5. Подтверждение cgroup OOM на ноде (а не глобального OOM ноды)
kubectl debug node/worker-node-04 -it --image=ubuntu:22.04 -- \
chroot /host sh -c 'dmesg -T | grep -iE "memory cgroup out of memory|oom-kill|Killed process" | tail -20'
kubectl describe node worker-node-04 | grep -A8 -E 'Conditions|Allocated resources'
# 6. Что изменилось в релизе и есть ли ограничения на увеличение ресурсов
kubectl rollout history deployment/payment-api -n $NS
kubectl rollout history deployment/payment-api -n $NS --revision=<N-1> # сравнить env/resources
kubectl get limitrange,resourcequota,hpa,pdb -n $NS
kubectl describe resourcequota -n $NS
Ожидаемые результаты, подтверждающие диагноз:
REASON=OOMKilled,EXIT=137у всех подов;- в
dmesgестьMemory cgroup out of memory: Killed process ... (java); memory.max= 268435456;MaxHeapSize= 268435456 иUseSerialGC;- нет событий
Unhealthyперед убийством.
Исправление
Этап A. Немедленная стабилизация (минуты)
- Снизить batch без пересборки образа. Это быстрее и безопаснее, чем
rollout undo, потому что v2.14.0 мог включать миграции БД, несовместимые с v2.13:
Если v2.14.0 ломается и приkubectl set env deployment/payment-api -n billing BATCH_SIZE=100 kubectl rollout status deployment/payment-api -n billingBATCH_SIZE=100, и совместимость схемы БД подтверждена, откатить релиз:kubectl rollout undo deployment/payment-api -n billing. - Проверить судьбу проблемного batch (88213 и соседних) в БД или очереди. Нет ли частично применённых или дублированных платёжных транзакций? Это обязательно до того, как сервис начнёт штатно обрабатывать поток.
Этап B. Правильный sizing памяти (в течение дня, через GitOps/CI, а не kubectl edit)
- Убрать жёсткий
-Xmx. Размер heap задаётся от лимита контейнера черезMaxRAMPercentage, чтобы оставить примерно 25–30% под non-heap. - Поднять лимит памяти. Стартовое значение 1Gi: heap около 720 MiB плюс запас около 300 MiB под non-heap. Итоговое значение уточнить по GC-логам и NMT под реальной нагрузкой с batch 1000.
- Сделать память Guaranteed (request = limit). CPU request поднять, CPU limit увеличить до 2 или убрать по политике кластера. Задать
ActiveProcessorCount=2и явно включить G1, чтобы JVM не выбрала SerialGC. - Добавить
ExitOnOutOfMemoryErrorи heap dump. Heap dump для платёжного сервиса может содержать чувствительные данные: писать на защищённый том и ограничить доступ. - Развести probes: liveness на
/actuator/health/liveness, readiness на/actuator/health/readiness, плюсstartupProbe. - Перед применением проверить квоты и ёмкость:
ResourceQuotaиLimitRangeвbillingдолжны разрешать 3 × 1Gi;- на worker-нодах должно хватать allocatable-ресурсов (
kubectl describe node | grep -A8 Allocated); - есть ли HPA по памяти: процент утилизации пересчитается от нового request.
- name: payment-api
image: registry.internal.example.com/billing/payment-api:v2.14.0
ports:
- containerPort: 8080
env:
- name: JAVA_OPTS
value: >-
-XX:MaxRAMPercentage=70.0
-XX:InitialRAMPercentage=50.0
-XX:+UseG1GC
-XX:ActiveProcessorCount=2
-XX:MaxMetaspaceSize=192m
-XX:ReservedCodeCacheSize=64m
-XX:MaxDirectMemorySize=64m
-XX:+ExitOnOutOfMemoryError
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/dumps
-XX:NativeMemoryTracking=summary
-Xlog:gc*:stdout:time,uptime,level,tags
- name: BATCH_SIZE
value: "100" # вернуть к 1000 только после исправления кода (шаг 9) и нагрузочного теста
resources:
requests:
cpu: "1"
memory: 1Gi
limits:
cpu: "2"
memory: 1Gi # request == limit по памяти
volumeMounts:
- name: dumps
mountPath: /dumps
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 5
failureThreshold: 36 # до 180 с на старт
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
# на уровне spec.template.spec:
# volumes:
# - name: dumps
# emptyDir:
# sizeLimit: 2Gi # или PVC Longhorn, если дампы нужны после рестарта
В application.yaml должно быть management.endpoint.health.probes.enabled: true. В Kubernetes Spring Boot 2.3+ включает это автоматически. Также убедитесь, что JAVA_OPTS реально подставляется в ENTRYPOINT (шаг 4 проверки). Если нет, используйте JAVA_TOOL_OPTIONS.
Этап C. Исправление в коде (корневая причина на уровне приложения)
-
Переписать
TxnBatchProcessor.loadTransactionEntities. Сейчас он материализует весь batch с графом сущностей в persistence context. Варианты:- обработка чанками по 100–200 в рамках batch 1000:
flush()+clear()EntityManager после каждого чанка; - DTO-проекции вместо полных сущностей; отказ от
EAGER-связей;JOIN FETCHтолько нужного; hibernate.jdbc.fetch_size/Stream<>с@QueryHints(HINT_FETCH_SIZE)вместоgetResultList();hibernate.jdbc.batch_sizeдля записи.
- обработка чанками по 100–200 в рамках batch 1000:
-
Нагрузочный тест с
BATCH_SIZE=1000на стенде с теми же лимитами. Снять:jcmd 1 GC.heap_info;jcmd 1 VM.native_memory summary;- пиковый
memory.current.
Лимит = пиковый RSS + 20–25%.
-
Поднимать
BATCH_SIZEпостепенно (100 → 250 → 500 → 1000) с наблюдением за метриками.
Профилактика
- Правило sizing для JVM: никогда не ставить
-Xmx≥limits.memory. ИспользоватьMaxRAMPercentage60–75%. - Параметры, влияющие на объём данных в памяти (batch, page size, cache size), считать изменением capacity: обязательный нагрузочный тест и ревью
resourcesв том же PR. - Алерты:
container_memory_working_set_bytes / limit > 0.85;kube_pod_container_status_last_terminated_reason{reason="OOMKilled"};increase(kube_pod_container_status_restarts_total[15m]) > 2;- JVM-метрики
jvm_memory_used_bytes{area="heap"}через Micrometer.
- Progressive delivery: canary (Argo Rollouts/Flagger) с автоматическим откатом по рестартам и error rate. Все 3 реплики не должны получать новый релиз одновременно.
- Идемпотентность batch-обработки платежей: фиксация прогресса по чанкам, DLQ или карантин для «ядовитого» batch после N неудач, чтобы один batch не валил все реплики.
- Политика в namespace:
LimitRangeс разумными дефолтами; policy-проверка (Kyverno/Gatekeeper) на соответствие-Xmxи лимитов; раздельные liveness/readiness и наличиеstartupProbeдля JVM-сервисов. - VPA в режиме
Off(recommendation only) для payment-api, чтобы видеть реальные рекомендации по памяти и CPU.
Недостающие данные
Для полной уверенности не хватает следующего:
- Применяются ли
JAVA_OPTSна деле (ENTRYPOINT образа,jcmd 1 VM.flags). Если нет, JVM берёт дефолтный heap в 25% от 256Mi, около 64 MiB. Тогда heap OOM ещё очевиднее, а расчётMaxRAMPercentageнужно проверить на фактическом образе. - Профиль памяти v2.13 при batch 100 (метрики за прошлую неделю). Он покажет, насколько близко к лимиту сервис работал до релиза.
- Heap dump или NMT на момент OOM. Это подтвердит, что память занимают именно сущности batch, а не утечка, например кеш в новом коде v2.14.0.
- Повторяется ли один и тот же
batchIdво всех падениях и состояние транзакций этого batch в БД. Это критично для целостности платёжных данных. - Содержит ли v2.14.0 миграции схемы БД. От этого зависит, безопасен ли
rollout undo.
Похожие промпты
Ревью и рефакторинг PowerShell-скрипта
Разбирает существующий скрипт PowerShell: что сломается на реальных данных, где ошибки глушатся вместо обработки и что переписать в первую очередь, а что оставить.
Скрипт PowerShell под задачу
Пишет скрипт PowerShell с обработкой ошибок, логированием и режимом пробного прогона — так, чтобы его можно было запустить на рабочей машине и не бояться последствий.
Диагностика проблем VPN-подключения
Методично диагностирует проблемы VPN: соединение не устанавливается, скорость падает, утекает DNS — по одному шагу, с командами под вашу ОС.
Помощник по настройке WireGuard и OpenVPN
Пошагово разворачивает собственный VPN-сервер на WireGuard или OpenVPN: готовые команды и конфиги, firewall, клиентские профили и проверка утечек.