使用多个主机 NIC(GPU 虚拟机)时应遵循的模式

某些加速器优化型机器(包括 A3 Ultra、A4 和 A4X)除了具有 MRDMA 接口之外,还具有两个主机网络接口。在主机上,这些是插入到单独的 CPU 插槽和非统一内存访问 (NUMA) 节点的 Titanium IPU。这些 IPU 以 Google 虚拟 NIC (gVNIC) 的形式在虚拟机内部可用,并为检查点创建、加载训练数据、加载模型和其他一般网络需求等存储活动提供网络带宽。机器的 NUMA 拓扑(包括 gVNIC 的 NUMA 拓扑)对客机操作系统 (OS) 可见。

本文档介绍了在这些机器上使用两个 gNIC 的最佳实践。

概览

一般来说,无论您打算如何使用多个主机 NIC,我们都建议您使用以下配置:

  • 网络设置每个 gVNIC 都必须具有唯一的 VPC 网络。对于 VPC 设置,请考虑以下事项:
    • 为每个 VPC 网络使用较大的最大传输单元 (MTU)。8896 是支持的最大 MTU,也是推荐的选择。 由于系统在接收器端丢弃传入的数据包,某些工作负载的入站流量性能可能会降低。您可以使用 ethtool 工具检查是否存在此问题。在这种情况下,调整 TCP MSS、接口 MTU 或 VPC MTU 可能有助于从页面缓存中高效分配数据,从而使传入的第 2 层帧能够容纳在两个 4KB 缓冲区中。
  • 应用设置
    • 使应用与 NUMA 对齐。使用来自同一 NUMA 节点的 CPU 核心、内存分配和网络接口。如果您要运行应用的专用实例来使用特定的 NUMA 节点或网络接口,可以使用 numactl 等工具将应用的 CPU 和内存资源附加到特定的 NUMA 节点。
  • 操作系统设置
    • 启用 TCP 细分分流 (TSO) 和大型接收分流 (LRO)。
    • 对于每个 gVNIC 接口,请确保设置 SMP 亲和性,以便在与接口相同的 NUMA 节点上处理其中断请求 (IRQ),并将中断分散到各个核心。如果您运行的是 Google 提供的客机操作系统映像,此过程会使用 google_set_multiqueue 脚本自动进行。
    • 评估 RFS、RPS 和 XPS 等设置,看看它们是否可能对您的工作负载有所帮助。
    • 对于 A4X,Nvidia 建议停用自动 NUMA 调度
    • 这些机器上的 gVNIC 不支持 Linux 内核绑定。

使用多个主机 NIC 时应遵循的模式

本部分概述了在Trusted Cloud by S3NS上使用多个主机 NIC 的一般模式。

支持的部署路径
模式 支持的流程布局 GCE(常规) GKE SLURM 备注
更改应用以使用特定接口 每个接口的进程分片 需要对应用进行代码更改
更改应用以同时使用这两个接口 双接口流程 需要对应用进行代码更改
为特定应用使用专用网络命名空间 每个接口的进程分片 ✅(仅限特权容器)
将整个容器的流量映射到单个接口 所有容器流量都映射到一个接口
对等互连 VPC,并让系统在接口之间对会话进行负载均衡 双接口流程 ✅* ✅* ✅* 难以或无法实现 NUMA 对齐;需要 Linux 内核 6.16 或更高版本*
跨网络分片流量 双接口流程;每个接口一个流程分片 ✅* ✅* ✅* 如果运行双接口进程,可能需要更改代码以实现 NUMA 对齐。
使用 SNAT 选择源接口 双接口流程;每个接口一个流程分片 ✅(设置需要管理员权限) ✅(设置需要管理员权限) 可能更难正确配置

* 一般不建议使用此选项,但对于 x86(A3 Ultra 和 A4)平台上的有限工作负载,此选项可能很有用。

更改应用以使用特定接口

要求

  • 此方法需要您更改应用的代码。
  • 需要以下一种或多种方法的权限:
    • bind() 仅在使用特权源端口时才需要特殊权限。
    • SO_BINDTODEVICE:需要 CAP_NET_RAW 权限。
  • 此方法可能需要您修改内核路由表,以建立路由并防止非对称路由。

