ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

Kubernetes NodePort и ExternalTrafficPolicy: как сохранить IP клиента и не сломать балансировку

NodePort и LoadBalancer нередко «съедают» реальный IP клиента из‑за SNAT в kube-proxy. Разберём режимы externalTrafficPolicy (Cluster/Local), healthCheckNodePort, особенности iptables/IPVS и практику с MetalLB без сюрпризов.
Kubernetes NodePort и ExternalTrafficPolicy: как сохранить IP клиента и не сломать балансировку

Зачем вообще говорить про NodePort, IP клиента и ExternalTrafficPolicy

Типичная задача: вы публикуете сервис в Kubernetes наружу через Service типа NodePort или через LoadBalancer (который в большинстве реализаций всё равно опирается на NodePort на нодах). Дальше внезапно выясняется, что приложение или reverse proxy (Nginx/Envoy/Traefik) видит не IP клиента, а адрес ноды или балансировщика. Логи становятся бесполезными, rate limit «на клиента» не работает, аудит усложняется.

Виновата не одна «галочка», а связка: режим kube-proxy (iptables или IPVS), необходимость обеспечить корректный обратный маршрут и настройка externalTrafficPolicy. Ниже разложим по полочкам, что именно происходит на уровне сети и как получить сохранение IP клиента без неожиданных побочных эффектов в балансировке.

Важно заранее разделить два требования: «сохранить TCP source IP в поде» и «получить корректный client IP на уровне HTTP». Это разные задачи и решаются разными способами.

Как NodePort принимает трафик: короткая модель в голове

NodePort открывает порт на каждой ноде кластера (обычно диапазон 30000–32767) и заставляет kube-proxy перенаправлять трафик на один из endpoints (подов), подходящих под селектор сервиса.

Ключевой нюанс: клиент приходит на IP ноды, а выбранный endpoint может жить на другой ноде. И тут появляется вопрос маршрутизации: что будет, если пакет «прилетел» на ноду A, а под находится на ноде B?

Почему появляется SNAT и куда пропадает реальный IP

Если нода A принимает соединение и пересылает его на под на ноде B, то без подмены адресов обратный ответ в ряде сетевых схем может уйти к клиенту напрямую (мимо ноды A). Для TCP это часто означает разрыв соединения из-за асимметрии маршрута.

Чтобы гарантировать симметричный путь (вход через A и выход через A), kube-proxy часто включает SNAT (обычно через MASQUERADE) и подменяет source IP на адрес ноды. В итоге под видит source как «адрес ноды», а не реального клиента.

Пока сервис может отправить внешний трафик на удалённые endpoints (на других нодах), Kubernetes вынужден выбирать компромисс между «работает при любой раскладке подов» и «сохраняем IP клиента». Компромисс задаёт externalTrafficPolicy.

Если вы разворачиваете кластер на собственных серверах или в облаке без managed-LB, часто это делают на VDS, и там особенно важно заранее продумать вход в кластер: какой IP должен видеть ingress/приложение и какой слой будет делать балансировку.

Поток трафика через NodePort: где появляется SNAT и теряется IP клиента

externalTrafficPolicy: Cluster vs Local — что меняется

У Service есть поле externalTrafficPolicy с двумя режимами:

  • Cluster (по умолчанию): трафик может быть отправлен на любой endpoint по всему кластеру. Балансировка равномернее, но часто включается SNAT и теряется реальный IP.
  • Local: нода принимает внешний трафик только если на ней есть локальные endpoints этого сервиса. Тогда «перепрыгивания» на другую ноду не происходит, SNAT обычно не нужен, и IP клиента сохраняется на уровне TCP.

Практический эффект externalTrafficPolicy=Local

Если включить externalTrafficPolicy: Local, вы обычно получаете:

  • поды видят реальный IP клиента (если его не «ломает» внешний балансировщик);
  • часть нод перестаёт быть точкой входа, если на них нет endpoints;
  • внешний балансировщик должен уметь отправлять трафик только на «здоровые» ноды (где есть локальные endpoints).

Пример манифеста NodePort с externalTrafficPolicy=Local

Минимальный пример сервиса, где мы явно хотим сохранить IP клиента:

apiVersion: v1
kind: Service
metadata:
  name: web-nodeport
spec:
  type: NodePort
  externalTrafficPolicy: Local
  selector:
    app: web
  ports:
  - name: http
    port: 80
    targetPort: 8080
    nodePort: 30080

Проверка «на пальцах»: если у вас 3 ноды, а поды app=web крутятся только на одной, то только эта нода будет корректной точкой входа на 30080. Остальные ноды не должны принимать этот трафик, иначе придётся проксировать на другую ноду и вы снова упрётесь в SNAT или в проблемы с возвратным маршрутом.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

HealthCheckNodePort: зачем он нужен и почему без него бывает плохо

