Сеть в Kubernetes часто кажется мистикой: сервис есть, а не отвечает; адрес резолвится, но соединение зависает; после деплоя соседняя команда ловит 503. Всё это объясняется четырьмя механизмами: Service, DNS, Ingress, NetworkPolicy. Разберём их по очереди.
Главная связка здесь — между именем сервиса и списком endpoints: имя резолвится всегда, а трафик идёт только туда, где прошла проверка готовности.
Имя и ClusterIP у Service есть всегда; за ними стоит список endpoints, куда под попадает только после readiness probe — пустой endpoints при живых подах означает probes, а не сеть.
Почему IP пода нельзя использовать напрямую
В Kubernetes у каждого пода свой IP-адрес, и все поды видят друг друга в одной плоской сети — без NAT. Казалось бы, просто обращайся по IP и живи.
Проблема: поды недолговечны. Перезапуск, обновление, сбой ноды — и под поднимается с новым IP. Жёстко прописать адрес в конфиге — значит сломать систему при первом же пересоздании.
Именно для этого и существует Service: абстракция, которая даёт постоянное сетевое имя изменчивому множеству подов.
Типы Service: что выбрать и когда
Service описывается в YAML и указывает, какие поды он объединяет (через selector с метками) и как они доступны снаружи.
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: payments
spec:
selector:
app: order # объединяет все поды с меткой app=order
ports:
- port: 80
targetPort: 8080
Типов у Service четыре, и появились они по мере того, как упирались в очередную задачу: сначала нужен был стабильный адрес внутри кластера, потом выход наружу без облака, потом облачный балансировщик, потом имя для того, что живёт снаружи.
ClusterIP — тип по умолчанию. Даёт виртуальный IP, доступный только внутри кластера. Для связи одного микросервиса с другим — это он, и почти всегда только он.
NodePort — открывает порт на каждой ноде кластера (30000–32767). Технический примитив; в продакшне напрямую не используется, но на нём строится тип LoadBalancer.
LoadBalancer — заказывает внешний балансировщик у облачного провайдера и подключает его к сервису. Дорого вешать на каждый сервис: обычно один LoadBalancer стоит перед Ingress-контроллером, а сервисы публикуются через Ingress (об этом ниже).
ExternalName — самый странный на вид: подов за ним нет вообще. Он просто говорит DNS: «имя payments-db внутри кластера означает вот этот внешний адрес». Пригождается, когда база или чужой сервис живут снаружи, а код хочет обращаться к ним по такому же короткому имени, как к соседям по кластеру, — и когда однажды сервис переедет внутрь, менять придётся один манифест, а не конфиги всех вызывающих.
Отдельно стоит headless-режим — его часто называют пятым типом, но это не так. Тип у такого сервиса обычный, ClusterIP, просто в нём явно написано clusterIP: None; значения type: Headless в Kubernetes не существует. Виртуального IP тогда нет, и DNS-имя резолвится сразу в список адресов конкретных подов. Нужен он, когда клиенту важно знать каждый экземпляр отдельно: кластеры баз данных, Kafka-клиенты, StatefulSet-ы.
DNS: как сервисы находят друг друга по имени
Сосед переехал в другой namespace — и http://order-service перестал находиться. Чтобы понять почему, нужно знать, как строится имя. Каждый Service автоматически получает DNS-имя:
имя-сервиса.namespace.svc.cluster.local
Внутри того же namespace достаточно короткого имени:
http://order-service
Из другого namespace нужен namespace-префикс:
http://order-service.payments
Это и есть правильный способ обращаться к соседнему сервису — никогда не IP, всегда DNS-имя. И всегда в паре с таймаутами: DNS-имя не делает соседа надёжным.
app:
clients:
order:
base-url: http://order-service.payments
connect-timeout: 1s
read-timeout: 3s
Полное имя из четырёх частей: внутри своего namespace хватает первой, из соседнего нужны первые две.
Почему короткое имя работает и когда оно стоит пяти запросов
Обращение по order-service работает не магией, а настройкой разрешения имён, которую kubelet кладёт в каждый под:
search payments.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
Две строки здесь важны. search — список доменов, которые по очереди дописываются к короткому имени: order-service превращается в order-service.payments.svc.cluster.local, и с первой попытки находится. ndots:5 говорит: если в имени меньше пяти точек, сначала пробуй дописать домены из search, и только потом считай имя полным.
Отсюда неприятный эффект, о котором обычно узнают по графику задержек. Имя order-service.payments содержит одну точку, то есть меньше пяти — значит перебираются все домены поиска: три неудачных запроса, потом попытка как есть. Для внешнего имени вроде api.partner.com (две точки) то же самое: три промаха и только потом успех. При каждом обращении. Библиотека, которая не кэширует адреса (а JVM кэширует, но с оговорками про время жизни записи), делает это на каждый вызов.
Лечится это тремя способами, от простого к серьёзному.
Писать имя целиком с точкой на конце. order-service.payments.svc.cluster.local. — завершающая точка означает «имя полное, ничего не дописывай», и лишних запросов нет. Некрасиво, зато работает без настроек.
Уменьшить ndots для своего пода. В манифесте есть dnsConfig, и ndots: 2 убирает перебор для имён с двумя точками, оставив короткие имена работающими:
dnsConfig:
options:
- name: ndots
value: "2"
Кэшировать рядом. В кластере обычно стоит локальный кэш DNS на каждом узле (NodeLocal DNSCache), и тогда даже перебор стоит дёшево. Это работа платформенной команды, но знать про неё стоит: половина жалоб на «медленный DNS в кластере» закрывается именно так.
И отдельно про JVM: она кэширует успешные ответы согласно настройке networkaddress.cache.ttl (в контейнерах обычно 30 секунд), а неудачные — по networkaddress.cache.negative.ttl (10 секунд); у Go, Node и Python кэша в самой программе нет, каждый новый запрос идёт в резолвер, зато держатся открытые соединения пула, о чём ниже. Слишком долгий кэш означает обращения к исчезнувшему поду, слишком короткий — лишние запросы; значения по умолчанию для кластера обычно подходят, и трогать их стоит только с измерением в руках.
Endpoints: где сеть встречается с readiness
За каждым Service Kubernetes поддерживает список endpoints — это реальные IP подов, которые прошли две проверки: совпадение по selector-меткам и успешный readiness probe.
Это ключевая связка:
- Под запустился, но ещё прогревается → readiness не проходит → его нет в endpoints → трафик не идёт к нему.
- Под перегружен, readiness провалилась → он выпадает из endpoints до восстановления.
- При rolling update под убирают из endpoints одновременно с началом его остановки, а не заранее: правила на узлах обновляются ещё несколько секунд, и всё это время запросы продолжают приходить. Поэтому выкатка не теряет запросы только в связке
preStop-пауза + graceful shutdown — подробности в Graceful shutdown в Kubernetes.
Первое, что нужно проверить, когда «сервис есть, но не отвечает»:
kubectl -n payments get endpoints order-service
Пустой список при живых подах означает: все реплики не проходят readiness probe. Это уже конкретный вопрос к probes, а не «что-то с сетью».
У этой команды есть нюанс возраста. Старый объект Endpoints — один список на весь сервис — плохо переносил сервисы на тысячи подов, поэтому ему на смену пришёл EndpointSlice: тот же список, но нарезанный на куски. В свежих кластерах (начиная с 1.33) старый объект объявлен устаревшим, и настоящая картина живёт именно в срезах:
kubectl -n payments get endpointslices -l kubernetes.io/service-name=order-service
Вывод подробнее: у каждого адреса видно ready, serving и terminating — то есть сразу понятно, под не прошёл проверку или уже останавливается.
Кто пишет правила на узлах: kube-proxy
В статье дважды сказано, что «правила на узлах обновляются ещё несколько секунд», и это ключ к целому классу инцидентов — стоит сказать, что за правила и кто их пишет.
У Service есть адрес (ClusterIP), но за ним не стоит никакая программа-посредник: этот адрес существует только в виде правил на каждом узле. Пишет их kube-proxy — агент, который работает на каждом узле, следит за объектами Service и Endpoints через api-server и превращает их в правила ядра. В режиме iptables (по умолчанию) это цепочки правил iptables, в режиме IPVS — таблица балансировщика ядра, в новых кластерах встречается nftables. Пакет, отправленный на адрес Service, переписывается ядром на адрес одного из готовых подов — и дальше идёт напрямую.
Из этого устройства следуют три практических вещи.
Задержка обновления. Когда под уходит, последовательность такая: под стал неготов или удалён → api-server обновил Endpoints → kube-proxy на каждом узле заметил изменение и переписал правила. Между первым и последним шагом проходят сотни миллисекунд, а на большом кластере и несколько секунд. Всё это время часть узлов продолжает отправлять трафик на уходящий под — и это ровно та причина, по которой остановке нужна задержка перед закрытием сокета (preStop), о чём статья про корректную остановку в Kubernetes.
Балансировка происходит в ядре и она случайная. Никакого «наименее загруженного пода» в режиме iptables нет: пакет уходит на случайный под из списка. Значит, о равномерности заботятся не настройкой Service, а тем, что поды одинаковые.
Балансировка на уровне соединения, а не запроса. Правило применяется, когда соединение устанавливается; дальше все пакеты этого соединения идут на тот же под. Следствие разбирает следующий раздел.
Долгоживущие соединения: почему одна реплика в полке, а остальные спят
Прямое следствие того, что балансировка работает на уровне соединения: одно установленное соединение навсегда прилипает к одному поду. Для обычного HTTP/1.1 без сохранения соединения это незаметно — каждый запрос приносит новое соединение и новый выбор пода. А вот где это ломается:
- HTTP/2 и gRPC. Одно соединение, в нём тысячи запросов. Клиент установил соединение с подом номер один — весь его трафик идёт туда, сколько бы реплик вы ни добавили.
- Пул соединений к базе или к сервису. Пул открыл десять соединений на старте и держит их. Если у сервиса три реплики, а соединения распределились неудачно, нагрузка будет неравномерной весь срок жизни пула.
- WebSocket и поток событий. Соединение живёт часами, и перебалансировки не происходит вовсе.
Узнаваемая картина инцидента: добавили реплик, а загружена по-прежнему одна; после перезапуска «полка» переезжает на другой под. Никакие лимиты и пробы этого не лечат, потому что ничего не сломано.
Что с этим делают.
Балансировать на уровне запросов, а не соединений. Это умеет прокси, понимающий HTTP/2: контроллер входящего трафика для внешних вызовов, сервисная сетка — для внутренних. Именно эта задача чаще всего и становится первым честным аргументом за сетку.
Заставить клиента переподключаться. У gRPC-клиента есть настройка максимального времени жизни соединения (maxConnectionAge на стороне сервера) — сервер сам закрывает старые соединения, и клиент открывает новое, попадая на другой под. Для пулов — ограничение срока жизни соединения (maxLifetime в HikariCP, MaxConnLifetime в pgxpool, maxLifetimeSeconds у пула pg, pool_recycle в SQLAlchemy).
Разрешать имена в адреса подов самому. Service без адреса (clusterIP: None, его называют headless) возвращает в DNS список адресов всех готовых подов, и клиент выбирает сам. Так работают клиенты, умеющие балансировку на своей стороне — например, gRPC с round_robin.
И то, что часто путают с решением: sessionAffinity: ClientIP у Service. Он делает обратное — намеренно закрепляет клиента за подом по его адресу (на три часа по умолчанию). Это нужно, когда состояние сессии лежит в памяти пода, и это плохая замена внешнему хранилищу сессий: при уходе пода все его клиенты теряют состояние.
Куда делся адрес клиента
Задача из разбора логов: в журнале приложения вместо адреса пользователя стоит адрес из подсети кластера. Причина в том же преобразовании адресов: пакет, пришедший на узел, переписывается — и обратный адрес заменяется адресом узла, чтобы ответ вернулся тем же путём.
Управляет этим поле Service externalTrafficPolicy, и у него два значения.
Cluster (по умолчанию): трафик, пришедший на любой узел, может быть отправлен на под на другом узле. Балансировка ровная, но адрес клиента теряется.
Local: трафик обслуживается только подами на том же узле; если их там нет, пакет отбрасывается. Адрес клиента сохраняется, зато балансировка становится неровной — узел с двумя подами получит столько же трафика, сколько узел с одним, — и требует, чтобы балансировщик перед кластером проверял, есть ли на узле готовые поды.
Для HTTP-трафика обычно не нужно ни то, ни другое: адрес клиента передаёт контроллер входящего трафика в заголовке X-Forwarded-For, и приложение читает его оттуда (в Spring Boot это server.forward-headers-strategy: framework, в chi middleware.RealIP, в Express app.set('trust proxy', 1), у uvicorn --proxy-headers). Важно помнить и обратную сторону: заголовок можно подделать, поэтому доверять ему можно только от своего контроллера, а не от произвольного клиента — об этом статьи про границу проверок.
Отсюда практический порядок: если адрес нужен для журнала и ограничений — читать заголовок; если трафик не HTTP (произвольный TCP) и адрес нужен по-настоящему — externalTrafficPolicy: Local с пониманием его цены.
Ingress: один вход для всех сервисов
Выпускать каждый сервис наружу через LoadBalancer — расточительно: отдельный балансировщик и отдельный IP для каждого. Решение — один LoadBalancer перед Ingress-контроллером, а маршрутизация описывается в Ingress-ресурсах.
Один домен и два пути: контроллер смотрит на Host и на префикс пути и выбирает, какому Service отдать запрос.
Ingress — это декларация «какой host/path куда направлять»:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
spec:
tls:
- hosts: [api.example.com]
secretName: api-tls
rules:
- host: api.example.com # по какому домену пришёл запрос
http:
paths:
- path: /orders # префикс пути в этом домене
pathType: Prefix # сравнение по сегментам, не по подстроке
backend:
service:
name: order-service # Service, которому уйдёт запрос
port: { number: 80 }
- path: /payments
pathType: Prefix
backend:
service:
name: payment-service
port: { number: 80 }
Обычно TLS разбирают на Ingress, а дальше внутрь кластера трафик идёт простым HTTP. Это выбор настройки, а не свойство Ingress: контроллер умеет ходить к сервису и по HTTPS — в nginx это включается аннотацией backend-protocol: HTTPS. Так делают, когда шифрование требуется и внутри кластера; тот же результат даёт service mesh с mTLS.
Ingress-контроллер — отдельный компонент, который нужно установить (nginx, Traefik, облачные варианты). Сам по себе Ingress-ресурс без контроллера ничего не делает.
NetworkPolicy: кто с кем может говорить
По умолчанию в Kubernetes любой под может обратиться к любому. Для среды с платёжными сервисами это неприемлемо.
NetworkPolicy — декларативный файрвол на уровне подов. Описывает, какой трафик разрешён, всё остальное — запрещено:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: limits-ingress
namespace: risk
spec:
podSelector:
matchLabels: { app: limits }
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: { team: payments }
ports:
- port: 8080
Этот манифест говорит: к подам app=limits в namespace risk разрешён входящий трафик только из namespace с меткой team=payments, и только на порт 8080.
Прежде чем на это полагаться, проверьте одну вещь. Сам Kubernetes NetworkPolicy не исполняет — этим занимается сетевой плагин кластера: Calico, Cilium, Weave и подобные. Если плагин политики не поддерживает (так живёт, например, обычный Flannel), манифест применится без единой ошибки, kubectl get networkpolicy честно покажет его в списке — и не будет делать ровно ничего: трафик как ходил куда угодно, так и ходит. Это самый частый сюрприз с NetworkPolicy — «правило есть, а запрета нет». Проверяется за минуту: примените запрещающую политику и попробуйте достучаться до пода из соседнего namespace.
Хорошая практика — deny-by-default на namespace, затем явные разрешения по потребителям. Включает его вот такой манифест: пустой podSelector означает «все поды этого namespace», а отсутствие правил — «разрешено ничего».
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: risk
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
После него в namespace risk не пройдёт ни входящий, ни исходящий трафик, пока его не разрешит отдельная политика вроде показанной выше. Про Egress стоит помнить заранее: он закроет и обращения к DNS, поэтому доступ к kube-dns по порту 53 разрешают сразу же, иначе сервисы перестанут резолвить вообще всё.
Если не можете достучаться до соседнего сервиса — kubectl describe networkpolicy проверяется раньше, чем пишется обращение к платформенной команде.
Пока политики нет, к подам app=limits доходит любой; deny-by-default пропускает только явно разрешённых.
Частые ошибки
| Ошибка | Что происходит | Как правильно |
|---|---|---|
| Обращение по IP пода | Ломается при первом пересоздании | DNS-имя Service |
LoadBalancer на каждый сервис | Лишние расходы, зоопарк точек входа | Один LB перед Ingress-контроллером |
NodePort в продакшне | Нестандартные порты, обход TLS | Ingress / Gateway API |
Жёсткий FQDN с cluster.local в коде | Ломается при смене кластера | Короткое имя + namespace в конфиге |
| Клиент без таймаутов «он же рядом» | Зависший сосед вешает вызывающего | Таймаут на каждый клиент |
| Ретраи и в mesh, и в приложении | Лавина запросов при деградации | Одно место для retry-политики |
Глубже: Gateway API: новый стандарт рядом с Ingressрасширенное
Gateway API — более новый стандарт для той же задачи. Ingress при этом не объявлен устаревшим и никуда не денется: он остаётся в стабильном API, просто новые возможности добавляют уже не в него. Главные отличия:
- чёткое разделение ролей: платформа управляет
Gateway(точкой входа), команды — своимиHTTPRoute; - отдельные виды маршрутов не только для HTTP:
GRPCRouteуже стабилен, а вотTCPRouteпока живёт в экспериментальном канале, и рассчитывать на него в проде рано (WebSocket отдельного маршрута не требует — он прекрасно ходит и через обычный Ingress); - нет зоопарка vendor-аннотаций, в который упирается Ingress при сложных сценариях.
Новые платформы строят на Gateway API; существующие Ingress-ресурсы продолжают работать.
Глубже: Service на внешнюю зависимостьрасширенное
Отдельный приём, который не очевиден: Service без селектора. Обычно Service находит поды по меткам, но можно не указывать метки вовсе и задать адреса вручную:
apiVersion: v1
kind: Service
metadata:
name: external-billing
spec:
ports:
- port: 8080
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: external-billing-1
labels:
kubernetes.io/service-name: external-billing
addressType: IPv4
ports:
- port: 8080
endpoints:
- addresses: ["10.10.5.17"]
Зачем это нужно. Приложение обращается к external-billing:8080 — обычному имени внутри кластера, — а за ним стоит машина вне кластера: унаследованный сервис, база на выделенном сервере, чужое API в соседней сети. Плюс в том, что адрес внешней зависимости перестаёт быть частью настроек приложения: переехала машина — поправили один объект, приложение не тронули.
Второй, более простой вариант того же — ExternalName: Service, который просто возвращает в DNS псевдоним на внешнее имя.
spec:
type: ExternalName
externalName: billing.partner.example.com
Разница практическая: ExternalName работает на уровне имени (клиент сам подключается к внешнему адресу, TLS и сертификат — тоже его забота), а Service без селектора работает на уровне адресов и позволяет держать внешнюю зависимость за обычным внутренним именем. Для чужого HTTPS API обычно достаточно первого, для базы или внутреннего адреса — второго.
Глубже: pathType и правило без hostрасширенное
Два поля правил входящего трафика, которые выглядят необязательными и решают, попадёт ли запрос куда надо.
pathType обязателен в современном API и имеет три значения. Prefix сравнивает путь по сегментам: правило /api совпадёт с /api и /api/orders, но не с /apixyz — это то, что нужно почти всегда. Exact требует полного совпадения, включая отсутствие завершающего слеша. ImplementationSpecific отдаёт решение контроллеру: у nginx это означает регулярное выражение, у облачных контроллеров — их собственные правила, и переносимость теряется.
paths:
- path: /api
pathType: Prefix
backend:
service:
name: orders
port:
number: 8080
Правило без host ловит любой запрос, пришедший на адрес контроллера. Это удобно на стенде (обращаешься по адресу, без имён) и опасно в общем кластере: такое правило перехватывает трафик, предназначенный другим, если оно оказалось первым по порядку разрешения. Практическое правило: на стенде без host можно, в общем кластере — всегда с host, причём имя должно быть тем, которое реально смотрит на контроллер.
Порядок разбора, когда «правило есть, а запрос не доходит»: совпадает ли host с заголовком запроса (проверяется curl -H 'Host: ...'), совпадает ли путь по правилам pathType, попал ли запрос в этот Ingress вообще (kubectl describe ingress покажет адрес и события), и указан ли нужный класс контроллера (ingressClassName) — без него правило может быть никем не обслужено.
Глубже: service mesh: когда нужно большерасширенное
Service mesh (Istio, Linkerd) — дополнительный уровень: рядом с каждым подом запускается прокси-контейнер, который перехватывает весь трафик. Это позволяет получить без изменения кода:
- mTLS между всеми сервисами автоматически;
- ретраи и таймауты на уровне сети;
- канареечное распределение трафика по весам;
- единую телеметрию всех вызовов.
Mesh оправдан при десятках сервисов и регулярной практике канареечных выкаток. При небольшом числе сервисов он добавляет заметную сложность в отладку — в каждом запросе появляется ещё один прокси.
Важное правило при использовании mesh: ретраи настраиваются в одном месте — либо в mesh, либо в приложении, не в обоих. Ретрай в приложении плюс ретрай в mesh — это лавина повторов при деградации соседнего сервиса.
Цена сервисной сетки
«Оправдана при десятках сервисов» — верно, но за этим стоят конкретные затраты, и их стоит назвать до того, как сетку поставят.
Задержка на каждый вызов. Запрос идёт через прокси на стороне отправителя и через прокси на стороне получателя: это два лишних перехода внутри узла. Обычная добавка — единицы миллисекунд на вызов, и для цепочки из четырёх сервисов она складывается. Для API, где бюджет ответа 50 миллисекунд, это заметная доля.
Ресурсы на каждый под. Прокси — отдельный контейнер: десятки-сотни мегабайт памяти и своя доля процессора рядом с каждым подом. На кластере из двухсот подов это отдельная статья расходов, сравнимая с несколькими узлами.
Вторая конфигурация маршрутизации. Появляются свои объекты для правил, повторов, таймаутов и разделения трафика. Это мощно и это же означает, что поведение сети теперь описано в двух местах: в манифестах приложения и в правилах сетки. Разбор инцидента усложняется ровно на этот второй слой — и типичная новая ошибка звучит как «таймаут стоит в сетке, а разработчик правит его в приложении».
Своя эксплуатация. Обновление сетки — отдельная процедура со своими ломающими изменениями; неверная настройка прокси ломает весь трафик кластера сразу. Нужен человек, который это ведёт.
Что берут, когда сетка не нужна, а задачи есть: шифрование между сервисами — на уровне приложения или сетевыми политиками; повторы, таймауты и предохранители — библиотекой в самом приложении; наблюдаемость вызовов — трассировкой; балансировка запросов HTTP/2 — контроллером входящего трафика или клиентской балансировкой через headless Service. Сетка выигрывает, когда всё это нужно сразу и одинаково для десятков сервисов на разных языках, и особенно когда шифрование между всеми сервисами требует регулятор.
Коротко
- У каждого пода свой IP, но IP поды меняют — поэтому нужен
Serviceкак постоянное имя. Типов Service четыре:ClusterIP(связь сервисов внутри кластера, основной),NodePort,LoadBalancer(вход снаружи, лучше один перед Ingress) иExternalName(имя в кластере для внешнего адреса). Headless — не тип, аClusterIPсclusterIP: None, когда клиенту важен каждый экземпляр отдельно. - DNS-имя сервиса:
имя.namespace.svc.cluster.local. Внутри namespace — простоимя. Короткое имя работает черезsearchиndots:5: имя с одной-двумя точками стоит трёх лишних запросов на каждый вызов, лечится точкой на конце, своимndotsили локальным кэшем DNS. - Endpoints — реальные IP подов за Service. Под попадает туда только если прошёл readiness probe. Пустой endpoints при живых подах — смотрите на probes. Адрес Service — это правила на узлах, которые пишет kube-proxy: обновляются они с задержкой (отсюда 502 при остановке), балансируют случайно и на уровне соединения.
- Ingress маршрутизирует внешний трафик по host/path к нужным Service. Gateway API — более новый стандарт с разделением ролей.
- NetworkPolicy — файрвол между подами. По умолчанию всё открыто; deny-by-default + явные разрешения — хорошая практика. Исполняет политики сетевой плагин: без его поддержки манифест применится и не сделает ничего.
- Service mesh переносит ретраи, таймауты и mTLS в инфраструктуру. Ретраи — либо в mesh, либо в приложении, не в обоих. Сетка стоит миллисекунд на вызов, прокси рядом с каждым подом, второй конфигурации маршрутизации и своей эксплуатации; отдельные задачи дешевле закрыть библиотекой, политиками и трассировкой.
- Долгоживущее соединение (HTTP/2, gRPC, пул) прилипает к одному поду: лечится прокси с балансировкой запросов, ограничением срока жизни соединения или headless Service с клиентской балансировкой;
sessionAffinityделает обратное. - Адрес клиента теряется при преобразовании: для HTTP его читают из
X-Forwarded-Forот своего контроллера,externalTrafficPolicy: Localсохраняет адрес ценой неровной балансировки. - Внешнюю зависимость заводят как Service без селектора с ручными адресами или
ExternalName: адрес перестаёт быть частью настроек приложения. - В правилах входящего трафика
pathType: Prefixсравнивает по сегментам, а правило безhostперехватывает любой запрос к контроллеру — в общем кластере это опасно.
Что почитать дальше
- K8s Fundamentals — pod, Deployment, Service в общей модели кластера.
- Spring Boot в Kubernetes — readiness probe, управляющая endpoints, на примере Spring Boot; для Go, Node и Python то же в статьях про остановку в Kubernetes (Go, Node, Python).
- Эксплуатация и отладка — диагностика цепочки Ingress → Service → endpoints → pod.
- Паттерны отказоустойчивости — таймауты и ретраи, которые DNS-имя не заменяет.