学习k8s 时,总觉得网络部分最难以理解,翻阅了大量资料,总算是有一些理解了,特此记录一下。
综述
k8s 网络模型遵循以下几个规则
- 每个pod 都有自己独立的ip, pod与pod之间不会有重复ip
- pod与pod之间的通信都是通过ip直接通信,没有任何Nat规则
- 在上面两条规则下,k8s把网络层抽象为了一组接口(CNI), 社区里针对k8s网络有多种不同的实现方式,使用时可针对不同的使用场景,自行选择适合的实现方式
单主机网络
首先了解一下一个node上不同pod 是如何通信的 (图片节选自极客时间 深入剖析Kubernetes专栏)
linux 下有一种叫 veth 的设备,是一种成对的设备,如图中,veth设备的一端插在container 上,一端插在一个叫docker0的网桥,任何一端收到网络包时,会自动发往其另一端。所以container对外发送网络包时,则可通过该veth设备将网络包发送到docker0网桥上,而其他容器的veth设备另一端也都插在该网桥上,已知同一个网桥上的设备的网络包时可以互通的,所以container1 和 container2 即可通过veth设备 + docker0网桥实现互通
跨主机网络
上述方式下,当container1发出的网络包不是本机上的container时,docker0 网桥就不知道发往谁了,此时就会发往默认网关:本机的eth0网卡(eth0网卡 也是插在docker0网桥上的),这种情况下,就需要eth0能知道 发往的container ip是在哪台node 上,且有一条链路使得本机的网络包能发往对方node,对于这个问题就引出了两类k8s 网络模型:一类是基于二层网络架构,一类是基于三层网络架构 (二层三层指TCP网络模型中分层:数据链路层和Ip层),接下来将分别举例介绍这两种网络模型。
二层网络架构(以Flannel 为例)
首先介绍一种简单的基于UDP模式的Flannel (图片节选自极客时间 深入剖析Kubernetes专栏)
这里需要介绍一下linux 中的TUN 设备,这种设备通常有两端,一端在用户态的进程中,一端在内核中,任何一端收到包后,就会发往另一端。图中的flannel0就是一种TUN设备, 负责从应用程序和内核之间传递网络包。接下来以图中node1 上的 container1 向 node2上的 container2 发送网络包为例,详细讲解一下网络包的流向。
- container1 通过veth设备将网络包发往docker0网桥
- docker0网桥 在自己网桥范围内找不到目标ip地址,将网络包发往flannel0 内核端
- flannel0 内核端将网络包发往 flanneld进程
- flanneld进程 是如何知道目标container2在哪台node上呢,首先Flannel模式每台机器上的所有pod都是在一个子网内,不同机器对应不同的子网,比如图中node1的子网范围是100.96.1.0/24, node2的子网范围是100.96.2.0/24。 而每个子网对应哪台node则是flanneld进程在 etcd 里来维护的(比如通过inforemer监控node的新建与删除来维护,这里没有细究,但大致猜测是这类似的方法),总之就是,flanneld进程通过etcd知道了container2 所在的node2 的ip, 然后将目标node2上的flanneld进程发送一个udp包(udp层包的内容则是container1的ip信息,目标container2的ip信息),当对方flanneld进程收到包后,即可通过拆udp包知道该包来源于哪一个container。
- node2内核收到包后,通过ip端口找到flanneld进程, flanneld进程拆包后得知来源是container1,目标是container2, 发往docker0网桥。
- node2上的docker0网桥即可找到对应container2。
这种udp模式因为效率不高已经被淘汰了,其在flannel0内核端发往flanneld进程, 和flanneld进程解析包后又重新封装包发往内核,涉及两次内核到进程态的切换,以及在node2上也有相同的两次,共4次内核用户态切换,而在操作系统里,内核用户态切换是很费性能了,我们在系统编程里,都要尽量避免内核用户态切换。 所以接下来介绍一种基于VXLAN的Flannel模式
基于VXLAN的Flannel (图片节选自极客时间 深入剖析Kubernetes专栏)
udp模式之所以存在一个用户内核态的切换就是因为需要一个flanneld进程在用户态监听一个端口,方便其他node可以通过ip:port的形式发送包到flanneld进程,然后flanneld进程会进行拆包拿到内部包,再发往具体的容器。而VXLAN方式下则是用一个完全工作在内核态的VETP设备(这里叫flannel.1)来替代了这个flanneld进程,网络包的具体流向如下:
- container1 通过veth设备将网络包发往docker0网桥
- docker0网桥 在自己网桥范围内找不到目标ip地址,将网络包发往flannel.1
- VETP设备也就是这里的flannel.1,是有自己的mac地址的,在flannel.1看来,就是需要知道对方node的flannel.1设备的mac地址,像是在二层网络上通信,这也是flannel网络模型被称为基于二层网络架构的原因。而各个node上的flannel.1设备的IP与mac地址与UDP一样,也是通过一个flanneld进程通过监听node的新建删除来维护的;另外为了让对方node内核端收到该包时能知道该包是发往哪个设备的,还需要在数据帧中添加一个VXLAN头部。然后就是需要知道对方node本身的ip地址呢,同样,也是通过flanneld进程来维护的,会存在叫作FDB(Forwarding Database)的转发数据库。最终就是如下图所示,将目的容器Ip地址,目的VETP设备mac地址,VXLAN头部信息封装到一个udp数据帧发往node2。
(图片节选自极客时间 深入剖析Kubernetes专栏) - node2收到网络包后,其内核网络栈拆包发现有一个VXLAN头部,通过查看VNI的值发往flannel.1设备(VNI用于标识不同的VXLAN网络,Flannel使用的值是1,所以这里的VETP设备成为flannel.1)
- flannel.1设备进一步拆包拿到原始数据包后发往目的容器
三层网络架构
二层网络架构下,不管是UDP模式,还是VXLAN模式,都存在将原始数据包封包和解包的过程,这也是十分损耗性能的,三层网络架构下,可以一定程度上解决这个问题,接下来将介绍两种三层网络架构方案,首先介绍一种比较简单的flannel的host-gw模式。
- flannel的host-gw模式 (图片节选自极客时间 深入剖析Kubernetes专栏)
这个模式就比较简单,主要还是通过flanneld进程维护一个路由表信息,比如container1将包发往container2, 路由表里就记录了container2的子网10.244.1.0/24的下一跳地址是node2的ip地址10.168.0.3。网络包信息就可以根据该路由表发往node2,到达node2后根据本机路由信息依次发往cni0网桥,container2(cni0网桥和前面提到的docker0网桥是一个意思)
可以看到整个流程下没有封包和解包的过程了,但这个模式也有一个问题,就是所有node必须二层可达。 - Calico模式
该模式与host-gw模式大致一样,不同的是Calico使用BGP协议+Felix(一个DaemonSet)来维护路由表信息,简单介绍一下BGP协议就是一个自治系统内有很多台机器,他们相互通信汇报自己的ip信息。如果机器太多,通信的流量是会以N²的规模快速增长,所以还有一种方案是选择一个或多个专门的节点来负责维护所有节点信息,其他节点只需要与专用节点交互来获取所有节点信息。Calico也与host-gw模式一样,要求所有node必须二层可达,但是也可以通过IPIP隧道模式来实现三层网络通信, IPIP隧道就和flannel下的封包解包一样了,需要将原始包封装再发包,也存在封包解包的效率问题。Calico相比host-gw的优点应该在于其支持高级网络策略和安全规则,可以精确控制Pod间的通信,以及更好地支持多租户环境,允许不同的租户拥有独立的网络空间等等,而host-gw模式非常简单,一般适合小型或简单的网络环境。
本文就先介绍到此了,下一篇将介绍k8s 各种类型的service, 以及各种端口策略下的pod,pod与其他service通信,与外网通信,外网通过loadblance访问pod 其网络流向是怎样的
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。