Patrones para usar varias NIC de host (VMs con GPU)


Algunas máquinas optimizadas para aceleradores, como A3 Ultra, A4 y A4X, tienen dos interfaces de red de host además de las interfaces de MRDMA en estas máquinas. En el host, son IPU Titanium que se conectan a sockets de CPU y nodos de acceso a la memoria no uniforme (NUMA) separados. Estas IPU están disponibles dentro de la VM como NIC virtuales de Google (gVNIC) y proporcionan ancho de banda de red para actividades de almacenamiento, como la creación de puntos de control, la carga de datos de entrenamiento, la carga de modelos y otras necesidades generales de redes. El sistema operativo (SO) invitado puede ver la topología NUMA de la máquina, incluida la de las gVNICs.

En este documento, se describen las prácticas recomendadas para usar las dos gVNICs en estas máquinas.

Descripción general

En general, recomendamos que uses las siguientes configuraciones, independientemente de cómo planees usar varias NIC de host:

  • Configuración de red: Cada gVNIC debe tener una red de VPC única. Para configurar una VPC, ten en cuenta lo siguiente:
    • Usa una unidad de transmisión máxima (MTU) grande para cada red de VPC. 8896 es la MTU máxima admitida y una opción recomendada. El rendimiento de entrada para algunas cargas de trabajo podría disminuir debido a que el sistema descarta paquetes de datos entrantes en el receptor. Puedes usar la herramienta ethtool para verificar si se presenta este problema. En esta situación, puede ser útil ajustar el MSS de TCP, la MTU de la interfaz o la MTU de la VPC para permitir una asignación eficiente de datos desde la caché de páginas, lo que permite que la trama entrante de capa 2 quepa dentro de dos búferes de 4 KB.
  • Configuración de la aplicación
    • Alinear la aplicación con NUMA Usa núcleos de CPU, asignaciones de memoria y una interfaz de red del mismo nodo NUMA. Si ejecutas una instancia dedicada de la aplicación para usar un nodo NUMA o una interfaz de red específicos, puedes usar herramientas como numactl para adjuntar los recursos de CPU y memoria de la aplicación a un nodo NUMA específico.
  • Configuración del sistema operativo
    • Habilita la descarga de segmentación de TCP (TSO) y la descarga de recepción grande (LRO).
    • Para cada interfaz gVNIC, asegúrate de que la afinidad de SMP esté configurada de manera que sus solicitudes de interrupción (IRQ) se controlen en el mismo nodo NUMA que la interfaz y que las interrupciones se distribuyan entre los núcleos. Si ejecutas una imagen de SO invitado proporcionada por Google, este proceso se realiza automáticamente con la secuencia de comandos google_set_multiqueue.
    • Evalúa parámetros de configuración como RFS, RPS y XPS para ver si pueden ser útiles para tu carga de trabajo.
    • Para A4X, Nvidia recomienda inhabilitar la programación automática de NUMA.
    • La vinculación del kernel de Linux no es compatible con las gVNIC en estas máquinas.

Patrones para usar varias NIC de host

En esta sección, se describen los patrones generales para usar varias NICs de host enGoogle Cloud.

Rutas de implementación admitidas
Patrón Diseño de proceso admitido GCE (general) GKE SLURM Notas
Cambia la aplicación para que use una interfaz específica Procesa fragmentos por interfaz Requiere cambios de código en la aplicación
Cambia la aplicación para que use ambas interfaces Proceso de interfaz doble Requiere cambios de código en la aplicación
Usa un espacio de nombres de red dedicado para aplicaciones específicas Procesa fragmentos por interfaz ✅ (solo contenedores privilegiados)
Cómo asignar el tráfico de todo un contenedor a una sola interfaz Todo el tráfico de contenedores se asigna a una interfaz
Intercambia tráfico entre las VPC y permite que el sistema balancee las sesiones de carga en las interfaces Proceso de interfaz doble ✅* ✅* ✅* Es difícil o imposible alinear con NUMA. Se requiere el kernel de Linux 6.16 o posterior*.
Divide el tráfico entre las redes Proceso de doble interfaz: Fragmento de proceso por interfaz ✅* ✅* ✅* Es posible que se requieran cambios de código para alinear con NUMA si se ejecuta un proceso de interfaz doble.
Usa SNAT para elegir la interfaz de origen Proceso de doble interfaz: Fragmento de proceso por interfaz ✅ (la configuración requiere privilegios de administrador) ✅ (la configuración requiere privilegios de administrador) Puede ser más difícil de configurar correctamente

