本文系统的系统介绍了Kubernates网络原理,包含Linux内核网络堆栈,Docker容器网络介绍,Kubernates网络Pod通信,本文主要来自于与笔者对《Docker容器与容器云》,《Kubernates权威指南》的读书笔记整理,对于部分内容如Linux网桥工作原理,Flannel网络工作原理引用自网络文档,在文中有标注出处与链接。本文未涉及service与pod之间通信,以及K8s集群内外南北向通信,在后面文章再系统介绍。
容器网络基础-Linux内核网络堆栈
得益于Linux内核的namespace和cgroup技术,使得同一主机上可以创建多个互相隔离的工作空间。

对于namespace的网络空间,一个网络空间内包含独立的网络设备,IPv4/ipv6协议栈,IP路由表,防火墙,socket等。且一个网络空间内的多进程间共享这些资源

Linux内核层面将上述能力通过API暴漏给了用户态,可以通过Linux的操作指令来手动创建网络空间,虚拟网卡,网桥等。

在一个主机上,因为网络空间的原因,通信就涉及同一个网络空间内的进程间通信以及跨网络空间的进程间通信。对于同一个网络空间内的进程间通信和传统的一个主机上不同进程间通信方式相同,使用回环地址:进程端口的模式。其在TCP/IP模型中传输层完成,不涉及网络层(IP),数据链路层(网卡/MAC)和物理层(电线)。而不同网络空间通信则等同于传统物理网络中两个主机之间的通信。传统物理网络主机间通信涉及:网卡设备,网桥设备,三层路由设备,网线。

而对于一个Linux主机内部不同网络空间通信同样需要如上设备,只是都是虚拟的。
l 直连网线模式

l 通过网桥模式(二层交换机转发模式)

关于Linux网桥的工作原理,参考:Linux 上的基础网络设备详解(见参考与说明链接)
Bridge(桥)是 Linux 上用来做 TCP/IP 二层协议交换的设备,与现实世界中的交换机功能相似。Bridge 设备实例可以和 Linux 上其他网络设备实例连接,既 attach 一个从设备,类似于在现实世界中的交换机和一个用户终端之间连接一根网线。当有数据到达时,Bridge 会根据报文中的 MAC 信息进行广播、转发、丢弃处理。
图 1.Bridge 设备工作过程

