Questo documento descrive come utilizzare i bilanciatori del carico di rete passthrough esterni regionali tramite il protocollo UDP (User Datagram Protocol). Il documento è rivolto a sviluppatori di app, operatori di app e amministratori di rete.
Informazioni su UDP
UDP è comunemente utilizzato nelle app. Il protocollo, descritto in RFC-768, implementa un servizio di pacchetti datagrammi senza stato e non affidabile. Ad esempio, il protocollo QUIC di Google migliora l'esperienza utente utilizzando UDP per velocizzare le app basate su stream.
La parte senza stato di UDP significa che il livello di trasporto non mantiene uno stato. Pertanto, ogni pacchetto in una "connessione" UDP è indipendente. In realtà, non esiste una connessione reale in UDP. Invece, i partecipanti di solito utilizzano una tupla a 2 elementi (ip:port) o una tupla a 4 elementi (src-ip:src-port, dest-ip:dest-port) per riconoscersi a vicenda.
Come le app basate su TCP, anche le app basate su UDP possono trarre vantaggio da un bilanciatore del carico, motivo per cui i bilanciatori del carico di rete passthrough esterni regionali vengono utilizzati negli scenari UDP.
Bilanciatore del carico di rete passthrough esterno regionale
I bilanciatori del carico di rete passthrough esterni regionali sono bilanciatori del carico passthrough; elaborano i pacchetti in entrata e li consegnano ai server di backend con i pacchetti intatti. I server di backend inviano quindi i pacchetti di ritorno direttamente ai client. Questa tecnica è chiamata Direct Server Return (DSR). Su ogni macchina virtuale (VM) Linux in esecuzione su Compute Engine che è un backend di un Cloud de Confiance by S3NS bilanciatore del carico di rete passthrough esterno regionale, una voce nella tabella di routing locale instrada il traffico destinato all'indirizzo IP del bilanciatore del carico al controller dell'interfaccia di rete (NIC). L'esempio seguente illustra questa tecnica:
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
Nell'esempio precedente, 198.51.100.2 è l'indirizzo IP del bilanciatore del carico. L'agente google-network-daemon.service è responsabile dell'aggiunta di questa voce.
Tuttavia, come mostra l'esempio seguente, la VM non ha effettivamente un'interfaccia che possiede l'indirizzo IP del bilanciatore del carico:
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
Il bilanciatore del carico di rete passthrough esterno regionale trasmette i pacchetti in entrata, con l'indirizzo di destinazione invariato, al server di backend. La voce della tabella di routing locale instrada il pacchetto al processo dell'app corretto e i pacchetti di risposta dell'app vengono inviati direttamente al client.
Il seguente diagramma mostra il funzionamento dei bilanciatori del carico di rete passthrough esterni regionali. I pacchetti in entrata vengono elaborati da un bilanciatore del carico chiamato Maglev, che distribuisce i pacchetti ai server di backend. I pacchetti in uscita vengono quindi inviati direttamente ai client tramite DSR.
Un problema con i pacchetti di ritorno UDP
Quando utilizzi DSR, esiste una leggera differenza nel modo in cui il kernel Linux tratta le connessioni TCP e UDP. Poiché TCP è un protocollo con stato, il kernel dispone di tutte le informazioni necessarie sulla connessione TCP, inclusi l'indirizzo client, la porta client, l'indirizzo server e la porta server. Queste informazioni vengono registrate nella struttura dei dati del socket che rappresenta la connessione. Pertanto, ogni pacchetto di ritorno di una connessione TCP ha l'indirizzo di origine impostato correttamente sull'indirizzo del server. Per un bilanciatore del carico, questo indirizzo è l'indirizzo IP del bilanciatore del carico.
Tieni presente, tuttavia, che UDP è senza stato, quindi gli oggetti socket creati nel processo dell'app per le connessioni UDP non contengono le informazioni di connessione. Il kernel non dispone delle informazioni sull'indirizzo di origine di un pacchetto in uscita e non conosce la relazione con un pacchetto ricevuto in precedenza. Per l'indirizzo di origine del pacchetto, il kernel può compilare solo l'indirizzo dell'interfaccia a cui è destinato il pacchetto UDP di ritorno. Oppure, se l'app ha precedentemente associato il socket a un determinato indirizzo, il kernel utilizza questo indirizzo come indirizzo di origine.
Il seguente codice mostra un semplice programma di eco:
#!/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)
Di seguito è riportato l'output di tcpdump durante una conversazione UDP:
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 è l'indirizzo IP del bilanciatore del carico e 203.0.113.2 è l'indirizzo IP del client.
Dopo che i pacchetti lasciano la VM, un altro dispositivo NAT, un gateway Compute Engine gateway–in the Cloud de Confiance network traduce l'indirizzo di origine nell'indirizzo esterno. Il gateway non sa quale indirizzo esterno deve essere utilizzato, quindi può essere utilizzato solo l'indirizzo esterno della VM (non quello del bilanciatore del carico).
Dal lato client, se controlli l'output di tcpdump, i pacchetti del server sono simili ai seguenti:
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 è l'indirizzo IP esterno della VM.
Dal punto di vista del client, i pacchetti UDP non provengono da un indirizzo a cui il client li ha inviati. Questo causa problemi: il kernel elimina questi pacchetti e, se il client si trova dietro un dispositivo NAT, anche il dispositivo NAT. Di conseguenza, l'app client non riceve alcuna risposta dal server. Il seguente diagramma mostra questo processo in cui il client rifiuta i pacchetti di ritorno a causa di indirizzi non corrispondenti.
Risolvere il problema UDP
Per risolvere il problema di mancata risposta, devi riscrivere l'indirizzo di origine dei pacchetti in uscita con l'indirizzo IP del bilanciatore del carico sul server che ospita l'app. Di seguito sono riportate diverse opzioni che puoi utilizzare per eseguire questa riscrittura dell'intestazione. La prima soluzione utilizza un approccio basato su Linux con iptables; le altre soluzioni adottano approcci basati su app.
Il seguente diagramma mostra l'idea di base di queste opzioni: riscrivere l'indirizzo IP di origine dei pacchetti di ritorno in modo che corrisponda all'indirizzo IP del bilanciatore del carico.
Utilizzare la policy NAT nel server di backend
La soluzione della policy NAT consiste nell'utilizzare il comando iptables di Linux per riscrivere l'indirizzo di destinazione dall'indirizzo IP del bilanciatore del carico all'indirizzo IP della VM.
Nell'esempio seguente, aggiungi una regola DNAT iptables per modificare l'indirizzo di destinazione dei pacchetti in entrata:
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
Questo comando aggiunge due regole alla tabella NAT del sistema iptables. La prima regola ignora tutti i pacchetti in entrata destinati all'indirizzo eth0 locale.
Di conseguenza, il traffico che non proviene dal bilanciatore del carico non è interessato.
La seconda regola modifica l'indirizzo IP di destinazione dei pacchetti in entrata con l'indirizzo IP interno della VM. Le regole DNAT sono con stato, il che significa che il kernel tiene traccia delle connessioni e riscrive automaticamente l'indirizzo di origine dei pacchetti di ritorno.
| Vantaggi | Svantaggi |
|---|---|
| Il kernel traduce l'indirizzo, senza che sia necessario apportare modifiche alle app. | La CPU aggiuntiva viene utilizzata per eseguire NAT. Inoltre, poiché DNAT è con stato, anche il consumo di memoria potrebbe essere elevato. |
| Supporta più bilanciatori del carico. |
Utilizzare nftables per modificare senza stato i campi dell'intestazione IP
Nella soluzione nftables, utilizzi il comando nftables per modificare l'indirizzo di origine nell'intestazione IP dei pacchetti in uscita. Questa modifica è senza stato, quindi consuma meno risorse rispetto all'utilizzo di DNAT. Per utilizzare nftables, è necessario un kernel Linux versione successiva alla 4.10.
Utilizza i seguenti comandi:
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
| Vantaggi | Svantaggi |
|---|---|
| Il kernel traduce l'indirizzo, senza che sia necessario apportare modifiche alle app. | Non supporta più bilanciatori del carico. |
| Il processo di traduzione degli indirizzi è senza stato, quindi il consumo di risorse è molto inferiore. | La CPU aggiuntiva viene utilizzata per eseguire NAT. |
nftables sono disponibili solo per le versioni più recenti del kernel Linux. Alcune distribuzioni, come Centos 7.x, non possono utilizzare
nftables.
|
Consentire all'app di eseguire il binding in modo esplicito all'indirizzo IP del bilanciatore del carico
Nella soluzione di binding, modifichi l'app in modo che esegua il binding in modo esplicito all'indirizzo IP del bilanciatore del carico. Per un socket UDP, l'operazione bind comunica al kernel quale indirizzo utilizzare come indirizzo di origine quando invia pacchetti UDP che utilizzano quel socket.
L'esempio seguente mostra come eseguire il binding a un indirizzo specifico in Python:
#!/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
Il codice precedente è un server UDP; ripete i byte ricevuti, con il
prefisso "ECHO: ". Presta attenzione alle righe 12 e 13, in cui il server è associato all'indirizzo 198.51.100.2, che è l'indirizzo IP del bilanciatore del carico.
| Vantaggi | Svantaggi |
|---|---|
| Può essere eseguito con una semplice modifica del codice dell'app. | Non supporta più bilanciatori del carico. |
Utilizzare recvmsg/sendmsg anziché recvfrom/sendto per specificare l'indirizzo
In questa soluzione, utilizzi le chiamate recvmsg/sendmsg anziché le chiamate recvfrom/sendto. Rispetto alle chiamate recvfrom/sendto, le chiamate recvmsg/sendmsg possono gestire i messaggi di controllo ausiliari insieme ai dati del payload. Questi messaggi di controllo ausiliari includono l'indirizzo di origine o di destinazione dei pacchetti. Questa soluzione ti consente di recuperare gli indirizzi di destinazione dai pacchetti in entrata e, poiché questi indirizzi sono indirizzi reali del bilanciatore del carico, puoi utilizzarli come indirizzi di origine quando invii le risposte.
Il seguente programma di esempio illustra questa soluzione:
#!/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)
Questo programma mostra come utilizzare le chiamate recvmsg/sendmsg. Per recuperare le informazioni sull'indirizzo dai pacchetti, devi utilizzare la chiamata setsockopt per impostare l'opzione IP_PKTINFO.
| Vantaggi | Svantaggi |
|---|---|
| Funziona anche se sono presenti più bilanciatori del carico, ad esempio quando sono configurati bilanciatori del carico interni ed esterni sullo stesso backend. | Richiede modifiche complesse all'app. In alcuni casi, questa operazione potrebbe non essere possibile. |
Passaggi successivi
- Scopri come configurare un bilanciatore del carico di rete passthrough esterno regionale e distribuire il traffico in Configurare un bilanciatore del carico di rete passthrough esterno regionale.
- Scopri di più sui bilanciatori del carico di rete passthrough esterni regionali.
- Scopri di più sulla Maglev alla base dei bilanciatori del carico di rete passthrough esterni regionali.