2、Kubernetes容器网络
1)、Docker网络原理
Docker默认使用的网络模型是bridge,这里只讲bridge网络模型
1)容器之间通信原理
当安装完docker之后,docker会在宿主机上创建一个名叫docker0的网桥,默认IP是172.17.0.1/16
[root@aliyun ~]# ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255inet6 fe80::42:3dff:fe43:6d70 prefixlen 64 scopeid 0x20<link>ether 02:42:3d:43:6d:70 txqueuelen 0 (Ethernet)RX packets 4472 bytes 254744 (248.7 KiB)RX errors 0 dropped 0 overruns 0 frame 0TX packets 5080 bytes 8741120 (8.3 MiB)TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500inet 172.19.216.110 netmask 255.255.240.0 broadcast 172.19.223.255inet6 fe80::216:3eff:fe2d:4ebc prefixlen 64 scopeid 0x20<link>ether 00:16:3e:2d:4e:bc txqueuelen 1000 (Ethernet)RX packets 156055 bytes 223972092 (213.5 MiB)RX errors 0 dropped 0 overruns 0 frame 0TX packets 56749 bytes 5515146 (5.2 MiB)TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536inet 127.0.0.1 netmask 255.0.0.0inet6 ::1 prefixlen 128 scopeid 0x10<host>loop txqueuelen 1000 (Local Loopback)RX packets 0 bytes 0 (0.0 B)RX errors 0 dropped 0 overruns 0 frame 0TX packets 0 bytes 0 (0.0 B)TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
凡是连接在docker0网桥上的容器就可以通过它来进行通信,而容器则通过veth pair连接到docker0网桥
nginx-1容器的路由规则如下:
root@f5e7b5b6b908:/# route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default 172.17.0.1 0.0.0.0 UG 0 0 0 eth0 # 默认路由
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0 # 所有对172.17.0.0/16网段的请求也会被交给eth0来处理
当在nginx-1容器里访问nginx-2容器的IP地址(比如ping 172.17.0.3)的时候,流程如下:
- 目的IP地址172.17.0.3匹配到nginx-1容器里的第二条路由规则,这条路由规则的网关(Gateway)是0.0.0.0,这是一条直连规则,即:凡是匹配到这条规则的IP包,应该经过本机的eth0网卡,通过二层网络直接发往目的主机。而要通过二层网络设备到达nginx-2容器,就需要有172.17.0.3这个IP地址对应的MAC地址。所以nginx-1容器的网络协议栈通过eth0网卡发送一个ARP广播,来通过IP地址查找对应的MAC地址
- nginx-1容器的这个eth0网卡是一个veth pair,它的一端在这个nginx-1容器的Network Namespace里,而另一端则位于宿主机上(Host Namespace),并且被插在了宿主机的docker0网桥上。在收到ARP请求之后,docker0网桥就会扮演二层交换机的角色,把ARP广播到其他被插在docker0上的虚拟网卡。这样,同样连接在docker0上的nginx-2容器的网络协议栈就会收到这个ARP请求,从而将172.17.0.3所对应的MAC地址回复给nginx-1容器,有了目的MAC地址,nginx-1容器的eth0网卡就可以将数据包发出去
- 数据包发后,这个数据包就直接流入到了docker0网桥里。docker0继续扮演二层交换机的角色,根据数据包的目的MAC地址(也就是nginx-2容器的MAC地址)在它的CAM表(即交换机通过MAC地址学习维护的端口和MAC地址的对应表)里查到对应的端口(Port),然后把数据包发往这个端口。这个端口就是nginx-2容器插在docker0网桥上的另一块虚拟网卡,也是一个veth pair设备。这样,数据包就进入到了nginx-2容器的Network Namespace里
- nginx-2容器从自己的eth0网卡上看到了流入的数据包,nginx-2的网络协议栈就会对请求进行处理,最后将响应(Pong)返回到nginx-1
2)容器访问外网
在nginx-1容器中ping www.bing.com
是能ping通的
root@f5e7b5b6b908:/# ping www.bing.com
PING china.bing123.com (202.89.233.101): 56 data bytes
64 bytes from 202.89.233.101: icmp_seq=0 ttl=114 time=28.361 ms
64 bytes from 202.89.233.101: icmp_seq=1 ttl=114 time=28.323 ms
nginx-1位于docker0这个私有bridge网络中(172.17.0.0/16),当nginx-1从容器向外ping时,数据包是怎样到达www.bing.com
的呢?
这里的关键就是NAT,宿主机上的iptables规则如下:
[root@aliyun ~]# iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE # 如果docker0网桥收到来自172.17.0.0/16网段外出包,把它交给MASQUERADE处理
-A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A POSTROUTING -s 172.17.0.3/32 -d 172.17.0.3/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 9001 -j DNAT --to-destination 172.17.0.2:80
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 9002 -j DNAT --to-destination 172.17.0.3:80
在NAT表中有这么一条规则:-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
其含义就是:如果docker0网桥收到来自172.17.0.0/16网段外出包,把它交给MASQUERADE处理,而MASQUERADE的处理方式是将包的源地址替换成host的网址发送出去,做了一次源地址转换(SNAT)
- nginx-1发送ping包:172.17.0.2 -> 202.89.233.101(
www.bing.com
对应的IP) - docker0收到包,发现是发送到外网的,交给NAT处理
- NAT将源地址换成eth0的IP:172.19.216.110 -> 202.89.233.101
- ping从eth0出去,到达
www.bing.com
2)、Flannel
Flannel支持三种backend:UDP、VXLAN、host-gw。目前VXLAN是官方最推崇的一种backend实现方式;host-gw一般用于对网络性能要求比较高的场景,但需要基础网络架构的支持;UDP则用于测试及一些比较老的不支持VXLAN的Linux内核
1)UDP
UDP模式是Flannel项目最早支持的一种方式,却也是性能最差的一种方式,这个模式目前已被弃用
在这个例子中,有两台宿主机:
- 宿主机Node1的IP地址是172.19.216.114,上面有一个容器A,它的IP地址是10.244.1.2,对应的cni0网桥的地址是10.244.1.1/24
- 宿主机Node2的IP地址是172.19.216.115,上面有一个容器B,它的IP地址是10.244.2.3,对应的cni0网桥的地址是10.244.2.1/24
UDP模式下容器A访问容器B的整个过程如下:
-
容器A里的进程发起的IP包,其源地址是10.244.1.2,目的地址是10.244.2.3
容器A路由表如下:
# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.244.1.1 0.0.0.0 UG 0 0 0 eth0 10.244.0.0 10.244.1.1 255.255.0.0 UG 0 0 0 eth0 10.244.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
匹配到10.244.0.0/16对应的这条路由规则,应该将IP包发送到网关10.244.1.1(cni0网桥),通过容器的网关进入cni0网桥从而出现在宿主机上
-
这时候,这个IP包的下一个目的地,就取决于宿主机上的路由规则了。此时,Flannel已经在宿主机上创建出了一系列的路由规则,Node1路由表如下:
[root@k8s-node1 ~]# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 172.19.223.253 0.0.0.0 UG 100 0 0 eth0 10.244.0.0 0.0.0.0 255.255.0.0 U 0 0 0 flannel0 10.244.1.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0 172.19.208.0 0.0.0.0 255.255.240.0 U 100 0 0 eth0
由于我们的IP包目的地址是10.244.2.3,它匹配不到本地cni0网桥对应的10.244.1.0/24网段,只能匹配到10.244.0.0/16对应的这条路由规则,从而进入一个叫作flannel0的设备中
flannel0为tun设备,tun设备是一种工作在三层的虚拟网络设备,它的功能就是在操作系统内核和用户应用程序之间传递IP包
以flannel0设备为例:像上面提到的情况,当操作系统将一个IP包发送给flannel0设备之后,flannel0就会把这个IP包交给创建这个设备的应用程序,也就是Flannel进程。这是一个从内核向用户态的流动方向
反之,如果Flannel进程向flannel0设备发送了一个IP包,那么这个IP包就会出现在宿主机网路栈中,然后根据宿主机的路由表进行下一步处理。这是一个从用户态向内核态的流动方向
-
当IP包从容器经过cni0出现在宿主机,然后又根据路由表进入flannel0设备后,宿主机上的flanneld进程(Flannel项目在每个宿主机上的主进程)就会收到这个IP包。然后,flanneld看到了这个IP包的目的地址是10.244.2.3,就把它发送给了Node2宿主机
-
flanneld又是如何知道这个IP地址对应的容器,是运行在Node2上的呢?
在由Flannel管理的容器网络里,一台宿主机上的所有容器都属于该宿主机被分配的一个子网。在我们的例子中,Node1的子网是10.244.1.0/24(cni0网桥的地址范围),容器A的IP地址是10.244.1.2;Node2的子网是10.244.2.0/24,容器B的IP地址是10.244.2.3
这些子网与宿主机的对应关系保存在Etcd中:
# etcdctl ls /coreos.com/network/subnets /coreos.com/network/subnets/10.244.1.0-24 /coreos.com/network/subnets/10.244.2.0-24
所以,flanneld进程在处理由flannel0传入的IP包时,就可以根据目的IP的地址(比如10.244.2.3),匹配到对应的子网(10.244.2.0/24),从Etcd中找到这个子网对应的宿主机的IP地址是172.19.216.115
# etcdctl get /coreos.com/network/subnets/10.244.2.0-24 {"PublicIP":"172.19.216.115"}
对于flanneld来说,只要Node1和Node2是互通的,那么flanneld作为Node1上的一个普通进程,就一定可以通过上述IP地址(172.19.216.115)访问到Node2
-
flanneld在收到容器A发给容器B的IP包之后,就会把这个IP包直接封装在一个UDP包里,然后发送给Node2。这个UDP包的源地址就是flanneld所在的Node1的地址,而目的地址就是容器B所在的宿主机Node2的地址
每台宿主机上的flanneld都监听着一个8285端口,所以flanneld只要把UDP包发往Node2的8285端口即可
-
通过这样一个普通的、宿主机之间的UDP通信,一个UDP包就从Node1到达了Node2。而Node2上监听8285端口的进程也是flanneld,所以这时候,flanneld就可以从这个UDP包里解析出封装在里面的、容器A发来的原IP包
-
Node2上的flanneld会直接把这个IP包发送给它所管理的tun设备,即flannel0设备
这是一个从用户态向内核态的流动方向(Flannel进程向tun设备发送数据包),所以Linux内核网络栈就会通过本机的路由表来寻找这个IP包的下一步流向
Node2路由表如下:
[root@k8s-node2 ~]# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 172.19.223.253 0.0.0.0 UG 100 0 0 eth0 10.244.0.0 0.0.0.0 255.255.0.0 U 0 0 0 flannel0 10.244.2.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0 172.19.208.0 0.0.0.0 255.255.240.0 U 100 0 0 eth0
由于这个IP包的目的地址是10.244.2.3,匹配到10.244.2.0/24对应的这条路由规则,把这个IP包转发给cni0网桥
-
接下来cni0网桥会扮演二层交换机的角色,将数据包发送给正确的端口,进而通过veth pair设备进入容器B的Network Namespace里(和主机内容器之间通信流程相同)
而容器B返回给容器A的数据包,则会经过与上述过程完全相反地路由回到容器A中
Flannel UDP模式提供一个三层的Overlay网络,即:它首先对发出端的IP包进行UDP封装,然后在接收端进行解封装拿到原始的IP包,进而把这个IP包转发给目标容器。这就好比,Flannel在不同宿主机上的两个容器之间打通了一条隧道,使得这两个容器可以直接使用IP地址进行通信,而无需关心容器和宿主机的分布情况
上述UDP模式有严重的性能问题,所以已经被废弃了。相比于两台宿主机之间的直接通信,基于Flannel UDP模式的容器通信多了一个额外的步骤,即flanneld的处理过程。而这个过程,由于使用到了flannel0这个tun设备,仅在发出IP包的过程中,就需要经过三次用户态与内核态之间的数据拷贝,如下所示:
- 用户态的容器进程发出的IP包经过cni0网桥进入内核态
- IP包根据路由表进入flannel0(tun)设备,从而回到用户态的flanneld进程
- flanneld进行UDP封包之后重新进入内核态,将UDP包通过宿主机的eth0发出去
此外,Flannel进行UDP封装和解封装的过程,也都是在用户态完成的。在Linux操作系统中,上述这些上下文切换和用户态操作的代价其实是比较高的,这也正是造成Flannel UDP模式性能不好的主要原因
2)VXLAN
VXLAN是Linux内核本身就支持的一种网络虚拟化技术,所以说,VXLAN可以完全在内核态实现上述封装和解封装的工作,从而通过与前面相似的隧道机制,构建出覆盖网络
VXLAN的覆盖网络的设计思想是:在现有的三层网络之上,覆盖一层虚拟的、由内核VXLAN模块负责维护的二层网络,使得连接在这个VXLAN二层网络上的主机之间可以像在同一个局域网(LAN)里那样自由通信
而为了能够在二层网络上打通隧道,VXLAN会在宿主机上设置一个特殊的网络设备作为隧道的两端,这个设备叫作VTEP。VTEP的作用其实跟前面的flanneld进程非常相似。只不过,它进行封装和解封装的对象是二层数据帧;而且这个工作的执行流程,全部是在内核里完成的(因为VXLAN本身就是Linux内核里的一个模块)
还是上面这个例子,宿主机Node1(IP地址是172.19.216.114)上的容器A的IP地址是10.244.1.2,要访问宿主机Node2(IP地址是172.19.216.115)上的容器B的IP地址是10.244.2.3
VXLAN模式下容器A访问容器B的整个过程如下:
-
与前面UDP模式的流程类似,当容器A发出请求之后,这个目的地址是10.244.2.3的IP包,通过容器的网关进入cni0网桥,然后被路由到本机flannel.1设备进行处理。也就是说,来到了隧道的入口。为了方便叙述,接下来会把这个IP包称为原始IP包
-
当Node2启动并加入Flannel网络之后,在Node1以及所有其他节点上,flanneld就会添加一条如下所示的路由规则:
[root@k8s-node1 ~]# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface ... 10.244.2.0 10.244.2.0 255.255.255.0 UG 0 0 0 flannel.1
这条规则的意思是:凡是发往10.244.2.0/24网段的IP包,都需要经过flannel.1设备发出,并且,它最后被发往的网关地址是:10.244.2.0,也就是Node2上的VTEP设备(也就是flannel.1设备)的IP地址。为了方便叙述,接下来会把Node1和Node2上的flannel.1设备分别称为源VTEP设备和目的VTEP设备
这些VTEP设备之间,就需要想办法组成一个虚拟的二层网络,即:通过二层数据帧进行通信。所以在我们的例子中,源VTEP设备收到原始IP包后,就要想办法把原始IP包加上一个目的MAC地址,封装成一个二层数据帧,然后发送给目的VTEP设备
这里需要解决的问题就是:目的VTEP设备的MAC地址是什么?此时,根据前面的路由记录,我们已经知道了目的VTEP设备的IP地址。而要根据三层IP地址查询对应的二层MAC地址,这正是ARP表的功能
而这里要用到的ARP记录,也是flanneld进程在Node2节点启动时,自动添加在Node1上的
[root@k8s-node1 ~]# ip neigh show dev flannel.1 10.244.2.0 lladdr 82:9e:ca:29:46:96 PERMANENT 10.244.0.0 lladdr 16:76:50:b4:c3:a5 PERMANENT
IP地址10.244.2.0对应的MAC地址是82:9e:ca:29:46:96
最新版本的Flannel并不依赖L3 MISS事件和ARP学习,而会在每台节点启动时把它的VTEP设备对应的ARP记录,直接下放到其他每台宿主机上
-
有了目的VTEP设备的MAC地址,Linux内核就可以开始二层封包工作了。这个二层帧的格式,如下所示:
可以看到,Linux内核会把目的VTEP设备的MAC地址,填写在图中的Inner Ethernet Header字段,得到一个二层数据帧。上述封包过程只是加一个二层头,不会改变原始IP包的内容。所以图中的Inner IP Header字段,依然是容器B的IP地址,即:10.244.2.3
但是,上面提到的这些VTEP设备的MAC地址,对于宿主机网络来说并没有什么实际意义。所以上面封装出来的这个数据帧,并不能在宿主机二层网络里传输。为了方便叙述,接下来把它称为内部数据帧
所以接下来,Linux内核还需要再把内部数据帧进一步封装成为宿主机网络里的一个普通的数据帧,好让它载着内部数据帧通过宿主机的eth0网卡进行传输。把这次要封装出来的、宿主机对应的数据帧称为外部数据帧
为了实现这个搭便车的机制,Linux内核会在内部数据帧前面,加上一个特殊的VXLAN头,用来表示这个乘客实际上是一个VXLAN要使用的数据帧。而在这个VXLAN头里有一个重要的标志叫作VNI,它是VTEP设备识别某个数据帧是不是应该归自己处理的重要标识。而在Flannel中,VNI的默认值是1,这也是为何,宿主机上的VTEP设备都叫作flannel.1的原因,这里的1其实就是VNI的值
然后,Linux内核会把这个数据帧封装进一个UDP包里发出去。跟UDP模式类似,在宿主机看来,它会以为自己的flannel.1设备只是在向另一台宿主机的flannel.1设备,发起一次普通的UDP链接
-
一个flannel.1设备只知道另一端的flannel.1设备的MAC地址,却不知道对应的宿主机地址是什么。也就是说,这个UDP包应该发给哪台宿主机呢?
在这种场景下,flannel.1设备实际上要扮演一个网桥的角色,在二层网络进行UDP包的转发。而在Linux内核里面,网桥设备进行转发的依据来自于一个叫作FDB的转发数据库
这个flannel.1网桥对应的FDB信息,也是flanneld进程负责维护的,可以通过
bridge fdb
命令查看到,如下所示:[root@k8s-node1 ~]# bridge fdb show flannel.1 | grep 82:9e:ca:29:46:96 82:9e:ca:29:46:96 dev flannel.1 dst 172.19.216.115 self permanent
在上面这条FDB记录里,指定了这样一条规则:发往目的VTEP设备(MAC地址是82:9e:ca:29:46:96)的二层数据帧,应该通过flannel.1设备,发往IP地址为172.19.216.115的主机。这台主机正是Node2,UDP包要发往的目的地就找到了
所以接下来的流程就是一个正常的、宿主机网络上的封包工作
-
UDP包是一个四层数据包,所以Linux内核会在它前面加一个IP头(Outer IP Header),组成一个IP包。并且,在这个IP头里,会填上通过FDB查询出来的目的主机的IP地址,即Node2的IP地址172.19.216.115
然后,Linux内核再在这个IP包前面加上二层数据帧头(Outer Ethernet Header),并把Node2的MAC地址填进去。这个MAC地址本身,是Node1的ARP表要学习的内容,无需Flannel维护。这时候,封装出来的外部数据帧的格式,如下图所示:
-
接下来,Node1上的flannel.1设备就可以把这个数据帧从Node1的eth0网卡发出来。这个帧经过宿主机网络来到Node2的eth0网卡
这时候,Node2的内核网络栈会发现是这个数据帧里有VXLAN Header,并且VNI=1。所以Linux内核会对它进行拆包,拿到里面的内部数据帧,然后根据VNI的值,把它交给Node2上的flannel.1设备
-
而flannel.1设备则会进一步拆包,取出原始IP包。接下来和主机内容器之间通信流程相同,最终,IP包进入到了容器B的Network Namespace里
3)host-gw
Flannel的host-gw模式是一种纯三层网络方案,它的工作原理如下图所示:
还是上面这个例子,宿主机Node1(IP地址是172.19.216.114)上的容器A的IP地址是10.244.1.2,要访问宿主机Node2(IP地址是172.19.216.115)上的容器B的IP地址是10.244.2.3
当设置Flannel使用host-gw模式之后,flanneld会在宿主机上创建这样一条规则,以Node1为例:
[root@k8s-node1 ~]# route -n
...
10.244.2.0 172.19.216.115 255.255.255.0 UG 0 0 0 eth0
这条路由规则的含义是:目的IP地址属于10.244.2.0/24网段的IP包。应该经过本机的eth0设备发出去;并且,它的下一跳(next-hop)是172.19.216.115
所谓下一跳地址就是:如果IP包从主机A发到主机B,需要经过路由设备X的中转。那么X的IP地址就应该配置为主机A的下一跳地址
一旦配置了下一跳地址,那么接下来,当IP包从网络层进入链路层封装成帧的时候,eth0设备就会使用下一跳地址对应的MAC地址,作为该数据帧的目的MAC地址。显然,这个MAC地址正是Node2的MAC地址
这样,这个数据帧就会从Node1通过宿主机的二层网络顺利到达Node2上
而Node2的内核网络栈从二层数据帧里拿到IP包后,会看到这个IP包的目的IP地址是10.244.2.3,即容器B的IP地址。这时候,根据Node2上的路由表,目的地址会匹配到10.244.2.0对应的路由规则,从而进入cni0网桥,进而进入到容器B中
host-gw模式的工作原理其实就是将每个Flannel子网的下一跳设置成了该子网对应的宿主机的IP地址。这个主机会充当这条容器通信路径里的网关,这也正是host-gw的含义
Flannel子网和主机的信息都是保存在Etcd当中的。flanneld只需要WATCH这些数据的变化,然后实时更新路由表即可
而在这种模式下,容器通信的过程就免除了额外的封包和解包带来的性能损耗
host-gw模式能够正常工作的核心,就在于IP包在封装成帧发出去的时候,会使用路由表的下一跳设置目的MAC地址。这样,它就会经过二层网络到达目的宿主机。所以说,Flannel host-gw模式必须要求集群宿主机之间是二层连通的
宿主机之间二层不连通的情况也是广泛存在的。比如,宿主机分布在了不同的子网(VLAN)里。但是,在一个Kubernetes集群里,宿主机之间必须可以通过IP地址进行通信,也就是说至少是三层可达的。三层可达也可以通过为几个子网设置三层转发来实现
3)、Calico
1)三层网络方案
Calico和Flannel的host-gw模式都是纯三层网络方案,不同的是Calico使用BGP来自动地在整个集群中分发路由信息,Calico由三个部分组成:
- Calico的CNI插件:这是Calico与Kubernetes对接的部分
- Felix:它是一个DaemonSet,负责在宿主机上插入路由规则(即:写入Linux内核的FIB转发信息库),以及维护Calico所需的网络设备等工作
- BIRD:它就是BGP的客户端,专门负责在集群中分发路由规则信息
Calico工作原理如下图:
上图中,有两台宿主机:
- 宿主机Node1的IP地址是172.19.83.134,上面有一个容器A(IP地址是10.244.36.65)和容器B(IP地址是10.244.36.66),容器网段是10.244.36.64/26
- 宿主机Node2的IP地址是172.19.83.135,上面有一个容器C(IP地址是10.244.169.129)和容器D(IP地址是10.244.169.130),容器网段是10.244.169.128/26
容器A访问容器C的整个过程如下:
-
除了对路由信息的维护方式之外,Calico与Flannel的host-gw模式的另一个不同之处,就是它不会在宿主机上创建任何网桥设备
Calico的CNI插件会为每个容器设置一个veth pair设备,然后把其中的一端放置在宿主机上(它的名字以cali前缀开头)
有了veth pair设备之后,容器A发出的IP包(目的地址IP地址是10.244.169.129)就会经过veth pair设备出现在宿主机上。然后,宿主机网络栈就会根据路由规则的下一跳IP地址,把它们转发给正确的网关。接下来的流程就跟Flannel host-gw模式完全一致了
-
这里最核心的下一跳路由规则,就是由Calico的Felix进程负责维护的。这些路由规则信息则是通过BGP Client也就是BIRD组件,使用BGP协议传输而来的
Node2中的BIRD组件通过BGP协议传输的消息,可以简单地理解为如下格式:
[BGP消息] 我是宿主机172.19.83.135 10.244.169.128/26网段的容器都在我这里 这些容器的下一跳地址是我
Node1的BIRD收到传输来的BGP消息,Felix会在宿主机上创建这样一条规则
[root@k8s-node1 ~]# ip route ... 10.244.169.128/26 via 172.19.83.135 dev eth0 proto bird
这条路由规则的含义是:目的IP地址属于10.244.169.128/26网段的IP包。应该经过本机的eth0设备发出去;并且,它的下一跳(next-hop)是172.19.83.135
-
一旦配置了下一跳地址,那么接下来,当IP包从网络层进入链路层封装成帧的时候,eth0设备就会使用下一跳地址对应的MAC地址,作为该数据帧的目的MAC地址。显然,这个MAC地址正是Node2的MAC地址
这样,这个数据帧就会从Node1通过宿主机的二层网络顺利到达Node2上
-
由于Calico没有使用CNI的网桥模式,Calico的CNI插件还需要在宿主机上为每个容器的veth pair设备配置一条路由规则,用于接收传入的IP包。宿主机Node2上容器C对应的路由规则,如下所示:
[root@k8s-node2 ~]# ip route ... 10.244.169.129 dev calibd1829b7599 scope link
即:发往10.244.169.129的IP包,应该进入calibd1829b7599设备
而Node2的内核网络栈从二层数据帧里拿到IP包后,会看到这个IP包的目的IP地址是10.244.169.129,匹配到上述路由表规则进入calibd1829b7599设备,从而进入容器C
Calico项目实际上将集群中的所有节点,都当作是边界路由器来处理,它们一起组成了一个全连通的网络,互相之间通过BGP协议交换路由规则。这些节点,我们称为BGP Peer
Calico维护的网络在默认配置下,是一个被称为Node-to-Node Mesh的模式。这时候,每台宿主机上的BGP Client都需要跟其他所有节点的BGP Client进行通信以便交换路由信息。但是,随着节点数量N的增加,这些连接的数量就会以 N 2 N^2 N2的规模快速增长,从而给集群本身的网络带来巨大的压力
所以,Node-to-Node Mesh模式一般推荐用在少于100个节点的集群里。而在更大规模的集群中,需要用到的是一个叫做Route Reflector的模式
在这种模式下,Calico会指定一个或几个专门的节点,来负责跟所有节点建立BGP连接从而学习到全局的路由规则。而其他节点,只需要跟这几个专门的节点交换路由信息,就可以获得整个集群的路由规则信息了
这些专门的节点就是所谓的Route Reflector节点,它们实际上扮演了中间代理的角色,从而把BGP连接的规模控制在N的数量级上
2)IPIP模式
Calico和Flannel的host-gw模式最主要的限制就是要求集群宿主机之间是二层连通的
举个例子,假设有两台处于不同子网的宿主机Node1和Node2,对应的IP地址分别是192.168.1.2和192.168.2.2。需要注意的是,这两台机器通过路由器实现了三层转发,所以这两个IP地址之间是可以相互通信的
现在的需求还是容器A要访问容器C
按照前面的讲述,Calico会尝试在Node1上添加如下所示的一条路由规则:
[root@k8s-node1 ~]# ip route
...
10.244.169.128/26 via 192.168.2.2 dev eth0 proto bird
但是,这时候问题就来了。上面这条规则里的下一跳地址是192.168.2.2,可是它对应的Node2跟Node1却根本不在一个子网里,没办法通过二层网络把IP包发送到下一跳地址
在这种情况下,就需要使用Calico的IPIP模式,Calico的IPIP模式工作原理如下图:
在Calico的IPIP模式下,Felix进程在Node1上添加的路由规则,会稍微不同,如下所示:
[root@k8s-node1 ~]# ip route
...
10.244.169.128/26 via 192.168.2.2 dev tunl0 proto bird onlink
可以看到,尽管这条规则的下一跳地址仍然是Node2的IP地址,但是要负责将IP包发出去的设备,变成了tunl0
Calico使用的这个tunl0设备,是一个IP隧道(IP tunnel)设备
在上面的例子中,IP包进入IP隧道设备之后,就会被Linux内核的IPIP驱动接管。IPIP驱动会将这个IP包直接封装在一个宿主机网络的IP包中,如下所示:
经过封装后的新的IP包的目的地址(Outer IP Header部分),正是原IP包的下一跳地址,即Node2的IP地址:192.168.2.2。而IP包本身,则会被直接封装成新IP包的Payload
这样,原先从容器到Node2的IP包,就被伪装成了一个从Node1到Node2的IP包
由于宿主机之间已经使用路由器配置了三层转发,也就是设置了宿主机之间的下一跳。所以这个IP包在离开Node1之后,就可以经过路由器,最终到达Node2
这时,Node2的网络内核栈会使用IPIP驱动进行解包,从而拿到原始的IP包。然后,原始IP包就会经过路由规则和veth pair设备到达目的容器内部
当Calico使用IPIP模式的时候,集群的网络性能会因为额外的封包和解包工作而下降。在实际测试中,Calico IPIP模式与Flannel VXLAN模式的性能大致相当
推荐阅读:
Docker网络原理
Kubernetes容器网络(一):Flannel网络原理
Kubernetes容器网络(二):Calico网络原理
Kubernetes容器网络(三):容器跨主机Overlay网络、路由模式实验