Когда вы включаете externalTrafficPolicy: Local на Service типа LoadBalancer, Kubernetes обычно выделяет дополнительный порт healthCheckNodePort. Идея простая: внешний балансировщик проверяет этот порт на каждой ноде и понимает, есть ли на ней локальные endpoints.

Без такой проверки балансировщик может продолжать слать трафик на ноды, где нет подов сервиса. Результат выглядит как «рандомные» таймауты, 503 или плавающие ошибки по сети.

Если у вас «железный» L4-балансировщик перед NodePort, задачу health check’ов он тоже должен решать: либо через отдельные проверки/скрипты, либо через интеграцию с Kubernetes-контроллером, иначе externalTrafficPolicy: Local превратится в лотерею.

Как посмотреть healthCheckNodePort у сервиса

kubectl get svc web-lb -o yaml

Ищите поле healthCheckNodePort в описании сервиса. Оно появляется не всегда: зависит от типа сервиса и контроллера, который реализует LoadBalancer.

kube-proxy: iptables vs IPVS и как это влияет на SNAT

Смысл в обоих режимах одинаковый: обеспечить виртуальный сервис и балансировку на endpoints. Но диагностика отличается:

  • iptables: правила в цепочках NAT/Filter создают сервис и распределяют трафик. SNAT обычно виден как MASQUERADE в таблице nat.
  • IPVS: ядро делает L4-балансировку через IPVS-сервисы, а iptables остаётся для части обвязки (включая некоторые правила NAT).

Если цель — сохранить TCP source IP, главное не «iptables или IPVS», а разрешаете ли вы пересылку на удалённые endpoints. Как только разрешаете «любой под в кластере» для внешнего трафика, вероятность SNAT резко растёт.

Быстрая диагностика: есть ли SNAT на ноде

На ноде (нужны права root) можно посмотреть правила NAT:

iptables -t nat -S | grep -E 'KUBE|MASQUERADE'

В IPVS-режиме дополнительно полезно посмотреть таблицы IPVS:

ipvsadm -Ln

Практическая цель диагностики: понять, где включается MASQUERADE, и связано ли это именно с вашим сервисом/входящим трафиком.

Почему «реальный IP» может теряться даже с Local

externalTrafficPolicy: Local решает проблему SNAT на уровне ноды Kubernetes, но не отменяет того, что перед кластером может стоять ещё один слой: облачный балансировщик, аппаратный L4, reverse proxy, CDN. Каждый из них может терять или заменять source IP.

  • L7 proxy (Ingress-контроллер как reverse proxy) завершает TCP/HTTP и подключается к вашему сервису сам. Тогда backend увидит IP ingress-пода, а реальный адрес будет только в HTTP-заголовках вроде X-Forwarded-For или X-Real-IP.
  • DNAT/SNAT на внешнем балансировщике: часть устройств/провайдеров по умолчанию NAT’ит клиентов.
  • PROXY protocol: иногда решает сохранение адреса на L4, но требует поддержки на стороне приёмника (ingress, сервис, приложение).

Отсюда рабочая формулировка задачи: «где именно мне нужен client IP — в backend-поде на уровне TCP, в логах ingress или только в HTTP-логике приложения». И уже под это выбирается схема.

Если у вас входной слой построен на TCP/UDP-прокси, полезно держать под рукой материал про балансировку на уровне stream-модуля: балансировка TCP/UDP в Nginx (stream).

MetalLB и Service LoadBalancer: как это связано с NodePort и IP клиента

В онпреме и на собственных серверах часто используют MetalLB, чтобы получить Service типа LoadBalancer без облачного провайдера. Важно помнить: многие реализации LoadBalancer в Kubernetes всё равно используют NodePort как «точку входа» на нодах, а уже затем трафик попадает в сервис.

Практически это означает:

  • В L2-режиме (ARP/NDP) MetalLB «размещает» внешний IP на одной из нод и трафик приходит туда. Дальше решает externalTrafficPolicy: с Cluster выше шанс SNAT, с Local выше шанс сохранить IP.
  • В BGP-режиме трафик может приходить на разные ноды по маршрутизации. С externalTrafficPolicy: Local это обычно сочетается хорошо, но критично, чтобы ноды без endpoints не становились точкой входа (health check и корректная реклама маршрутов).

В обоих вариантах распределение подов по нодам и понятная схема health check становятся ключевыми. Если вы не контролируете, где запущены поды, вы не контролируете, какие ноды должны принимать внешний трафик.

Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

Паттерны, которые реально работают в проде

1) Local + распределение подов по нодам (anti-affinity/spread)

Самый прямой способ избежать «дыр» — гарантировать, что на каждой ноде, куда может прийти трафик, есть endpoint. Обычно делают так:

  • увеличивают число реплик;
  • используют podAntiAffinity, чтобы реплики не собирались на одной ноде;
  • добавляют topologySpreadConstraints для более ровного распределения.

Так вы получаете и сохранение IP клиента, и предсказуемое распределение нагрузки.

