Padrões para usar várias NICs de anfitrião (VMs de GPU)

Algumas máquinas otimizadas para aceleradores, incluindo A3 Ultra, A4 e A4X, têm duas interfaces de rede do anfitrião, além das interfaces MRDMA nestas máquinas. No anfitrião, estes são IPUs de titânio que estão ligados a tomadas de CPU separadas e nós de acesso à memória não uniforme (NUMA). Estas IPUs estão disponíveis na VM como NICs virtuais da Google (gVNICs) e oferecem largura de banda de rede para atividades de armazenamento, como a criação de pontos de verificação, o carregamento de dados de treino, o carregamento de modelos e outras necessidades de rede gerais. A topologia NUMA da máquina, incluindo a dos gVNICs, é visível para o sistema operativo (SO) convidado.

Este documento descreve as práticas recomendadas para usar as duas gVNICs nestas máquinas.

Vista geral

Em geral, recomendamos que use as seguintes configurações, independentemente de como planeia usar várias NICs de anfitrião:

  • Definições de rede: cada gVNIC tem de ter uma rede de VPC única. Para uma VPC configurada, considere o seguinte:
    • Use uma unidade de transmissão máxima (MTU) grande para cada rede de VPC. 8896 é a MTU máxima suportada e uma escolha recomendada. O desempenho de entrada para algumas cargas de trabalho pode ser mais lento devido a o sistema rejeitar pacotes de dados recebidos no lado do recetor. Pode usar a ferramenta ethtool para verificar se existe este problema. Neste cenário, pode ser útil ajustar o MSS de TCP, o MTU da interface ou o MTU da VPC para permitir a alocação eficiente de dados a partir da cache de páginas, o que permite que a frame de camada 2 recebida se ajuste a dois buffers de 4 KB.
  • Definições da aplicação
    • Alinhe a aplicação com a NUMA. Use núcleos da CPU, atribuições de memória e uma interface de rede do mesmo nó NUMA. Se estiver a executar uma instância dedicada da aplicação para usar um nó NUMA específico ou uma interface de rede, pode usar ferramentas como numactl para anexar os recursos de CPU e memória da aplicação a um nó NUMA específico.
  • Definições do sistema operativo
    • Ative a transferência de segmentação TCP (TSO) e a transferência de receção grande (LRO).
    • Para cada interface gVNIC, certifique-se de que a afinidade SMP está configurada de modo que os pedidos de interrupção (IRQs) sejam processados no mesmo nó NUMA que a interface e que as interrupções sejam distribuídas pelos núcleos. Se estiver a executar uma imagem do SO convidado fornecida pela Google, este processo ocorre automaticamente através do script google_set_multiqueue.
    • Avalie definições como RFS, RPS e XPS para ver se podem ser úteis para a sua carga de trabalho.
    • Para o A4X, a Nvidia recomenda desativar o agendamento NUMA automático.
    • A agregação do kernel do Linux não é suportada para os gVNICs nestas máquinas.

Padrões para usar várias NICs de anfitrião

Esta secção descreve os padrões gerais de utilização de várias NICs de anfitrião no Google Cloud.

Caminhos de implementação suportados
Padrão Esquema de processo suportado GCE (geral) GKE SLURM Notes
Altere a aplicação para usar uma interface específica Processar fragmento por interface Requer alterações ao código da aplicação
Altere a aplicação para usar ambas as interfaces Processo de interface dupla Requer alterações ao código da aplicação
Use um espaço de nomes de rede dedicado para aplicações específicas Processar fragmento por interface ✅ (apenas contentores privilegiados)
Mapeie o tráfego de um contentor inteiro para uma única interface Todo o tráfego do contentor mapeado para uma interface
Estabeleça uma relação de interligação entre as VPCs e permita que o sistema equilibre a carga das sessões entre interfaces Processo de interface dupla ✅* ✅* ✅* Difícil ou impossível de alinhar com NUMA. É necessário o kernel do Linux 6.16 ou posterior*
Divida o tráfego entre redes Processo de interface dupla: fragmento de processo por interface ✅* ✅* ✅* Pode exigir alterações ao código para o alinhamento NUMA se estiver a executar um processo de interface dupla.
Use o SNAT para escolher a interface de origem Processo de interface dupla: fragmento de processo por interface ✅ (a configuração requer privilégios de administrador) ✅ (a configuração requer privilégios de administrador) Pode ser mais difícil de configurar corretamente

