UDP和VXLAN都有一个共性,就是用户的容器都连接到docker0网桥上,而网络插件为宿主机插件了一个特殊的设备,docekr0与这个设备之间,通过IP转发进行协作
网络插件真正做的事情,是通过方法,把不同宿主机上的特殊设备连通,从而到达容器跨主机通信的目的
Kubernetes对容器网络的主要处理方式,只不过Kubernetes是通过了一个叫做CNI的接口,维护了一个单独的网桥代替docker0,这是网桥名字叫做CNI网桥,对应宿主上的设备名称默认为cni0
我们以Flannel 的VXLAN为例,在Kubernetes环境中,工作环境类似,不过docker0网桥替换为了CNI网桥罢了
这里,我们为Flannel分配的子网范围是10.244.0.0/16 这个参数可以在部署的时候指定
$ kubeadm init –pod-network-cidr=10.244.0.0/16
或者通过kube-controller-manager的配置文件来指定
这时候,假设Infra-container-1要访问Infra-container-2,也就是Pod-1要访问Pod2,如何办呢?
IP的源地址是10.244.0.2 目的IP是10.244.1.3.
这个流程如下
这时候Infra-container-1里的eth0设备,同样是以Veth Pair的方式连接到Node1的cni0网桥上,这个IP包就会经过cni0网桥出现在宿主机上
这时候Node1上的路由表,就是如下的结构
# 在Node 1上
$ route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface … 10.244.0.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0 10.244.1.0 10.244.1.0 255.255.255.0 UG 0 0 0 flannel.1 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0 |
在宿主机上,这个IP包的IP地址是10.244.1.3,匹配的规则是第二条,就是10.244.1.0对应的路由规则
可以看到,这条规则这个相关地址的IP包交给flannel.1处理,并且,flannel.1处理完成后,将IP包转发给的Gateway,正是隧道另一端的VTEP设备,就是Node 2的flannel.1设备,和上文的Flannel的VXLAN模式一致
CNI网桥只会管理所有交由Kubernetes创建的容器Pod,如果是利用Docker启动的容器,还是会连接到docker0网桥上,所以这个容器的IP地址,一定属于docker0网桥的172.17.0.0/16网段
Kubernetes之所以要设置一个和Docker0网桥一致功能的CNI网桥,主要是关于
不希望和Docker有过多的耦合
kubernetes也不具有使用docker网络模型的能力,而且这还和Kubernetes创建Pod,Infra容器的Network Namespace密切相关
而CNI的设计思路和我们Pod中的Infra容器相关,我们的Pod必然先启动一个Infra容器,用来hold住整个Pod的Network Namespace,Kubernetes创建一个Pod的第一步,CNI的设计思想,就是在Kubernetes启动Infra容器后,直接调用CNI网络插件,为这个Infra容器的Network Namespace,配置符合预期的网络栈,这个网络栈是如何配置的呢?
在部署Kubernetes的时候,有一个步骤是安装kubernetes-cni的包,目的是在宿主机上安装CNI插件所需的基础可执行文件
安装完成后,可以在宿主机的/opt/cni/bin目录下看到对应CNI的基础文件
基础文件可以分为三类
第一类,叫做Main插件,可以用来创建具体的网络设备的二进制文件,比如bridge,ipvlan,loopback lo设备,macvlan,ptp(Veth Pair设备) 以及Vlan
我们其中的Flannle Weave等项目,都属于网桥类型的CNI插件,再具体实现中,往往调用bridge 这个二进制的文件
第二类,叫做IPAM插件,这是负责分配IP地址的二进制文件,比如DHCP,向DHCP服务器发起请求,获取自己的IP地址
第三类,是由CNI社区维护的CNI插件,比如Flannl,就是专门为Flannel项目提供的CNI插件;tuning,一个通过sysctl调整网络设备参数的二进制文件.portmap.用来通过iptables来配置端口映射的二进制文件,bandwidth,一个使用Token Bucket Filter TBF来进行限流的二进制文件
那么,我们需要实现一个容器的网路方案,需要做两件事,以Flannel为例
实现这个网络方案本身,需要编写的,就是flanneld进程中的主要逻辑,创建和配置flannel.1设备,配置宿主机的路由,ARP FDB等信息
然后实现对应的CNI插件,将Infra容器中网络栈进行配置并连接到CNI网桥上
由于Flannel对应的CNI插件已经被内置了,所以无需再单独安装,对于Weave Calico等项目,就需要在安装插件的时候,将对应的CNI插件的可执行文件放在/opt/cni/bin目录
然后flanneld启动后就会在每台宿主机上生成对应的CNI配置文件,告诉Kubernetes,使用Flannel作为容器解决方案
CNI的配置文件如下
$ cat /etc/cni/net.d/10-flannel.conflist
{ “name”: “cbr0”, “plugins”: [ { “type”: “flannel”, “delegate”: { “hairpinMode”: true, “isDefaultGateway”: true } }, { “type”: “portmap”, “capabilities”: { “portMappings”: true } } ] } |
而且Kubernetes并不会实际的去执行相关的网络配置,而是调用给容器项目去执行,利用的是CRI 容器运行时借口而,对应的CRI实现是dockershim,是Kubelet的代码实现
然后,在dockershim中,会加载上述的CNI配置文件
Kubernetes目前不支持CNI插件混用,而是在CNI配置目的(/etc/cni.net,d)防止多个CNI配置文件的话,dockershim会加载按照字母顺序的第一个插件
CNI会允许在CNI配置文件,通过plugins字段,定义多个插件进行协同操作
Flannel就指定了flannel和portmap两个插件
当Kubelet组件需要创建Pod的时候,先创建了Infra容器,这一步,dockershim会调用Docker API创建并启动Infra容器,然后执行一个叫做SetUpPod的方法,为CNI插件准备相关的参数,然后调用CNI插件为Infra容器配置网络
第一部分,是由dockershim设置一组CNI环境变量
最重要的变量叫做CNI_COMMAND,取值只有两种 ADD和DEL
这个ADD和DEL,就是CNI插件需要的接口方法
ADD是为了添加容器进入CNI网络,DEL为了将容器从CNI网络中移除
CNI的ADD中需要一些参数,容器网卡的名字 eth0(CNI_IFNAME),Pod的Network Namespace文件的路径,容器的ID等,这些参数都属于环境变量
Pod的Network Namespace文件路径,就是 /proc/<容器进程PID>/ns/net
在这个CNI环境变量中,还有一个CNI_ARGS的参数,通过这个参数,CRI实现,可以以Key-Value的格式,传递自定义信息给网络插件,这是用户来自定义CNI协议的重要实现
然后是dockershim从CNI配置文件中加载的,默认插件的配置信息
这个配置信息在CNI中叫做Network Configuration,dockershim会将Network Configuration以JSON数据的格式通过标准输入的方式传给Flannel CNI插件
有了这两个部分,CNI就可以进行ADD操作了
Flannel的CNI配置文件中有一个字段delegate
…
“delegate”: { “hairpinMode”: true, “isDefaultGateway”: true } |
Delegate字段说明了这个CNI会通过调用Delegate指定的容器内置插件完成,这个Flannel就会指定使用之前说的CNI bridge插件
所以说,dockershim对于Flannel CNI插件的调用,就是走了个过场,Flannel CNI插件唯一需要做的,就是对dockershim传来的Network Configuration进行补充,比如将Delegate的Type字段设置为bridge,将Delegate的IPAM字段设置为host-local
补充完成的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” } |
ipam字段里的信息,比如10.244.1.0/24,读取自Flannel的Fannel配置文件,即为宿主机上的/run/flannel/subnet.env文件
接下来Flannel CNI插件会调用CNI bridge 插件,执行/opt/cni/bin/bridge二进制文件
这一次,调用CNI bridge插件需要两个部分的第一部分,进行网络的配置,其中的CNI_COMMAND值为ADD,
第二个部分Network Configration,正是上面补充好的Delegate字段,Flannel CNI插件会将Delegate 字段以标准输入的方式传给CNI brige插件
而且,还会将Delegate字段以JSON文件的方式,保存在/var/lib/cni/flannel目录下,方便后续的DEL操作使用
有了这两个参数,接下来CNI bridge插件可以代表Flannel,进行将容器加入到CNI网络中这一步操作
这样,CNI bridge 插件会通过Infra容器的Newwork Namespace文件,进入到这个Network Namespace里面,创建一对Veth Pair设备
将这个Veth Pair中的一端,移动到宿主机上
#在容器里
# 创建一对Veth Pair设备。其中一个叫作eth0,另一个叫作vethb4963f3 $ ip link add eth0 type veth peer name vethb4963f3 # 启动eth0设备 $ ip link set eth0 up # 将Veth Pair设备的另一端(也就是vethb4963f3设备)放到宿主机(也就是Host Namespace)里 $ ip link set vethb4963f3 netns $HOST_NS # 通过Host Namespace,启动宿主机上的vethb4963f3设备 $ ip netns exec $HOST_NS ip link set vethb4963f3 up |
这样,vethb4963f3就在宿主机上了,这样,就是完成了容器中的eth0
这样,创建了Veth Pair设备的操作,可以在宿主机上进行执行,然后再将其放在容器的Network Namespace中一样
不过之所以反着来,是因为CNI 中对Namespace操作函数的设计
err := containerNS.Do(func(hostNS ns.NetNS) error {
…
return nil
})
这个设计其实不难理解,容器的Namespace是可以通过Namespace中拿到的,而Host Namespace,是一个隐含了上下文参数,先通过容器的Namespace进入到容器内,然后反向操作Host Namespace,比较方便
CNI bridge插件就可以将vethb4963f3设备放在CNI网桥上,就好比在宿主机上执行
# 在宿主机上
$ ip link set vethb4963f3 master cni0
将veth4963f3设备连接在CNI网桥之后,CNI bridge插件还会为其设置Hairpin Mode,在默认情况下,网桥设备是不允许一个数据包从一个端口进来在从这个端口出去的,但是可以开启Hairpin Mode,消除这个限制
这个限制经常出现在一些场景,诸如执行docker run -p 8080:80 ,这会将宿主机端口8080转为容器80端口,这个请求最终会经过docker0网桥进入到容器内
如果容器内访问宿主机的8080,那么到达了宿主机后,并且处理完成并返回了,这个包需要返回对应的网桥,通过vethb4963端口进入到容器中的,所以我们需要开启veth4963f3端口的Hairpin Mode了
接下来,CNI bridge插件会调用CNI ipam插件,为容器分配一个可用的IP地址,然后CNI bridge插件就会将这个IP地址添加到容器的eth0网卡上,设置默认路由
# 在容器里
$ ip addr add 10.244.0.2/24 dev eth0
$ ip route add default via 10.244.0.1 dev eth0
最后,CNI插件会为CNI网桥添加IP地址,相当于在宿主机上执行
ip addr add 10.244.0.1/24 dev cni0
执行完上述操作后,CNI插件会将容器的IP地址等信息返回给dockershim,然后被kubelet添加到Pod的status字段
CNI插件的ADD方法就完成了,接下来,我们就可以直接使用了
在本章中,我们讲解了Kubernetes中CNI网络的实现
所有容器使用IP地址和其他容器通信,无须使用NAT
所有宿主机可以直接使用IP地址和其他容器进行通信,无须使用NAT
容器看到的自己的IP地址和别人看到的地址是一样
那么,这样这个网络模型,就可以总结为一个字,通.
容器和容器之间要通,容器和宿主机要通,Kubernetes要求这个通,是基于容器和宿主机的IP进行的
那么为何Kubernetes不自己实现容器网络,而是通过CNI进行一个简单的假设呢?
CNCF一直遵循的开放的,交给使用者自定义的插件 原则,方便提供更多的便利选择,有了CNI,就可以自定义插件,方便应对不同的,复杂的网络环境