Neste documento, você verá como trabalhar com balanceadores de carga de rede de passagem externa usando o protocolo de datagramas do usuário (UDP, na sigla em inglês). O documento é destinado a desenvolvedores de apps, operadores de app e administradores de rede.
Sobre o UDP
O UDP é geralmente usado em aplicativos. O protocolo, descrito no RFC-768, implementa um serviço de pacote de datagrama sem estado e não confiável. Por exemplo, o protocolo QUIC do Google melhora a experiência do usuário usando o UDP para acelerar os aplicativos baseados em stream.
A parte sem estado do UDP significa que a camada de transporte não mantém um
estado. Portanto, cada pacote em uma "conexão" UDP é independente. Na verdade,
não há uma conexão real no UDP. Em vez disso, os participantes geralmente usam uma
(ip:port
) de duas tuplas ou uma (src-ip:src-port
, dest-ip:dest-port
) de quatro tuplas para
identificarem uns aos outros.
Assim como os aplicativos baseados em TCP, os aplicativos baseados em UDP também podem se beneficiar de um balanceador de carga, e é por isso que os balanceadores de carga de rede de passagem externa são usados em cenários UDP.
Balanceador de carga de rede de passagem externa
Balanceadores de carga de rede de passagem externa são balanceadores de carga de passagem; eles processam pacotes de entrada e os entregam aos servidores de back-end com os pacotes intactos. Os servidores de back-end enviam os pacotes de retorno diretamente para os clientes. Essa técnica é chamada de Retorno direto do servidor (DSR, na sigla em inglês). Em cada máquina virtual (VM) do Linux em execução no Compute Engine que é um back-end de um balanceador de carga de rede de passagem externa do Google Cloud, uma entrada na tabela de roteamento local roteia o tráfego destinado ao endereço IP do balanceador de carga ao controlador de interface de rede (NIC, na sigla em inglês). O exemplo a seguir demonstra essa 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
No exemplo anterior, 198.51.100.2
é o endereço IP do balanceador de carga. O
agente google-network-daemon.service
é responsável por adicionar essa entrada.
No entanto, como mostrado no exemplo a seguir, a VM não tem uma
interface que tem o endereço IP do 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
O balanceador de carga de rede de passagem externa transmite os pacotes de entrada, com o endereço de destino intacto, para o servidor de back-end. A entrada da tabela de roteamento local roteia o pacote para o processo correto do aplicativo, e os pacotes de resposta do aplicativo são enviados diretamente para o cliente.
O diagrama a seguir mostra como os balanceadores de carga de rede de passagem externa funcionam. Os pacotes de entrada são processados por um balanceador de carga denominado Maglev, que distribui os pacotes para os servidores de back-end. Os pacotes de saída são enviados diretamente aos clientes por meio de DSR.
Um problema com pacotes de retorno do UDP
Quando você trabalha com o DSR, há uma pequena diferença entre como o kernel do Linux trata conexões TCP e UDP. Como o TCP é um protocolo com estado, o kernel tem todas as informações necessárias sobre a conexão TCP, incluindo endereço do cliente, porta do cliente, endereço do servidor e porta do servidor. Essas informações são registradas na estrutura de dados do soquete que representa a conexão. Assim, cada pacote retornado de uma conexão TCP tem o endereço de origem definido corretamente para o endereço do servidor. Para um balanceador de carga, esse endereço é o endereço IP do balanceador de carga.
Lembre-se de que o UDP é sem estado. Portanto, os objetos de soquete criados no processo do app para conexões UDP não têm as informações de conexão. O kernel não tem as informações sobre o endereço de origem de um pacote de saída e não sabe a relação com um pacote recebido anteriormente. Para o endereço de origem do pacote, o kernel só pode preencher o endereço da interface para onde o pacote UDP é retornado. Ou, se o aplicativo vinculou anteriormente o soquete a um determinado endereço, o kernel usa esse endereço como o endereço de origem.
O código a seguir mostra um programa de eco simples:
#!/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)
Veja a seguir a saída tcpdump
durante uma conversa 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
é o endereço IP do balanceador de carga e 203.0.113.2
é o
endereço IP do cliente.
Depois que os pacotes saem da VM, outro dispositivo NAT, um gateway do Compute Engine, na rede do Google Cloud converte o endereço de origem para o endereço externo. O gateway não sabe qual endereço externo deve ser usado, portanto, somente o endereço externo da VM (não o do balanceador de carga) pode ser usado.
No lado do cliente, se você verificar a saída de tcpdump
, os pacotes do
servidor serão assim:
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
é o endereço IP externo da VM.
Do ponto de vista do cliente, os pacotes UDP não vêm de um endereço para o qual o cliente os enviou. Isso causa problemas: o kernel descarta esses pacotes e, se o cliente estiver atrás de um dispositivo NAT, o dispositivo NAT fará o mesmo. Como resultado, o app do cliente não recebe resposta do servidor. O diagrama a seguir mostra esse processo em que o cliente rejeita pacotes de retorno por causa de incompatibilidades de endereço.
Como resolver o problema do UDP
Para resolver o problema de falta de resposta, você precisa reescrever o endereço de origem dos
pacotes de saída para o endereço IP do balanceador de carga no servidor que hospeda
o aplicativo. Veja a seguir várias opções que podem ser usadas para
reescrever o cabeçalho. A primeira solução usa uma abordagem baseada em Linux com iptables
,
e as outras usam abordagens baseadas em aplicativos.
O diagrama a seguir mostra a ideia principal dessas opções: reescrever o endereço IP de origem dos pacotes retornados para corresponder ao endereço IP do balanceador de carga.
Usar a política NAT no servidor de back-end
A solução de política NAT é usar o comando iptables
do Linux para reescrever o
endereço de destino do endereço IP do balanceador de carga para o endereço IP da VM.
No exemplo a seguir, você adiciona uma regra iptables
DNAT para alterar o
endereço de destino dos pacotes de entrada:
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
Esse comando adiciona duas regras à tabela NAT do sistema iptables
. A
primeira regra ignora todos os pacotes de entrada destinados ao endereço eth0
local.
Como resultado, o tráfego que não vem do balanceador de carga não é afetado.
A segunda regra altera o endereço IP de destino dos pacotes de entrada para o
endereço IP interno da VM. As regras DNAT têm estado, o que significa que o
kernel rastreia as conexões e reescreve o endereço de origem dos pacotes retornados
automaticamente.
Prós | Contras |
---|---|
O kernel converte o endereço, sem necessidade de alteração nos aplicativos. | A CPU extra é usada para fazer o NAT. Como o DNAT tem estado, o consumo de memória também pode ser alto. |
Compatível com vários balanceadores de carga. |
Use nftables
para ajustar os campos do cabeçalho IP sem estado
Na solução nftables
, use o comando nftables
para ajustar o endereço de
origem no cabeçalho de IP dos pacotes de saída. Esse ajuste é sem estado, então
consome menos recursos do que a DNAT. Para usar nftables
, você precisa de uma
versão do kernel do Linux maior que 4.10.
Use os seguintes 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
Prós | Contras |
---|---|
O kernel converte o endereço, sem necessidade de alteração nos aplicativos. | Não é compatível com vários balanceadores de carga. |
O processo de conversão do endereço é sem estado, de modo que o consumo de recursos é muito mais baixo. | A CPU extra é usada para fazer o NAT. |
nftables estão disponíveis apenas para versões mais recentes do kernel do
Linux. Algumas distribuições, como Centos 7.x, não podem usar
nftables .
|
Permita que o app se vincule explicitamente ao endereço IP do balanceador de carga.
Na solução de vinculação, modifique o aplicativo para que ele seja vinculado explicitamente ao
endereço IP do balanceador de carga. Para um soquete UDP, a operação bind
permite que o
kernel saiba qual endereço usar como o endereço de origem ao enviar pacotes UDP
que usam esse soquete.
O exemplo a seguir mostra como vincular a um endereço específico em 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
O código anterior é um servidor UDP. Ele ecoa os bytes recebidos, com um
"ECHO: "
precedente. Preste atenção nas linhas 12 e 13, em que o servidor
está vinculado ao endereço 198.51.100.2
, que é o endereço IP do balanceador de carga.
Prós | Contras |
---|---|
Pode ser obtido com uma simples alteração de código no aplicativo. | Não é compatível com vários balanceadores de carga. |
Use recvmsg
/sendmsg
em vez de recvfrom
/sendto
para especificar o endereço
Nesta solução, você usa chamadas recvmsg
/sendmsg
em vez de
chamadas recvfrom
/sendto
. Em comparação com chamadas recvfrom
/sendto
, as
chamadas recvmsg
/sendmsg
podem processar mensagens de controle complementares junto com os
dados de payload. Essas mensagens de controle auxiliares incluem o endereço de origem ou de
destino dos pacotes. Essa solução permite buscar endereços de destino de
pacotes de entrada e, como esses endereços são endereços de balanceador de carga
reais, você pode usá-los como endereços de origem ao enviar respostas.
O programa de exemplo a seguir demonstra essa solução:
#!/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 demonstra como usar chamadas recvmsg
/sendmsg
. Para
buscar informações de endereço de pacotes, você precisa usar a chamada setsockopt
para
definir a opção IP_PKTINFO
.
Prós | Contras |
---|---|
Funciona mesmo se houver vários balanceadores de carga, por exemplo, quando houver balanceadores de carga internos e externos configurados no mesmo back-end. | Exige que você faça alterações complexas no app. Em alguns casos, isso pode não ser possível. |
A seguir
- Saiba como configurar um balanceador de carga de rede de passagem externa e distribuir o tráfego em Configurar um balanceador de carga de rede de passagem externa.
- Leia mais sobre Balanceadores de carga de rede de passagem externa.
- Leia mais sobre a técnica Maglev por trás dos balanceadores de carga de rede de passagem externa.