Wie behält man in einem K8s-Cluster die Quell-IP der Anfragen nach der Lastverteilung bei?

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:

  1. 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.
  2. 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.

Zugriffsweg-Einschränkung

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-IP setzen. Bei Mehrschicht-Proxys X-Forwarded-For verwenden, das eine IP-Liste als Stapel mit Quell-IP und Proxy-Pfad speichert.
  • NodePort-Dienste im Cluster mit externalTrafficPolicy: Local erhalten Quell-IP, verlieren aber Lastverteilung.
  • ingress-nginx-controller als DaemonSet auf allen LoadBalancer-Rollen-Nodes mit externalTrafficPolicy: Local erhält Quell-IP und behält Lastverteilung.

Referenzen