使用多個主機 NIC (GPU VM) 的模式

部分加速器最佳化機器 (包括 A3 Ultra、A4 和 A4X) 除了機器上的 MRDMA 介面外,還有兩個主機網路介面。在主機上,這些是 Titanium IPU,會插入獨立的 CPU 插槽和非一致性記憶體存取 (NUMA) 節點。這些 IPU 可在 VM 內以 Google 虛擬 NIC (gVNIC) 的形式使用,並為儲存活動提供網路頻寬,例如檢查點、載入訓練資料、載入模型,以及其他一般網路需求。訪客作業系統 (OS) 可以看到機器的 NUMA 拓撲,包括 gVNIC 的拓撲。

本文說明在這些機器上使用兩個 gVNIC 的最佳做法。

總覽

一般來說,無論您打算如何使用多個主機 NIC,都建議採用下列設定:

  • 網路設定: 每個 gVNIC 都必須有專屬的 VPC 網路。如要設定虛擬私有雲,請考慮下列事項:
    • 為每個虛擬私有雲網路使用較大的最大傳輸單元 (MTU)。8896 是支援的 MTU 上限,建議選擇這個值。 系統可能會在接收端捨棄傳入的資料封包,導致部分工作負載的傳入效能降低。您可以使用 ethtool 工具檢查這個問題。在此情境下,調整 TCP MSS、介面 MTU 或 VPC MTU 可能會有幫助,可讓網頁快取有效分配資料,進而讓傳入的第 2 層影格符合兩個 4 KB 緩衝區。
  • 應用程式設定
    • NUMA 對齊應用程式。使用來自相同 NUMA 節點的 CPU 核心、記憶體配置和網路介面。如果您執行應用程式的專屬執行個體,以使用特定 NUMA 節點或網路介面,可以使用 numactl 等工具,將應用程式的 CPU 和記憶體資源附加至特定 NUMA 節點。
  • 作業系統設定
    • 啟用 TCP 區隔卸載 (TSO) 和大型接收卸載 (LRO)。
    • 請為每個 gVNIC 介面設定 SMP 親和性,確保介面的中斷要求 (IRQ) 與介面在同一個 NUMA 節點上處理,並將中斷分散到各個核心。如果您執行的是 Google 提供的客體 OS 映像檔,系統會使用 google_set_multiqueue 指令碼自動執行這項程序。
    • 評估 RFS、RPS 和 XPS 等設定,看看這些設定是否對您的工作負載有幫助。
    • 對於 A4X,Nvidia 建議停用自動 NUMA 排程
    • 這些機器上的 gVNIC 不支援 Linux 核心繫結。

使用多個主機 NIC 的模式

本節概述在Trusted Cloud by S3NS上使用多個主機 NIC 的一般模式。

支援的部署路徑
模式 支援的流程版面配置 GCE (一般) GKE SLURM 附註
變更應用程式,使用特定介面 每個介面的程序分片 需要變更應用程式的程式碼
變更應用程式,同時使用這兩個介面 雙介面程序 需要變更應用程式的程式碼
為特定應用程式使用專屬網路命名空間 每個介面的程序分片 ✅ (僅限特殊權限容器)
將整個容器的流量對應至單一介面 所有容器流量都對應至一個介面
對等互連虛擬私有雲,讓系統在介面間平衡負載工作階段 雙介面程序 ✅* ✅* ✅* 難以或無法進行 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 介面。您可以透過多種方式達成這個目標,但建議您為 VM 的次要介面建立 IP 別名範圍,並從這個範圍將 IP 位址指派給命名空間中的子項介面。
  5. 從新的網路命名空間執行應用程式。您可以使用 numactl 指令,透過與所選網路介面位於相同 NUMA 節點的記憶體和 CPU 執行應用程式。

視訪客和工作負載設定而定,您也可以使用 IPvlan 驅動程式,並將 IPvlan 介面連結至次要 gVNIC,而不需建立 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,並讓系統在介面之間平衡負載工作階段

必要操作:

  • 主要和次要 gVNIC 的虛擬私有雲之間必須建立虛擬私有雲對等互連。
  • 如要將 TCP 工作階段的負載平衡分配至來源介面,並傳送至單一目的地 IP 和通訊埠,則必須使用 Linux 核心 6.16 版。
  • 即使網路堆疊產生跨插槽記憶體傳輸,工作負載仍可滿足效能需求。

高階總覽

在某些情況下,應用程式內或應用程式執行個體之間的網路連線很難分片。在這種情況下,對於在 A3U 或 A4 VM 上執行的某些應用程式,如果對跨 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 位於不同的虛擬私有雲和子網路。這個模式需要將目的地位址分散到 nic0nic1 的虛擬私有雲。

高階總覽

根據預設,Linux 核心會為 nic0 的子網路和 nic1 的子網路建立路徑,將流量依目的地透過適當的網路介面轉送。

舉例來說,假設 nic0 使用的 VPC 是 net1,子網路是 subnet-a,而 nic1 使用的 VPC 是 net2,子網路是 subnet-b。根據預設,與 subnet-a 中對等互連 IP 位址的通訊會使用 nic0,與 subnet-b 中對等互連 IP 位址的通訊則會使用 nic1。舉例來說,如果一組對等互連的單一 NIC VM 連線至 net1,另一組連線至 net2,就可能發生這種情況。

使用 SNAT 選擇來源介面

必要操作:

  • 設定初始 iptables 規則時需要 CAP_NET_ADMIN,但執行應用程式時則不需要。
  • 如果規則與其他非簡單的 iptables 規則或轉送設定搭配使用,請務必仔細評估。

注意:

  • NIC 繫結只在建立連線時正確無誤。如果執行緒移至與不同 NUMA 節點相關聯的 CPU,連線就會受到跨 NUMA 的懲罰。因此,如果某種機制可將執行緒繫結至特定 CPU 集,這個解決方案就最實用。
  • 只有這部電腦發起的連線會繫結至特定 NIC。 傳入連線會與 NIC 相關聯,並與連線目的地相符。

高階總覽

如果難以使用網路命名空間或進行應用程式變更,可以使用 NAT 選取來源介面。您可以根據傳送應用程式的屬性 (例如 cgroup、使用者或 CPU),使用 iptables 等工具重新編寫流程的來源 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"