2) Local + «входные» ноды (nodeSelector/taints)

Если не хотите распылять поды по всему кластеру, выделяют 2–3 «ingress-ноды»:

  • на них запускают только входные компоненты (ingress/controller или gateway);
  • внешний балансировщик или MetalLB направляет трафик только туда;
  • externalTrafficPolicy: Local включают, чтобы source IP не терялся на последнем прыжке.

Плюс: проще управлять. Минус: это архитектурное решение, которое требует дисциплины в планировании и отказоустойчивости.

3) Не бороться за TCP source IP, а работать с X-Forwarded-For

Иногда требование «видеть IP клиента» относится только к логам/аналитике на уровне HTTP. Тогда проще принять, что ingress завершает соединение, а IP передаётся заголовками. Но обязательно:

  • ограничить, кто может присылать эти заголовки (иначе их подделают);
  • настроить доверенные прокси на стороне Nginx/Envoy/приложения;
  • чётко понимать цепочку проксирования и где именно формируется «истинный» client IP.

Для эксплуатации полезно заранее продумать, как вы будете хранить и разбирать логи, особенно если IP важен для расследований: практика по логам, ротации и алертам.

Связка MetalLB и LoadBalancer: почему важны health checks и локальные endpoints

Частые симптомы и быстрые проверки

Симптом: приложение видит IP ноды вместо клиента

  • Проверьте externalTrafficPolicy у сервиса.
  • Убедитесь, что перед сервисом не стоит L7-прокси, который завершает соединение.
  • Проверьте, есть ли endpoints на той ноде, куда приходит трафик (особенно при Local).

Симптом: часть запросов падает/таймаутит после включения Local

  • Чаще всего балансировщик продолжает слать трафик на ноды без локальных endpoints.
  • Проверьте наличие и использование healthCheckNodePort (если это LoadBalancer).
  • Проверьте распределение реплик по нодам (anti-affinity/spread) или схему «входных нод».

Симптом: всё работает, но в логах ingress «не тот IP»

  • Если ingress завершает HTTP, backend увидит IP ingress, а клиентский адрес будет в заголовках (если вы их настроили и доверяете источнику).
  • Проверьте, что вы не смешиваете требования «TCP source IP» и «HTTP client IP».

Мини-чеклист: как добиться сохранения client IP с NodePort

  1. Определите, где нужен IP: в backend-поде на уровне TCP, в ingress, или только в HTTP-заголовках.

  2. Если нужен TCP source IP в поде — используйте externalTrafficPolicy: Local и убедитесь, что трафик попадает на ноды с локальными endpoints.

  3. Обеспечьте health check на уровне внешнего балансировщика. Для LoadBalancer проверьте healthCheckNodePort.

  4. Распределите поды по нодам (anti-affinity/spread) или выделите «входные ноды» и маршрутизируйте на них.

  5. Если есть L7-прокси — настройте доверенную передачу IP (заголовки или PROXY protocol) и защиту от подмены.

Итоги

NodePort — быстрый способ опубликовать сервис, но он часто приводит к потере IP клиента из-за SNAT в kube-proxy. Режим externalTrafficPolicy: Local помогает сохранить source IP, но требует дисциплины: трафик должен попадать на ноды с локальными endpoints, а внешний балансировщик обязан корректно отсекать «пустые» ноды проверками здоровья.

Если вы используете MetalLB или любой Service типа LoadBalancer в своём кластере, логика та же: сохранение client IP — это цепочка решений от входа в кластер до конкретного пода. При правильно собранной схеме вы получаете и стабильную доставку трафика, и корректный IP для логов, лимитов и безопасности.

Поделиться статьей

Вам будет интересно

Linux watchdog: soft lockup/hard lockup, rcu stall и hung task — диагностика по dmesg и связь с I/O OpenAI Статья написана AI (GPT 5)

Linux watchdog: soft lockup/hard lockup, rcu stall и hung task — диагностика по dmesg и связь с I/O

Если сервер подтормаживает или «замирает», а в логе ядра появляются soft lockup/hard lockup, rcu stall или hung task — это не всег ...
Kubernetes: ErrImagePull и 401 Unauthorized — настройка imagePullSecrets для private registry OpenAI Статья написана AI (GPT 5)

Kubernetes: ErrImagePull и 401 Unauthorized — настройка imagePullSecrets для private registry

Если Pod застрял в ErrImagePull/ImagePullBackOff и в Events виден 401 Unauthorized, почти всегда виноваты imagePullSecrets: секрет ...
Apache 503 Service Unavailable: уперлись в MaxRequestWorkers, KeepAlive и scoreboard — как найти и исправить OpenAI Статья написана AI (GPT 5)

Apache 503 Service Unavailable: уперлись в MaxRequestWorkers, KeepAlive и scoreboard — как найти и исправить

503 в Apache часто означает не «упал сайт», а «закончились воркеры». Разберём, как по AH-ошибкам и scoreboard понять причину, учес ...