如图所示,Bridge 的功能主要在内核里实现。当一个从设备被 attach 到 Bridge 上时,相当于现实世界里交换机的端口被插入了一根连有终端的网线。这时在内核程序里,netdev_rx_handler_register()被调用,一个用于接受数据的回调函数被注册。以后每当这个从设备收到数据时都会调用这个函数可以把数据转发到 Bridge 上。当 Bridge 接收到此数据时,br_handle_frame()被调用,进行一个和现实世界中的交换机类似的处理过程:判断包的类别(广播/单点),查找内部 MAC 端口映射表,定位目标端口号,将数据转发到目标端口或丢弃,自动更新内部 MAC 端口映射表以自我学习。
Bridge 和现实世界中的二层交换机有一个区别,图中左侧画出了这种情况:数据被直接发到 Bridge 上,而不是从一个端口接受。这种情况可以看做 Bridge 自己有一个 MAC 可以主动发送报文,或者说 Bridge 自带了一个隐藏端口和寄主 Linux 系统自动连接,Linux 上的程序可以直接从这个端口向 Bridge 上的其他端口发数据。所以当一个 Bridge 拥有一个网络设备时,如 bridge0 加入了 eth0 时,实际上 bridge0 拥有两个有效 MAC 地址,一个是 bridge0 的,一个是 eth0 的,他们之间可以通讯。由此带来一个有意思的事情是,Bridge 可以设置 IP 地址。通常来说 IP 地址是三层协议的内容,不应该出现在二层设备 Bridge 上。但是 Linux 里 Bridge 是通用网络设备抽象的一种,只要是网络设备就能够设定 IP 地址。当一个 bridge0 拥有 IP 后,Linux 便可以通过路由表或者 IP 表规则在三层定位 bridge0,此时相当于 Linux 拥有了另外一个隐藏的虚拟网卡和 Bridge 的隐藏端口相连,这个网卡就是名为 bridge0 的通用网络设备,IP 可以看成是这个网卡的。当有符合此 IP 的数据到达 bridge0 时,内核协议栈认为收到了一包目标为本机的数据,此时应用程序可以通过 Socket 接收到它。一个更好的对比例子是现实世界中的带路由的交换机设备,它也拥有一个隐藏的 MAC 地址,供设备中的三层协议处理程序和管理程序使用。设备里的三层协议处理程序,对应名为 bridge0 的通用网络设备的三层协议处理程序,即寄主 Linux 系统内核协议栈程序。设备里的管理程序,对应 bridge0 寄主 Linux 系统里的应用程序。
Bridge 的实现当前有一个限制:当一个设备被attach 到 Bridge 上时,那个设备的 IP 会变的无效,Linux 不再使用那个 IP 在三层接受数据。举例如下:如果 eth0 本来的 IP 是192.168.1.2,此时如果收到一个目标地址是 192.168.1.2 的数据,Linux 的应用程序能通过 Socket 操作接受到它。而当 eth0 被 attach 到一个bridge0 时,尽管 eth0 的 IP 还在,但应用程序是无法接受到上述数据的。此时应该把 IP 192.168.1.2 赋予 bridge0。
另外需要注意的是数据流的方向。对于一个被 attach 到 Bridge 上的设备来说,只有它收到数据时,此包数据才会被转发到 Bridge 上,进而完成查表广播等后续操作。当请求是发送类型时,数据是不会被转发到Bridge 上的,它会寻找下一个发送出口。用户在配置网络时经常忽略这一点从而造成网络故障
Kubernates网络基础-容器网络
主要说的是Docker容器的网络,其都是是调用的Linux内核的API来完成网络部分的动态的创建配置事项,Docker网络自身涉及四种,bridge模式,host模式,container模式,none模式,通过docker引擎启动参数可指定。其中docker默认的是bridge模式,kubernates使用的是其container模式。
Bridge模式:
Docker引擎安装完毕会在主机上增加iptables规则,用于网桥与主机网络之间的IP路由。
Docker引擎启动阶段调用Linux内核指令在rootns网络空间(主机网络空间)创建网桥,并取名为docker0.并根据启动参数指定的子网段给网桥配置IP段和IP。
在容器创建阶段,Docker引擎为每个容器创建单独的网络空间,网络设备对,其中一端设置到容器网络空间,另一端设置到主机网络空间的网桥上,并给容器网络空间的网络设备(虚拟网卡)配置MAC地址与IP地址,IP地址由网桥配置的子网段内获取任意一个未使用的,并未容器设置默认路由,其中网关指向docker0网桥的IP地址。

Host模式
Docker引擎为每个容器创建的namespace不包含network namespace,容器和宿主机共用同一个网络空间(rootns),使用宿主机的网卡,IP和端口信息。容器与外界通信直接使用主机IP。此模式缺点很明显,容器没有网络隔离,所以容器内应用的端口要求不允许冲突。同时会引起网络资源的竞争(带宽)。这种模式非常适合于Container running on VM环境下,在running onVM环境中,由IAAS层成熟的多租户网络系统保证了VM粒度的多租户网络隔离型,容器间通信方式:vm IP地址:容器端口即可执行,等价于vm上两个进程间通信。
Container模式
Container模式与host模式类似,指定新创建的容器和已经存在的某个容器共享同一个network namespace。与host模式区别在于container模式共享自定义的network namespace的网络。而非rootns空间的网络(宿主机的网络)。Container模式下,创建的容器不会创建自己的网卡,也不配置IP。两个容器除了网络外,其它都是隔离的。这样两个容器进程可以通过回环网络地址localhost:进程端口来互访。

