基于K8s的云原生AI基础设施:架构、部署与实践【007】-AI算力的多网络接入

在 Kubernetes 中,Pod 默认通常只有一张网络接口,也就是常见的 eth0。这张接口承担的是集群默认网络职责,用于 Pod 之间的基础互通,以及 Pod 访问 Service、DNS 等集群能力。Multus 的作用并不是替代这张默认接口,而是在此基础上继续为 Pod 附加额外网络,使 Pod 成为一个 multi-homed pod。这也是 Multus 官方对自己的核心定位。

对于 AI 算力平台来说,这类能力并不只是“多一张网卡”这么简单。很多场景下,默认网络更适合承载 Kubernetes 基础通信,而训练、推理、存储访问或特定业务流量,则希望走独立网络平面。这样做的价值在于:不同用途的流量可以被显式拆开,并拥有独立的接口、地址和路由语义。

1. Multus 解决的是什么问题

Multus 本身不是某一种底层网络实现,而是一个 meta-plugin。它的职责,是在默认网络之外,再调用其他 CNI 插件,为 Pod 附加额外网络接口。也就是说:

  • 默认网络仍然存在;
  • eth0 仍然负责集群基础通信;
  • Multus 负责把第二张、第三张网卡接进 Pod。

在具体实现上,额外网络通常通过 NetworkAttachmentDefinition 来描述。Pod 通过注解引用这些定义,Multus 再调用诸如 macvlanhost-devicebridge 等底层 CNI,把对应网络真正附加到 Pod。所以,Multus 的关键价值不是“替代 Flannel/Calico”,而是:

  • 保留默认网络作为基础入口;
  • 让附加网络具备标准化定义方式;
  • 让 Pod 可以同时接入多个网络平面。

2. Multus 的安装

官方 README 说明,自 4.0 起引入了基于 multus-daemonmultus-shim 的 thick plugin;相比 thin plugin,它带来了额外能力,但资源开销也会更高。但官方同时建议大多数环境优先使用 thick plugin。所以,默认用 thick plugin,除非你的环境资源非常紧张。

# 下载yaml文件
wget <https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/master/deployments/multus-daemonset-thick.yml>
 
# 应用yaml
kubectl apply -f multus-daemonset-thick.yml

# 检查
kubectl -n kube-system get pods -o wide | grep multus

3. 附加的网络定义

Multus 只负责“把额外网络挂进 Pod”,但“额外网络长什么样”要通过 NetworkAttachmentDefinition 定义。这个对象的本质,就是把某个附加网络的 CNI 配置保存为 Kubernetes 资源,供 Pod 通过注解引用。在本文的样例中,第二张、第三张网络都是基于 macvlan 构建的。这也是一种非常典型的做法,因为 macvlan 的职责就是创建一个新的虚拟网卡,并把它连接到宿主机某个已有物理接口之上。官方文档也明确说明:macvlan 会为每个新虚拟接口分配独立 MAC 地址,宿主机接口则作为上联 master。从配置角度看,一个 NAD 里最关键的通常是这几项:

  • type:底层 CNI 类型,例如 macvlan
  • master:绑定到哪张宿主机物理网卡
  • mode:例如 bridge
  • ipam:地址从哪里来、如何分配

通常来说AI算力网络的计算平面是独立出来的,如果你有多个平面分别绑定到不同物理网卡,那么你可以将**一个 NAD 对应一个附加网络平面。**这样后面无论是 YAML、测试还是抓包,逻辑都会很清楚。

4. IP 地址分配的三种方式:

在这一小节里,真正决定附加网络“怎么活起来”的,不只是 Multus 和底层 CNI,还有 IPAM。

4.1 host-local:适合简单测试

host-local 是 CNI 官方提供的本地 IPAM 插件。它会从指定地址范围中分配地址,并把状态保存在本地文件系统中,因此只能保证单个节点内地址唯一,不具备集群级协调能力。所以它适合:

  • 快速验证
  • 小规模测试
  • 地址冲突边界可控的场景

但如果多个节点上的 Pod 需要共享同一地址池,它就不再理想。

A平面子网
________________________________________________________
# 配置pod使用到的第2张网卡的定义,计算A平面
cat <<EOF | kubectl apply -f -
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
  name: compute-a-net
  namespace: default
spec:
  config: '{
    "cniVersion": "0.3.1",
    "type": "macvlan",
    "master": "ens18np0",
    "mode": "bridge",
    "ipam": {
      "type": "host-local",
      "subnet": "10.8.10.0/24",
      "rangeStart": "10.8.10.150",
      "rangeEnd": "10.8.10.199",
      "routes": [],
      "gateway": ""
    }
  }'
