簡介
CNI 是什麼
- CNI: Container Network Interface 定義的是 將 container 插入 network, 和 將 container 從 network 移除的操作
- CNI Plugin: 實現了 CNI 的二進制程序, 區別於 runtime, 調用方式為 runtime 調用 CNI plugin
- Container = Linux network namespace, 可能是一個 pod 即多個 container 對應了一個 network namespace
- Network 指 a group of entities that are uniquely addressable that can communicate amongst each other, Containers 可以加到一個或者多個 network 裡
CNI Plugin 處於什麼位置
image
CNI/CNI Plugins
CNI 項目包括兩個部分
- The CNI specification documents 即 上面說的 CNI 的部分libcni, 實現 CNI runtime 的 SDK,比如 kubernetes 裡面的 NetworkPlugin 部分就使用了 libcni 來調用 CNI plugins. 這裡面也有一些 interface,容易混淆,這個 interface 是對於 runtime 而言的,並不是對 plugin 的約束,比如 AddNetworkList, runtime 調用這個方法後,會按順序執行對應 plugin 的 add 命令.值得一提的是 libcni 裡面的 config caching:解決的是如果 ADD 之後配置變化了,如何 DEL 的問題.skel provides skeleton code for a CNI plugin, 實現 CNI plugin 的骨架代碼cnitool 一個小工具,可以模擬 runtime 執行比如 libcni 的 AddNetworkList,觸發執行 cni pluginsgithub.com/containernetworking/cni
- A set of reference and example plugins 即 CNI Plugin 的部分Interface plugins: ptp, bridge, macvlan,..."Chained" plugins: portmap, bandwidth, tuninggithub.com/containernetworking/plugins
CNI Plugin 對 runtime 的假設
- Container runtime 先為 container 創建 network 再調用 plugins.
- Container runtime 決定 container 屬於哪個網絡,繼而決定執行哪個 CNI plugin.
- Network configuration 為 JSON 格式,包含一些必選可選參數.
- 創建時 container runtime 串行添加 container 到各個 network (ADD).
- 銷燬時 container runtime 逆序將 container 從各個 network 移除 (DEL).
- 單個 container 的 ADD/DEL 操作串行,但是多個 container 之間可以併發.
- ADD 之後必有 DEL,多次 DEL 操作冪等.
- ADD 操作不會執行兩次(對於同樣的 KEY-network name, CNI_CONTAINERID, CNI_IFNAME)
CNI 的執行流程
- 基本操作: ADD, DEL, CHECK and VERSION
- Plugins 是二進制,當需要 network 操作時,runtime 執行二進制對應 命令
- 通過 stdin 向 plugin 輸入 JSON 格式的配置文件,以及其他 container 相關的信息 比如:ADD 操作的參數有 Container ID, Network namespace path, Network configuration, Extra arguments, Name of the interface inside the container, 返回 Interfaces list, IP configuration assigned to each interface, DNS informationDEL/CHECK 操作的參數 基本相同,具體參考 SPEC
- 通過 stdout 返回結果
輸出參數示例
<code>{ "cniVersion": "0.4.0", "interfaces": [ (this key omitted by IPAM plugins) { "name": "<name>", "mac": "", (required if L2 addresses are meaningful) "sandbox": "<netns>" (required for container/hypervisor interfaces, empty/omitted for host interfaces) } ], "ips": [ { "version": "<4-or-6>", "address": "<ip-and-prefix-in-cidr>", "gateway": "<ip-address-of-the-gateway>", (optional) "interface": <numeric> }, ... ], "routes": [ (optional) { "dst": "<ip-and-prefix-in-cidr>", "gw": "<ip-of-next-hop>" (optional) }, ... ], "dns": { (optional) "nameservers": <list-of-nameservers> (optional) "domain": <name-of-local-domain> (optional) "search": <list-of-additional-search-domains> (optional) "options": <list-of-options> (optional) }}/<list-of-options>/<list-of-additional-search-domains>/<name-of-local-domain>/<list-of-nameservers>/<ip-of-next-hop>/<ip-and-prefix-in-cidr>/<numeric>/<ip-address-of-the-gateway>/<ip-and-prefix-in-cidr>/<netns> /<name>/<code>
Network Configuration
Network Configuration 是 CNI 輸入參數中最重要當部分, 可以存儲在磁盤上
Network Configuration 示例
<code>// ------------------------------------{ "cniVersion": "0.4.0", "name": "dbnet", "type": "bridge", // type (plugin) specific "bridge": "cni0", "ipam": { "type": "host-local", // ipam specific "subnet": "10.1.0.0/16", "gateway": "10.1.0.1" }, "dns": { "nameservers": [ "10.1.0.1" ] }}// ------------------------------------{ "cniVersion": "0.4.0", "name": "pci", "type": "ovs", // type (plugin) specific "bridge": "ovs0", "vxlanID": 42, "ipam": { "type": "dhcp", "routes": [ { "dst": "10.3.0.0/16" }, { "dst": "10.4.0.0/16" } ] }, // args may be ignored by plugins "args": { "labels" : { "appVersion" : "1.0" } }}/<code>
IPAM plugin
- IPAM (IP Address Management) plugin 作為 CNI plugin 的一部分存在
- 之所以設計成兩部分是因為 Ip 分配的邏輯 在很多 CNI plugin 之間可以複用
- CNI plugin 負責調用 IPAM plugin
- IPAM plugin 完成確定 ip/subnet/gateway/route 的操作,然後返回給 main plugin
- IPAM plugin 可能通過例如 dhcp 協議分配 ip,並在本地存儲相關信息
- IPAM plugin 和 CNI plugin 輸入參數相同,返回參數見下面的示例
<code>{ "cniVersion": "0.4.0", "ips": [ { "version": "<4-or-6>", "address": "<ip-and-prefix-in-cidr>", "gateway": "<ip-address-of-the-gateway>" (optional) }, ... ], "routes": [ (optional) { "dst": "<ip-and-prefix-in-cidr>", "gw": "<ip-of-next-hop>" (optional) }, ... ] "dns": { (optional) "nameservers": <list-of-nameservers> (optional) "domain": <name-of-local-domain> (optional) "search": <list-of-search-domains> (optional) "options": <list-of-options> (optional) }}/<list-of-options>/<list-of-search-domains>/<name-of-local-domain>/<list-of-nameservers>/<ip-of-next-hop>/<ip-and-prefix-in-cidr>/<ip-address-of-the-gateway>/<ip-and-prefix-in-cidr>/<code>
CNI plugins
plugins 項目中有幾個 cni team 維護的常用 plugin, 並且進行了分類 (儘管 main plugin 可能會調用其他 plugin, 但是對於實現來講,幾種 plugin 的對外接口並無區別):
- Main: bridge, loopback, vlan, macvlan, ipvlan, host-device, ptp, Windows bridge, Windows overlay
- IPAM: host-local, DHCP, static
- Meta: bandwidth, firewall, flannel, portmap, source-based routing, tuning
常見 CNI plugins
IPAM host-local
- host-local 的應用範圍很廣: kubenet、bridge、ptp、ipvlan 等 cni 插件的 IPAM 部分常配置成由 host-local 進行處理, 比如下面這個配置.
<code>root@VM-4-10-ubuntu:/etc/cni/net.d/multus# cat bridge.conf{ "cniVersion": "0.1.0", "name": "bridge", "type": "bridge", "bridge": "cbr0", "mtu": 1500, "addIf": "eth0", "isGateway": true, "forceAddress": true, "ipMasq": false, "hairpinMode": false, "promiscMode": true, "ipam": { "type": "host-local", "subnet": "10.4.10.0/24", "gateway": "10.4.10.1", "routes": [ { "dst": "0.0.0.0/0" } ] }}/<code>
- host-local 在本地完成對 subnet 中的 ip 的分配, 這裡值得注意的是: subnet = node 的 podcidr, 這在 node_ipam_controller 中完成, 原始分配的範圍來自 ControllerManager 的配置; 即 常見的 IP 分配流程如下圖:
<code>graph TDControllerManager配置subnet --> NodeSpec中生成子subnet NodeSpec中生成子subnet --> CNIAgent在各個node上生成配置文件,寫入子subnet CNIAgent在各個node上生成配置文件,寫入子subnet --> CNIplugin在子subnet中分配Ip/<code>
MAIN bridge
brige模式,即網橋模式。在node上創建一個linux bridge,並通過 vethpair 的方式在容器中設置網卡和 IP。只要為容器配置一個二層可達的網關:比如給網橋配置IP,並設置為容器ip的網關。容器的網絡就能建立起來。
ADD 流程:
- setupBridge: brdige 組件創建一個指定名字的網橋,如果網橋已經存在,就使用已有的網橋, promiscMode 打開時開啟混雜模式, 這一步關心的參數為 MTU, PromiscMode, Vlan
- setupVeth: 在容器空間創建 vethpair,將 node 端的 veth 設備連接到網橋上
- 如果由 ipam 配置:從ipam獲取一個給容器使用的 ip,並根據返回的數據計算出容器對應的網關
- 進入容器網絡名字空間,修改容器中網卡名和網卡ip,以及配置路由,並進行 arp 廣播(注意我們只為vethpair的容器端配置ip,node端是沒有ip的)
- 如果IsGW=true,將網橋配置為網關,具體方法是:將第三步計算得到的網關IP配置到網橋上,同時根據需要將網橋上其他ip刪除。最後開啟網橋的ip_forward內核參數;
- 如果IPMasq=true,使用iptables增加容器私有網網段到外部網段的masquerade規則,這樣容器內部訪問外部網絡時會進行snat,在很多情況下配置了這條路由後容器內部才能訪問外網。(這裡代碼中會做exist檢查,防止生成重複的iptables規則)
- 配置結束,整理當前網橋的信息,並返回給調用者
MAIN host-device
本段來自
相比前面兩種cni main組件,host-device顯得十分簡單因為他就只會做兩件事情:
- 收到ADD命令時,host-device根據命令參數,將網卡移入到指定的網絡namespace(即容器中)。
- 收到DEL命令時,host-device根據命令參數,將網卡從指定的網絡namespace移出到root namespace。
在bridge和ptp組件中,就已經有“將vethpair的一端移入到容器的網絡namespace”的操作。那這個host-device不是多此一舉嗎?
並不是。host-device組件有其特定的使用場景。假設集群中的每個node上有多個網卡,其中一個網卡配置了node的IP。而其他網卡都是屬於一個網絡的,可以用來做容器的網絡,我們只需要使用host-device,將其他網卡中的某一個丟到容器裡面就行。
host-device模式的使用場景並不多。它的好處是:bridge、ptp 等方案中,node上所有容器的網絡報文都是通過node上的一塊網卡出入的,host-device方案中每個容器獨佔一個網卡,網絡流量不會經過node的網絡協議棧,隔離性更強。缺點是:在node上配置數十個網卡,可能並不好管理;另外由於不經過node上的協議棧,所以kube-proxy直接廢掉。k8s集群內的負載均衡只能另尋他法了。
META portmap
meta組件通常進行一些額外的網絡配置(tuning),或者二次調用(flannel)
portmap 的主要作用是修改防火牆 iptables 規則, 配置 SNAT,DNAT 和端口轉發
實踐
用 bash 實現一個 bridge CNI plugin
- 在機器上準備 cni 配置和 網橋 (這一步實踐也可以在 cni plugin 中 ensure) brctl addbr cni0;ip link set cni0 up; ip addr add <bridge-ip>/24 dev cni0/<bridge-ip>
- 安裝 nmap 和 jq
- bash-cni 腳本,完整腳本參考自 bash-cni
<code>#!/bin/bash -eif [[ ${DEBUG} -gt 0 ]]; then set -x; fiexec 3>&1 # make stdout available as fd 3 for the resultexec &>> /var/log/bash-cni-plugin.logIP_STORE=/tmp/reserved_ips # all reserved ips will be stored thereecho "CNI command: $CNI_COMMAND" stdin=`cat /dev/stdin`echo "stdin: $stdin"# 分配 ip,從所有 ip 中選擇沒有 reserved 的, 同時更新到 store 的reserved ip 列表allocate_ip(){for ip in "${all_ips[@]}"doreserved=falsefor reserved_ip in "${reserved_ips[@]}"doif [ "$ip" = "$reserved_ip" ]; thenreserved=truebreakfidoneif [ "$reserved" = false ] ; thenecho "$ip" >> $IP_STOREecho "$ip"returnfidone}# 實現 cni plugin 的4個命令case $CNI_COMMAND inADD)network=$(echo "$stdin" | jq -r ".network") # network 配置subnet=$(echo "$stdin" | jq -r ".subnet") # 子網配置subnet_mask_size=$(echo $subnet | awk -F "/" '{print $2}') all_ips=$(nmap -sL $subnet | grep "Nmap scan report" | awk '{print $NF}') # 所有ipall_ips=(${all_ips[@]})skip_ip=${all_ips[0]}gw_ip=${all_ips[1]}reserved_ips=$(cat $IP_STORE 2> /dev/null || printf "$skip_ip\\n$gw_ip\\n") # reserving 10.244.0.0 and 10.244.0.1 預留 ipreserved_ips=(${reserved_ips[@]})printf '%s\\n' "${reserved_ips[@]}" > $IP_STORE # 預留 ip 存儲到文件container_ip=$(allocate_ip)# ---- 以上是 ip 分配流程mkdir -p /var/run/netns/ln -sfT $CNI_NETNS /var/run/netns/$CNI_CONTAINERIDrand=$(tr -dc 'A-F0-9' < /dev/urandom | head -c4)host_if_name="veth$rand"ip link add $CNI_IFNAME type veth peer name $host_if_name ip link set $host_if_name up ip link set $host_if_name master cni0 ip link set $CNI_IFNAME netns $CNI_CONTAINERIDip netns exec $CNI_CONTAINERID ip link set $CNI_IFNAME upip netns exec $CNI_CONTAINERID ip addr add $container_ip/$subnet_mask_size dev $CNI_IFNAMEip netns exec $CNI_CONTAINERID ip route add default via $gw_ip dev $CNI_IFNAME# ------ 以上是創建 veth, 綁定到 網橋,設置 容器端的 ip, route 的過程mac=$(ip netns exec $CNI_CONTAINERID ip link show eth0 | awk '/ether/ {print $2}')echo "{ \"cniVersion\": \"0.3.1\", \"interfaces\": [ { \"name\": \"eth0\", \"mac\": \"$mac\", \"sandbox\": \"$CNI_NETNS\" } ], \"ips\": [ { \"version\": \"4\", \"address\": \"$container_ip/$subnet_mask_size\", \"gateway\": \"$gw_ip\", \"interface\": 0 } ]}" >&3;;DEL)ip=$(ip netns exec $CNI_CONTAINERID ip addr show eth0 | awk '/inet / {print $2}' | sed s%/.*%% || echo "")if [ ! -z "$ip" ]thensed -i "/$ip/d" $IP_STOREfi;;GET)echo "GET not supported"exit 1;;VERSION)echo '{ "cniVersion": "0.3.1", "supportedVersions": [ "0.3.0", "0.3.1", "0.4.0" ] }' >&3;;*) echo "Unknown cni commandn: $CNI_COMMAND" exit 1;;esac/<code>
使用 cni-tool 測試
<code>$ ip netns add testing$ CNI_PATH=/opt/cni/bin/ CNI_IFNAME=test CNI_CONTAINERID=testing cnitool add mynet /var/run/netns/testing{ "cniVersion": "0.3.1", "interfaces": [ { "name": "eth0", "sandbox": "/var/run/netns/testing" } ], "ips": [ { "version": "4", "interface": 0, "address": "10.244.149.16/24", "gateway": "10.244.149.1" } ], "dns": {}}/<code>
使用 k8s yaml 測試
<code>$ kubectl apply -f https://raw.githubusercontent.com/s-matyukevich/bash-cni-plugin/master/01_gcp/test-deployment.yml/<code>
參考
- writing-your-own-simple-cni-plug-in-with-bash
- Container Networking Interface Specification
- introduction-to-cni-the-container-network-interface-project
- 淺談K8S cni和網絡方案
- CNI - jimmysong
- https://github.com/containernetworking/cni
- https://github.com/containernetworking/plugins
閱讀更多 Echa攻城獅 的文章