None模式
容器拥有自己的网络空间,网络空间自带的回环网卡(loopback),除此外没有任何其它自定义网络设备(网卡),IP,路由的信息。此模式下容器间无法通信,需要用户自定义完成网卡添加,网络配置,留给用户较大操作空间。
Kubernates网络模型
前面介绍了Linux内核网络堆栈能力以及Docker容器如何使能Linux内核网络能力并对上游开放了哪些网络配置能力,此章节则介绍作为容器的上游Kubernates,其是如何使能容器这一层暴漏的网络能力的,以及Kubernates面临的网络问题和解决方案。
Kubernates中不再以容器为管理单元,而是以Pod为最小管理单元,Pod内可以包含多个容器,一个Pod属于一个网络空间,同一个Pod内的容器共享同一个网络空间。(实际使用上一个Pod一般只包含一个业务容器,也就是对应一个微服务实例,可以包含其它支撑容器,譬如采集的Agent等。)。而Kubernates之所以可以做到以Pod为网络空间分配单位,起采用了容器的container网络模式。

Kubernates在创建Pod的时候,通过调用Docker接口会先创建一个默认的容器pause(也叫网络容器),并指定为bridge模式,由docker则会创建网络空间,网络设备对,配置IP等,其次才开始创建一个正真客户的容器,并指定为container网络模式,复用pause容器的网络空间。对于Kubernates而言,Pod等同于传统主机,Pod内通信等同于主机上进程间通信,Pod间通信等同于主机间通信。
Kubernates网络-Pod内容器通信
因为Pod内容器属于同一个网络空间,共用此网络空间内的网卡设备,协议栈,路由,socket等。所以Pod内容器通信直接使用回环网络地址和容器端口即可:localhost:容器应用端口,直接由网络协议栈的传输层完成,不涉及网络层(IP),数据链路层和物理层。

Kubernates网络-主机内Pod间通信
主内机Pod间通信等价于两个同网段的主机的通信,同网段主机间通信通过二层交换机来完成转发,工作在数据链路层,主机内Pod间通信原理相同。在同一主机内,Pod的IP地址都是由主机内网桥分配的,在同一个网络段,且都指向了相同的网关地址:网桥的IP,当Pod1发送的请求报文会经过其虚拟网卡发送到网桥上,网桥基于MAC地址寻址转发到对应的端口,再进入目标Pod的网卡设备,最后到达Pod内容器应用。

假设一个网络数据包要由pod1到pod2。
1. 它由pod1中netns1的eth0网口离开,通过vethxxx进入root netns。
2. 然后被传到docker0网桥,docker0网桥使用ARP请求,说“谁拥有这个IP”,从而发现目标地址。
3. vethyyy说它有这个IP,因此网桥就知道了往哪里转发这个包。
4. 数据包到达vethyyy,跨过管道对,到达pod2的netns1。
源端Pod利用自己网络空间的网络协议栈进行包的封装,由位于主机网络空间的网桥在数据链路层基于MAC地址进行转发到目标Pod,目标Pod使用自己网络协议栈层层解包。

Kubernates网络-跨主机Pod间-直接路由模式通信
跨主机间Pod通信在于源Pod所在主机的网桥报文如何经过宿主机物理网口发送到目的端Pod所在宿主机的网桥上。网桥与宿主机是通过iptables进行nat转发到宿主机物理网卡的,但此时目标IP地址携带的是目标Pod的私有IP,此IP在物理网络上并不可见,物理网络上无法转发此报文,所以问题是源Pod所在宿主机如何知道目标Pod的所在宿主机IP。如下图,主要解决宿主机之间的寻址路由问题。

直接路由模式就是在每个宿主机上添加路由规则

