Wie behält man in einem K8s-Cluster die Quell-IP der Anfragen nach der Lastverteilung bei?
Categories:
Einleitung
Anwendungsdeployments sind nicht immer einfach nur Installieren und Ausführen, manchmal muss man auch Netzwerk-Probleme berücksichtigen. Dieser Artikel erklärt, wie man in einem K8s-Cluster dafür sorgt, dass Dienste die Quell-IP der Anfrage erhalten können.
Anwendungen, die Dienste bereitstellen, basieren in der Regel auf Eingabeinformationen. Wenn diese Eingabeinformationen nicht auf der fünfzeiligen Gruppe (Quell-IP, Quellport, Ziel-IP, Zielport, Protokoll) angewiesen sind, hat der Dienst eine geringe Netzwerkkopplung und muss sich nicht um Netzwerkdetails kümmern.
Daher ist es für die meisten Menschen nicht notwendig, diesen Artikel zu lesen. Wenn Sie sich für Netzwerke interessieren oder Ihre Sichtweise erweitern möchten, können Sie den Rest lesen, um mehr über Dienstenszenarien zu erfahren.
Dieser Artikel basiert auf K8s v1.29.4. Einige Beschreibungen verwenden Pod und Endpoint durcheinander; in diesem Szenario können sie als äquivalent betrachtet werden.
Falls Fehler vorliegen, freue ich mich über Korrekturen, ich werde sie zeitnah beheben.
Warum geht die Quell-IP-Information verloren?
Zuerst klären wir, was die Quell-IP ist: Wenn A eine Anfrage an B sendet und B diese an C weiterleitet, sieht C zwar in der IP-Protokoll-Quell-IP die IP von B, aber in diesem Artikel betrachten wir die IP von A als Quell-IP.
Es gibt hauptsächlich zwei Verhaltensweisen, die zum Verlust der Quellinformation führen:
- Netzwerkadressübersetzung (NAT), deren Zweck die Einsparung öffentlicher IPv4-Adressen, Lastverteilung usw. ist. Dadurch sieht der Server die Quell-IP des NAT-Geräts statt der echten Quell-IP.
- Proxy, Reverse Proxy (RP) und Load Balancer (LB) gehören zu dieser Kategorie, im Folgenden als Proxy-Server bezeichnet. Diese Proxy-Dienste leiten Anfragen an Backend-Dienste weiter, ersetzen aber die Quell-IP durch ihre eigene IP.
- NAT ist im Wesentlichen Port-Raum gegen IP-Raum austauschen. IPv4-Adressen sind begrenzt; eine IP-Adresse kann 65535 Ports abbilden. In den meisten Fällen werden diese Ports nicht ausgeschöpft, sodass mehrere Subnetz-IPs eine öffentliche IP teilen können, unterteilt durch Ports. Die Form lautet:
public IP:public port -> private IP_1:private port. Weitere Informationen finden Sie unter Netzwerkadressübersetzung. - Proxy-Dienste dienen zum Verstecken oder Exponieren. Proxy-Dienste leiten Anfragen an Backend-Dienste weiter und ersetzen die Quell-IP durch ihre eigene, um die echte IP des Backend-Diensts zu schützen. Die Form lautet:
client IP -> proxy IP -> server IP. Weitere Informationen finden Sie unter Proxy.
NAT und Proxy-Server sind sehr häufig; die meisten Dienste können die Quell-IP der Anfrage nicht erhalten.
Das sind die zwei gängigen Wege, die Quell-IP zu ändern. Ergänzungen sind willkommen.
Wie behält man die Quell-IP bei?
Hier ein Beispiel für eine HTTP-Anfrage:
| Feld | Länge (Bytes) | Bit-Offset | Beschreibung |
|---|---|---|---|
| IP-Kopf | |||
Quell-IP | 4 | 0-31 | IP-Adresse des Senders |
| Ziel-IP | 4 | 32-63 | IP-Adresse des Empfängers |
| TCP-Kopf | |||
| Quellport | 2 | 0-15 | Sender-Portnummer |
| Zielport | 2 | 16-31 | Empfänger-Portnummer |
| Sequenznummer | 4 | 32-63 | Identifiziert den Byte-Stream des Senders |
| Bestätigungsnummer | 4 | 64-95 | Bei gesetztem ACK-Flag die nächste erwartete Sequenznummer |
| Datenoffset | 4 | 96-103 | Bytes vom TCP-Kopf bis zum Datenstart |
| Reserviert | 4 | 104-111 | Reserviertes Feld, auf 0 setzen |
| Flag-Bits | 2 | 112-127 | Steuerflags wie SYN, ACK, FIN usw. |
| Fenstergröße | 2 | 128-143 | Empfänger-Datenmenge |
| Prüfsumme | 2 | 144-159 | Fehlererkennung während der Übertragung |
| Dringendkeitszeiger | 2 | 160-175 | Position dringender Daten |
| Optionen | Variabel | 176-… | Kann Zeitstempel, maximale Segmentlänge usw. enthalten |
| HTTP-Kopf | |||
| Anfragerzeile | Variabel | …-… | Enthält Anfrage-Methode, URI und HTTP-Version |
Kopf-Felder | Variabel | …-… | Verschiedene Kopf-Felder wie Host, User-Agent usw. |
| Leere Zeile | 2 | …-… | Trennt Kopf und Body |
| Body | Variabel | …-… | Optionaler Anfrage- oder Antwortkörper |
Bei der Betrachtung der HTTP-Anfragenstruktur fällt auf, dass TCP-Optionen, Anfragerzeile, Kopf-Felder und Body variabel sind. TCP-Optionen haben begrenzten Platz und werden normalerweise nicht für Quell-IPs verwendet. Die Anfragerzeile hat feste Informationen, die nicht erweitert werden können. Der HTTP-Body kann nach Verschlüsselung nicht geändert werden. Nur die HTTP-Kopf-Felder eignen sich zur Erweiterung für die Quell-IP-Übertragung.
Im HTTP-Header kann das Feld X-REAL-IP hinzugefügt werden, um die Quell-IP zu übertragen. Diese Operation erfolgt normalerweise auf dem Proxy-Server, der die Anfrage dann an den Backend-Dienst sendet, der die Quell-IP über dieses Feld abrufen kann.
Achten Sie darauf, dass der Proxy-Server vor dem NAT-Gerät stehen muss, um die echte Quell-IP zu erhalten. In Produkten von Aliyun gibt es die Kategorie Load Balancer, deren Position im Netzwerk sich von normalen App-Servern unterscheidet.
K8S-Bedienungsanleitung
Am Beispiel des whoami-Projekts.
Deployment erstellen
Zuerst den Dienst erstellen:
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
Dieser Schritt erstellt ein Deployment mit 3 Pods, wobei jeder Pod einen Container enthält, der den whoami-Dienst ausführt.
Service erstellen
Man kann einen NodePort- oder LoadBalancer-Dienst für externen Zugriff erstellen oder einen ClusterIP-Dienst für internen Zugriff und dann einen Ingress-Dienst hinzufügen, um externen Zugriff freizugeben.
NodePort kann über NodeIP:NodePort oder über IngressDienst zugänglich sein, was Tests erleichtert. Dieser Abschnitt verwendet NodePort.
apiVersion: v1
kind: Service
metadata:
name: whoami-service
spec:
type: NodePort
selector:
app: whoami
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30002
Nach der Erstellung des Diensts, z. B. mit curl whoami.example.com:30002, sieht man, dass die zurückgegebene IP die NodeIP ist, nicht die Quell-IP der Anfrage.
Bitte beachten Sie, dass dies nicht die korrekte Client-IP ist; es handelt sich um interne Cluster-IPs. So läuft es ab:
- Client sendet Paket an node2:nodePort
- node2 ersetzt die Quell-IP des Pakets durch seine eigene IP (SNAT)
- node2 ersetzt die Ziel-IP des Pakets durch die Pod-IP
- Paket wird an node1 und dann zum Endpoint geroutet
- Pod-Antwort wird zurück an node2 geroutet
- Pod-Antwort wird an den Client gesendet
Als Diagramm:

externalTrafficPolicy: Local konfigurieren
Um das zu vermeiden, hat Kubernetes eine Funktion, um die Client-Quell-IP zu erhalten. Wenn service.spec.externalTrafficPolicy auf Local gesetzt wird, leitet kube-proxy Anfragen nur an lokale Endpoints weiter, ohne Traffic zu anderen Nodes weiterzuleiten.
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
Testen Sie mit curl whoami.example.com:30002. Wenn whoami.example.com auf IPs mehrerer Cluster-Nodes zeigt, gibt es eine gewisse Wahrscheinlichkeit, dass der Zugriff fehlschlägt. Stellen Sie sicher, dass der DNS-Eintrag nur die IP des Nodes mit dem Endpoint (Pod) enthält.
Diese Konfiguration hat ihren Preis: Der Verlust der Lastverteilung im Cluster. Clients erhalten nur Antworten, wenn sie den Node mit dem Endpoint ansprechen.

Beim Zugriff auf Node 2 gibt es keine Antwort.
Ingress erstellen
Die meisten Dienste werden über HTTP/HTTPS bereitgestellt; https://ip:port wirkt fremd für Benutzer. Normalerweise verwendet man Ingress, um den oben erstellten NodePort-Dienst auf Port 80/443 eines Domains zu laden.
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
Nach Anwenden testen Sie mit curl whoami.example.com; ClientIP ist immer die Pod-IP des Ingress Controller auf dem Endpoint-Node.
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 als Reverse Proxy für NodePort-Dienst bedeutet zwei Service-Schichten vor dem Endpoint. Das Diagramm zeigt den Unterschied.
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]Im Pfad 1 erreicht externer Traffic zuerst den Ingress Controller-Endpoint, dann den whoami-Endpoint.
Der Ingress Controller ist im Wesentlichen ein LoadBalancer-Dienst.
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
Daher kann man externalTrafficPolicy auf dem Ingress Controller setzen, um die Quell-IP zu erhalten.
Zusätzlich use-forwarded-headers im configmap des ingress-nginx-controller auf true setzen, damit der Ingress Controller X-Forwarded-For oder X-REAL-IP erkennt.
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
Der Unterschied zwischen NodePort-Dienst und ingress-nginx-controller-Dienst liegt darin, dass NodePort-Backends normalerweise nicht auf jedem Node deployt sind, während ingress-nginx-controller-Backends auf jedem extern exponierten Node deployt sind.
Im Gegensatz zu NodePort, wo externalTrafficPolicy Anfragen über Nodes blockiert, kann Ingress zuerst den HEADER setzen und dann weiterleiten, was Quell-IP-Erhaltung und Lastverteilung kombiniert.
Zusammenfassung
- Adressübersetzung (NAT), Proxy, Reverse Proxy, Load Balancing führen zum Verlust der Quell-IP.
- Um Verlust zu verhindern, kann der Proxy-Server die echte IP im HTTP-Header-Feld
X-REAL-IPsetzen. Bei Mehrschicht-ProxysX-Forwarded-Forverwenden, das eine IP-Liste als Stapel mit Quell-IP und Proxy-Pfad speichert. - NodePort-Dienste im Cluster mit
externalTrafficPolicy: Localerhalten Quell-IP, verlieren aber Lastverteilung. - ingress-nginx-controller als DaemonSet auf allen LoadBalancer-Rollen-Nodes mit
externalTrafficPolicy: Localerhält Quell-IP und behält Lastverteilung.