CNI介面很簡單,特別一些新手一定要克服恐懼心裡,和我一探究竟,本文結合原理與實踐,認真讀下來一定會對原理理解非常透徹。
環境介紹
我們安裝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實現
既然這麼簡單,那麼就可以去看看實現了:
//cmdAdd 負責建立網路
func cmdAdd(args *skel.CmdArgs) error
//入引數都已經寫到這裡面了,前面的引數從環境變數讀取的,CNI配置從stdin讀取的
type CmdArgs struct {
ContainerID string
Netns string
IfName string
Args string
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)
}
}
總結
CNI介面層面是非常簡單的,所以更多的就是在CNI本身的實現了,懂了上文這些就可以自己去實現一個CNI了,是不是很酷,也會讓大家更熟悉網路以更從容的姿態排查網路問題了。
掃碼關注sealyun
探討可加QQ群:98488045