* Geralmente, esta opção não é recomendada, mas pode ser útil para cargas de trabalho limitadas em plataformas x86 (A3 Ultra e A4).

Altere a aplicação para usar uma interface específica

Requisitos:

  • Este método requer alterações ao código da sua aplicação.
  • Requer autorizações para um ou mais dos seguintes métodos:
    • O bind() só requer autorizações especiais se usar uma porta de origem privilegiada.
    • SO_BINDTODEVICE: requer a autorização CAP_NET_RAW.
  • Este método pode exigir que modifique a tabela de encaminhamento do kernel para estabelecer rotas e evitar o encaminhamento assimétrico.

Vista geral de alto nível

Com este padrão, conclui o seguinte:

  1. Adicione a associação da interface de rede ao código-fonte da sua aplicação através de uma das seguintes opções:
    • Use bind()para associar um socket a um endereço IP de origem específico
    • Use a opção de entrada SO_BINDTODEVICE para associar uma entrada a uma interface de rede específica
  2. Modifique a tabela de encaminhamento do kernel conforme necessário para garantir que existe um caminho da interface de rede de origem para o endereço de destino. Além disso, podem ser necessárias rotas para evitar o encaminhamento assimétrico. Recomendamos que configure o encaminhamento de políticas conforme descrito no artigo Configure o encaminhamento para uma interface de rede adicional.
  3. Também pode usar o comando numactl para executar a sua aplicação. Nesta abordagem, usa a memória e as CPUs que estão no mesmo nó NUMA que a interface de rede escolhida.

Depois de concluir os passos anteriores, as instâncias da sua aplicação são executadas através de uma interface de rede específica.

Altere a aplicação para usar ambas as interfaces

Requisitos:

  • Este método requer alterações ao código da sua aplicação.
  • Precisa de autorizações para um ou mais dos seguintes métodos:
    • O bind() só requer autorizações especiais se usar uma porta de origem privilegiada.
    • SO_BINDTODEVICE: requer a autorização CAP_NET_RAW.
  • Este método pode exigir que modifique a tabela de encaminhamento do kernel para estabelecer rotas e evitar o encaminhamento assimétrico.

Vista geral de alto nível

Para implementar este padrão, faça o seguinte:

  1. Adicione a associação da interface de rede ao código-fonte da sua aplicação através de uma das seguintes opções:
    1. Use a chamada do sistema bind() para associar uma porta a um endereço IP de origem específico
    2. Use a opção de entrada SO_BINDTODEVICE para associar uma entrada a uma interface de rede específica
  2. Se a sua aplicação estiver a atuar como cliente, tem de criar um soquete de cliente separado para cada interface de rede de origem.
  3. Modifique a tabela de encaminhamento do kernel conforme necessário para garantir que existe um caminho da interface de rede de origem para o endereço de destino. Além disso, também pode precisar de trajetos para evitar o encaminhamento assimétrico. Recomendamos que configure o encaminhamento de políticas conforme descrito em Configure o encaminhamento para uma interface de rede adicional.
  4. Recomendamos que particione a atividade de rede em threads que são executados no mesmo nó NUMA que a interface gVNIC. Uma forma comum de pedir um nó NUMA específico para um segmento é chamar pthread_setaffinity_np.
    1. Uma vez que a aplicação usa recursos em vários nós NUMA, evite usar numactl ou certifique-se de que o comando numactl inclui os nós NUMA de todas as interfaces de rede usadas pela sua aplicação.

Use um espaço de nomes de rede dedicado para aplicações específicas