简要概览

使用此模式,您将完成以下操作:

  1. 使用以下任一选项将网络接口绑定添加到应用源代码:
    • 使用 bind() 将套接字绑定到特定来源 IP 地址
    • 使用 SO_BINDTODEVICE 套接字选项将套接字绑定到特定网络接口
  2. 根据需要修改内核路由表,以确保存在从源网络接口到目标地址的路由。此外,可能还需要路由来防止非对称路由。我们建议您按照为额外网络接口配置路由中的说明配置政策路由。
  3. 您还可以使用 numactl 命令来运行应用。在这种方法中,您可以使用与所选网络接口位于同一 NUMA 节点上的内存和 CPU。

完成上述步骤后,应用实例将使用特定的网络接口运行。

更改应用以同时使用这两个接口

要求

  • 此方法需要您更改应用的代码。
  • 您需要具备以下一种或多种方法的权限:
    • bind() 仅在使用特权源端口时才需要特殊权限。
    • SO_BINDTODEVICE:需要 CAP_NET_RAW 权限。
  • 此方法可能需要您修改内核路由表,以建立路由并防止非对称路由。

简要概览

如需实现此模式,请执行以下操作:

  1. 使用以下任一选项将网络接口绑定添加到应用源代码:
    1. 使用 bind() 系统调用将套接字绑定到特定的源 IP 地址
    2. 使用 SO_BINDTODEVICE 套接字选项将套接字绑定到特定网络接口
  2. 如果您的应用充当客户端,则需要为每个源网络接口创建一个单独的客户端套接字。
  3. 根据需要修改内核路由表,以确保存在从源网络接口到目标地址的路由。此外,您可能还需要路由来防止非对称路由。我们建议您按照为其他网络接口配置路由中的说明配置政策路由。
  4. 我们建议您将网络活动划分为在与 gVNIC 接口相同的 NUMA 节点上运行的线程。为线程请求特定 NUMA 节点的一种常见方式是调用 pthread_setaffinity_np
    1. 由于应用会使用多个 NUMA 节点上的资源,因此请避免使用 numactl,或确保 numactl 命令包含应用使用的所有网络接口的 NUMA 节点。

为特定应用使用专用网络命名空间

要求

  • 需要 CAP_SYS_ADMIN 功能。
  • 与 GKE Autopilot 不兼容。
  • 如果使用 GKE,您必须拥有特权容器。

本部分介绍了可用于创建使用辅助网络接口的网络命名空间的模式。适合工作负载的模式取决于您的具体情况。使用虚拟交换机或 IPvlan 的方法更适合以下情况:多个应用需要使用来自不同网络命名空间的辅助接口。

简要概览:将辅助接口移至专用网络命名空间

此模式涉及创建网络命名空间、将辅助 gVNIC 接口移入新命名空间,然后从该命名空间运行应用。与使用虚拟交换机相比,这种模式的设置和调整可能更简单。不过,新网络命名空间之外的应用将无法访问辅助 gVNIC。

以下示例展示了一系列可用于将 eth1 移入名为 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>

运行此命令后,<command> 表达式会在网络命名空间内执行,并使用 eth1 接口。

在新网络命名空间内运行的应用现在使用辅助 gVNIC。您还可以使用 numactl 命令,通过与所选网络接口位于同一 NUMA 节点上的内存和 CPU 来运行应用。

简要概览:使用虚拟交换机和网络命名空间来实现辅助接口此模式涉及创建虚拟交换机设置,以使用来自网络命名空间的辅助 gVNIC。

简要步骤如下:

  1. 创建虚拟以太网 (veth) 设备对。调整每个设备上的最大传输单元 (MTU),使其与辅助 gVNIC 的 MTU 相匹配。
  2. 运行以下命令,确保已为 IPv4 启用 IP 转发:sysctl -w net.ipv4.ip_forward=1
  3. 将 veth 对的一端移入新的网络命名空间,将另一端留在根命名空间中。
  4. 将来自 veth 设备的流量映射到辅助 gVNIC 接口。有多种方法可以实现此目的,不过,我们建议您为虚拟机的辅助接口创建 IP 别名范围,并将此范围内的 IP 地址分配给命名空间中的子接口。
  5. 从新的网络命名空间运行应用。您可以使用 numactl 命令,通过与所选网络接口位于同一 NUMA 节点上的内存和 CPU 来运行应用。