EOF

B平面子网
_________________________________________________________
# 配置pod使用到的第3张网卡的定义,计算B平面
cat <<EOF | kubectl apply -f -
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
  name: compute-b-net
  namespace: default
spec:
  config: '{
    "cniVersion": "0.3.1",
    "type": "macvlan",
    "master": "ens20np0",
    "mode": "bridge",
    "ipam": {
      "type": "host-local",
      "subnet": "10.8.15.0/24",
      "rangeStart": "10.8.15.150",
      "rangeEnd": "10.8.15.199",
      "routes": [],
      "gateway": ""
    }
  }'
EOF

4.2 Whereabouts:适合集群级地址分配

Whereabouts 的定位正是为了解决 host-local 只能感知单节点的问题。

它官方将自己定义为一个 cluster-wide IPAM plugin,用于在集群范围内分配地址,并追踪这些地址在 Pod 生命周期中的占用情况。所以,如果你的附加网络需要:

  • 多节点共享地址池
  • 避免跨节点地址冲突
  • 在集群范围管理地址生命周期

那么 Whereabouts 更适合长期使用。

# 通过helm下载安装chart
helm pull oci://ghcr.io/k8snetworkplumbingwg/whereabouts-chart \\
  --version v0.9.2 \\
  --untar

helm install whereabouts ./ -n kube-system

# 检查
kubectl get pods -n kube-system | grep whereabouts
whereabouts-whereabouts-chart-2wt4p                         1/1     Running   0          28s
whereabouts-whereabouts-chart-48g46                         1/1     Running   0          28s
whereabouts-whereabouts-chart-4wwc2                         1/1     Running   0          28s
whereabouts-whereabouts-chart-5n9m9                         1/1     Running   0          28s
whereabouts-whereabouts-chart-9q2td                         1/1     Running   0          28s
whereabouts-whereabouts-chart-c9xpv                         1/1     Running   0          28s
whereabouts-whereabouts-chart-controller-7f655f9f4d-dxl4h   1/1     Running   0          28s
whereabouts-whereabouts-chart-wxlrm                         1/1     Running   0          28s
whereabouts-whereabouts-chart-xzrxv                         1/1     Running   0          28s

kubectl get crd | grep whereabouts
ippools.whereabouts.cni.cncf.io                          2025-11-27T13:47:42Z
nodeslicepools.whereabouts.cni.cncf.io                   2025-11-27T13:47:42Z
overlappingrangeipreservations.whereabouts.cni.cncf.io   2025-11-27T13:47:42Z  
kubectl create ns nad

A平面子网1
________________________________________________________
cat <<EOF | kubectl apply -f -
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
  name: compute-a-net
  namespace: nad
spec:
  config: '{
    "cniVersion": "0.3.1",
    "type": "macvlan",
    "master": "ens2np0",
    "mode": "bridge",
    "ipam": {
      "type": "whereabouts",
      "range": "10.50.10.0/24",
      "range_start": "10.50.10.70",
      "range_end": "10.50.10.199",
      "routes": [
        { "dst": "10.50.20.0/24", "gw": "10.50.10.254" }
      ]    
    }
  }'
EOF

A平面子网2
_________________________________________________________
cat <<EOF | kubectl apply -f -
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
  name: compute-a-net
  namespace: nad
spec:
  config: '{
    "cniVersion": "0.3.1",
    "type": "macvlan",
    "master": "ens2np0",
    "mode": "bridge",
    "ipam": {
      "type": "whereabouts",
      "range": "10.50.20.0/24",
      "range_start": "10.50.20.70",
      "range_end": "10.50.20.199",
      "routes": [
        { "dst": "10.50.10.0/24", "gw": "10.50.20.254" }
      ]    
    }
  }'
EOF

kubectl get network-attachment-definition.k8s.cni.cncf.io -A

kubectl get network-attachment-definition.k8s.cni.cncf.io compute-a-net -n default -o yaml

4.3 DHCP:适合接入既有地址体系

DHCP 方式并不只是“把地址交给外部网络去分”。CNI 官方文档明确指出,DHCP 插件依赖后台 daemon,因为租约需要续租。换句话说,它不是一次性拿到地址就结束,而是要持续维护租约状态。因此,DHCP 更适合以下场景:

  • 现有网络体系本来就依赖 DHCP
  • 希望 Pod 的附加网络融入既有地址管理体系
  • 能接受额外维护 DHCP daemon

它不是最省事的方案,但在某些接入现网的场景里反而最自然。

