Как сохранить исходный IP-адрес запроса после балансировки нагрузки в кластере K8s
Categories:
Введение
Развертывание приложений не всегда сводится к простому установке и запуску, иногда также приходится учитывать проблемы сети. В этой статье описывается, как в кластере k8s сделать так, чтобы сервис мог получить исходный IP запроса.
Приложения, предоставляющие услуги, обычно зависят от входной информации. Если входная информация не зависит от пятерки (исходный IP, исходный порт, целевой IP, целевой порт, протокол), то такое приложение имеет низкую связанность с сетью и не нуждается в деталях сети.
Поэтому большинству людей нет необходимости читать эту статью. Если вы интересуетесь сетями или хотите расширить кругозор, можете продолжить чтение, чтобы узнать о дополнительных сценариях сервисов.
Статья основана на k8s v1.29.4. В тексте частично смешиваются термины pod и endpoint; в контексте статьи их можно считать эквивалентными.
Если есть ошибки,欢迎 исправить, я timely исправлю.
Почему теряется информация о исходном IP?
Сначала уточним, что такое исходный IP. Когда A отправляет запрос B, а B пересылает запрос C, хотя C видит в протоколе IP исходный IP как IP B, в этой статье IP A считается исходным IP.
Есть два основных типа поведения, приводящих к потере исходной информации:
- Преобразование сетевых адресов (NAT), цель — экономия публичных IPv4, балансировка нагрузки и т.д. Это приводит к тому, что сервер видит исходный IP как IP устройства NAT, а не реальный исходный IP.
- Прокси (Proxy), обратный прокси (RP, Reverse Proxy) и балансировщик нагрузки (LB, Load Balancer) относятся к этой категории, в дальнейшем统称为 прокси-серверы. Такие прокси-серверы пересылают запросы backend-сервисам, но заменяют исходный IP на свой собственный.
- NAT простыми словами — это обмен пространством портов на пространство IP. Адреса IPv4 ограничены, один IP может отображать 65535 портов, и в большинстве случаев эти порты не исчерпываются, поэтому несколько подсетей IP могут использовать один публичный IP, отличаясь портами. Форма использования:
public IP:public port -> private IP_1:private port. Подробнее читайте в Преобразование сетевых адресов - Прокси-сервисы используются для скрытия или экспонирования. Прокси-сервер пересылает запрос backend-сервису, заменяя исходный IP на свой, чтобы скрыть реальный IP backend-сервиса и защитить его безопасность. Форма использования:
client IP -> proxy IP -> server IP. Подробнее читайте в Прокси
NAT и прокси-серверы очень распространены, большинство сервисов не могут получить исходный IP запроса.
Это два распространенных способа изменения исходного IP. Дополнения欢迎.
Как сохранить исходный IP?
Вот пример HTTP-запроса:
| Поле | Длина (байты) | Смещение бита | Описание |
|---|---|---|---|
| Заголовок IP | |||
Исходный IP |
4 | 0-31 | IP-адрес отправителя |
| Целевой IP | 4 | 32-63 | IP-адрес получателя |
| Заголовок TCP | |||
| Исходный порт | 2 | 0-15 | Номер отправного порта |
| Целевой порт | 2 | 16-31 | Номер целевого порта |
| Номер последовательности | 4 | 32-63 | Для идентификации потока байтов, отправленного отправителем |
| Номер подтверждения | 4 | 64-95 | Если установлен флаг ACK, то это следующий ожидаемый номер последовательности |
| Смещение данных | 4 | 96-103 | Количество байтов от начала данных относительно заголовка TCP |
| Зарезервировано | 4 | 104-111 | Зарезервированное поле, не используется, устанавливается в 0 |
| Флаги | 2 | 112-127 | Различные контрольные флаги, такие как SYN, ACK, FIN и т.д. |
| Размер окна | 2 | 128-143 | Объем данных, который может принять получатель |
| Контрольная сумма | 2 | 144-159 | Для обнаружения ошибок в данных во время передачи |
| Указатель срочности | 2 | 160-175 | Позиция срочных данных, которые отправитель хочет, чтобы получатель обработал как можно скорее |
| Опции | Переменная | 176-… | Может включать временные метки, максимальную длину сегмента и т.д. |
| Заголовок HTTP | |||
| Строка запроса | Переменная | …-… | Включает метод запроса, URI и версию HTTP |
Поля заголовков |
Переменная | …-… | Содержит различные поля заголовков, такие как Host, User-Agent и т.д. |
| Пустая строка | 2 | …-… | Для разделения заголовков и тела |
| Тело | Переменная | …-… | Необязательное тело запроса или ответа |
Просматривая структуру HTTP-запроса выше, видно, что опции TCP, строка запроса, поля заголовков, тело переменны. Пространство опций TCP ограничено и обычно не используется для передачи исходного IP. Строка запроса несет фиксированную информацию и не расширяется. Тело HTTP после шифрования нельзя изменять. Только поля заголовков HTTP подходят для расширения и передачи исходного IP.
В заголовке HTTP можно добавить поле X-REAL-IP для передачи исходного IP. Это обычно делается на прокси-сервере, после чего прокси-сервер отправляет запрос backend-сервису, и backend может получить исходный IP через это поле.
Обратите внимание: нужно гарантировать, что прокси-сервер находится до устройства NAT, чтобы получить реальный исходный IP запроса whoami. В продуктах Alibaba Cloud есть отдельная категория товара Load Balancer, позиция которого в сети отличается от обычных серверов приложений.
Руководство по операциям в K8S
В качестве примера развертывание проекта whoami.
Создание Deployment
Сначала создайте сервис:
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami-deployment
spec:
replicas: 3
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: docker.io/traefik/whoami:latest
ports:
- containerPort: 8080
Это создаст Deployment с 3 Pod, каждый pod содержит один контейнер, запускающий сервис whoami.
Создание Service
Можно создать сервис типа NodePort или LoadBalancer для внешнего доступа или ClusterIP для внутреннего доступа кластера, плюс Ingress для экспонирования внешнего доступа.
NodePort доступен через NodeIP:NodePort или через сервис Ingress, удобно для тестирования. В этом разделе используется сервис NodePort.
apiVersion: v1
kind: Service
metadata:
name: whoami-service
spec:
type: NodePort
selector:
app: whoami
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30002
После создания сервиса доступ curl whoami.example.com:30002 покажет IP как NodeIP, а не исходный IP запроса whoami.
Обратите внимание, это не правильный IP клиента, это внутренние IP кластера. Вот что происходит:
- Клиент отправляет пакет на node2:nodePort
- node2 заменяет исходный IP пакета на свой IP (SNAT)
- node2 заменяет целевой IP пакета на Pod IP
- Пакет маршрутизируется на node1, затем на endpoint
- Ответ Pod маршрутизируется обратно на node2
- Ответ Pod отправляется клиенту
Схематично:

Настройка externalTrafficPolicy: Local
Чтобы избежать этого, в Kubernetes есть функция сохранения исходного IP клиента. Если установить service.spec.externalTrafficPolicy в Local, kube-proxy будет проксировать запросы только на локальные endpoints, не пересылая трафик на другие узлы.
apiVersion: v1
kind: Service
metadata:
name: whoami-service
spec:
type: NodePort
externalTrafficPolicy: Local
selector:
app: whoami
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30002
Тестирование curl whoami.example.com:30002: когда whoami.example.com разрешается в IP нескольких узлов кластера, есть вероятность, что доступ не сработает. Нужно убедиться, что DNS-записи содержат только IP узлов с endpoints (pod).
Эта настройка имеет цену: теряется способность балансировки нагрузки в кластере. Клиент получит ответ только при доступе к узлу с endpoint.

При доступе клиента к Node 2 ответа не будет.
Создание Ingress
Большинство сервисов предоставляются пользователям через http/https, форма https://ip:port может показаться пользователям незнакомой. Обычно используется Ingress для балансировки сервиса NodePort из предыдущего раздела на порт 80/443 домена.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami-ingress
namespace: default
spec:
ingressClassName: external-lb-default
rules:
- host: whoami.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami-service
port:
number: 80
После применения тестирование curl whoami.example.com покажет ClientIP всегда как IP Pod Ingress Controller на узле с endpoint.
root@client:~# curl whoami.example.com
...
RemoteAddr: 10.42.1.10:56482
...
root@worker:~# kubectl get -n ingress-nginx pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ingress-nginx-controller-c8f499cfc-xdrg7 1/1 Running 0 3d2h 10.42.1.10 k3s-agent-1 <none> <none>
Использование Ingress как обратного прокси для сервиса NodePort означает две вложенные service перед endpoint. На рисунке ниже показаны различия.
graph LR
A[Client] -->|whoami.example.com:80| B(Ingress)
B -->|10.43.38.129:32123| C[Service]
C -->|10.42.1.1:8080| D[Endpoint]
graph LR
A[Client] -->|whoami.example.com:30001| B(Service)
B -->|10.42.1.1:8080| C[Endpoint]
В пути 1 внешний доступ к Ingress сначала достигает endpoint Ingress Controller, затем endpoint whoami.
Ingress Controller по сути — сервис LoadBalancer,
kubectl -n ingress-nginx get svc
NAMESPACE NAME CLASS HOSTS ADDRESS PORTS AGE
default echoip-ingress nginx ip.example.com 172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707 80 18h
default whoami-ingress nginx whoami.example.com 172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707 80 16h
Поэтому можно установить externalTrafficPolicy, упомянутый ранее, в Ingress Controller для сохранения исходного IP.
Также нужно установить в configmap ingress-nginx-controller параметр use-forwarded-headers в true, чтобы Ingress Controller распознавал поля X-Forwarded-For или X-REAL-IP.
apiVersion: v1
data:
allow-snippet-annotations: "false"
compute-full-forwarded-for: "true"
use-forwarded-headers: "true"
enable-real-ip: "true"
forwarded-for-header: "X-Real-IP" # X-Real-IP or X-Forwarded-For
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.10.1
name: ingress-nginx-controller
namespace: ingress-nginx
Разница между сервисом NodePort и сервисом ingress-nginx-controller в основном в том, что backend NodePort обычно не развертывается на каждом узле, а backend ingress-nginx-controller обычно развертывается на каждом узле с внешним экспонированием.
В отличие от сервиса NodePort, где externalTrafficPolicy приводит к отсутствию ответа на запросы между узлами, Ingress сначала устанавливает HEADER, а затем проксирует, реализуя сохранение исходного IP и балансировку нагрузки.
Заключение
- Преобразование адресов (NAT), прокси (Proxy), обратный прокси (Reverse Proxy), балансировка нагрузки (Load Balance) приводят к потере исходного IP.
- Чтобы предотвратить потерю исходного IP, при пересылке прокси-сервер может установить реальный IP в поле заголовка HTTP
X-REAL-IP. При многоуровневом прокси используется полеX-Forwarded-For, которое в форме стека записывает список IP исходного IP и пути прокси. - Для сервиса NodePort кластера установка
externalTrafficPolicy: Localсохраняет исходный IP, но теряет балансировку нагрузки. - ingress-nginx-controller, развернутый в форме daemonset на всех узлах роли loadbalancer, с установкой
externalTrafficPolicy: Localсохраняет исходный IP и балансировку нагрузки.