或者,您也可以使用 IPvlan 驱动程序,并使用与辅助 gVNIC 关联的 IPvlan 接口,而不是创建 veth 设备,具体取决于访客和工作负载设置。

将整个容器的流量映射到单个接口

要求

  • 您的应用必须在为容器联网使用网络命名空间的容器内运行,例如 GKE、Docker 或 Podman。您无法使用主机网络。

许多容器技术(例如 GKE、Docker 和 Podman)都为容器使用专用网络命名空间来隔离其流量。然后,可以直接修改此网络命名空间,也可以使用容器技术工具将流量映射到其他网络接口。

GKE 要求主接口存在,以便进行 Kubernetes 内部通信。不过,可以更改 pod 中的默认路由以使用辅助接口,如以下 GKE pod 清单所示。

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

此方法无法保证默认网络接口与 CPU 或内存之间的 NUMA 对齐。

对等互连 VPC,并让系统在接口之间对会话进行负载均衡

要求

  • 必须在主 gNIC 和辅助 gNIC 的 VPC 之间建立 VPC 对等互连。
  • 如果发送到单个目标 IP 和端口,则需要 Linux 内核版本 6.16 才能在来源接口之间对 TCP 会话进行负载均衡。
  • 当网络堆栈生成跨套接字内存传输时,工作负载仍可满足性能要求。

简要概览

在某些情况下,对应用内或应用实例之间的网络连接进行分片是一项具有挑战性的任务。在此场景中,对于在 A3U 或 A4 虚拟机上运行且对跨 NUMA 或跨插槽传输不敏感的某些应用,将这两个接口视为可互换可能很方便。

实现此目的的一种方法是使用 fib_multipath_hash_policy sysctl 和多路径路由:

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}

跨网络分片流量

要求

  • 虚拟机上的 nic0nic1 位于不同的 VPC 和子网中。此模式要求将目标地址分片到 nic0nic1 的 VPC 中。

简要概览

默认情况下,Linux 内核会为 nic0 的子网和 nic1 的子网创建路由,这些路由将通过相应的网络接口按目的地路由流量。

例如,假设 nic0 使用具有子网 subnet-a 的 VPC net1,而 nic1 使用具有子网 subnet-b 的 VPC net2。默认情况下,与 subnet-a 中的对等 IP 地址的通信将使用 nic0,而与 subnet-b 中的对等 IP 地址的通信将使用 nic1。例如,如果一组对等单 NIC 虚拟机连接到 net1,另一组连接到 net2,则可能会出现此场景。

使用 SNAT 选择源接口

要求

  • 设置初始 iptables 规则需要 CAP_NET_ADMIN,但运行应用不需要。
  • 将规则与其他非平凡的 iptables 规则或路由配置结合使用时,您必须仔细评估规则。

注意

  • NIC 绑定仅在创建连接时正确。如果线程移至与不同 NUMA 节点关联的 CPU,连接将受到跨 NUMA 惩罚。因此,当存在将线程绑定到特定 CPU 集的机制时,此解决方案最为有用。
  • 只有由此机器发起的连接才会绑定到特定 NIC。 入站连接将与匹配其目标地址的 NIC 相关联。

简要概览

在难以使用网络命名空间或进行应用更改的情况下,您可以使用 NAT 来选择源接口。您可以使用 iptables 等工具来重写流量的源 IP,以根据发送应用的属性(例如 cgroup、用户或 CPU)将源 IP 与特定接口的 IP 相匹配。

以下示例使用基于 CPU 的规则。最终结果是,源自在任何给定 CPU 上运行的线程的流量都由附加到相应 CPU 的 NUMA 节点的 gVNIC 传输。

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