**1.确认 dhcp二进制在哪
# 路径写进变量**
for p in /opt/cni/bin/dhcp /usr/lib/cni/dhcp; do
  [ -x "$p" ] && echo "dhcp_bin=$p" && DHCP_BIN="$p" && break
done
[ -n "$DHCP_BIN" ] || { echo "ERROR: dhcp binary not found"; exit 1; }

**2.确认有flock**
command -v flock && flock --version | head -n 1 || echo "NO flock"

**3.写入tmpfiles规则**
#**保证重启后自动有/run/cni目录**
sudo tee /etc/tmpfiles.d/cni-dhcp.conf >/dev/null <<'EOF'
d /run/cni 0755 root root -
EOF

#检查
sudo cat /etc/tmpfiles.d/cni-dhcp.conf

**4.写systemd 服务文件**
**# 使用$DHCP_BIN**
sudo tee /etc/systemd/system/cni-dhcp-daemon.service >/dev/null <<EOF
[Unit]
Description=CNI DHCP Daemon (provides /run/cni/dhcp.sock)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStartPre=/usr/bin/install -d -m 0755 /run/cni
ExecStart=/usr/bin/flock -n /run/cni/dhcp-daemon.lock ${DHCP_BIN} daemon
Restart=always
RestartSec=2
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target
EOF

# 检查
grep -nE 'ExecStart=|ExecStartPre=' /etc/systemd/system/cni-dhcp-daemon.service
8:ExecStartPre=/usr/bin/install -d -m 0755 /run/cni
9:ExecStart=/usr/bin/flock -n /run/cni/dhcp-daemon.lock /opt/cni/bin/dhcp daemon

**5.加载 systemd 配置、设置开机自启并启动服务**
sudo systemctl daemon-reload
sudo systemctl enable --now cni-dhcp-daemon

# 检查
sudo systemctl is-enabled cni-dhcp-daemon
enabled

sudo systemctl is-active cni-dhcp-daemon
active

sudo ss -xlpn | grep '/run/cni/dhcp.sock'
u_str LISTEN 0      4096      /run/cni/dhcp.sock 73184466            * 0    users:(("dhcp",pid=195537,fd=4))   
                                                                  /run/cni/dhcp.sock 73184466            * 0    users:(("dhcp",pid=195537,fd=4))    
6.**最终确认“单实例锁”生效
# 防止重复启动**
sudo /usr/bin/flock -n /run/cni/dhcp-daemon.lock -c 'echo lock-acquired; sleep 1' && echo OK || echo "LOCK-BLOCKED (expected)"

5. 创建多网口 Pod 并验证

有了默认网络、Multus 和 NAD 之后,下一步就是把这些网络真正挂到 Pod 上。这里的重点不是 Pod 能不能启动,而是:

  • Pod 是否真的多出了 net1net2
  • 这些接口的地址是否来自预期地址池
  • 路由是否进入了预期网络平面
  • 实际流量是否真的走到了目标物理口
# 创建位于指定节点的POD1 
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: multi-net-pod-4
  namespace: demo
  annotations:
    k8s.v1.cni.cncf.io/networks: '[
      { "name": "compute-a-net", "namespace": "nad" },
      { "name": "compute-b-net", "namespace": "nad" }
    ]'
spec:
  nodeName: gpu-worker-4
  containers:
  - name: tester
    image: x.x.x.x:xxxx/linux/alpine:3.19
    command: ["/bin/sh", "-c", "sleep 36000"]
EOF

# 创建位于指定节点的POD2
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: multi-net-pod-5
  namespace: demo
  annotations:
    k8s.v1.cni.cncf.io/networks: '[
      { "name": "compute-a-net", "namespace": "nad" },
      { "name": "compute-b-net", "namespace": "nad" }
    ]'
spec:
  nodeName: gpu-worker-5
  containers:
  - name: tester
    image: x.x.x.x:xxxx/linux/alpine:3.19
    command: ["/bin/sh", "-c", "sleep 36000"]
EOF
# 查看POD中的网络接口
kubectl exec -it multi-net-pod-4 -n demo -- ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    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@if38: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue state UP qlen 1000
    link/ether ee:24:c7:90:4e:2a brd ff:ff:ff:ff:ff:ff
    inet 192.168.3.9/24 brd 192.168.3.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::ec24:c7ff:fe90:4e2a/64 scope link 
       valid_lft forever preferred_lft forever