* Por lo general, no se recomienda esta opción, pero puede ser útil para cargas de trabajo limitadas en plataformas x86 (A3 Ultra y A4).

Cambia la aplicación para que use una interfaz específica

Requisitos:

  • Este método requiere cambios de código en tu aplicación.
  • Requiere permisos para uno o más de los siguientes métodos:
    • bind() solo requiere permisos especiales si se usa un puerto de origen con privilegios.
    • SO_BINDTODEVICE: Requiere el permiso CAP_NET_RAW.
  • Este método puede requerir que modifiques tu tabla de enrutamiento del kernel para establecer rutas y evitar el enrutamiento asimétrico.

Descripción general

Con este patrón, completarás lo siguiente:

  1. Agrega la vinculación de la interfaz de red al código fuente de tu aplicación con una de las siguientes opciones:
    • Usa bind()para vincular un socket a una dirección IP de origen en particular
    • Usa la opción de socket SO_BINDTODEVICE para vincular un socket a una interfaz de red en particular
  2. Modifica la tabla de enrutamiento del kernel según sea necesario para garantizar que exista una ruta desde la interfaz de red de origen hasta la dirección de destino. Además, es posible que se requieran rutas para evitar el enrutamiento asimétrico. Te recomendamos que configures el enrutamiento de la política como se describe en Configura el enrutamiento para una interfaz de red adicional.
  3. También puedes usar el comando numactl para ejecutar tu aplicación. En este enfoque, usas la memoria y las CPU que se encuentran en el mismo nodo NUMA que la interfaz de red que elegiste.

Después de completar los pasos anteriores, las instancias de tu aplicación se ejecutarán con una interfaz de red específica.

Cambia la aplicación para que use ambas interfaces

Requisitos:

  • Este método requiere cambios de código en tu aplicación.
  • Necesitas permisos para uno o más de los siguientes métodos:
    • bind() solo requiere permisos especiales si se usa un puerto de origen con privilegios.
    • SO_BINDTODEVICE: Requiere el permiso CAP_NET_RAW.
  • Este método puede requerir que modifiques tu tabla de enrutamiento del kernel para establecer rutas y evitar el enrutamiento asimétrico.

Descripción general

Para implementar este patrón, haz lo siguiente:

  1. Agrega la vinculación de la interfaz de red al código fuente de tu aplicación con una de las siguientes opciones:
    1. Usa la llamada al sistema bind() para vincular un socket a una dirección IP de origen en particular.
    2. Usa la opción de socket SO_BINDTODEVICE para vincular un socket a una interfaz de red en particular
  2. Si tu aplicación actúa como cliente, deberás crear un socket de cliente independiente para cada interfaz de red de origen.
  3. Modifica la tabla de enrutamiento del kernel según sea necesario para garantizar que exista una ruta desde la interfaz de red de origen hasta la dirección de destino. Además, es posible que también necesites rutas para evitar el enrutamiento asimétrico. Te recomendamos que configures la política de enrutamiento como se describe en Configura el enrutamiento para una interfaz de red adicional.
  4. Te recomendamos que dividas la actividad de red en subprocesos que se ejecuten en el mismo nodo de NUMA que la interfaz de gVNIC. Una forma común de solicitar un nodo NUMA específico para un subproceso es llamar a pthread_setaffinity_np.
    1. Dado que la aplicación utiliza recursos en varios nodos NUMA, evita usar numactl o asegúrate de que tu comando numactl incluya los nodos NUMA de todas las interfaces de red que utiliza tu aplicación.

