En este documento se explica cómo trabajar con balanceadores de carga de red de transferencia externos mediante el protocolo de datagramas de usuario (UDP). Este documento está dirigido a desarrolladores de aplicaciones, operadores de aplicaciones y administradores de redes.
Información sobre UDP
UDP se usa con frecuencia en aplicaciones. El protocolo, que se describe en el RFC-768, implementa un servicio de paquetes de datagramas sin estado y poco fiable. Por ejemplo, el protocolo QUIC de Google mejora la experiencia de usuario mediante el uso de UDP para acelerar las aplicaciones basadas en streaming.
La parte sin estado de UDP significa que la capa de transporte no mantiene un estado. Por lo tanto, cada paquete de una "conexión" UDP es independiente. De hecho, no hay una conexión real en UDP. En su lugar, los participantes suelen usar una tupla de dos elementos (ip:port
) o una tupla de cuatro elementos (src-ip:src-port
, dest-ip:dest-port
) para reconocerse entre sí.
Al igual que las aplicaciones basadas en TCP, las aplicaciones basadas en UDP también pueden beneficiarse de un balanceador de carga, por lo que los balanceadores de carga de red con paso a través externos se utilizan en situaciones de UDP.
Balanceador de carga de red de paso a través externo
Los balanceadores de carga de red de paso a través externos son balanceadores de carga de paso a través. Procesan los paquetes entrantes y los envían a los servidores de backend con los paquetes intactos. A continuación, los servidores backend envían los paquetes de retorno directamente a los clientes. Esta técnica se llama retorno directo del servidor (DSR). En cada máquina virtual (VM) Linux que se ejecute en Compute Engine y que sea un backend de unGoogle Cloud balanceador de carga de red con paso a través externo, una entrada de la tabla de enrutamiento local enruta el tráfico destinado a la dirección IP del balanceador de carga al controlador de interfaz de red (NIC). En el siguiente ejemplo se muestra esta técnica:
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
En el ejemplo anterior, 198.51.100.2
es la dirección IP del balanceador de carga. El agente google-network-daemon.service
es el responsable de añadir esta entrada.
Sin embargo, como se muestra en el siguiente ejemplo, la VM no tiene una interfaz que sea propietaria de la dirección IP del balanceador de carga:
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
El balanceador de carga de red de paso a través externo transmite los paquetes entrantes al servidor de backend sin modificar la dirección de destino. La entrada de la tabla de enrutamiento local dirige el paquete al proceso de la aplicación correcto y los paquetes de respuesta de la aplicación se envían directamente al cliente.
En el siguiente diagrama se muestra cómo funcionan los balanceadores de carga de red de paso a través externos. Los paquetes entrantes se procesan mediante un balanceador de carga llamado Maglev, que distribuye los paquetes a los servidores de backend. Los paquetes salientes se envían directamente a los clientes a través de DSR.
Un problema con los paquetes de retorno UDP
Cuando trabajas con DSR, hay una ligera diferencia entre la forma en que el kernel de Linux trata las conexiones TCP y UDP. Como TCP es un protocolo con reconocimiento del estado, el kernel tiene toda la información que necesita sobre la conexión TCP, como la dirección y el puerto del cliente, así como la dirección y el puerto del servidor. Esta información se registra en la estructura de datos de socket que representa la conexión. Por lo tanto, cada paquete de retorno de una conexión TCP tiene la dirección de origen configurada correctamente como la dirección del servidor. En el caso de un balanceador de carga, esa dirección es la dirección IP del balanceador de carga.
Recuerda que UDP no tiene estado, por lo que los objetos de socket que se crean en el proceso de la aplicación para las conexiones UDP no tienen la información de la conexión. El kernel no tiene información sobre la dirección de origen de un paquete saliente y no conoce la relación con un paquete recibido anteriormente. En el caso de la dirección de origen del paquete, el kernel solo puede rellenar la dirección de la interfaz a la que va el paquete UDP devuelto. O bien, si la aplicación ha enlazado previamente el socket a una dirección determinada, el kernel usa esa dirección como dirección de origen.
El siguiente código muestra un programa de eco sencillo:
#!/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)
A continuación, se muestra la salida de tcpdump
durante una conversación 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
es la dirección IP del balanceador de carga y 203.0.113.2
es la dirección IP del cliente.
Una vez que los paquetes salen de la VM, otro dispositivo NAT (una pasarela de Compute Engine) de la red Google Cloud traduce la dirección de origen a la dirección externa. La pasarela no sabe qué dirección externa debe usarse, por lo que solo se puede usar la dirección externa de la VM (no la del balanceador de carga).
En el lado del cliente, si compruebas el resultado de tcpdump
, los paquetes del servidor tendrán un aspecto similar al siguiente:
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
es la dirección IP externa de la VM.
Desde el punto de vista del cliente, los paquetes UDP no proceden de una dirección a la que el cliente los haya enviado. Esto provoca problemas: el kernel descarta estos paquetes y, si el cliente está detrás de un dispositivo NAT, también lo hace el dispositivo NAT. Por lo tanto, la aplicación cliente no recibe ninguna respuesta del servidor. En el siguiente diagrama se muestra este proceso, en el que el cliente rechaza los paquetes devueltos debido a que las direcciones no coinciden.
Solucionar el problema de UDP
Para solucionar el problema de falta de respuesta, debe reescribir la dirección de origen de los paquetes salientes a la dirección IP del balanceador de carga en el servidor que aloja la aplicación. A continuación, se muestran varias opciones que puede usar para llevar a cabo esta reescritura de encabezado. La primera solución usa un enfoque basado en Linux con iptables
, mientras que las otras soluciones se basan en aplicaciones.
En el siguiente diagrama se muestra la idea principal de estas opciones: reescribir la dirección IP de origen de los paquetes devueltos para que coincida con la dirección IP del balanceador de carga.
Usar la política de NAT en el servidor backend
La solución de la política de NAT consiste en usar el comando iptables
de Linux para reescribir la dirección de destino de la dirección IP del balanceador de carga a la dirección IP de la VM.
En el siguiente ejemplo, se añade una regla iptables
DNAT para cambiar la dirección de destino de los paquetes entrantes:
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
Este comando añade dos reglas a la tabla NAT del sistema iptables
. La primera regla omite todos los paquetes entrantes que tienen como destino la dirección local eth0
.
Por lo tanto, el tráfico que no procede del balanceador de carga no se ve afectado.
La segunda regla cambia la dirección IP de destino de los paquetes entrantes a la dirección IP interna de la VM. Las reglas de DNAT tienen estado, lo que significa que el kernel monitoriza las conexiones y reescribe automáticamente la dirección de origen de los paquetes devueltos.
Ventajas | Inconvenientes |
---|---|
El kernel traduce la dirección sin que sea necesario cambiar las aplicaciones. | Se usa CPU adicional para hacer la NAT. Además, como DNAT tiene estado, el consumo de memoria también puede ser alto. |
Admite varios balanceadores de carga. |
Usa nftables
para manipular sin estado los campos de encabezado IP
En la solución nftables
, se usa el comando nftables
para manipular la dirección de origen en el encabezado IP de los paquetes salientes. Este proceso no tiene estado, por lo que consume menos recursos que usar DNAT. Para usar nftables
, necesitas una versión del kernel de Linux superior a 4.10.
Usa los siguientes comandos:
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
Ventajas | Inconvenientes |
---|---|
El kernel traduce la dirección sin que sea necesario cambiar las aplicaciones. | No admite varios balanceadores de carga. |
El proceso de traducción de direcciones no tiene estado, por lo que el consumo de recursos es mucho menor. | Se usa CPU adicional para realizar la NAT. |
nftables solo están disponibles en versiones más recientes del kernel de Linux. Algunas distribuciones, como Centos 7.x, no pueden usar nftables .
|
Permitir que la aplicación se vincule explícitamente a la dirección IP del balanceador de carga
En la solución de vinculación, modifica tu aplicación para que se vincule explícitamente a la dirección IP del balanceador de carga. En el caso de un socket UDP, la operación bind
permite que el kernel sepa qué dirección usar como dirección de origen al enviar paquetes UDP que usen ese socket.
En el siguiente ejemplo se muestra cómo enlazarse a una dirección específica en 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
El código anterior es un servidor UDP que devuelve los bytes recibidos con un "ECHO: "
delante. Presta atención a las líneas 12 y 13, donde el servidor está enlazado a la dirección 198.51.100.2
, que es la dirección IP del balanceador de carga.
Ventajas | Inconvenientes |
---|---|
Se puede conseguir con un simple cambio en el código de la aplicación. | No admite varios balanceadores de carga. |
Usa recvmsg
/sendmsg
en lugar de recvfrom
/sendto
para especificar la dirección
En esta solución, se usan llamadas recvmsg
/sendmsg
en lugar de llamadas recvfrom
/sendto
. En comparación con las llamadas recvfrom
/sendto
, las llamadas recvmsg
/sendmsg
pueden gestionar mensajes de control auxiliares junto con los datos de la carga útil. Estos mensajes de control auxiliares incluyen la dirección de origen o de destino de los paquetes. Esta solución te permite obtener direcciones de destino de los paquetes entrantes y, como esas direcciones son direcciones de balanceadores de carga reales, puedes usarlas como direcciones de origen al enviar respuestas.
En el siguiente programa de ejemplo se muestra esta solución:
#!/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)
Este programa muestra cómo usar las llamadas recvmsg
/sendmsg
. Para obtener información de direcciones de los paquetes, debes usar la llamada setsockopt
para definir la opción IP_PKTINFO
.
Ventajas | Inconvenientes |
---|---|
Funciona incluso si hay varios balanceadores de carga. Por ejemplo, cuando hay balanceadores de carga internos y externos configurados en el mismo backend. | Requiere que hagas cambios complejos en la aplicación. En algunos casos, puede que no sea posible. |
Siguientes pasos
- Consulta cómo configurar un balanceador de carga de red de paso a través externo y distribuir el tráfico en Configurar un balanceador de carga de red de paso a través externo.
- Consulta más información sobre los balanceadores de carga de red de paso a través externos.
- Consulta más información sobre la técnica Maglev que se usa en los balanceadores de carga de red de paso a través externos.