UDP mit externen Passthrough-Network-Load-Balancern verwenden

In diesem Dokument wird erläutert, wie Sie mit externen Passthrough-Network-Load-Balancern mithilfe des User Datagram Protocol (UDP) arbeiten. Das Dokument richtet sich an Anwendungsentwickler, Anwendungsoperatoren und Netzwerkadministratoren.

Über UDP

UDP wird häufig in Anwendungen verwendet. Das in RFC-768 beschriebene Protokoll implementiert einen zustandslosen, unzuverlässigen Datagram-Paketdienst. Beispielsweise verbessert das QUIC-Protokoll von Google mithilfe von UDP zur Beschleunigung streambasierter Anwendungen die Nutzererfahrung.

Der zustandslose Teil von UDP bedeutet, dass die Transportschicht keinen Status behält. Daher ist jedes Paket in einer UDP-„Verbindung“ unabhängig. Es gibt keine echten Verbindungen in UDP. Stattdessen verwenden die Teilnehmer normalerweise ein 2-Tupel (ip:port) oder ein 4-Tupel (src-ip:src-port, dest-ip:dest-port), um sich gegenseitig zu erkennen.

Wie TCP-basierte Anwendungen können auch UDP-basierte Anwendungen von einem Load-Balancer profitieren. Daher werden externe Passthrough-Network-Load-Balancer in UDP-Szenarien verwendet.

Externer Passthrough-Network-Load-Balancer

Externe Passthrough-Network-Load-Balancer sind Passthrough-Load-Balancer. Sie verarbeiten eingehende Pakete und senden sie mit den Paketen an Backend-Server. Die Backend-Server senden die zurückgegebenen Pakete dann direkt an die Clients. Diese Technik wird als Direct Server Return (DSR) bezeichnet. Auf jeder virtuellen Linux-Maschine (VM), die auf Compute Engine ausgeführt wird, die ein Backend eines externen Google Cloud-Passthrough-Network-Load-Balancers ist, leitet ein Eintrag in der lokalen Routing-Tabelle den Traffic, der für die IP-Adresse des Load-Balancers bestimmt ist, an den Netzwerkschnittstellen-Controller (NIC) weiter. Im folgenden Beispiel wird diese Technik veranschaulicht:

root@backend-server:~# ip ro ls table local
local 10.128.0.2 dev eth0 proto kernel scope host src 10.128.0.2
broadcast 10.128.0.2 dev eth0 proto kernel scope link src 10.128.0.2
local 198.51.100.2 dev eth0 proto 66 scope host
broadcast 127.0.0.0 dev lo proto kernel scope link src 127.0.0.1
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1

Im vorherigen Beispiel ist 198.51.100.2 die IP-Adresse des Load-Balancers. Der Agent google-network-daemon.service ist für das Hinzufügen dieses Eintrags verantwortlich. Das folgende Beispiel zeigt jedoch, dass die VM keine Schnittstelle hat, die die IP-Adresse des Load-Balancers besitzt:

root@backend-server:~# ip ad ls
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc mq state UP group default qlen 1000
    link/ether 42:01:0a:80:00:02 brd ff:ff:ff:ff:ff:ff
    inet 10.128.0.2/32 brd 10.128.0.2 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::4001:aff:fe80:2/64 scope link
       valid_lft forever preferred_lft forever

Der externe Passthrough-Network-Load-Balancer überträgt die eingehenden Pakete mit der Zieladresse unverändert an den Backend-Server. Der Eintrag der lokalen Routingtabelle leitet das Paket an den richtigen Anwendungsprozess weiter und die Antwortpakete werden von der Anwendung direkt an den Client gesendet.

Das folgende Diagramm zeigt, wie externe Passthrough-Network-Load-Balancer funktionieren. Die eingehenden Pakete werden von einem Load-Balancer namens Maglev verarbeitet, der die Pakete an die Backend-Server verteilt. Ausgehende Pakete werden dann direkt über DSR an die Clients gesendet.

Maglev verteilt eingehende Pakete an Back-End-Server, die die Pakete über DSR verteilen.

Problem mit UDP-Rückgabepaketen

Wenn Sie mit DSR arbeiten, gibt es einen kleinen Unterschied zwischen der Verarbeitung des TCP-Kernels und der UDP-Verbindungen. Da TCP ein zustandsorientiertes Protokoll ist, verfügt das Kernel über alle erforderlichen Informationen über die TCP-Verbindung, einschließlich Client-Adresse, Client-Port, Serveradresse und Serverport. Diese Informationen werden in der Socket-Datenstruktur aufgezeichnet, die die Verbindung darstellt. Somit ist für jedes zurückkehrende Paket einer TCP-Verbindung die Quelladresse korrekt auf die Serveradresse eingestellt. Bei einem Load-Balancer ist diese Adresse die IP-Adresse des Load-Balancers.