3: net1@net2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue state UP qlen 1000
    link/ether ba:ab:09:8f:43:7d brd ff:ff:ff:ff:ff:ff
    inet 10.8.10.200/24 brd 10.8.10.255 scope global net1
       valid_lft forever preferred_lft forever
    inet6 fe80::b8ab:9ff:fe8f:437d/64 scope link 
       valid_lft forever preferred_lft forever
4: net2@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 9000 qdisc noqueue state UP qlen 1000
    link/ether 2e:13:0f:e7:d5:22 brd ff:ff:ff:ff:ff:ff
    inet 10.8.15.200/24 brd 10.8.15.255 scope global net2
       valid_lft forever preferred_lft forever
    inet6 fe80::2c13:fff:fee7:d522/64 scope link 
       valid_lft forever preferred_lft forever
       
       
kubectl exec -it multi-net-pod-5 -n demo -- ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    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@if76: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue state UP qlen 1000
    link/ether 5e:92:17:05:e5:06 brd ff:ff:ff:ff:ff:ff
    inet 192.168.4.16/24 brd 192.168.4.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::5c92:17ff:fe05:e506/64 scope link 
       valid_lft forever preferred_lft forever
3: net1@net2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue state UP qlen 1000
    link/ether be:14:82:ec:5f:4b brd ff:ff:ff:ff:ff:ff
    inet 10.8.10.201/24 brd 10.8.10.255 scope global net1
       valid_lft forever preferred_lft forever
    inet6 fe80::bc14:82ff:feec:5f4b/64 scope link 
       valid_lft forever preferred_lft forever
4: net2@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 9000 qdisc noqueue state UP qlen 1000
    link/ether 02:ed:75:54:9b:4a brd ff:ff:ff:ff:ff:ff
    inet 10.8.15.201/24 brd 10.8.15.255 scope global net2
       valid_lft forever preferred_lft forever
    inet6 fe80::ed:75ff:fe54:9b4a/64 scope link 
       valid_lft forever preferred_lft forever       

# 验证网络通信       
kubectl exec -it multi-net-pod-4 -n demo -- ping 10.8.10.5
PING 10.8.10.5 (10.8.10.5): 56 data bytes
64 bytes from 10.8.10.5: seq=0 ttl=63 time=0.435 ms
64 bytes from 10.8.10.5: seq=1 ttl=63 time=0.150 ms

kubectl exec -it multi-net-pod-5 -n default -- ping 10.8.10.200
PING 10.8.10.200 (10.8.10.200): 56 data bytes
64 bytes from 10.8.10.200: seq=0 ttl=64 time=0.405 ms
64 bytes from 10.8.10.200: seq=1 ttl=64 time=0.170 ms

kubectl exec -it multi-net-pod-4 -n demo -- ip route get 10.8.20.1
10.8.20.1 via 10.8.10.254 dev net1  src 10.8.10.200 

kubectl exec -it multi-net-pod-5 -n demo -- ip route get 10.8.20.1
10.8.20.1 via 192.168.4.1 dev eth0  src 192.168.4.16 

6. 生产环境里的注意点

在 thick plugin、Whereabouts、频繁创建/销毁多网 Pod 并发存在的情况下,默认资源限制可能偏小,导致网络组件反复重启。尤其是在日志较多、地址分配频繁变动的场景下,Multus 和 Whereabouts 都应该适当上调资源限制,避免因为 OOM 导致网络能力本身变得不稳定,这里一。

# 报错
kubectl -n kube-system get pod $POD -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\\n"}'
OOMKilled

kubectl -n kube-system get pod $POD -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}{"\\n"}'
137

#1 例如修改multus组件的资源大小 

kubectl -n kube-system patch ds kube-multus-ds --type='json' -p='[
  {"op":"replace","path":"/spec/template/spec/containers/0/resources/limits/memory","value":"256Mi"},
  {"op":"replace","path":"/spec/template/spec/containers/0/resources/requests/memory","value":"128Mi"}
]'

#2 例如修改**whereabouts组件的资源大小** 
 kubectl -n kube-system edit ds whereabouts-whereabouts-chart
    
    - name: whereabouts-chart
      image: 10.8.17.100:60066/whereabouts/whereabouts:v0.9.2
      resources:
        requests:
          cpu: "50m"
          memory: "128Mi"
        limits:
          cpu: "200m"
          memory: "256Mi"

7. 小结

对于 AI 算力平台的最佳实践来说,管理面、默认业务面以及额外的数据面网络通常会被显式拆开,并由 Pod 以多网口方式同时接入。

而在具体落地上,真正决定附加网络行为的,不只是 Multus 本身,还包括底层网络插件和 IPAM 模型


Leave a Reply