Pod Running, а клиент видит 502
Разбор инцидента, где Pod здоров, но Service форвардит трафик в закрытый порт.
Инцидент выглядит издевательски. Pod в статусе Running, рестартов ноль, логи чистые, приложение внутри контейнера отвечает. А пользователи получают 502.
Такая картина почти всегда означает разрыв между Service и Pod. Ingress controller тут ни при чём, он честно передаёт то, что ему вернул upstream.
Три разных порта в одной цепочке
Service кажется простым, пока кто-то не путает порты, которые называются почти одинаково. Их удобно держать в голове как трубопровод:
client → svc.clusterIP:port → pod:targetPort == container:containerPort → listener
port. Порт самого Service внутри кластера (ClusterIP:port). К нему ходят клиенты изнутри кластера.targetPort. Порт на pod-е, куда Service форвардит трафик.containerPort. Декларация порта в spec контейнера. Информационное поле для документации; на самом деле проксируется любой открытый порт.- 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 server | listener есть, но падает на запросе |
| 502 через ingress controller | upstream (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"}'
Совпасть должно вот что:
targetPortв Service равен порту, на котором слушает приложение;containerPortв Deployment лучше выставить в тот же порт. Формально для работы Service он не обязателен, но критичен для readinessProbe и для ingress.
Быстрее всего разрыв видно в одном выводе:
kubectl describe svc webapp
describe печатает TargetPort, Endpoints и Selector рядом.
Откуда берётся расхождение
- Service создан через
kubectl expose deploy --port=80: targetPort по умолчанию равен port. Контейнер слушает 8080, Service попадает в никуда. - Скопировали манифест с nginx (порт 80) для nginx-unprivileged (порт 8080): containerPort и listener сдвинулись, targetPort остался.
- В Deployment поменяли образ или конфиг, listener переехал, а Service-манифест никто не тронул.
Как не наступать снова
Порт приложения стоит держать в одном месте манифеста и подставлять его
в targetPort, containerPort и probe.
Домашка
Теорию закрепляет задание на своём кластере: 2 шага с проверкой. Кластер поднимается по треку Kubernetes локально, задания лежат в репозитории k8s-practice.
./run.sh start k8s-port-mismatch-502 # развернуть стартовое состояние
./run.sh verify k8s-port-mismatch-502 # проверить текущий шаг
Коротко
- Pod Running плюс 502 у клиента - смотри Service, а не приложение.
kubectl get endpointsотделяет проблему selector-а от проблемы порта.- Порт в endpoints это разрешённый
targetPort; сравнивай его с реальным listener-ом внутри контейнера. kubectl describe svcпоказывает TargetPort, Endpoints и Selector сразу.- readinessProbe на том же порту превращает тихий разрыв в остановленный раскат.
Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс