← Kubernetes: разбор поломок
7 мин · жёстко · Урок 7 из 12

Pod Running, а клиент видит 502

Разбор инцидента, где Pod здоров, но Service форвардит трафик в закрытый порт.

Инцидент выглядит издевательски. Pod в статусе Running, рестартов ноль, логи чистые, приложение внутри контейнера отвечает. А пользователи получают 502.

Такая картина почти всегда означает разрыв между Service и Pod. Ingress controller тут ни при чём, он честно передаёт то, что ему вернул upstream.

Три разных порта в одной цепочке

Service кажется простым, пока кто-то не путает порты, которые называются почти одинаково. Их удобно держать в голове как трубопровод:

client → svc.clusterIP:port → pod:targetPort == container:containerPort → listener

nodePort в этой цепочке нет: у type=NodePort он добавил бы внешний хоп nodeIP:nodePort перед svc.port. У ClusterIP-сервиса снаружи ноды ничего не видно.

Service не проверяет, открыт ли targetPort внутри Pod. Он просто форвардит. Если приложение слушает :8080, а Service ставит targetPort: 80, kube-proxy упрётся в закрытый порт. Запрос на ClusterIP вернёт connection refused, а тот же путь через ingress controller отдаст наружу 502.

Без readinessProbe на том же порту Pod остаётся Ready с закрытым listener-ом: он попадает в endpoints и получает трафик. Раскат проходит зелёным, 502 видит клиент.

ClusterIP-сервис проверяют не снаружи, а изнутри кластера, из другого Pod:

curl http://webapp.demo.svc:80/

Симптом 502 vs Connection Refused

Где видишьЧто значит
Connection refused на ClusterIP:portв endpoints нет Pod ИЛИ targetPort бьёт в закрытый порт
Empty reply from serverlistener есть, но падает на запросе
502 через ingress controllerupstream (Pod) есть, но порт не отвечает

Главная путаница инцидента: клиент видит 502, а причина живёт между Service и Pod.

Диагностика по шагам

Не вилами, а по цепочке.

Шаг 1. Service видит Pod?

kubectl get endpoints webapp

Если <none> или пустой ENDPOINTS: selector в Service не совпадает с labels на Pod, либо Pod ещё не Ready. Это первая причина 502.

Если в ENDPOINTS есть IP:PORT, Pod подцеплен, читаем порт.

Объект v1 Endpoints deprecated с 1.33, и kubectl печатает про это предупреждение. Тот же список адресов через актуальный объект:

kubectl get endpointslice -l kubernetes.io/service-name=webapp

Шаг 2. Какой порт прописан в endpoints?

kubectl get endpoints webapp -o yaml

Поле subsets[0].ports[0].port это и есть тот самый targetPort из Service, разрешённый kube-proxy.

Шаг 3. Pod реально слушает этот порт?

POD=$(kubectl get pod -l app=webapp -o name | head -1)

# Проверка порта ВНУТРИ контейнера
kubectl exec "$POD" -- wget -qO- http://localhost:8080/ \
  || echo "listener мёртв на 8080"

# Альтернатива: посмотреть netstat, если он есть в образе
kubectl exec "$POD" -- ss -tlnp 2>/dev/null

Listener на одном порту, а Service форвардит на другой - вот и разрыв.

Шаг 4. Сравнить спеки

kubectl get svc webapp \
  -o jsonpath='{"port="}{.spec.ports[0].port}{" targetPort="}{.spec.ports[0].targetPort}{"\n"}'

kubectl get deploy webapp \
  -o jsonpath='{.spec.template.spec.containers[0].ports[*].containerPort}{"\n"}'

Совпасть должно вот что:

Быстрее всего разрыв видно в одном выводе:

kubectl describe svc webapp

describe печатает TargetPort, Endpoints и Selector рядом.

Откуда берётся расхождение

Как не наступать снова

Порт приложения стоит держать в одном месте манифеста и подставлять его в targetPort, containerPort и probe.

Домашка

Теорию закрепляет задание на своём кластере: 2 шага с проверкой. Кластер поднимается по треку Kubernetes локально, задания лежат в репозитории k8s-practice.

./run.sh start k8s-port-mismatch-502     # развернуть стартовое состояние
./run.sh verify k8s-port-mismatch-502    # проверить текущий шаг

Коротко

Прошли материал до конца? Отметьте урок пройденным.