← назад к разделу

Сеть в Kubernetes часто кажется мистикой: сервис есть, а не отвечает; адрес резолвится, но соединение зависает; после деплоя соседняя команда ловит 503. Всё это объясняется четырьмя механизмами: Service, DNS, Ingress, NetworkPolicy. Разберём их по очереди.

Главная связка здесь — между именем сервиса и списком endpoints: имя резолвится всегда, а трафик идёт только туда, где прошла проверка готовности.

Service order-service · ns payments · 3 реплики · port 80 → targetPort 8080клиентorder-service.paymentsService order-serviceClusterIP 10.96.40.7:80endpointsкто реально приметв endpoints попадает под с меткой app=order И пройденным readiness probe 1. все три реплики прошли readiness probe10.244.1.5 · готов10.244.2.8 · готов10.244.3.4 · готовendpoints: 3 из 3 — запросы Service делятся на три пода 2. под 10.244.3.4 перегружен — readiness провалилась10.244.1.5 · готов10.244.2.8 · готов10.244.3.4 · не готовendpoints: 2 из 3 — под жив и в Running, но трафика не получает 3. readiness провалилась у всех трёх реплик10.244.1.5 · не готов10.244.2.8 · не готов10.244.3.4 · не готовendpoints: 0 из 3 — клиент ловит отказ, kubectl get pods показывает 3 Running DNS-имя и ClusterIP живы всегда — доступность решает список endpointskubectl -n payments get endpoints order-service — пусто? значит probes, а не сеть

Имя и 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
order-service .payments .svc .cluster.local имя Service namespace это Service, не под домен кластера

Полное имя из четырёх частей: внутри своего 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-ресурсах.

клиент Ingress-контроллер Host: api.example.com order-service /orders payment-service /payments

Один домен и два пути: контроллер смотрит на 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 проверяется раньше, чем пишется обращение к платформенной команде.

пакет NetworkPolicy в risk к подам app=limits пропустить from team=payments:8080 отбросить всё остальное без политики доходит кто угодно

Пока политики нет, к подам app=limits доходит любой; deny-by-default пропускает только явно разрешённых.

Частые ошибки

ОшибкаЧто происходитКак правильно
Обращение по IP подаЛомается при первом пересозданииDNS-имя Service
LoadBalancer на каждый сервисЛишние расходы, зоопарк точек входаОдин LB перед Ingress-контроллером
NodePort в продакшнеНестандартные порты, обход TLSIngress / 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 перехватывает любой запрос к контроллеру — в общем кластере это опасно.

Что почитать дальше