Da UDP jedoch zustandslos ist, haben die im Anwendungsprozess für UDP-Verbindungen erstellten Socket-Objekte keine Verbindungsinformationen. Der Kernel hat nicht die Informationen zur Quelladresse eines ausgehenden Pakets und kennt auch die Beziehung zu einem zuvor empfangenen Paket nicht. Für die Quelladresse des Pakets kann der Kernel nur die Adresse der Schnittstelle ausfüllen, an die das zurückgegebene UDP-Paket gesendet wird. Wenn die Anwendung den Socket zuvor an eine bestimmte Adresse gebunden hat, verwendet der Kernel diese Adresse als Quelladresse.

Der folgende Code zeigt ein einfaches Echo-Programm:

#!/usr/bin/python3
import socket,struct
def loop_on_socket(s):
  while True:
    d, addr = s.recvfrom(1500)
    print(d, addr)
    s.sendto("ECHO: ".encode('utf8')+d, addr)

if __name__ == "__main__":
   HOST, PORT = "0.0.0.0", 60002
   sock = socket.socket(type=socket.SocketKind.SOCK_DGRAM)
   sock.bind((HOST, PORT))
   loop_on_socket(sock)

Hier sehen Sie die Ausgabe tcpdump während einer UDP-Unterhaltung:

14:50:04.758029 IP 203.0.113.2.40695 > 198.51.100.2.60002: UDP, length 3
14:50:04.758396 IP 10.128.0.2.60002 > 203.0.113.2.40695: UDP, length 2T

198.51.100.2 ist die IP-Adresse des Load-Balancers und 203.0.113.2 die Client-IP-Adresse.

Wenn die Pakete die VM verlassen, übersetzt ein anderes NAT-Gerät (ein Compute Engine-Gateway) im Google Cloud-Netzwerk die Quelladresse in die externe Adresse. Das Gateway weiß nicht, welche externe Adresse verwendet werden soll. Daher kann nur die externe VM-Adresse (nicht die des Load-Balancers) verwendet werden.

Wenn Sie auf der Clientseite die Ausgabe von tcpdump prüfen, sehen die Pakete vom Server so aus:

23:05:37.072787 IP 203.0.113.2.40695 > 198.51.100.2.60002: UDP, length 5
23:05:37.344148 IP 198.51.100.3.60002 > 203.0.113.2.40695: UDP, length 4

198.51.100.3 ist die externe IP-Adresse der VM.

Aus Sicht des Clients stammen die UDP-Pakete nicht von einer Adresse, an die der Client sie gesendet hat. Das führt zu Problemen: Der Kernel löscht diese Pakete und falls der Client hinter einem NAT-Gerät liegt, wird auch das NAT-Gerät verwendet. Daher erhält die Clientanwendung keine Antwort vom Server. Das folgende Diagramm zeigt diesen Prozess, bei dem der Client die Rückgabe von Paketen aufgrund von Adresskonflikten ablehnt.

Der Client lehnt zurückgesendete Pakete ab.

UDP-Problem lösen

Zum Lösen dieses Problems müssen Sie die Quelladresse ausgehender Pakete in die IP-Adresse des Load-Balancers auf dem Server umschreiben, auf dem die Anwendung gehostet wird. Es gibt mehrere Möglichkeiten, diese Header-Umschreibung durchzuführen. Die erste Lösung besteht aus einem Linux-basierten Ansatz mit iptables. Die anderen Lösungen bestehen aus anwendungsbasierten Ansätzen.

Das folgende Diagramm veranschaulicht die Kernidee dieser Optionen: Die Quell-IP-Adresse der zurückgegebenen Pakete umschreiben, damit sie mit der IP-Adresse des Load-Balancers übereinstimmt.

Schreiben Sie die Quell-IP-Adresse der zurückgegebenen Pakete neu, damit sie mit der IP-Adresse des Load-Balancers übereinstimmt.

NAT-Richtlinie auf dem Back-End-Server verwenden

Die NAT-Richtlinienlösung besteht darin, mit dem Linux-Befehl iptables die Zieladresse von der IP-Adresse des Load-Balancers in die IP-Adresse der VM umzuschreiben. Im folgenden Beispiel fügen Sie eine iptables-NDT-Regel hinzu, um die Zieladresse der eingehenden Pakete zu ändern:

iptables -t nat -A POSTROUTING -j RETURN -d 10.128.0.2 -p udp --dport 60002
iptables -t nat -A PREROUTING -j DNAT --to-destination 10.128.0.2 -d 198.51.100.2 -p udp --dport 60002

Mit diesem Befehl werden der NAT-Tabelle des Systems iptables zwei Regeln hinzugefügt. Die erste Regel umgeht alle eingehenden Pakete, die auf die lokale eth0-Adresse gerichtet sind. Daher ist der Traffic, der nicht vom Load-Balancer stammt, nicht betroffen. Mit der zweiten Regel wird die Ziel-IP-Adresse eingehender Pakete in die interne IP-Adresse der VM geändert. Die DNAT-Regeln sind zustandsorientiert. Das bedeutet, dass der Kernel die Verbindungen verfolgt und die Quelladresse der zurückgegebenen Pakete automatisch neu schreibt.

Vorteile Nachteile
Der Kernel übersetzt die Adresse, ohne dass Anwendungen geändert werden müssen. Zusätzliche CPU wird für die NAT verwendet. Und weil DNAT zustandsorientiert ist, kann auch der Arbeitsspeicherverbrauch hoch sein.
Unterstützung mehrerer Load-Balancer.