在源Pod在所主机与目标Pod所在主机分别添加上述路由规则,如此后整个通信流程为:
假设一个数据包要从pod1到达pod4(在不同的节点上)。
1. 它由pod1中netns的eth0网口离开,通过vethxxx进入root netns。
2. 然后被传到cbr0,cbr0通过发送ARP请求来找到目标地址。
3. 本节点上没有Pod拥有pod4的IP地址,因此数据包由cbr0 传到 主网络接口 eth0.
4. 数据包的源地址为pod1,目标地址为pod4,它以这种方式离开node1进入电缆。
5. 路由表有每个节点的CIDR块的路由设定,它把数据包路由到CIDR块包含pod4的IP的节点。(手工在节点上配置的)
6. 因此数据包到达了node2的主网络接口eth0。现在即使pod4不是eth0的IP,数据包也仍然能转发到cbr0,因为节点配置了IPforwarding enabled。节点的路由表寻找任意能匹配pod4 IP的路由。它发现了 cbr0 是这个节点的CIDR块的目标地址。你可以用route-n命令列出该节点的路由表,它会显示cbr0的路由,类型如下:
7. 网桥接收了数据包,发送ARP请求,发现目标IP属于vethyyy。
8. 数据包跨过管道对到达pod4。
此模式下的弊端问题很明显:手工配置工作量巨大,主机上路由条目数据随着主机规模直线上升,譬如一个集群包含上千个Kubernates slave节点,则每个节点上需要配置上千个路由条目,路由条目的增多进而影响网络转发性能。
Kubernates网络-跨主机Pod间通信-Overlay网络(Flannel)
本节详细介绍参考:图解Kubernetes网络(二)。见参考与说明链接。
Overlay就是在跨节点的本地网络上的包中再封装一层包。为了理解Overlay网络中流量的流向,拿Flannel做例子,它是CoreOS 的一个开源项目。

Kubernetes Node with route table(cross node pod-to-popTraffic flow with flannel overlay network)这里我们注意到它和之前我们看到的设施是一样的,只是在root netns中新增了一个虚拟的以太网设备,称为flannel0。它是虚拟扩展网络Virtual Extensible LAN(VXLAN)的一种实现,但是在Linux上,它只是另一个网络接口。从pod1到pod4(在不同节点)的数据包的流向类似如下:
1、它由pod1中netns的eth0网口离开,通过vethxxx进入root netns。
2、然后被传到cbr0,cbr0通过发送ARP请求来找到目标地址。
3a、由于本节点上没有Pod拥有pod4的IP地址,因此网桥把数据包发送给了flannel0,因为节点的路由表上flannel0被配成了Pod网段的目标地址。
3b、flanneld daemon和Kubernetesapiserver或者底层的etcd通信,它知道所有的PodIP,并且知道它们在哪个节点上。因此Flannel创建了PodIP和Node IP之间的映射(在用户空间)。flannel0取到这个包,并在其上再用一个UDP包封装起来,该UDP包头部的源和目的IP分别被改成了对应节点的IP,然后发送这个新包到特定的VXLAN端口(通常是8472)。

Packet-in-packet encapsulation(notice the packet isencapsulated from 3c to 6b in previous diagram)尽管这个映射发生在用户空间,真实的封装以及数据的流动发生在内核空间,因此仍然是很快的。
3c、封装后的包通过eth0发送出去,因为它涉及了节点间的路由流量。
4、包带着节点IP信息作为源和目的地址离开本节点。
5、云提供商的路由表已经知道了如何在节点间发送报文,因此该报文被发送到目标地址node2。
6a、包到达node2的eth0网卡,由于目标端口是特定的VXLAN端口,内核将报文发送给了 flannel0。
6b、flannel0解封报文,并将其发送到 root 命名空间下。
6c、由于IP forwarding开启着,内核按照路由表将报文转发给了cbr0。
7、网桥获取到了包,发送ARP请求,发现目标IP属于vethyyy。
8、包跨过管道对到达pod4。
参考与说明
文中部分内容引用出处如下:
图解Kubernetes网络
http://dockone.io/article/3211
Linux 上的基础网络设备详解
https://www.ibm.com/developerworks/cn/linux/1310_xiawc_networkdevice/
了解KubernatesNetworking:Pods:
https://medium.com/google-cloud/understanding-kubernetes-networking-pods-7117dd28727