Requisitos:

  • Requer a capacidade do CAP_SYS_ADMIN.
  • Não é compatível com o GKE Autopilot.
  • Se usar o GKE, tem de ter um contentor privilegiado.

Esta secção descreve padrões que pode usar para criar um espaço de nomes de rede que usa uma interface de rede secundária. O padrão certo para a sua carga de trabalho depende do seu cenário específico. As abordagens que usam o comutador virtual ou o IPvlan são mais adequadas para casos em que várias aplicações precisam de usar a interface secundária de diferentes espaços de nomes de rede.

Vista geral: mover a interface secundária para o espaço de nomes de rede dedicado

Este padrão envolve a criação de um espaço de nomes de rede, a movimentação da interface gVNIC secundária para o novo espaço de nomes e, em seguida, a execução da aplicação a partir deste espaço de nomes. Este padrão pode ser menos complicado de configurar e ajustar em comparação com a utilização de um interruptor virtual. No entanto, as aplicações fora do novo espaço de nomes de rede não vão poder aceder ao gVNIC secundário.

O exemplo seguinte mostra uma série de comandos que podem ser usados para mover eth1 para o novo espaço de nomes de rede denominado second.

ip netns add second
ip link set eth1 netns second
ip netns exec second ip addr add ${ETH1_IP}/${PREFIX} dev eth1
ip netns exec second ip link set dev eth1 up
ip netns exec second ip route add default via ${GATEWAY_IP} dev eth1
ip netns exec second <command>

Quando este comando é executado, a expressão <command> é executada no espaço de nomes de rede e usa a interface eth1.

As aplicações executadas no novo espaço de nomes de rede usam agora o gVNIC secundário. Também pode usar o comando numactl para executar a sua aplicação usando a memória e as CPUs que estão no mesmo nó NUMA que a interface de rede escolhida.

Vista geral de alto nível: usar um comutador virtual e um espaço de nomes de rede para uma interface secundária Este padrão envolve a criação de uma configuração de comutador virtual para usar o gVNIC secundário de um espaço de nomes de rede.

Os passos de alto nível são os seguintes:

  1. Crie um par de dispositivos Ethernet virtuais (veth). Ajuste a unidade de transmissão máxima (MTU) em cada um dos dispositivos para corresponder à MTU do gVNIC secundário.
  2. Execute o seguinte para garantir que o encaminhamento de IP está ativado para IPv4: sysctl -w net.ipv4.ip_forward=1
  3. Mova uma extremidade do par veth para um novo espaço de nomes de rede e deixe a outra extremidade no espaço de nomes raiz.
  4. Mapeie o tráfego do dispositivo veth para a interface gVNIC secundária. Existem várias formas de o fazer. No entanto, recomendamos que crie um intervalo de alias de IP para a interface secundária da VM e atribua um endereço IP deste intervalo à interface secundária no espaço de nomes.
  5. Execute a aplicação a partir do novo espaço de nomes de rede. Pode usar o comando numactl para executar a sua aplicação usando a memória e as CPUs que estão no mesmo nó NUMA que a interface de rede escolhida.

Em alternativa, consoante a configuração do convidado e da carga de trabalho, pode usar o controlador IPvlan com uma interface IPvlan associada ao gVNIC secundário em vez de criar os dispositivos veth.

Mapeie o tráfego de um contentor completo para uma única interface

Requisitos:

  • A sua aplicação tem de ser executada num contentor que use um espaço de nomes de rede para redes de contentores, como o GKE, o Docker ou o Podman. Não pode usar a rede do anfitrião.

Muitas tecnologias de contentores, como o GKE, o Docker e o Podman, usam um espaço de nomes de rede dedicado para um contentor isolar o respetivo tráfego. Em seguida, este espaço de nomes de rede pode ser modificado, diretamente ou através das ferramentas da tecnologia de contentores, para mapear o tráfego para uma interface de rede diferente.

O GKE requer que a interface principal esteja presente para a comunicação interna do Kubernetes. No entanto, a rota predefinida no pod pode ser alterada para usar a interface secundária, conforme mostrado no seguinte manifesto do pod do GKE.