Zustandsloses „Mangling“ von IP-Headerfeldern mit nftables

In der nftables-Lösung verwenden Sie den Befehl nftables, um „Mangling“ auf die Quelladresse im IP-Header ausgehender Pakete anzuwenden. Dieses „Mangling“ ist zustandslos, weshalb es weniger Ressourcen benötigt als DNAT. Zur Verwendung von nftables benötigen Sie eine Linux-Kernel-Version 4.10.

Sie verwenden die folgenden Befehle:

nft add table raw
nft add chain raw postrouting {type filter hook postrouting priority 300)
nft add rule raw postrouting ip saddr 10.128.0.2 udp sport 60002 ip saddr set 198.51.100.2
Vorteile Nachteile
Der Kernel übersetzt die Adresse, ohne dass Anwendungen geändert werden müssen. Unterstützt nicht mehrere Load Balancer.
Der Adressübersetzungsprozess ist zustandslos, daher ist der Ressourcenverbrauch wesentlich geringer. Zusätzliche CPU wird für die NAT verwendet.
nftables sind nur für neuere Linux-Kernel-Versionen verfügbar. Einige Distributionen wie CentOS 7.x können nftables nicht verwenden.

Explizite Bindung der Anwendung an die IP-Adresse des Load-Balancers zulassen

In der Bindungslösung ändern Sie die Anwendung so, dass sie explizit an die IP-Adresse des Load-Balancers gebunden wird. Bei einem UDP-Socket teilt der Vorgang bind dem Kernel mit, welche Adresse als Quelladresse verwendet werden soll, wenn UDP-Pakete gesendet werden, die diesen Socket verwenden.

Im folgenden Beispiel wird gezeigt, wie die Bindung an eine bestimmte Adresse in Python erfolgt:

#!/usr/bin/python3
import socket
def loop_on_socket(s):
  while True:
    d, addr = s.recvfrom(1500)
    print(d, addr)
    s.sendto("ECHO: ".encode('utf8')+d, addr)

if __name__ == "__main__":
   # Instead of setting HOST to "0.0.0.0",
   # we set HOST to the Load Balancer IP
   HOST, PORT = "198.51.100.2", 60002
   sock = socket.socket(type=socket.SocketKind.SOCK_DGRAM)
   sock.bind((HOST, PORT))
   loop_on_socket(sock)

# 198.51.100.2 is the load balancer's IP address
# You can also use the DNS name of the load balancer's IP address

Der vorherige Code ist ein UDP-Server, der die empfangenen Byte mit einem vorangestellten "ECHO: " zurückgibt. Achten Sie auf die Zeilen 12 und 13, in denen der Server an die IP-Adresse des Load-Balancers (198.51.100.2) gebunden wird.

Vorteile Nachteile
Kann mit einer einfachen Codeänderung an der Anwendung erreicht werden. Unterstützt nicht mehrere Load Balancer.

Die Adresse mit recvmsg/sendmsg anstelle von recvfrom/sendto angeben

In dieser Lösung verwenden Sie recvmsg/sendmsg-Aufrufe anstelle von recvfrom/sendto-Aufrufen. Im Vergleich zu recvfrom/sendto-Aufrufen können die recvmsg/sendmsg-Aufrufe zusammen mit den Nutzlastdaten Nachrichten verarbeiten. Diese zusätzlichen Steuerungsnachrichten enthalten die Quell- oder Zieladresse der Pakete. Mit dieser Lösung können Sie Zieladressen von eingehenden Paketen abrufen. Da diese Adressen echte Load-Balancer-Adressen sind, können Sie sie beim Senden von Antworten als Quelladressen verwenden.

Das folgende Beispielprogramm veranschaulicht diese Lösung:

#!/usr/bin/python3
import socket,struct
def loop_on_socket(s):
  while True:
    d, ctl, flg, addr = s.recvmsg(1500, 1024)
    # ctl contains the destination address information
    s.sendmsg(["ECHO: ".encode("utf8"),d], ctl, 0, addr)

if __name__ == "__main__":
   HOST, PORT = "0.0.0.0", 60002
   s = socket.socket(type=socket.SocketKind.SOCK_DGRAM)
   s.setsockopt(0,   # level is 0 (IPPROTO_IP)
                8,   # optname is 8 (IP_PKTINFO)
                1)

   s.bind((HOST, PORT))
   loop_on_socket(s)

In diesem Programm wird veranschaulicht, wie recvmsg/sendmsg-Aufrufe verwendet werden. Zum Abrufen von Adressinformationen aus Paketen müssen Sie den Befehl setsockopt verwenden, um die Option IP_PKTINFO festzulegen.

Vorteile Nachteile
Dies funktioniert auch, wenn mehrere Load-Balancer vorhanden sind, z. B. wenn sowohl interne als auch externe Load-Balancer für dasselbe Back-End konfiguriert sind. Erfordert komplexe Änderungen an der Anwendung. In einigen Fällen ist dies jedoch nicht möglich.

Nächste Schritte