kubernetes CNI詳解

CNI接口很簡單,特別一些新手一定要克服恐懼心裏,和我一探究竟,本文結合原理與實踐,認真讀下來一定會對原理理解非常透徹。
<!--more-->

環境介紹

我們安裝kubernetes時先不安裝CNI. 如果使用了sealyun離線包 那麼修改下 kube/conf/master.sh

只留如下內容即可:

[root@helix105 shell]# cat master.sh 
kubeadm init --config ../conf/kubeadm.yaml
mkdir ~/.kube
cp /etc/kubernetes/admin.conf ~/.kube/config

kubectl taint nodes --all node-role.kubernetes.io/master-

清空CNI相關目錄:

rm -rf /opt/cni/bin/*
rm -rf /etc/cni/net.d/*

啓動kubernetes, 如果已經裝過那麼kubeadm reset一下:

cd kube/shell && sh init.sh && sh master.sh

此時你的節點是notready的,你的coredns也沒有辦法分配到地址:

[root@helix105 shell]# kubectl get pod -n kube-system -o wide
NAME                                            READY   STATUS    RESTARTS   AGE   IP              NODE                    NOMINATED NODE   READINESS GATES
coredns-5c98db65d4-5fh6c                        0/1     Pending   0          54s   <none>          <none>                  <none>           <none>
coredns-5c98db65d4-dbwmq                        0/1     Pending   0          54s   <none>          <none>                  <none>           <none>
kube-controller-manager-helix105.hfa.chenqian   1/1     Running   0          19s   172.16.60.105   helix105.hfa.chenqian   <none>           <none>
kube-proxy-k74ld                                1/1     Running   0          54s   172.16.60.105   helix105.hfa.chenqian   <none>           <none>
kube-scheduler-helix105.hfa.chenqian            1/1     Running   0          14s   172.16.60.105   helix105.hfa.chenqian   <none>           <none>
[root@helix105 shell]# kubectl get node
NAME                    STATUS     ROLES    AGE   VERSION
helix105.hfa.chenqian   NotReady   master   86s   v1.15.0

安裝CNI

創建CNI配置文件
$ mkdir -p /etc/cni/net.d
$ cat >/etc/cni/net.d/10-mynet.conf <<EOF
{
    "cniVersion": "0.2.0",
    "name": "mynet",
    "type": "bridge",
    "bridge": "cni0",
    "isGateway": true,
    "ipMasq": true,
    "ipam": {
        "type": "host-local",
        "subnet": "10.22.0.0/16",
        "routes": [
            { "dst": "0.0.0.0/0" }
        ]
    }
}
EOF
$ cat >/etc/cni/net.d/99-loopback.conf <<EOF
{
    "cniVersion": "0.2.0",
    "name": "lo",
    "type": "loopback"
}
EOF

這裏兩個配置一個是給容器塞一個網卡掛在網橋上的,另外一個配置負責擼(本地迴環)。。

配置完後會發現節點ready:

[root@helix105 shell]# kubectl get node
NAME                    STATUS   ROLES    AGE   VERSION
helix105.hfa.chenqian   Ready    master   15m   v1.15.0

但是coredns會一直處於ContainerCreating狀態,是因爲bin文件還沒有:

failed to find plugin "bridge" in path [/opt/cni/bin]

plugins裏實現了很多的CNI,如我們上面配置的bridge.

$ cd $GOPATH/src/github.com/containernetworking/plugins
$ ./build_linux.sh
$ cp bin/* /opt/cni/bin
$ ls bin/
bandwidth  dhcp      flannel      host-local  loopback  portmap  sbr     tuning
bridge     firewall  host-device  ipvlan      macvlan   ptp      static  vlan

這裏有很多二進制,我們學習的話不需要關注所有的,就看ptp(就簡單的創建了設備對)或者bridge

再看coredns已經能分配到地址了:

[root@helix105 plugins]# kubectl get pod -n kube-system -o wide
NAME                                            READY   STATUS    RESTARTS   AGE     IP              NODE                    NOMINATED NODE   READINESS GATES
coredns-5c98db65d4-5fh6c                        1/1     Running   0          3h10m   10.22.0.8       helix105.hfa.chenqian   <none>           <none>
coredns-5c98db65d4-dbwmq                        1/1     Running   0          3h10m   10.22.0.9

看一下網橋,cni0上掛了兩個設備,與我們上面的cni配置裏配置的一樣,type字段指定用哪個bin文件,bridge字段指定網橋名:

[root@helix105 plugins]# brctl show
bridge name    bridge id        STP enabled    interfaces
cni0        8000.8ef6ac49c2f7    no        veth1b28b06f
                                        veth1c093940

原理

爲了更好理解kubelet幹嘛了,我們可以找一個腳本來解釋 script 這個腳本也可以用來測試你的CNI:

爲了易讀,我刪除一些不重要的東西,原版腳本可以在連接中去拿

# 先創建一個容器,這裏只爲了拿到一個net namespace
contid=$(docker run -d --net=none golang:1.12.7 /bin/sleep 10000000) 
pid=$(docker inspect -f '{{ .State.Pid }}' $contid)
netnspath=/proc/$pid/ns/net # 這個我們需要

kubelet啓動pod時也是創建好容器就有了pod的network namespaces,再去把ns傳給cni 讓cni去配置

./exec-plugins.sh add $contid $netnspath # 傳入兩個參數給下一個腳本,containerid和net namespace路徑

docker run --net=container:$contid $@
NETCONFPATH=${NETCONFPATH-/etc/cni/net.d}

i=0
# 獲取容器id和網絡ns
contid=$2 
netns=$3

# 這裏設置了幾個環境變量,CNI命令行工具就可以獲取到這些參數
export CNI_COMMAND=$(echo $1 | tr '[:lower:]' '[:upper:]')
export PATH=$CNI_PATH:$PATH # 這個指定CNI bin文件的路徑
export CNI_CONTAINERID=$contid 
export CNI_NETNS=$netns

for netconf in $(echo $NETCONFPATH/10-mynet.conf | sort); do
        name=$(jq -r '.name' <$netconf)
        plugin=$(jq -r '.type' <$netconf) # CNI配置文件的type字段對應二進制程序名
        export CNI_IFNAME=$(printf eth%d $i) # 容器內網卡名

        # 這裏執行了命令行工具
        res=$($plugin <$netconf) # 這裏把CNI的配置文件通過標準輸入也傳給CNI命令行工具
        if [ $? -ne 0 ]; then
                # 把結果輸出到標準輸出,這樣kubelet就可以拿到容器地址等一些信息
                errmsg=$(echo $res | jq -r '.msg')
                if [ -z "$errmsg" ]; then
                        errmsg=$res
                fi

                echo "${name} : error executing $CNI_COMMAND: $errmsg"
                exit 1
        let "i=i+1"
done

總結一下:

         CNI配置文件
         容器ID
         網絡ns
kubelet -------------->  CNI command
   ^                        |
   |                        |
   +------------------------+
       結果標準輸出

bridge CNI實現

既然這麼簡單,那麼就可以去看看實現了:

bridge CNI代碼

//cmdAdd 負責創建網絡
func cmdAdd(args *skel.CmdArgs) error 

//入參數都已經寫到這裏面了,前面的參數從環境變量讀取的,CNI配置從stdin讀取的
type CmdArgs struct {
    ContainerID string
    Netns       string
    IfName      string
    Args        string //這個裏面攜帶一些額外參數, 如pod name等
    Path        string
    StdinData   []byte
}

所以CNI配置文件除了name type這些特定字段,你自己也可以加自己的一些字段.然後自己去解析

然後啥事都得靠自己了

//這裏創建了設備對,並掛載到cni0王橋上
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode, n.Vlan)

具體怎麼掛的就是調用了netlink 這個庫,sealos在做內核負載時同樣用了該庫。

err := netns.Do(func(hostNS ns.NetNS) error { //創建設備對
    hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
    ...
    //配置容器內的網卡名mac地址等
    contIface.Name = containerVeth.Name
    contIface.Mac = containerVeth.HardwareAddr.String()
    contIface.Sandbox = netns.Path()
    hostIface.Name = hostVeth.Name
    return nil
})
...

// 根據index找到宿主機設備對名
hostVeth, err := netlink.LinkByName(hostIface.Name)
...
hostIface.Mac = hostVeth.Attrs().HardwareAddr.String()

// 把宿主機端設備對掛給網橋
if err := netlink.LinkSetMaster(hostVeth, br); err != nil {}

// 設置hairpin mode
if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
}

// 設置vlanid
if vlanID != 0 {
    err = netlink.BridgeVlanAdd(hostVeth, uint16(vlanID), true, true, false, true)
}

return hostIface, contIface, nil

最後把結果返回:

type Result struct {
    CNIVersion string         `json:"cniVersion,omitempty"`
    Interfaces []*Interface   `json:"interfaces,omitempty"`
    IPs        []*IPConfig    `json:"ips,omitempty"`
    Routes     []*types.Route `json:"routes,omitempty"`
    DNS        types.DNS      `json:"dns,omitempty"`
}

// 這樣kubelet就收到返回信息了
func (r *Result) PrintTo(writer io.Writer) error {
    data, err := json.MarshalIndent(r, "", "    ")
    if err != nil {
        return err
    }
    _, err = writer.Write(data)
    return err
}

如:

{
  "cniVersion": "0.4.0",
  "interfaces": [                                            (this key omitted by IPAM plugins)
      {
          "name": "<name>",
          "mac": "<MAC address>",                            (required if L2 addresses are meaningful)
          "sandbox": "<netns path or hypervisor identifier>" (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 index into 'interfaces' list>
      },
      ...
  ],
  "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)
  }
}
獲取pod名稱

CNI_ARGS 環境變量存了一些額外信息, 值的格式爲:FOO=BAR;ABC=123, 比如其中就有我們挺需要的podname. K8S_POD_NAME=xxxx

總結

CNI接口層面是非常簡單的,所以更多的就是在CNI本身的實現了,懂了上文這些就可以自己去實現一個CNI了,是不是很酷,也會讓大家更熟悉網絡以更從容的姿態排查網絡問題了。

探討可加QQ羣:98488045
kubernetes一鍵HA

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章