metadata:
  …
  annotations:
    networking.gke.io/default-interface: 'eth1'
    networking.gke.io/interfaces: |
      [
        {"interfaceName":"eth0","network":"default"},
        {"interfaceName":"eth1","network":"secondary-network"},
      ]

Esta abordagem não garante o alinhamento NUMA entre a interface de rede predefinida e as CPUs ou a memória.

Estabeleça uma relação de interligação entre as VPCs e permita que o sistema equilibre a carga das sessões entre interfaces

Requisitos:

  • O peering de VPC tem de ser estabelecido entre as VPCs das gVNICs primárias e secundárias.
  • A versão 6.16 do kernel do Linux é necessária para equilibrar a carga das sessões TCP nas interfaces de origem se estiver a enviar para um único endereço IP e porta de destino.
  • A carga de trabalho ainda pode cumprir os seus requisitos de desempenho quando a pilha de rede gera transferências de memória entre sockets.

Vista geral de alto nível

Em alguns casos, é difícil dividir as ligações de rede numa aplicação ou entre instâncias de uma aplicação. Neste cenário, para algumas aplicações executadas em VMs A3U ou A4 que não são sensíveis à transferência entre NUMAs ou entre sockets, pode ser conveniente tratar as duas interfaces como fungíveis.

Um método para o conseguir é usar o sysctl fib_multipath_hash_policy e uma rota de vários caminhos:

PRIMARY_GW=192.168.1.1  # gateway of nic0
SECONDARY_GW=192.168.2.1  # gateway of nic1
PRIMARY_IP=192.168.1.15  # internal IP for nic0
SECONDARY_IP=192.168.2.27  # internal IP nic1

sysctl -w net.ipv4.fib_multipath_hash_policy=1  # Enable L4 5-tuple ECMP hashing
ip route add <destination-network/subnet-mask> nexthop via ${PRIMARY_GW} nexthop
via ${SECONDARY_GW}

Divida o tráfego entre redes

Requisitos:

  • nic0 e nic1 na VM estão em VPCs e sub-redes separadas. Este padrão requer que os endereços de destino sejam divididos em fragmentos nas VPCs do nic0 e do nic1.

Vista geral de alto nível

Por predefinição, o kernel do Linux cria rotas para a sub-rede de nic0 e a sub-rede de nic1 que encaminham o tráfego por destino através da interface de rede adequada.

Por exemplo, suponhamos que nic0 usa a VPC net1 com a sub-rede subnet-a e que nic1 usa a VPC net2 com a sub-rede subnet-b. Por predefinição, as comunicações para endereços IP de pares em subnet-a usam nic0 e as comunicações para endereços IP de pares em subnet-b usam nic1. Por exemplo, este cenário pode ocorrer com um conjunto de VMs de NIC única ponto a ponto ligadas a net1 e um conjunto ligado a net2.

Use o SNAT para escolher a interface de origem

Requisitos:

  • O CAP_NET_ADMIN é necessário para configurar as regras iniciais do iptables, embora não seja necessário para executar a aplicação.
  • Tem de avaliar cuidadosamente as regras quando as usar em combinação com outras regras iptables não triviais ou configurações de encaminhamento.

Nota:

  • A associação da NIC só está correta no momento em que a ligação é criada. Se um processo for movido para uma CPU associada a um nó NUMA diferente, a ligação vai sofrer penalizações de NUMA cruzado. Por conseguinte, esta solução é mais útil quando existe algum mecanismo para associar threads a conjuntos de CPUs específicos.
  • Apenas as ligações originadas por este computador vão ser associadas a uma NIC específica. As associações de entrada são associadas à NIC que corresponde ao endereço para o qual se destinam.

Vista geral de alto nível

Em cenários em que é difícil usar espaços de nomes de rede ou fazer alterações às aplicações, pode usar a NAT para escolher uma interface de origem. Pode usar ferramentas como o iptables para reescrever o IP de origem de um fluxo de modo a corresponder ao IP de uma interface específica com base numa propriedade da aplicação de envio, como cgroup, utilizador ou CPU.

