02.28 擴展 Kubernetes 之 CNI

簡介

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 處於什麼位置

擴展 Kubernetes 之 CNI

image

CNI/CNI Plugins

CNI 項目包括兩個部分

  1. 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
  2. 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 的假設

  1. Container runtime 先為 container 創建 network 再調用 plugins.
  2. Container runtime 決定 container 屬於哪個網絡,繼而決定執行哪個 CNI plugin.
  3. Network configuration 為 JSON 格式,包含一些必選可選參數.
  4. 創建時 container runtime 串行添加 container 到各個 network (ADD).
  5. 銷燬時 container runtime 逆序將 container 從各個 network 移除 (DEL).
  6. 單個 container 的 ADD/DEL 操作串行,但是多個 container 之間可以併發.
  7. ADD 之後必有 DEL,多次 DEL 操作冪等.
  8. ADD 操作不會執行兩次(對於同樣的 KEY-network name, CNI_CONTAINERID, CNI_IFNAME)

CNI 的執行流程

  1. 基本操作: ADD, DEL, CHECK and VERSION
  2. Plugins 是二進制,當需要 network 操作時,runtime 執行二進制對應 命令
  3. 通過 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
  4. 通過 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 流程:

  1. setupBridge: brdige 組件創建一個指定名字的網橋,如果網橋已經存在,就使用已有的網橋, promiscMode 打開時開啟混雜模式, 這一步關心的參數為 MTU, PromiscMode, Vlan
  2. setupVeth: 在容器空間創建 vethpair,將 node 端的 veth 設備連接到網橋上
  3. 如果由 ipam 配置:從ipam獲取一個給容器使用的 ip,並根據返回的數據計算出容器對應的網關
  4. 進入容器網絡名字空間,修改容器中網卡名和網卡ip,以及配置路由,並進行 arp 廣播(注意我們只為vethpair的容器端配置ip,node端是沒有ip的)
  5. 如果IsGW=true,將網橋配置為網關,具體方法是:將第三步計算得到的網關IP配置到網橋上,同時根據需要將網橋上其他ip刪除。最後開啟網橋的ip_forward內核參數;
  6. 如果IPMasq=true,使用iptables增加容器私有網網段到外部網段的masquerade規則,這樣容器內部訪問外部網絡時會進行snat,在很多情況下配置了這條路由後容器內部才能訪問外網。(這裡代碼中會做exist檢查,防止生成重複的iptables規則)
  7. 配置結束,整理當前網橋的信息,並返回給調用者

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

  1. 在機器上準備 cni 配置和 網橋 (這一步實踐也可以在 cni plugin 中 ensure) brctl addbr cni0;ip link set cni0 up; ip addr add <bridge-ip>/24 dev cni0/<bridge-ip>
  2. 安裝 nmap 和 jq
  3. 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


分享到:


相關文章: