如果说Kubernetes这座房子是由一个个Pod堆积而成,那Kubernetes的网络就是给这座房子注入了灵魂,通过它可以驱动这座房子里的所有Pod,让其保持强大的灵活性,从而保证Kubernetes这座房子不会倒下,为了方便描述,下面统一叫k8s。
我们知道Pod是k8s的最小调度单位,而容器就是k8s里一个一个的进程,而这些进程之间又免不了要进行通信,这就是k8s网络要解决的问题。
多数情况下,我们讲到k8s的网络都不自觉会把容器考虑进来,这是因为到目前为止k8s主要还是以Docker为主的容器来管理进程。但今天我们先忘掉容器,从Linux本身提供的能力开始来一点一点搭建一个虚拟的网络世界,通过这个过程可以更深刻的理解k8s的网络底层逻辑。
下面的操作对环境没有特别的要求,但有一个前提是必须使用Linux系统,ubuntu或者CentOS都可以。主要使用两个工具集,一个是ip工具集,另一个是brctl,ip工具集一般都默认安装了,brctl可以通过apt install -y bridge-utils来安装。
首先,我们要在同一台机器上创建两个虚拟的空间,这两个空间的网络是相互隔离的,有自己的网络协议栈、网卡以及iptables,大概像下面这样:
图1-1 Network Namespace
我们思考一下,linux有什么工具可以实现这样的功能呢?答案是Network Namespace,使用ip netns就可以对Network Namespace进行管理,这里我们使用下面的命令创建两个Network Namespace。
ip netns add nts0
ip netns add nts1
通过上面的两条命令我们就创建好了两个网络空间nts0和nts1,nts0、nts1和宿主机之间是相互隔离的,为了说明这个问题,我们接着往下。
上面我们创建的两个Network Namespace想象成两台电脑,电脑与电脑之间怎么实现通信呢?没错,路由器就是这样的设备,可以收发二、三层数据包实现两台电脑之间的通信,在Linux中也有一个这样的虚拟设备,叫tun/tap设备,接下来我们创建两个tun设备tap0和tap1,命令如下:
ip tuntap add dev tap0 mode tap
ip tuntap add dev tap1 mode tap
接下来就可以将它们安装到Network Namespace里了,命令如下:
ip link tap0 netns nts0
ip link tap1 netns nts1
上面将tap0安装到了nts0,将tap1安装到了nts1中,我们可以看一下tun设备的状态,如下:
可以看到,tap0还没有分配IP地址,和lo一样,状态都是DOWN。所以我们要给tun设备分配IP地址,并将状态设置成UP,命令如下:
ip netns exec nts0 ip link set lo up
ip netns exec nts0 ip link set tap0 up
ip netns exec nts0 ip addr add 10.0.0.1/24 dev tap0
ip netns exec nts1 ip link set lo up
ip netns exec nts1 ip link set tap1 up
ip netns exec nts1 ip addr add 10.0.0.2/24 dev tap1
上面我们给tap0分配的ip地址是10.0.0.1/24,tap1分配的ip地址是10.0.0.2/24,这样,就成功创建了两个虚拟的网络空间,每个空间里都有一个tun网络设备。然后我们来测试一下隔离性,首先在宿主机上ping一下看是不是通的。命令如下:
root@ubuntu# ping 10.0.0.2
ping: connect: Network is unreachable
然后在nts0中去ping nts1中的tap1设备
root@ubuntu# ip netns exec nts0 ping 10.0.0.2
ping: connect: Network is unreachable
可以看到,相互之间是不通的。这说明宿主机、nts0、nts1之间是相互隔离的。
为了避免因为我们配置错误导致的不连通,我们可以在Network Namespace里ping一下自己看网络设备是否正常在工作,如下:
可以看到,网络设备是在正常工作的,说明我们的配置没有问题。
现在,我们要将这两个网络空间给连起来,让他们之间可以正常通信,Linux中有一个虚拟的网络设备叫做Veth Pair,它们总是成对出现,一端连接到网络协议栈,另一端彼此相连,数据发送到一端可以直接出现在另一端,你可以把它想象成一条网线。下面我们来创建一对Veth Pair, 如下:
ip link add veth0 type veth peer name veth1
veth0和veth1就是我们创建的一对Veth Pair,前面我们说Veth Pair可以成对出现,发到一端的数据可以直接出现在另一端,那是不是只要将veth0和veth1分别插到两个Network Namespace里就可以将两个网络空间给连接起来呢?我们来试一下,将Veth Pair的一端veth0插到nts0,veth1插到nts1上,命令如下:
ip link set veth0 netns nts0
ip link set veth1 netns nts1
ip netns exec nts0 ip link set veth0 up
ip netns exec nts1 ip link set veth0 up
这个时候网络结构大概是下面这样的,如下图:
图1-2 Veth Pair
然后我们在nts0里去ping nts1,如下:
root@ubuntu# ip netns exec nts0 ping 10.0.0.2
ping: connect: Network is unreachable
咦,不好使啊,怎么还是ping不通呢?别着急,我们来看一下nts0的路由信息:
可以看到,这是由于我们前面做隔离试验的时候所有请求都会发送到tap0设备,没有走veth0。所以,我们只需要将路由指向Veth Pair应该就可以了,如下:
ip netns exec nts0 ip route change 10.0.0.0/24 via 0.0.0.0 dev veth0
ip netns exec nts1 ip route change 10.0.0.0/24 via 0.0.0.0 dev veth1
设置完之后,我们再来看一下路由信息
可以看到,已经指向Veth Pair设备了,然后我们再用nts0去ping一下nts1,如下:
可以看到,是通的,然后我们用nts1去ping一下nts0,如下:
可以看到,nts0和nts1之间就连通了。
但是,这事似乎没那么简单,假如一台机器上创建了N多个这样的网络空间,而它们之间又要连起来,就得将每个空间和另外所有的空间创建一对Veth Pair将它们连起来,这也太复杂了。我们可以回想一下现实中我们的电脑在同一个局域网内相互通信,并不需要每台电脑都和其它电脑插一根网线,如果这样的话那一台电脑的网卡有多少的网线插槽都不太合适啊。那有没有一种设备可以实现这样的功能呢?没错,交换机就可以实现这个功能,所有电脑网线都插到交换机上,当两台电脑需要通信的时候,先发到交换机,然后交换机通过ip地址和mac地址的映射关系就可以找到对应的主机,将数据包发给对应的主机。在Linux中也有这样的虚拟设备,就是网桥(Bridge)。
上面的例子,我们可以创建一个网桥,将Veth Pair的一端插在网桥上,另一端插在对应的Network Namespace,这样就可以实现不同的网络空间之间的通信了。
下面,我们就通过网桥来实现不同网络空间的通信,首先,我们要先创建一个网桥,命令如下:
brctl addbr br1
ip link set br1 up
然后我们通过show来查看网桥信息,如下:
可以看到,成功创建一个网桥br1
接下来,我们再创建两对Veth Pair,如下:
ip link add veth0-ns type veth name veth0-br
ip link add veth1-ns type veth name veth1-br
接着创建两个Network Namespace,如下:
ip netns add nts0
ip netns add nts1
然后将前面创建的两对Veth Pair分别插到nts0和nts1,如下:
ip link set veth0-ns netns nts0
ip link set veth1-ns netns nts1
接着我们设置Network Namespace里的Veth Pair设备的ip地址,如下:
ip netns exec nts0 ip link set lo up
ip netns exec nts0 ip link set veth0-ns up
ip netns exec nts0 ip addr add 10.0.0.1/24 dev veth0-ns
ip netns exec nts1 ip link set lo up
ip netns exec nts1 ip link set veth1-ns up
ip netns exec nts1 ip addr add 10.0.0.2/24 dev veth1-ns
然后我们看一下Network Namespace里的Veth Pair设备的信息,如下:
可以看到,ip地址已经设置好了。
通过上面的步骤我们将Veth Pair的一端接到了Network Namespace里了,接下来我们将Veth Pair的另一端插到前面创建的网桥上面,命令如下:
brctl addif br1 veth0-br
brctl addif br1 veth1-br
插好了之后我们来确认一下是否成功了,如下:
可以看到,veth0-br和veth1-br都插到了网桥br1上了,现在的网络关系如下图:
图1-3 网桥和Veth Pair
然后我们来测试一下,在nts0中去ping nts1,如下:
接着在nts1中去ping nts0,如下:
到此,我们就通过一虚拟的网桥和两对Veth Pair将不同的Network Namespace打通了。离我们的目标又近了一步,目前k8s还是使用Docker来作为进程管理的容器。所以,搞明白Docker的网络之后再回过头来理解k8s的网络会容易很多。
容器网络
实际上容器网络和我们上面讲的几乎是一样的,本文里的容器默认指的是Docker项目,Docker启动之后会创建一个docker0的网桥,它的功能和我们上面创建网桥br1目的是一样的,如下:
当我们通过docker run一个容器的时候,这个容器会创建一对Veth Pair,一端插在容器中也就是容器中的eth0,一端插在docker0网桥上,下面我们创建一个centos的容器,命令如下:
docker run -itd --name centos-1 centos
然后我们在宿主机上查看网络设备
可以看到多了一个vethd7a90bc设备,然后我们使用brctl来查看docker0网桥 ,如下:
可以看到,刚刚创建的设备veth852a0a9被插在了docker0上,然后我们再创建一个容器,如下:
docker run -itd --name centos-2 centos
我们再看一下docker0网桥,如下:
可以看到,刚刚创建的容器的一端也被插到了docker0网桥上。
接着我们看一下容器的另一端,我们进入到容器里面,如下:
其中eth0就是Veth Pair的另一端,我们ping一下另一个容器看是否是连通的,如下:
可以看到,容器之间是连通的,到这里为止,容器之间通过一个网桥docker0和Veth Pair就可以相互通信了。
就这么完了吗?想想看,要是容器不在一台机器上又该如何进行通信呢?
很明显通过前面讲到的内容,是不能实现跨主机之间的通信的,在容器的跨主通信的方案里面,有一个绕不过的项目Flannel。
Flannel实现了一个跨主通信的网络框架,背后实际的网络功能的实现有三种,分别是UDP、VXLAN和host-gw。
UDP模式
UDP实现最为简单,有助于理解跨主通信的实现原理,但同时UDP模式性能也是最糟糕的,目前几乎已经弃用了。Flannel项目会在每个节点上启动一个flanneld的进程,并创建一个flannel网络设备,比如:
假如现在有两台机器分别是Node1和Node2,在Node1上有容器c1,ip地址10.244.0.1/24,Node2上有容器c2,ip地址10.244.2.1/24,现在当c1要访问c2时,IP数据包会发送到Veth Pair设备的一端也就是容器中的eth0设备,然后直接出现在docker0网桥(回忆一下Veth Pair的特点),docker0网桥拿到这个数据包,发现不在自己的管辖范围,将数据包交给网络协议栈,网络协议栈根据路由规则将数据包交给flannel.1设备。flannel.1设备根据路由规则将IP包发到Node2上,Node2上的网络协议栈发现数据包是发给本机的flanneld进程的,接着就将数据包交给Node2上的flannel.1设备,根据路由规则又将请求转到docker0上,后面的过程和Docker同主网络就是一样的了。
上面这个过程有几个问题,我们来一个一个看。
第一个问题,docker0网桥是如何知道要将请求交给flannel.1设备的呢?
我们看一下Node1上的路由信息,如下:
可以看到10.244.2.1这个ip地址会匹配到第三条路由,10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink,也就是转发到flannel.1这个设备。这里的路由规则是由flanneld进程自动创建的。
第二个问题,flannel.1又是如何知道要将请求转发到Node2的呢?
为了弄清这个问题,我们要先弄明白flannel.1这个设备是怎么运作的,实际上,flannel.1是一个Tunnel设备,它做的事情就是在内核和用户程序之间发送IP数据包。
上面的例子,当数据包通过默认的路由规则发送到flannel.1设备之后,flannel.1就会将IP数据包交给创建这个设备的应用程序,也就是flanneld进程(这是一个从内核态流向用户态的过程)。
当flanneld进程收到数据包的时候,就会将这个IP数据包通过宿主机的网络协议栈发送出去(这是一个用户态向内核态扭转的过程)。那flanneld是如何知道10.244.2.1这个地址在Node2上呢?假如还有Node3、Node4,该怎么区分呢?实际上,从IP地址也大概可以看出来,10.244.2.1和10.244.0.1是不在一个子网里的,而flanneld也正是利用不同的子网来寻找对应的宿主机。而子网与宿主机之间的对应关系是放在etcd里面的,如下:
$ etcdctl ls /coreos.com/network/subnets
/coreos.com/network/subnets/10.244.0.0-24
/coreos.com/network/subnets/10.244.1.0-24
/coreos.com/network/subnets/10.244.2.0-24
flanneld通过目地ip地址就可以找到对应的宿主机的地址,如下:
$ etcdctl get /coreos.com/network/subnets/10.244.2.0-24
{
"PublicIP":"10.0.12.17"
}
然后就将这个IP包封装到一个四层的UDP包里面,通过宿主机的物理网卡将请求发送到了Node2上,但是还缺少一个信息,IP地址知道了,发给哪个端口呢?Flannel在每台宿主机上都安装了flanneld,端口一般是8285。当Node2收到这个UDP的包之后,去掉UDP的头,拿到原始的IP包,这个IP包的目标地址是10.244.2.1,接着将数据包交给Node1上的flannel.1设备(也就是Tunnel设备),然后通过flanneld进程UDP数据包发送出去,发送的目的地是Node2上的8285端口,也就是Node2上的flanneld进程。同样的,在Node2上flanneld进程也配置了相应的路由,如下:
[root@node-002 ~]# ip route
default via 10.0.12.1 dev eth0
10.0.12.0/22 dev eth0 proto kernel scope link src 10.0.12.17
10.244.0.0/24 via 10.244.0.0 dev flannel.1 onlink
10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink
10.244.2.0/24 dev docker0 proto kernel scope link src 10.244.2.1
169.254.0.0/16 dev eth0 scope link metric 1002
最后会匹配到第4条路由规则,进入到docker0网桥中。后面就和前面同主的流程是一样的了。
当c2返回时,也是以同样的方式匹配默认路由,进入到flannel.1设备,然后由flanneld将数据包打包成UDP数据包发送到Node1上,接着进入到docker0网桥,然后再到Veth Pair,最终进入到容器c1中。
上面的过程要注意的是,docker0网桥的地址范围必须是Flannel为宿主机分配的子网。以Node1为例,给Docker Daemon启动时加上下面的配置就可以了:
docker --bip=10.244.0.0/24
UDP模式存在的问题
通过上面的过程,我们可以看到,在发送端数据包先从网络协议栈到flanneld进程,然后flanneld进程封包之后又会发送到网络协议栈,当收到返回的时候又要从网络协议栈将数据拷贝到flanneld进程,一进一出有3次内核态和用户态的拷贝,而我们知道内核态和用户态之间的切换是低效的。
所以,这也是为什么UDP模式被淘汰的原因。
VXLAN模式
VXLAN(Virtual Extensible LAN)虚拟可扩展局域网,是Linux内核提供的一种网络虚拟化技术,可以实现在内核态封包和解包,从而减少内核态和用户态的切换。
VXLAN是运行在二层的,核心思想是通过一条隧道在不同的宿主机之间构建出一层覆盖网络(Overlay Network)。VXLAN在各个宿主机上创建一个特殊的VTEP(VXLAN Tunnel End Point)设备作为端点。
这里的VTEP相当于前面的flanneld进程,只不过它操作的是二层数据包,而且所有工作都在内核态完成。
假设现在有两个容器,container01和container02,ip地址分别为10.244.0.1和10.244.2.1,分别运行在两台宿主机上,如果container01要访问container02,和UDP模式类似,IP包会先出现在docker0网桥,匹配不到路由,然后通过默认的路由规则,进入到本机的flannel.1设备,也就是VXLAN隧道的入口,flannel.1就是VXLAN创建出来的VTEP端点。
然后VXLAN需要找到隧道的出口,这个出口还是一条路由规则,而这个规则依然是由flanneld进程维护的。
container02的网段是10.244.2.0,所以flannel.1设备最终会将请求发往10.244.2.0这个网关,通过观察我们发现这个正是Node2上的flannel.1设备,如下:
这里将Node1上的VTEP和Node2上的VTEP设备用1-VTEP和2-VTEP表示,根据VXLAN的原理,1-VTEP和2-VTEP之间需要想办法组成一个虚拟的二层网络。我们知道二层网络是通过MAC地址来进行数据帧的发送。所以,需要加上目标MAC地址,封装成一个二层数据帧发出去。
flanneld进程启动时,会自动维护IP地址和MAC地址的对应关系,所以不需要通过ARP协议来获取MAC地址,如下:\
可以看到,10.244.2.0 对应的mac地址是46:af:53:96:f6:7a,VXLAN通过这个MAC地址就可以封装二层的数据帧了,封装完之后如下:
图1-4 二层数据帧
可以看到,原来的IP包原封不动,只是在它外层套了一个MAC地址,得到一个二层数据帧。
上面虽然得到了一个二层数据帧,但是在宿主机上仍然是没法通信,这是因为上面的二层数据帧的mac地址是flannel.1设备的,最终宿主机之间的通信是通过宿主机的物理网卡,比如eth0发出去的。
所以,要实现宿主机之间的通信,我们还需要知道目标机器的IP地址,但是我们前面说flannel.1是一个二层设备,似乎不能直接拿到IP地址。但是,假如能在二层实现转发就能解决这个问题了,在linux内核中是有这样的能力的,转发的依据,源自叫作FDB(Forwarding Database) 的转发数据库,而对应的FDB信息也是由flanneld进程维护的,如下:
可以看到上面的mac地址对应的ip地址正是Node2宿主机的ip地址。
在实际发送之前,还有一个非常重要的细节,VXLAN会在上面封装好的二层帧外面加一个VNI标识,这个是VTEP设备用来判断数据包是不是属于自己的依据,默认为1。所以,我们的VTEP设备都叫flannel.1,后面的1和VNI是对应的,这里讲的VNI也是云计算中VXLAN方案实现网络隔离的重要依据。
紧接着VXLAN会将上面封装好的数据依次套上UDP头、IP头、MAC地址,然后将数据包通过宿主机的物理网卡发送出去,最终数据包格式如下:\
图1-5 VXLAN数据包格式
可以看到,这个数据包还是有点复杂的,这也是为什么VXLAN的效率比host-gw低的原因(后面会讲host-gw),至于为什么加上IP头和MAC地址,这是网络协议栈规定的, 数据包在发送端的网络协议栈总是依次加上四层头、IP头、MAC地址,接收端网络协议栈总是依次剥掉MAC地址、IP头、四层头,然后交给应用程序。
当数据包来到Node2,本机的网络协议栈会去掉MAC地址、IP头、UDP头,发现VXLAN Header里的VNI=1,然后根据VNI的值把数据包交给Node2上的flannel.1设备。
flannel.1设备拿到数据包之后,剥掉MAC地址,取出IP包,接下来就和前面容器网络的流程是一样的了。
其实,到这里为止,k8s的网络所需要具备的各个组件都齐活了,但整个看起来总还差那么点意思。
CNI网络模型
到目前为止,同一个主机之间的通信还是依赖docker0网桥,而这个网桥是必不可少的,这就相当于各个子网之间的通信,可以简单把一台宿主机想象成一个小的局域网,这个局域网里的各个设备之间的通信就需要网桥,各个局域网之间的通信需要借助Flannel项目(后面讲的host-gw可以打破这个规则)。
我们知道k8s里面的网络是到pod这个级别,也就是说不管pod里有多少个容器,它们都是共享所属pod的网络协议栈。
k8s在创建pod的时候总是先创建一个infra的容器来hold住Network Namespace,同时kubelet进程的一个子任务dockershim会去配置infra网络。很明显这个操作通过docker0网桥是非常不灵活的,所以k8s又搞了一个CNI网桥,这个设备在物理机上一般叫cni0。
cni0网桥的作用和docker0几乎是一模一样。只是k8s在响应pod不同的事件的时候可以实时的去更新cni0网络配置,比如pod启动的时候,kubelet的dockershim进程就会进行add操作,给infra容器配上对应的IP地址,而这个IP地址是怎么来的呢?我们通过kubeadm部署k8s的时候可以指定网络CIDR,这样就划定了IP地址的范围,比如:
kubeadm init --pod-network-cidr=10.244.0.0/16
CNI实际上是一整套工具集,如下:
CNI的网络配置可以分为两个部分,第一,CNI通过flanneld进程来创建flannel.1设备、配置路由、维护FDB数据库。第二,dockershim使用CNI插件配置infra容器的网络栈,并连接到CNI网桥。
Kubernetes的三层网络方案
前面讲了Flannel的UDP和VXLAN模式,还剩最后一种host-gw模式。host-gw模式相比VXLAN要简单很多,如下图:
图1-6 Flannel的host-gw模式
Flannel的host-gw模式通过一条条路由规则来实现跨主机容器之间的通信,比如container01(10.244.0.2)要和container02(10.244.2.2)通信,container01所在机器会有下面这样一条路由规则:
ip route
...
10.244.2.0/24 via 10.0.12.17 dev eth0
其中,10.0.12.17是container02所在宿主机的IP地址,有了IP地址就可以通过ARP协议获取到MAC地址了,实际上,这里的MAC地址也是由flanneld进程自动维护的。
有了MAC地址之后,就可以在IP包的基础上套上目标MAC地址,在上面的例子中就是container02所在宿主机的MAC地址,得到一个二层的数据帧发送出去。
到了Node2之后,网络协议栈去掉MAC地址,拿到IP包,发现目标IP是10.244.2.2,根据路由规则就可以转发到container02中了。
可以看到,host-gw其实就是将下一跳的目地地址设置成网关,这也是为什么叫作host-gw的原因了。而各个宿主机内的子网(比如各个pod中infra容器的ip)都是保存在Etcd中的,flanneld进程只要watch对应的key,就可以实时更新自己的路由表。
要注意,Flannal的host-gw模式只能适用于二层网络连通的集群,这是由它的实现原理决定的,也就是通过路由表里的下一跳设置目标MAC地址,然后经过二层网络到达目标宿主机。
不知道你有没有通过云主机来搭建k8s的经历,各个云主机之间很大概率是不在同一个子网,这个时候二层是不通的(无法通过mac地址直接通信)。这种情况在Flannel的host-gw模式中,可以通过配置3层的路由转发来实现。但很明显,由于多了三层的封包与解包的过程性能是比较差的。
在host-gw模式中还有一个强大的网络方案Calico,Calico的实现原理和Flannel 的host-gw几乎是一样的,也是在每个宿主机上维护对应的路由表。唯一不一样的是,Calico使用的是一个叫BGP的内核模块来维护路由信息,BGP(Border Gateway Protocol)边缘网关协议,如下:
图1-7 BGP原理
每个宿主机上BGP都会维护一份全量的路由表。
比如,AS1的10.10.0.3要访问AS2里的172.17.0.3,请求会先经过AS1中的Router1,Router1里发现目地地址172.17.0.3要经过C口,而C口正是发往Router2的,这样就从AS1到达了AS2中。
AS2拿到数据包之后,发现目标地址是172.17.0.3,然后就通过B口把数据包发出去,这样就到达了172.17.0.3上面了。
因为反过来172.17.0.3访问10.10.0.3也要能够通信,所以Router2里也要维护对应的路由信息,所以,这也是为什么BGP需要在每个节点维护所有的路由信息。
而BGP维护路由的原理也很简单,在每个宿主机上都有一个BGP的进程,将本机的路由信息广播到所有其它所有节点。可以预见,如果一个集群里面有成千上万的节点,这个效率是很低的,路由信息是呈指数增长的。
针对上面的性能问题,BGP有一个Route Reflector模式,其原理是选出几个节点出来,这几个节点从其它节点学习所有路由信息,其它节点只需要和这几个节点进行同步就可以了。
我们回过头来看Calico的实现原理。Calico项目由三个部分组成:
- Calico的CNI插件,这个CNI其实就是上面讲的那个CNI
- Felix,是一个DaemonSet,负责维护Calico需要的网络设备(比如Veth Pair)以及路由规则的管理。
- BIRD,这个是BGP的客户端,负责分发路由信息。
Colico的运行模式如下:
图1-8 Calico原理
可以看到Calico和Flannel的host-gw另一个不一样的地方就是Calico不会创建网桥设备,它为每个容器创建一对Veth Pair,一端插在容器的eth0,另一端插在宿主机,以cali打头。而容器之间的通信全部都过路由规则。
这样,容器里发的数据包通过Veth Pair会直接出现在宿主机cali开头的设备中,宿主机网络协议栈根据路由规则将数据包发送到网关。后面的步骤和Flannel 的host-gw几乎是一样的了。
Calico默认情况下也是不支持不同子网之间的通信的,但其提供了IPIP模式可以实现不同子网之间的通信,如下:
图1-9 Calico的IPIP模式
可以看到,IPIP模式下,多了一个tunl0设备,要注意这里的tunl0和上面说的tun设备完全不是一个东西,不要搞混了。
开启IPIP模式之后的路由规则如下:
10.244.0.0/24 via 10.0.12.17 tunl0
可以看到,现在虽然网关的IP地址还是10.0.12.17,但对应的设备是tunl0,这个tunl0是一个IP隧道设备(IP tunnel),当IP数据包进入到Linux内核之后,内核的IPIP驱动会接管,然后在IP数据包的外层套上宿主机的IP头,如下:
图1-10 CalicoIPIP封包原理
其中,“宿主机IP头”中的目标IP地址就是10.0.12.17。这样就可以伪装成宿主机的IP地址将包发出去,而这个包里实际是容器发出去的一个完整的IP数据包。
当目标宿主收到这个包之后,内核的IPIP将外层的IP包解出来,通过本机的路由规则就可以找到对应的容器了。
好了,到这里为止Kubernetes的网络原理就讲完了。
小结
计算机发展到现在,已经积累了相当多的基础技术,今天讲的k8s的网络方案其实也都是基于Linux已经实现的底层技术,只是它将这些零散的技术用到了极致,而这也是为什么绝大部分时候实习生无法取代一个10年经验的资深程序员的主要原因。同时,也是区分高级工程师和CURD Boy的一个重要标志。
实际上除了上面讲的Flannel和Calico网络方案,还有很多后起之秀,比如基于BPF技术的Cilium,实现了非常强大的网络控制和监控能力,并且暴露了一组使用友好的接口,使用Golang也可以很轻松的调用BPF相关的接口。
通过这些年云原生发展的趋势来看,越来越多的能力被抽象到内核态,从而实现性能的大幅提升,比如我们上面讲的VXLAN,Cilium都是在实现这样的目标。可以预见,未来还会有更多的项目加入到其中,云原生会变得越来越标准化。当达到一定程度的时候,会极大的解放生产力。