Usa un espacio de nombres de red dedicado para aplicaciones específicas

Requisitos:

  • Requiere la capacidad de CAP_SYS_ADMIN.
  • No es compatible con GKE Autopilot.
  • Si usas GKE, debes tener un contenedor privilegiado.

En esta sección, se describen los patrones que puedes usar para crear un espacio de nombres de red que use una interfaz de red secundaria. El patrón adecuado para tu carga de trabajo depende de tu situación específica. Los enfoques que usan un conmutador virtual o IPvlan son más adecuados para los casos en los que varias aplicaciones necesitan usar la interfaz secundaria desde diferentes espacios de nombres de red.

Descripción general de alto nivel: Cómo mover la interfaz secundaria a un espacio de nombres de red dedicado

Este patrón implica crear un espacio de nombres de red, mover la interfaz gVNIC secundaria al espacio de nombres nuevo y, luego, ejecutar la aplicación desde este espacio de nombres. Este patrón podría ser menos complicado de configurar y ajustar en comparación con el uso de un interruptor virtual. Sin embargo, las aplicaciones fuera del nuevo espacio de nombres de red no podrán acceder a la gVNIC secundaria.

En el siguiente ejemplo, se muestra una serie de comandos que se pueden usar para mover eth1 al nuevo espacio de nombres de red llamado 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>

Cuando se ejecuta este comando, la expresión <command> se ejecuta dentro del espacio de nombres de la red y usa la interfaz eth1.

Las aplicaciones que se ejecutan dentro del nuevo espacio de nombres de red ahora usan la gVNIC secundaria. También puedes usar el comando numactl para ejecutar tu aplicación con la memoria y las CPU que se encuentran en el mismo nodo NUMA que la interfaz de red que elegiste.

Descripción general: Uso de un conmutador virtual y un espacio de nombres de red para una interfaz secundaria Este patrón implica la creación de una configuración de conmutador virtual para usar la gVNIC secundaria desde un espacio de nombres de red.

Los pasos de alto nivel son los siguientes:

  1. Crea un par de dispositivos Ethernet virtuales (veth). Ajusta la unidad de transmisión máxima (MTU) en cada uno de los dispositivos para que coincida con la MTU de la gVNIC secundaria.
  2. Ejecuta el siguiente comando para asegurarte de que el reenvío de IP esté habilitado para IPv4: sysctl -w net.ipv4.ip_forward=1
  3. Mueve un extremo del par veth a un nuevo espacio de nombres de red y deja el otro extremo en el espacio de nombres raíz.
  4. Asigna el tráfico del dispositivo veth a la interfaz gVNIC secundaria. Hay varias formas de hacerlo. Sin embargo, te recomendamos que crees un rango de alias de IP para la interfaz secundaria de la VM y que asignes una dirección IP de este rango a la interfaz secundaria en el espacio de nombres.
  5. Ejecuta la aplicación desde el nuevo espacio de nombres de la red. Puedes usar el comando numactl para ejecutar tu aplicación con la memoria y las CPUs que se encuentran en el mismo nodo NUMA que la interfaz de red elegida.

Según la configuración del invitado y la carga de trabajo, también puedes usar el controlador IPvlan con una interfaz IPvlan vinculada al gVNIC secundario en lugar de crear los dispositivos veth.

Cómo asignar el tráfico de un contenedor completo a una sola interfaz

Requisitos:

  • Tu aplicación debe ejecutarse dentro de un contenedor que use un espacio de nombres de red para la conexión en red de contenedores, como GKE, Docker o Podman. No puedes usar la red del host.

Muchas tecnologías de contenedores, como GKE, Docker y Podman, usan un espacio de nombres de red dedicado para que un contenedor aísle su tráfico. Luego, este espacio de nombres de red se puede modificar, ya sea directamente o con las herramientas de la tecnología de contenedores para asignar el tráfico a una interfaz de red diferente.

