谈到K8S的网络模型,就不能不提它著名的“单Pod单IP”模型,即每个Pod都有一个独立的IP,Pod内所有容器共享网络namespace(同一个网络协议栈和IP)。“单Pod单IP”网络模型为我们勾勒了一个K8S扁平网络的蓝图,在这个网络世界里:容器之间直接通信,不需要额外的NAT(网络地址转换);Node与容器之间,同样不需要额外的NAT;在其他容器和容器自身看到的IP也一样的。扁平化网络的优点在于:没有NAT的性能损耗,可追溯源地址进而为后面的网络策略做铺垫,易排错等。
总体而言,如果集群内要访问Pod,走Service,至于集群外要访问Pod,走的是Ingress。Service和Ingress是K8S专门的抽象出来的和服务发现相关的概念,后面会做详细讨论。
类似于CRI之于K8S的runtime,K8S使用CNI(container network interface)作为Pod网络配置的标准接口。需要注意的是,CNI并不支持docker网络,也就是说docker0网桥会被CNI的各类插件“视而不见”。
图1
上图描绘了当用户在K8S里创建了一个Pod后,CRI和CNI协同创建所有容器并为他们初始化网络栈的全过程。
具体过程如下:当用户在K8S的master那边创建了一个Pod后,Kubelet观察到新Pod的创建,于是首先调用CRI(后面的Runtime实现,比如:dockershim,containerd等)创建Pod内的若干个容器。在这些容器里面,第一个被创建的Pause容器是比较特殊的,这是K8S系统“赠送”的容器,里面跑着一个功能十分简单的go语言程序,具体逻辑是一启动就去select一个空的go语言channel,自然就永远阻塞在那里了。一个永远阻塞而且没有实际业务逻辑的pause容器到底有什么用呢?用处大了。我们知道容器的隔离功能利用的是Linux内核的namespace机制,而只要是一个进程,不管这个进程是否处于运行状态(挂起亦可),它都能“占”着一个namespace。因此,每个Pod内的第一个系统容器Pause的作用就是为占用一个Linux的network namespace,而Pod内其他用户容器通过加入到这个network namespace的方式来共享同一个network namespace。用户容器和pause容器之间的关系有点类似于寄居蟹和海螺的关系。
因此,Container Runtime创建Pod内所有容器时,调用的都是同一个命令:
$ docker run --net=none
意思是只创建一个network namespace,而不初始化网络协议栈。如果这个时候通过nsenter方式进入到容器,会看到里面只有一个本地回环设备lo。那么容器的eth0是怎么创建出来的呢?答案是CNI。
CNI主要负责容器的网络设备初始化工作。Kubelet目前支持两个网络驱动,分别是:kubenet和CNI。
Kubenet是一个历史产物,即将废弃,因此这里也不准备过多介绍。CNI有多个实现,官方自带的插件就有p2p,bridge等,这些插件负责初始化pause容器的网络设备,也就是给eth0分配IP等,到时候Pod内其他容器就用这个IP与外界通信。Flanne,calico这些第三方插件解决Pod之间的跨机通信问题。容器之间的跨机通信,可以通过bridge网络或overlay网络来完成。
图2
上图是一个bridge网络的示意图。Node1上Pod的网段是10.1.1.0/24,接的Linux网桥是10.1.1.1,Node2上Pod的网段是10.1.2.0/24,接的Linux网桥是10.1.2.1,接在同一个网桥上的Pod通过局域网广播通信。我们发现,Node1上的路由表的第二条是:
10.1.1.0/24 dev cni0
意思是所有目的地址是本机上Pod的网络包,都发到cni0这个Linux网桥去,进而广播给Pod。
注意看第三条路由规则:
10.1.2.0/24 via 192.168.1.101
10.1.2.0/24是Node2上Pod的网段,192.168.1.101又恰好是Node2的IP。意思是,目的地址是10.1.2.0/24的网络包,发到Node2上。
这时候我们观察Node2上面的第二条路由信息:
10.1.2.0/24 dev cni0
就会知道这个包会被接着发给Node2上的Linux网桥cni0,然后再广播给目标Pod。回程报文同理走一条逆向的路径。因此,我们可以得出一个小小的结论:bridge网络本身不解决容器的跨机通信问题,需要显式地书写主机路由表,映射目标容器网段和主机IP的关系,集群内如果有N个主机,需要N-1条路由表项。
至于overlay网络,它是构建在物理网络之上的一个虚拟网络,其中VXLAN是当前最主流的overlay标准。VXLAN就是用UDP包头封装二层帧,即所谓的MAC in UDP。
图3
上图即一个典型overlay网络的拓扑图。和bridge网路类似,Pod同样接在Linux网桥上,目的地址是本机Pod的网络包同样发给Linux网桥cni0。不一样的是,目的Pod在其他节点上的路由表规则,例如:
10.1.0.0/16 dev tun0
这次是直接发给本机的TAP设备tun0,而tun0就是overlay隧道网络的入口。我们注意到,集群内所有机器都只需要这么一条路由表,而不需要像bridge网络那样,写N-1条路由表项。那如何才能将网络包正确地传递到目标主机的隧道口另一端呢?一般情况下,例如flannel的实现,会借助一个分布式的数据库,用于记录目的容器IP与所在主机的IP的映射关系,而且每个节点上都会运行一个agent,例如flanneld,会监听在tun0上,进行封包一部手机住家创业把花钱的事情变成赚钱的事,你愿意了解吗? 感兴趣者加微信一商一耳零漆二零久巴零和解包操作。例如:Node1上的容器发包给Node2上的容器,flanneld会在tun0处将一个目的地址是192.168.1.101:8472的UDP包头(校验和置成0)封装到这个包的外层,然后接着主机网络的东风顺利到达Node2。监听在Node2的tun0上的flanneld捕获这个特殊的UDP包(检验和为0),知道这是一个overlay的封包,于是解开UDP包头,将它发给本机的Linux网桥cni0,进而广播给目的容器。
网友评论