Multus CNI
Pod에 네트워크 인터페이스를 여러 개 붙이는 CNI meta-plugin
Multus는 Pod에 네트워크 인터페이스를 여러 개 붙여 주는 CNI meta-plugin이다. 기존 CNI가 만들던 eth0은 그대로 두고, 그 옆에 net1, net2를 추가한다.
Intro
Kubernetes 네트워크 모델은 Pod 하나에 인터페이스 하나를 전제한다. Pod이 뜨면 CNI 플러그인이 eth0을 하나 만들어 주고, 클러스터 안의 모든 통신이 그 위로 지나간다.
하나로 충분한 데는 이유가 있다. Kubernetes는 트래픽 종류별 처리를 인터페이스가 아니라 플랫폼 계층으로 빼놨다. 외부 노출은 Service와 Ingress가 대신 받아 넘겨주고, 스토리지는 CSI가 노드에서 마운트해 볼륨으로 꽂아 주므로 그 트래픽은 Pod의 인터페이스를 지나지 않는다. 관리 접근도 SSH가 아니라 API server를 거치는 kubectl exec다. 그래서 eth0에 남는 트래픽은 사실상 애플리케이션 요청 하나고, 인터페이스도 하나면 된다.
그런데 KubeVirt로 VM을 Pod 안에 넣는 순간 이 전제가 애매해진다.
VM은 원래 NIC를 여러 장 꽂아 쓰는 물건이다. 물리 서버를 운영하던 관례가 그대로 오기 때문이다. 스토리지와 백업 트래픽이 서비스 대역폭을 침범하지 못하게 NIC 단위로 격리하고, 관리 인터페이스는 내부망에만 붙여 공격면을 줄이고, 데이터망이 포화돼도 관리망으로는 들어가 디버깅할 수 있게 경로를 나눈다.
Pod 안의 VM에서는 이 요구를 받아줄 플랫폼 대체재가 하나씩 어긋난다. guest는 자기 커널로 NFS를 마운트하므로 스토리지 트래픽이 VM의 인터페이스를 직접 지나가고, root SSH와 임의 포트 접근은 선언된 포트만 여는 Service 모델과 맞지 않는다. 그런데 주어진 것은 eth0 하나뿐이라, 성격이 다른 트래픽 전부가 그 한 줄로 수렴한다.
가장 크게 부딪히는 곳이 외부 접근이다. VM이 가진 주소가 클러스터 내부용 pod IP뿐이라, 밖에서 접근하려면 proxy와 터널과 NAT를 겹겹이 쌓게 된다. NAT를 두 번 거치면 source IP가 사라지고, 모든 트래픽이 가상 NIC 하나로 몰려 대역폭도 거기서 막힌다.
이 문제의 해답은 인터페이스를 늘리는 것이고, Kubernetes에서 그 일을 맡는 표준 도구가 Multus다.
Meta-plugin
먼저 CNI 호출이 원래 어떻게 흘러가는지 보자.
Pod이 노드에 배치되면 container runtime이 /etc/cni/net.d/에서 알파벳순으로 첫 번째 설정 파일을 읽고, 거기 적힌 CNI 플러그인을 실행한다. Calico든 Flannel이든 플러그인 하나가 호출되고, 인터페이스 하나가 생긴다.
Multus를 설치하면 이 디렉토리에 00-multus.conf가 생긴다. 알파벳순으로 가장 앞서므로, 이제 runtime은 Multus만 호출한다.
그런데 Multus는 인터페이스를 직접 만들지 않는다. 다른 CNI 플러그인을 대신 호출한다. 그래서 meta-plugin이다.
위임 대상은 두 부류다.
첫째는 default network다. Calico, Flannel 같은 기존 클러스터 CNI가 그대로 이 자리에 들어간다. Multus는 모든 Pod에 대해 이 플러그인을 먼저 호출해 eth0을 만든다. 그래서 Multus를 설치해도 기존 pod 간 통신은 아무것도 달라지지 않는다. API server, kube-dns, Service 트래픽은 전부 eth0으로 다니던 그대로다.
둘째는 additional network, 흔히 secondary network라 부르는 쪽이다. Pod이 annotation으로 요청한 경우에만 해당 플러그인을 추가로 호출하고, 결과 인터페이스는 net1, net2 순으로 붙는다.
Multus 4.0부터는 thick plugin 구조가 기본 권장이다. runtime이 호출하는 얇은 shim 바이너리가 노드마다 떠 있는 multus-daemon에 요청을 넘기고, daemon이 NAD 조회와 delegate 호출을 처리한다. 뒤에 나올 인터페이스 hot-plug도 이 daemon이 있어야 동작한다.
NetworkAttachmentDefinition
secondary network의 정의는 NetworkAttachmentDefinition(줄여서 NAD)이라는 CRD에 담는다. Network Plumbing Working Group이 표준화한 리소스로, 내용물은 CNI 설정 JSON 문자열 하나다.
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
name: macvlan-conf
spec:
config: '{
"cniVersion": "0.3.0",
"type": "macvlan",
"master": "eth0",
"mode": "bridge",
"ipam": {
"type": "host-local",
"subnet": "192.168.1.0/24",
"rangeStart": "192.168.1.200",
"rangeEnd": "192.168.1.216",
"gateway": "192.168.1.1"
}
}'
type이 실제로 호출될 CNI 플러그인이고, 나머지는 그 플러그인의 설정이다. 여기서는 host의 eth0 위에 macvlan 인터페이스를 만들고 host-local IPAM으로 주소를 준다.
Pod은 annotation으로 이 정의를 참조한다.
apiVersion: v1
kind: Pod
metadata:
name: samplepod
annotations:
k8s.v1.cni.cncf.io/networks: macvlan-conf
spec:
...
쉼표로 나열하면 여러 개를 붙일 수 있고(macvlan-conf-1, macvlan-conf-2), <namespace>/<name> 형식으로 다른 namespace의 NAD를 참조하거나 <name>@<ifname>으로 인터페이스 이름을 지정할 수도 있다.
붙은 결과는 Multus가 k8s.v1.cni.cncf.io/network-status annotation에 다시 적어 준다. 어떤 네트워크가 어떤 인터페이스로, 어떤 IP를 받아 붙었는지가 Pod 메타데이터에 남는 것이다.
k8s.v1.cni.cncf.io/network-status:
[{
"name": "cbr0",
"ips": ["10.244.1.73"],
"default": true
},{
"name": "macvlan-conf",
"interface": "net1",
"ips": ["192.168.1.205"],
"mac": "86:1d:96:ff:55:0d"
}]
기본 라우팅은 여전히 eth0이다. Pod의 default route는 default network를 향하고, secondary는 자기 서브넷 안에서만 쓰인다. 특정 attachment로 default route를 옮기고 싶으면 annotation을 JSON 형식으로 쓰고 default-route 키를 준다.
Delegate Plugins
Multus 자체는 배선공일 뿐이고, 실제 인터페이스의 성격은 delegate로 어떤 플러그인을 쓰는지가 정한다. secondary network에 자주 등장하는 플러그인은 이렇다.
| 플러그인 | 하는 일 |
|---|---|
| bridge | 노드의 linux bridge에 veth로 연결한다. bridge를 물리 NIC에 이어 두면 L2로 외부와 통한다 |
| macvlan | 물리 NIC 위에 MAC 주소가 다른 하위 인터페이스를 만든다. Pod이 물리 L2 네트워크에 직접 붙는 효과 |
| ipvlan | macvlan과 비슷하지만 모든 하위 인터페이스가 부모의 MAC을 공유한다. 스위치가 포트당 MAC 수를 제한하는 환경에서 쓴다 |
| host-device | host의 NIC 하나를 통째로 Pod의 network namespace로 옮긴다. 그동안 host에서는 그 장치가 사라진다 |
| sriov | SR-IOV VF를 설정해 Pod으로 옮긴다. 물리 NIC 한 장을 하드웨어 수준에서 여러 Pod에 나눠 주는 방식 |
몇 가지 함정도 플러그인 쪽에 있다. macvlan은 kernel 동작 특성상 host와 하위 인터페이스가 부모 NIC를 통해 직접 통신하지 못하고, ipvlan도 마찬가지로 컨테이너가 ipvlan 인터페이스로는 host에 닿지 못한다. 한 부모 NIC를 macvlan과 ipvlan이 동시에 쓸 수도 없다.
IPAM도 secondary에서는 따로 고민해야 한다. 위 예시의 host-local IPAM은 이름 그대로 노드 안에서만 할당을 기억한다. 같은 NAD를 여러 노드에서 쓰면 노드마다 같은 대역에서 IP를 나눠 주다가 충돌한다. 그래서 secondary network에는 클러스터 전체에서 할당을 추적하는 whereabouts IPAM을 흔히 쓴다. DHCP 서버 없이 지정한 range 안에서 클러스터 단위로 중복 없는 IP를 나눠 준다.
Multus with KubeVirt
KubeVirt에서 Multus가 어떻게 쓰이는지 보자. VM의 네트워크는 VMI spec의 두 곳에서 정해진다. spec.networks가 어떤 네트워크에 붙을지를, spec.domain.devices.interfaces가 그 네트워크를 guest에 어떤 방식으로 노출할지를 정한다. 둘은 이름으로 1:1 대응한다.
networks의 소스는 두 가지다. pod: {}는 클러스터 CNI가 만드는 기본 pod 네트워크이고, multus: {networkName: ...}가 NAD로 정의된 네트워크다.
apiVersion: kubevirt.io/v1
kind: VirtualMachine
spec:
template:
spec:
domain:
devices:
interfaces:
- name: default
masquerade: {}
- name: data-net
bridge: {}
networks:
- name: default
pod: {}
- name: data-net
multus:
networkName: linux-bridge-net
이 VM은 인터페이스를 두 개 갖는다. 하나는 pod 네트워크로, 하나는 NAD가 가리키는 bridge 네트워크로 연결된다.
interface 바인딩은 그 네트워크를 guest에 잇는 방식이다.
- masquerade는 Pod 안에서 NAT를 한 번 더 한다. VM은 내부 주소를 받고, 나가는 트래픽이 pod IP로 SNAT된다. pod 네트워크 전용이고, VM이 옮겨 다녀도 Service로 감싸면 접근이 유지되므로 기본 선택지다.
- bridge는 Pod의 인터페이스를 linux bridge로 VM에 직결한다. pod 네트워크에 쓰면 pod IP가 DHCP로 VM에 그대로 넘어간다. VM이 pod IP를 직접 가지므로 모든 포트가 열리지만, 그 대가로 pod 네트워크에서는 live migration이 막힌다.
- sriov는 SR-IOV VF를 vfio로 guest에 넘긴다. 다음 섹션에서 본다.
여기에도 조합 함정이 있다. bridge 바인딩은 pod 인터페이스의 MAC을 VM으로 옮기는데, macvlan과 ipvlan은 pod 인터페이스가 원래 MAC을 유지해야 동작한다. 그래서 이 둘은 KubeVirt secondary로는 bridge 바인딩과 함께 쓰지 못한다.
secondary를 넘어 아예 primary를 교체할 수도 있다. multus.default: true를 주면 NAD 네트워크가 pod 네트워크 자리를 대신한다. VM의 유일한 인터페이스가 외부에서 라우팅 가능한 네트워크에 직접 붙는, direct-IP 구성이 이것이다. proxy와 이중 NAT로 흉내 내던 외부 접근을 주소 하나로 끝내므로, 고대역폭이 필요한 환경일수록 이 그림이 낫다. 대신 이 delegate가 IP를 반드시 반환해야 하고, pod 네트워크가 주던 Service 연동은 포기한다.
실행 중인 VM에 인터페이스를 더하는 hot-plug도 가능하다. NAD를 만들고 VM spec에 인터페이스를 추가하면 되는데, Multus가 thick plugin으로 떠 있고 Multus Dynamic Networks Controller가 설치되어 있어야 한다. 없으면 live migration을 겸한 방식으로만 붙는다.
SR-IOV Path vs PF Passthrough
고성능 NIC를 VM에 주는 방법은 Multus 경로 안팎에 하나씩 있다.
Multus 경로가 SR-IOV다. 물리 NIC(PF)가 하드웨어에서 가상 기능(VF)을 여러 개 만들고, 그 VF를 Pod 단위로 나눠 준다. 부품이 셋 필요하다.
sriov-network-device-plugin이 노드의 VF들을 Kubernetes 자원으로 광고하고, 스케줄러가 그 자원을 요청한 Pod을 배치한다. Pod이 뜰 때 Multus가 할당된 VF의 PCI 주소를 sriov-cni에 넘기고, sriov-cni가 그 VF의 MAC과 VLAN을 설정한다. KubeVirt의 sriov 바인딩은 이렇게 준비된 VF를 VFIO로 guest에 넘긴다. guest kernel이 VF를 직접 구동하므로 성능은 passthrough 수준인데, 장치가 VF라서 나눠 쓸 수 있다. live migration도 된다. migration 시작 전에 VF를 자동으로 hot-unplug했다가 목적지에서 다시 붙인다.
Multus 경로 밖이 PF passthrough다. VF를 만들지 않고 물리 NIC를 hostDevices로 VM에 통째로 넘긴다. 이때 NIC는 CNI 입장에서 존재하지 않는 장치가 된다. NAD도, IPAM도, network-status도 없다.
어느 쪽을 쓰는지는 fabric이 정한다. Ethernet NIC는 SR-IOV 경로가 잘 맞는다. 반면 InfiniBand는 사정이 다르다. IB VF는 subnet manager와의 관리 트래픽을 host PF 드라이버가 중계해 줘야 하는데, VF를 vfio로 떼어 내면 이 중계가 끊겨 VF가 LID를 받지 못하고 Down 상태에 머무는 경우가 있다. 그래서 IB로 노드 간 학습 트래픽을 받는 GPU VM에서는 PF를 통째로 넘기는 쪽을 택하게 된다.
kernel 네트워크 스택을 지나는 트래픽, 즉 IP 주소와 라우팅이 필요한 경로는 Multus의 영역이다. kernel을 우회하는 RDMA fabric은 CNI 모델 바깥이고, PCI passthrough의 영역이다. 한 VM 안에서 둘은 얼마든지 공존한다.
Summary
Kubernetes CNI는 Pod당 인터페이스 하나를 만들고, Multus는 meta-plugin으로 그 호출을 가로채 default network 위에 secondary network들을 얹는다.
secondary의 정의는 NetworkAttachmentDefinition CRD에 CNI 설정으로 담기고, Pod은 annotation으로 참조한다. 실제 인터페이스의 성격은 delegate 플러그인(bridge, macvlan, ipvlan, host-device, sriov)이 정하며, 클러스터 전체 IPAM에는 whereabouts를 쓴다.
KubeVirt VM은 networks와 interfaces의 1:1 대응으로 여러 네트워크에 붙는다. pod 네트워크는 masquerade로, secondary는 bridge나 sriov 바인딩으로 잇고, multus.default: true면 primary 자체를 NAD 네트워크로 교체해 NAT 없는 direct-IP 구성이 된다.
SR-IOV VF는 device plugin, Multus, sriov-cni의 파이프라인으로 VM까지 간다. kernel을 우회하는 InfiniBand 같은 fabric은 이 경로 밖에서 PF passthrough로 처리한다.
References
- Multus CNI — README
https://github.com/k8snetworkplumbingwg/multus-cni - Multus CNI — Quickstart Guide
https://github.com/k8snetworkplumbingwg/multus-cni/blob/master/docs/quickstart.md - Multus CNI — How to Use
https://github.com/k8snetworkplumbingwg/multus-cni/blob/master/docs/how-to-use.md - Multus CNI — Thick Plugin
https://github.com/k8snetworkplumbingwg/multus-cni/blob/master/docs/thick-plugin.md - KubeVirt — Interfaces and Networks
https://kubevirt.io/user-guide/network/interfaces_and_networks/ - KubeVirt — Hotplug Network Interfaces
https://kubevirt.io/user-guide/network/hotplug/interfaces/ - CNI Plugins — macvlan / ipvlan / host-device / bridge
https://www.cni.dev/plugins/current/main/ - SR-IOV CNI
https://github.com/k8snetworkplumbingwg/sriov-cni - Whereabouts IPAM
https://github.com/k8snetworkplumbingwg/whereabouts