GKE requiere que la interfaz principal esté presente para la comunicación interna de Kubernetes. Sin embargo, la ruta predeterminada en el Pod se puede cambiar para usar la interfaz secundaria, como se muestra en el siguiente manifiesto del Pod de GKE.

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

Este enfoque no garantiza la alineación de NUMA entre la interfaz de red predeterminada y las CPU o la memoria.

Intercambia tráfico entre las VPC y permite que el sistema balancee la carga de las sesiones en las interfaces.

Requisitos:

  • Se debe establecer el intercambio de tráfico de VPC entre las VPC de las gVNIC principal y secundaria.
  • Se requiere la versión 6.16 del kernel de Linux para balancear la carga de las sesiones de TCP en las interfaces de origen si se envía a una sola dirección IP y puerto de destino.
  • La carga de trabajo aún puede cumplir con tus requisitos de rendimiento cuando la pila de redes genera transferencias de memoria entre sockets.

Descripción general

En algunos casos, es difícil fragmentar las conexiones de red dentro de una aplicación o entre instancias de una aplicación. En este caso, para algunas aplicaciones que se ejecutan en VMs A3U o A4 que no son sensibles a la transferencia entre NUMA o entre sockets, puede ser conveniente tratar las dos interfaces como fungibles.

Un método para lograr esto es usar el parámetro sysctl fib_multipath_hash_policy y una ruta de múltiples rutas:

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}

Fragmenta el tráfico en las redes

Requisitos:

  • nic0 y nic1 en la VM están en VPCs y subredes independientes. Este patrón requiere que las direcciones de destino se fragmenten en las VPC de nic0 y nic1.

Descripción general

De forma predeterminada, el kernel de Linux crea rutas para la subred de nic0 y la subred de nic1 que enrutarán el tráfico por destino a través de la interfaz de red adecuada.

Por ejemplo, supongamos que nic0 usa la VPC net1 con la subred subnet-a, y nic1 usa la VPC net2 con la subred subnet-b. De forma predeterminada, las comunicaciones a direcciones IP de socios en subnet-a usarán nic0, y las comunicaciones a direcciones IP de socios en subnet-b usarán nic1. Por ejemplo, este caso puede ocurrir con un conjunto de VMs de un solo NIC conectadas a net1 y otro conjunto conectado a net2.

Usa SNAT para elegir la interfaz de origen

Requisitos:

  • CAP_NET_ADMIN es necesario para configurar las reglas iniciales de iptables, pero no para ejecutar la aplicación.
  • Debes evaluar cuidadosamente las reglas cuando las uses en combinación con otras reglas de iptables o configuraciones de enrutamiento no triviales.

Nota:

  • La vinculación de la NIC solo es correcta en el momento en que se crea la conexión. Si un subproceso se mueve a una CPU asociada a un nodo NUMA diferente, la conexión sufrirá penalizaciones entre nodos NUMA. Por lo tanto, esta solución es más útil cuando hay algún mecanismo para vincular subprocesos a conjuntos de CPU específicos.
  • Solo las conexiones originadas por esta máquina se vincularán a una NIC específica. Las conexiones entrantes se asociarán con la NIC que coincida con la dirección a la que están destinadas.

Descripción general

En situaciones en las que es difícil usar espacios de nombres de red o realizar cambios en la aplicación, puedes usar NAT para elegir una interfaz de origen. Puedes usar herramientas como iptables para reescribir la IP de origen de un flujo y que coincida con la IP de una interfaz en particular según una propiedad de la aplicación de envío, como cgroup, usuario o CPU.

En el siguiente ejemplo, se usan reglas basadas en la CPU. El resultado final es que el gVNIC conectado al nodo NUMA correspondiente de esa CPU transmite un flujo que se origina en un subproceso que se ejecuta en cualquier CPU determinada.

# --- 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"