上一篇文章中我们介绍了同主机内的pod如何通信,这个模型相对简单,并且在一个主机内,我们可以让pod间通过二层网络通信(bridge),也可以通过三层网络通信(路由),但是如果在主机间,我们可以选择的范围就相对较小了,CNI需要去适配这些不同的组网。
虽然组网各有差异,但是背后的原理是大同小异的。
主机间组网模型
两个主机在网络拓扑中,可能在同一个子网,也可能在不同的子网。同一个子网中,主机间可以进行二层通信,不同子网间主机需要通过三层网络设备通信,如下图所示:
虽然实际的网络可能不一样,但这样的划分时足以覆盖主机间通信模型的。
同时,为了简化后面对网络设备的描述,我们使用二层通信和三层通信来代替交换机或路由器等网络设备。如果提及网络设备,这里也约定:交换机工作在第二层(传输层),路由器工作在第三层(网络层)
同subnet通信
在同一个subnet下,并且主机间可以进行二层通信,那这件事情就变得简单了。
我们说主机内的Pod通信,限定在主机内,数据始终在主机内部流转,而这里介绍的模型也类似,数据都在同一个子网下。
细心的同学已经发现了,上图中我在主机上加了一个路由的图标,为什么呢?因为主机可以承担转发的能力,我们后文所介绍的所有模型,都要基于主机的转发能力。
host gateway模型
既然容器有集群看不见的虚拟IP,而主机有转发的能力且有集群相互看得见的IP,那不是把容器的专有IP地址绑定到主机的路由表上就OK了
事实确实是这样,我们来看下图。
上图中我们将A主机上的所有pod划分到同一个虚拟的网段上,然后在集群中所有节点上配置路由规则
此时pod-1-1要和pod-2-1通信,按照上图中cidr地址划分,我们假设pod-1-1的ip地址为10.10.1.1,pod-2-1的ip地址为10.10.2.1,那么它的数据流转如下图所示
详细通信过程如下:
- 主机A上pod-1-1发起IP报文,源ip:10.10.1.1,目的ip:10.10.2.1
- 主机A上pod-1-1协议栈查找路由表获知,从eth0口发送
- 主机A上veth1收到报文,交给主机A协议栈
- 主机A已开启转发功能,于是协议栈查找路由表,通过目标IP匹配到规则1
- 主机A将报文从eth0转发,下一跳为196.128.2.1
- 主机B eth0收到报文
- 主机B eth0将报文交给主机B协议栈
- 主机B发现目的IP不是本机,查找路由表
- 匹配到规则2,将报文从主机B veth1转发
- 主机B中pod-2-1 eth0收到报文,将报文交付给协议栈处理
通过报文数据流,可以得知,host gateway将主机当作一个网关,执行路由转发,并且严格要求集群中主机间可以直接路由,也就是说两个主机间不存在任何其他路由设备(这些设备不认识集群IP)
如果不满足这个规则,则这个模型无法工作。
此外,也可以看到,路由表和集群节点数量成线性关系,当有N个节点时,每个主机要维护N-1条路由规则。
等等,这不是和上文我们将主机内通信的veth pair一样吗,确实有点类似,这里的网线就好比主机内的veth pair,但是路由规则不再是单个pod IP,而是一个网段。
总结
这种模式对基础的组网有严格的要求,同时在路由表维护上也有弊端,但是它的好处也是显而易见的:整个通信过程都是MAC转发,没有NAT或者报文封装。
这种模型在flannel中也有实现,flannel中的host gateway就是假定基础的网络组网满足二层通信的要求。
如果基础的物理网络早已经搭建,而且主机就分布在不同的子网下,那又该如何处理呢?
跨subnet通信
当主机跨子网,不能直接二层通信时,事情就变得复杂了,例如下图。
两个主机间的通过"network"通信,而这里的network可能存在多种网络设备,工作在不同的层级。枚举这些网络设备似乎是一件难事,好在我们也不需要去枚举。
overlay模型
上面的host gateway模型假设两台主机可以通过 Underlay网络直连,那在Overlay模型下,提供网络连接功能的仍然是 Underlay网络,Overlay网络则基于 Underlay网络构建一个逻辑网络。主要利用封装技术在底层 Underlay网络上传输数据包。因此在这个模型下都是围绕封包去进行的。
IP tunnel
关于tunnel设备的介绍,网上有很多资料,这里不详细展开,如果要展开,可能需要单独为它写一篇文章。这里只简单展示它的工作原理。
当app1发送的数据包流经tun0时,位于用户态的app2就可以看到该报文,并且对其处理,其处理完成后可以将报文返回给协议栈。这里的app2可能是一个VPN程序,也可能是一些流量处理软件。
除此之外,linux原生支持5种隧道:IPIP、GRE、SIT、ISATAP和VTI
这里的IP tunnel,就是在原始的IP数据包上在封装一层IP协议。
上图为例,当129.128.1.2主机上有一个pod-1,假设其IP为10.10.1.1,129.128.2.2主机上也有一个pod-2,假设其IP为10.10.2.1。当pod-1要和pod-2通信时,其发送的IP报文格式如下:
在这个报文中,源IP和目的IP在整个网络中是无法完成传输的。
如果说host A要和host B通信,host发送的IP报文格式应该如下:
这个报文之是可以发送到目的主机,因为整个网络下的上一个网络设备都可以直接路由或通过默认路由或端口转发将报文交付给下一个网络设备,最终交付给目的主机
在这里如果将pod发送的原始IP报文封装一层IP报文,那是不是就可以发送了。
上图中展示了封装后的报文格式。
下面进行一个小实验,看看IPIP模式是如何工作的
模拟实验
首先我们在host 1上创建一个network namespace代表pod,并配置设备IP等
# ip netns add pod-1
# ip link add eth0 type veth peer name veth1
# ip link set eth0 netns pod-1
# ip netns exec pod-1 ip addr add 10.10.1.10 dev eth0
# ip netns exec pod-1 ip link set dev eth0 up
# ip netns exec pod-1 ip route add 169.254.1.1 dev eth0
# ip netns exec pod-1 ip route add default via 169.254.1.1 dev eth0
# ip link set dev veth1 up
# echo 1 > /proc/sys/net/ipv4/conf/veth1/proxy_arp
# echo 1 > /proc/sys/net/ipv4/ip_forward
然后我们创建tunnel设备
# ip tunnel add tunl0 mode ipip ttl 64
# ip link set tunl0 mtu 1480
# ip link set tunl0 up
注意,这里我们设置了tun0模式为IPIP,并且设置mtu等于1480,为什么要这么设置呢?
我们刚才说了,ipip是在ip报文上封装一层ip报文,一般eth0网卡的mtu为1500,而我们的ipip报文多了一层ip首部,所以这里需要减去ip首部占用的空间。
配置好tun设备后,我们配置路由表,让发往pod-2的流量经过tun设备
# ip route add 10.10.2.0/16 via 192.168.11.127 dev tunl0 onlink
这里,192.168.11.127是另外一台主机的IP
同样,在另外一台主机做类似配置
# ip netns add pod-2
# ip link add eth0 type veth peer name veth2
# ip link set eth0 netns pod-2
# ip netns exec pod-2 ip addr add 10.10.2.10 dev eth0
# ip netns exec pod-2 ip link set dev eth0 up
# ip netns exec pod-2 ip route add 169.254.1.1 dev eth0
# ip netns exec pod-2 ip route add default via 169.254.1.1 dev eth0
# ip link set dev veth2 up
# ip route add 10.10.2.10 dev veth2 scope link
# echo 1 > /proc/sys/net/ipv4/conf/veth2/proxy_arp
# echo 1 > /proc/sys/net/ipv4/ip_forward
# ip tunnel add tunl0 mode ipip ttl 64
# ip link set tunl0 mtu 1480
# ip link set tunl0 up
# ip route add 10.10.1.0/16 via 192.168.11.126 dev tunl0 onlink
配置完成,我们在pod-1中ping pod-2
# ip netns exec pod-1 ping -c 1 10.10.2.10
PING 10.10.2.10 (10.10.2.10) 56(84) bytes of data.
64 bytes from 10.10.2.10: icmp_seq=1 ttl=62 time=0.186 ms
--- 10.10.2.10 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.186/0.186/0.186/0.000 ms
没有任何问题,我们在pod-2主机上抓包看一下
# tcpdump -n -i ens33 | grep 10.10.2.10
10:08:20.249278 IP 192.168.11.126 > 192.168.11.127: IP 10.10.1.10 > 10.10.2.10: ICMP echo request, id 48763, seq 1, length 64 (ipip-proto-4)
10:08:20.249301 IP 192.168.11.127 > 192.168.11.126: IP 10.10.2.10 > 10.10.1.10: ICMP echo reply, id 48763, seq 1, length 64 (ipip-proto-4)
可以看到在原始IP报文上多了一层IP,而这层IP刚好是两台主机可以进行物理通信的两张网卡的IP。
小结
整个ipip过程非常简单,详细流程如下
说明
- pod-1应用程序发起请求,经过pod-1协议栈组网成IP报文
- IP报文经过veth pair到达宿主机协议栈
- 宿主机协议栈查找路由,发现10.10.2.0/24网段报文应该转发给tunl0设备
- tunl0设备接收报文,并对其封装,然后交给协议栈
- 协议栈收到封装报文,发现目的IP应该从eth0发给默认网关
- host A默认网关将报文交给“network”
- host B eth0网口收到报文,发现目的IP是自己,将报文交给host B协议栈
- hostB协议栈发现协议为IPIP,于是进行解包
- 解包后发现目的IP为10.10.2.10,于是查找路由表,发现应该转发给veth2设备
- veth2的另一端,即pod-2的eth0收到报文,交给pod-2的协议栈
- pod-2协议栈将报文交给上层应用程序处理完后发送响应请求
- 响应报文通过veth2进入主机协议栈
- host B主机协议栈查找路由,发现10.10.1.0/24网段报文应该转发给tunl0设备
- tunl0设备接收报文,并对其封装,然后交给协议栈
- 协议栈收到封装报文,发现目的IP应该从eth0发给默认网关
- ....
- pod-1收到响应报文
ipip技术实现overlay网络到这里差不多就讲完了,在结束之前,还有一个问题想要说明一下
ipip封装时是如何知道那个IP应该作为源IP,毕竟本地可能有多张网卡;同时集群中存在多台主机,tunl0又是如何知道以那个IP应该作为目的IP呢?
在回头看我们创建tunl0设备命令:
# ip tunnel add tunl0 mode ipip ttl 64
并没有指定local和remote,如果指定local和remote,其命令应该类似下面这样
# ip tunnel add tunl0 mode ipip local 192.128.11.126 remote 192.168.11.127 ttl 64
如果指定local和remote,很容易理解,local就是封装的源IP,remote就是封装的目的IP
但是我们创建的tunl0设备local和remote都是any
# ip tunnel
tunl0: ip/ip remote any local any ttl inherit nopmtudisc
实际上,这里tunnel获知源IP和目的IP是通过路由表知道的。
其中目的IP来自于我们配置的路由表
# ip route add 10.10.2.0/16 via 192.168.11.127 dev tunl0 onlink
路由表中这一条规则意味着所有发送到10.10.2.0/26网段的报文,都应该发给192.168.11.127,所以tunl0设备将目的IP设置为192.168.11.127。
获得了目的IP,那源IP也是同样
# ip route get 169.68.11.127
169.68.11.127 via 192.168.11.2 dev ens33 src 192.168.11.126
tunl0通过查找路由表,知道要发送给192.168.11.127,应该从ens33出去,所以将源IP为192.168.11.126。
知道了这一点,你应该就清楚IPIP封包规则了。
总结
在这篇文章中,我们介绍了同一个host gateway模型和overlay模型,在overlay模型中我们又介绍了IPIP技术完成网络虚拟化,其实在k8s网络虚拟化中,基本都是通过封包完成的,例如calico就使用的是IPIP,flannel的vxlan和udp模式都是基于封包完成的,他们之间大同小异。
至此,k8s基本的网络通信就简单介绍完了,但是这只是实现了基本的通信,这些基本的通信要想实际应用是远远不够的。
因此再后面的文章中,我们将深入k8s和CNI插件,将介绍基于这个基本的通信模型下,K8S是如何实现service,cni是如何实现网络策略的。