O exemplo seguinte usa regras baseadas na CPU. O resultado final é que um fluxo que tem origem num segmento em execução numa determinada CPU é transmitido pela gVNIC associada ao nó NUMA correspondente dessa CPU.

# --- Begin Configuration ---
OUTPUT_INTERFACE_0="enp0s19"        # CHANGEME: NIC0
OUTPUT_INTERFACE_1="enp192s20"      # CHANGEME: NIC1

CPUS_0=($(seq 0 55; seq 112 167))   # CHANGEME: CPU IDs for NIC0
GATEWAY_0="10.0.0.1"                # CHANGEME: Gateway for NIC0
SNAT_IP_0="10.0.0.2"                # CHANGEME: SNAT IP for NIC0
CONNMARK_0="0x1"
RT_TABLE_0="100"

CPUS_1=($(seq 56 111; seq 168 223)) # CHANGEME: CPU IDs for NIC1
GATEWAY_1="10.0.1.1"                # CHANGEME: Gateway for NIC1
SNAT_IP_1="10.0.1.2"                # CHANGEME: SNAT IP for NIC1
CONNMARK_1="0x2"
RT_TABLE_1="101"
# --- End Configuration ---

# This informs which interface to use for packets in each table.
ip route add default via "$GATEWAY_0" dev "$OUTPUT_INTERFACE_0" table "$RT_TABLE_0"
ip route add default via "$GATEWAY_1" dev "$OUTPUT_INTERFACE_1" table "$RT_TABLE_1"

# This is not required for connections we originate, but replies to
# connections from peers need to know which interface to egress from.
# Add it before the fwmark rules to implicitly make sure fwmark takes precedence.
ip rule add from "$SNAT_IP_0" table "$RT_TABLE_0"
ip rule add from "$SNAT_IP_1" table "$RT_TABLE_1"

# This informs which table to use based on the packet mark set in OUTPUT.
ip rule add fwmark "$CONNMARK_0" table "$RT_TABLE_0"
ip rule add fwmark "$CONNMARK_1" table "$RT_TABLE_1"

# Relax reverse path filtering.
# Otherwise, we will drop legitimate replies to the SNAT IPs.
sysctl -w net.ipv4.conf."$OUTPUT_INTERFACE_0".rp_filter=2
sysctl -w net.ipv4.conf."$OUTPUT_INTERFACE_1".rp_filter=2

# Mark packets/connections with a per-nic mark based on the source CPU.
# The `fwmark` rules will then use the corresponding routing table for this traffic.
for cpu_id in "${CPUS_0[@]}"; do
    iptables -t mangle -A OUTPUT -m state --state NEW -m cpu --cpu "$cpu_id" -j CONNMARK --set-mark "$CONNMARK_0"
    iptables -t mangle -A OUTPUT -m state --state NEW -m cpu --cpu "$cpu_id" -j MARK --set-mark "$CONNMARK_0"
done
for cpu_id in "${CPUS_1[@]}"; do
    iptables -t mangle -A OUTPUT -m state --state NEW -m cpu --cpu "$cpu_id" -j CONNMARK --set-mark "$CONNMARK_1"
    iptables -t mangle -A OUTPUT -m state --state NEW -m cpu --cpu "$cpu_id" -j MARK --set-mark "$CONNMARK_1"
done

# For established connections, restore the connection mark.
# Otherwise, we will send the packet to the wrong NIC, depending on existing
# routing rules.
iptables -t mangle -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark

# These rules NAT the source address after the packet is already destined to
# egress the correct interface. This lets replies to this flow target the correct NIC,
# and may be required to be accepted into the VPC.
iptables -t nat -A POSTROUTING -m mark --mark "$CONNMARK_0" -j SNAT --to-source "$SNAT_IP_0"
iptables -t nat -A POSTROUTING -m mark --mark "$CONNMARK_1" -j SNAT --to-source "$SNAT_IP_1"