根据张磊的《深入剖析kubernetes》中可以了解到k8s CNI模式的主要工作流程,加上近期老版本测试环境发现的flannel ip分配冲突问题,个人觉得有必要整理一下CNI工作原理以及Flannel IP 分配流程,供需要的朋友在出现类似问题时进行问题定位和原理理解。
本文不会特别详细的回顾docker网络基础和k8s网络基础,因此适合在实际中使用过docker和k8s的看官阅读~
众所周知,k8s通过cni网络模型来规定集群内网络实现的基本流程和模式,使用各种网络插件来完成具体的容器网络配置,从而完成集群内部的网络打通(本人近似的理解为Java中的模板模式,cni定义了流程和模板)
而Flannel是k8s中最容易上手的一个网络组件,并且其代码就直接位于k8s的仓库中,所以这就很容易理解,为何我们安装完k8s之后,在 /opt/cni/bin
下面就可以找到flannel的二进制文件
本部分根据张磊对CNI模型的讲述做了一定的归纳和总结。
从Docker网络模型出发,实现一个物理机上所有Docker容器的网络互通通常是基于一个虚拟网桥设备docker0
,这个docker0设备既充当网桥,同时又是一个具有ip的网关设备,做为Docker容器的网关。
而通过 route -n
命令,也可以看到原生的docker0会有一条默认的路由规则,从而打通容器和外界之间的网络。
而k8s中的网络插件真正要做的事情,也是通过某种方法,将所有的容器通过特殊设备联通(可以是网桥,可以是路由规则等等,根据不同插件实现而异),从而达到容器跨主机通信的目的。
需要注意的是,在使用flannel时,一旦使用了k8s创建Pod,那么会产生一个CNI网桥来接管所有CNI插件负责的Pod,而我们单纯使用docker run命令创建的容器,还是由docker0负责。
张磊提到:k8s之所以不直接使用docker0的原因,因为是k8s项目认为Docker网络模型(CMM)不够优秀,所以其不想采用,所以也就不想去也不能够直接配置Docker网桥;再一个就是CNI模型与k8s如何配置 infra容器的Network Namespace有关
在此明确一下
/opt/cni/bin/flannel
这是个二进制文件,是CNI网络插件的一种,同样的还有calico,weave
等,这些flannel二进制文件负责配置Infra容器的网络栈,并将其插入到cni0网桥而
flannel
本身还应该有一个名为flanneld
的进程,这个进程可能会根据我们使用的不同backend,来进行一些操作,比如创建VETP设备等
所以,CNI的设计思想,就是k8s在启动infra容器之后,就可以直接调用CNI网络插件,为这个Infra容器的Network Namespace配置符合预期的网络栈。
使用过kubeadm安装k8s集群的朋友应该了解,有一个步骤是安装kubernetes-cni
这个包,其完成了在宿主机安装CNI插件所需的可执行文件。
$ ls -al /opt/cni/bin/
total 73088
-rwxr-xr-x 1 root root 3890407 Aug 17 2017 bridge
-rwxr-xr-x 1 root root 9921982 Aug 17 2017 dhcp
-rwxr-xr-x 1 root root 2814104 Aug 17 2017 flannel
-rwxr-xr-x 1 root root 2991965 Aug 17 2017 host-local
-rwxr-xr-x 1 root root 3475802 Aug 17 2017 ipvlan
-rwxr-xr-x 1 root root 3026388 Aug 17 2017 loopback
-rwxr-xr-x 1 root root 3520724 Aug 17 2017 macvlan
-rwxr-xr-x 1 root root 3470464 Aug 17 2017 portmap
-rwxr-xr-x 1 root root 3877986 Aug 17 2017 ptp
-rwxr-xr-x 1 root root 2605279 Aug 17 2017 sample
-rwxr-xr-x 1 root root 2808402 Aug 17 2017 tuning
-rwxr-xr-x 1 root root 3475750 Aug 17 2017 vlan
这些 CNI 的基础可执行文件,按照功能可以分为三类:
而我们安装flanneld这个进程后,其启动后会在每个宿主机上生成对应的CNI配置文件(同时也是一个ConfigMap存在ETCD中),从而让k8s可以进行读取。
$ cat /etc/cni/net.d/10-flannel.conflist
{
"name": "cbr0",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
下面贴出来我们使用过的一个flannel的清单文件,结合这个配置文件进行理解:
---
kind: ConfigMap
apiVersion: v1
metadata:
name: kube-flannel-cfg
namespace: kube-system
labels:
tier: node
app: flannel
data:
cni-conf.json: |
{
"name": "cbr0",
"ipMasq": false,
"plugins": [
{
"type": "flannel",
"ipMasq": false,
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
net-conf.json: |
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "host-gw"
}
}
---
apiVersion: extensions/v1beta1
kind: DaemonSet
spec:
spec:
initContainers:
- name: install-cni
image: quay.io/coreos/flannel:v0.10.0-amd64
command:
- cp
args:
- -f
- /etc/kube-flannel/cni-conf.json
- /etc/cni/net.d/10-flannel.conflist
volumeMounts:
- name: cni
mountPath: /etc/cni/net.d
- name: flannel-cfg
mountPath: /etc/kube-flannel/
containers:
- name: kube-flannel
image: quay.io/coreos/flannel:v0.10.0-amd64
command:
- /opt/bin/flanneld
args:
- --ip-masq=false
- --kube-subnet-mgr
- --iface=ens192
volumeMounts:
- mountPath: /etc/localtime
name: flannel-server-time
- name: run
mountPath: /run
- name: flannel-cfg
mountPath: /etc/kube-flannel/
volumes:
- name: flannel-server-time
hostPath:
path: /etc/localtime
- name: run
hostPath:
path: /run
- name: cni
hostPath:
path: /etc/cni/net.d
- name: flannel-cfg
configMap:
name: kube-flannel-cfg
可以看出,flannel 清单中创建了一个configMap,其内包含两个json文件,其中cni-conf.json,就是上图的/etc/cni/net/10-flannel.conflist**
这个configmap会被flannel的init-container挂载,挂载完毕后会执行cp命令到/etc/cni/net下面。
下面是kubelet如何通过CRI(容器运行时接口)来完成网络处理的一个简单流程:
/etc/cni/net.d/10-flannel.conflist
,这个文件flannel的init-container从configMap中拷贝到pod内部的存储中的。而kubelet创建pod,最终完成网络配置的详细流程,张磊在讲述这一部分的时候较为详细,但是不太好懂,我个人总结了一下:
kubelet需要创建pod的时候,肯定优先创建Infra容器,所以这一步,dockershim 调用Docker API创建一个Infra容器,然后执行SetUpPod方法,为CNI插件准备参数,并且调用CNI插件完成配置网络栈,这里以flannel为例,则调用的肯定就是 /opt/cni/bin/flannel
为CNI插件准备的参数分为两部分,一部分是一组CNI环境变量,用于定义当前的操作(ADD或者DEL,也就是添加一个veth pair或者拆除一个veth pair),根据这两个操作类型,flannel则会对应的实现两个方法,从而实现ADD和DEL流程;另一部分则是从上文中提到的 CNI配置文件中拿到的一些配置信息(被flannel从configMap挂载到了 宿主机的 /etc/cni/net.d/10-flannel.conflist
),组装成json格式的Network COnfiguration
通过 stdin 传递给 flannel 插件
所以有了这两部分参数,flannel插件就可以实现具体的网络栈配置了;但是我们可以发现,在Flannel 的 CNI 配置文件( /etc/cni/net.d/10-flannel.conflist)里有这么一个字段,叫作 delegate
...
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
Delegate 字段的意思是,这个 CNI 插件并不会自己做事儿,而是会调用 Delegate 指定的某种 CNI 内置插件来完成。对于 Flannel 来说,它调用的 Delegate 插件,就是前面介绍到的 CNI bridge 插件**。所以说,dockershim 对 Flannel CNI 插件的调用,其实就是走了个过场。Flannel CNI 插件唯一需要做的,就是对 dockershim 传来的 Network Configuration 进行补充**。比如,将 Delegate 的 Type 字段设置为 bridge,将 Delegate 的 IPAM 字段设置为 host-local 等。经过 Flannel CNI 插件补充后的、完整的 Delegate 字段如下所示
{
"hairpinMode":true,
"ipMasq":false,
"ipam":{
"routes":[
{
"dst":"10.244.0.0/16"
}
],
"subnet":"10.244.1.0/24",
"type":"host-local"
},
"isDefaultGateway":true,
"isGateway":true,
"mtu":1410,
"name":"cbr0",
"type":"bridge"
}
上面提到过,CNI中会预置一些插件,比如bridge,比如host-local等,而flannel完成的事情,则是调用这些插件,来完成将pod插入到网桥,并且给pod分配ip;而每个宿主机都会有不同的ip地址段,这个配置信息则是flanneld在宿主机生成的一个配置文件/run/flannel/subnet.env
,而集群内所有主机的地址段都存在etcd中,所以每个宿主机都可以有不同的ip段
最后,flannel通过代理bridge完成将容器插入的CNI网络,如果没有CNI网桥,则会创建一个,这也是为什么如果集群为空的时候,其实宿主机看不到cni0
网桥,创建了第一个pod后才会出现;而上面hairpinMode
参数意思是 发夹模式
,默认情况下,网桥设备不允许一个数据包从一个端口进来,再从一个端口出去,开启了这个模式则是取消这个限制,因为我们要允许在 NAT模式下,容器自己访问自己:比如将宿主机的8080端口映射到容器的80端口,完全可能在容器内访问宿主机的8080端口来访问自己,这样就成了报文从容器出去,又会原路返回,所以要打开这个限制。
至此,其实flannel为容器分配ip的流程算是大体理清楚了
之前踩过一个坑,由于在删除节点时删除了一些flannel 保存的ip历史数据,导致flannel再次分配ip时会发生ip冲突
阅读了一位台湾博主的博客后,对其进行了摘抄和整理,由于时间关系,繁体字这里就不做转换了,不影响观看~
承接张磊讲的cni流程,现扩展一下 flannel的进阶原理:
结论是:
执行命令验证:
$ kubectl describe nodes | grep PodCIDR
PodCIDR: 10.244.0.0/24
PodCIDR: 10.244.1.0/24
PodCIDR: 10.244.2.0/24
$ sudo cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.0.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
$ sudo ls /var/lib/cni/networks/cbr0
10.244.0.2 10.244.0.3 10.244.0.8 last_reserved_ip.0 lock
$ sudo cat /var/lib/cni/networks/cbr0/10.244.0.8
2d39d5afb81e56314a7fd6bdd57c9ccf6d02c32b556273cfb6b9bb8a248c851b
$ sudo docker ps --no-trunc | grep $(sudo cat /var/lib/cni/networks/cbr0/10.244.0.8)
2d39d5afb81e56314a7fd6bdd57c9ccf6d02c32b556273cfb6b9bb8a248c851b k8s.gcr.io/pause:3.1 "/pause" Up 4 hours k8s_POD_k8s-udpserver-6576555bcb-7h8jh_default_87196597-ccda-4643-ac5d-85343a3b6c90_0
透過 kubectl describer node 可以觀察到每個節點上都有一個 PodCIDR 的欄位,代表的是該節點可以使用的網段
由於我的節點是對應到的 PodCIDR 是 10.244.0.0/24,接下來去觀察 /run/flannel/subnet.env,確認裡面的數值一致。
接下來由於我的系統上有跑過一些 Pod,這些Pod 形成的過程中會呼叫 flannel CNI 來處理,而該 CNI 最後會再輾轉呼叫 host-loacl IPAM CNI 來處理,所以就會在這邊看到有 host-local 的產物
由於前篇介紹 IPAM 的文章有介紹過 host-local 的運作,該檔案的內容則是對應的 **CONTAINER_ID**
,因此這邊得到的也是 CONTAINER_ID
最後則是透過 docker 指令去尋該 CONTAINER_ID,最後就看到對應到的不是真正運行的 Pod,而是先前介紹過的 Infrastructure Contaienr: Pause
當我們透過 –pod-network-cidr 去初始化 kubeadm** 後,其創造出來的 controller-manager 就會自帶三個參數
這邊就標明的整個 cluster network 會使用的網段,除了 cidr 大網段之外還透過 node-cide–mask 去標示寫網段,所以根據上述的範例,這個節點的數量不能超過255台節點,不然就沒有足够的 可用網段去分配了。
此外很有趣的一點是,這邊的逻辑在 controller-managfer 代码中叫做 nodeipam,也就是kubernetes 自己跳下來负责做 IPAM 的工作,幫忙分配 IP/Subnet,只是單位是以 Node 為基準,不是以 Pod。
也就是说 k8s 自己先预先给 每个node 分配 pod cidr的一个subnet
最终Controller-Manager 会通过一定逻辑,产生一个CidrSet的 结构体
这个结构体描述的信息,就跟subnet文件中所有参数基本一致
type CidrSet struct {
sync.Mutex
clusterCIDR *net.IPNet
clusterIP net.IP
clusterMaskSize int
maxCIDRs int
nextCandidate int
used big.Int
subNetMaskSize int
}
4、一旦當 host-local 處理結束後,就會再 /var/run/cni/cbr0/networks 看到一系列由 host-local 所維護的正在使用 IP 清單。
3、最终调用 ipam-hostLocal时,传了一个字典进去,里面定义了所有host-local所用到的参数,flannel最後其實是產生一個使用 bridge 作為主體 CNI 且 IPAM 使用 host-local 的設定檔案
。
补充完毕delegate字段后,调用其他的代理插件,也印证了张磊说的flannel只是个过客
所以,flanneld这个Pod完成的工作就是从 k8s的api中读取podCidr信息,并写入**/run/flannel/subnet.env**
而flannel的代码分为两部分存放:
1、CoreOS-Pod (flanneld的代码)
2、ContainetNetworking-CNI(flannel CNI的代码)
所以这也解释了为什么k8s一旦被装好,/opt/cni/bin下就有一个flannel的二进制文件了,因为代码本身就在k8s的repo中