概述
在Kubernetes中要保证容器之间网络互通,网络至关重要。而Kubernetes本身并没有自己实现容器网络,而是而是借助CNI标准,通过插件化的方式自由接入进来。在容器网络接入进来需要满足如下基本原则:
- Pod无论运行在任何节点都可以互相直接通信,而不需要借助NAT地址转换实现。
- Node与Pod可以互相通信,在不限制的前提下,Pod可以访问任意网络。
- Pod拥有独立的网络栈,Pod看到自己的地址和外部看见的地址应该是一样的,并且同个Pod内所有的容器共享同个网络栈。
kubernetes中的网络通信
kubernetes中的网络通信大致分为以下几种:
- Pod内容器间的通信(lo)
- Pod之间的通信 pod IP <—–> pod IP(flannel、calico)
- Pod与Service之间的通信 podIP <—–> ClusterIP(iptables、ipvs)
- Service与集群外部的通信 ClusterIP <—–> 集群外部
常见的CNI插件:
- flannel
- calico
- canel
- kube-router
Flannel
Flannel是CoreOS开源的,Overlay模式的CNI网络插件,Flannel在每个集群节点上运行一个flanneld的代理守护服务,为每个集群节点(host)分配一个子网(subnet),同时为节点上的容器组(pod)分配IP,在整个集群节点间构建一个虚拟的网络,实现集群内部跨节点通信。Flannel的数据包在集群节点间转发是由backend实现的,目前,已经支持核心官方推荐的模式有UDP、VXLAN、HOST-GW,以及扩展试用实验的模式有IPIP,AWS VPC、GCE、Ali VPC、Tencent VPC等路由,其中vxlan模式在实际的生产中使用最多,下面以vxlan模式为例。
从图里看每个宿主机都有一个flannel1(flannel.1)的设备,就是VXLAN所需的VTEP设备(flannel1“用于VXLAN报文的封装和解封装”),它既有IP地址也有MAC地址。如现在container1 访问 container2,当container1发出请求后,这个目的的地址是10.244.1.3的IP包,会先出现在cni0网桥,然后被路由到本机flanner1设备上处理,也就是说,来到了“隧道”的出口。既目的宿主机的VTEP设备(就是flannel1 设备)。
margu
当所有Node启动后,我们可以在k8s-m2上可以看到多个flannel1 网卡的路由信息,是因为flanneld启动后创建的。
[root@k8s-m2 ~]# ifconfig flannel.1
flannel.1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1450
inet 10.244.0.0 netmask 255.255.255.255 broadcast 0.0.0.0
ether 96:69:e2:7d:bd:32 txqueuelen 0 (Ethernet)
RX packets 10177 bytes 977133 (954.2 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 10177 bytes 1787417 (1.7 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
[root@k8s-m2 ~]# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.2.254 0.0.0.0 UG 0 0 0 ens32
10.244.0.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0
10.244.1.0 10.244.1.0 255.255.255.0 UG 0 0 0 flannel.1
10.244.2.0 10.244.2.0 255.255.255.0 UG 0 0 0 flannel.1
169.254.0.0 0.0.0.0 255.255.0.0 U 1002 0 0 ens32
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0 ens32
从上图看到10.244.1.0就是k8s-m3的VTEP设备(flannel1)的IP地址,而这些VTEP设备之间通讯就需要想办法组成一个虚拟的二层网络,既:通过二层数据帧进行通信,而k8s-m2上的VTEP设备在收到原始报文后,就要想办法把原始报文加一个目的MAC地址,封装成二层数据帧,然后发送给目的VTEP设备。这里需要解决一个问题目的VTEP设备的MAC地址是什么?
根据路由表信息我们知道了目的VTEP设备的IP地址,而根据三层IP地址查询二层MAC地址正是ARP表的功能。而这里用ARP表的记录,也就是flanneld进程在Node2节点启动时,自动添加到Node1上的。如下:
$ ip neigh show dev flannel.1
10.244.1.0 lladdr e6:05:f1:5f:d7:13 PERMANENT
10.244.2.0 lladdr 46:1b:96:8c:b0:cf PERMANENT
有了这个MAC地址linux内核就可以开始二层封装了,上面提到的MAC地址,对宿主机的二层网络没有任何意义,所以上述封装的数据帧不能在宿主机的二层网络里传输,为了方便概述,我们把上述数据帧称为内部数据帧。所以Linux内核还要把内部数据帧进一步封装成宿主机网络的一个普通数据帧,好让它载着内部数据帧,通过外网网卡(如eth0 、ens33等)进行传输。这次封装我们称为外部数据帧,为了实现这个搭便车的机制,Linux内核在封装内部数据帧前面,加上特殊的VXLAN头,用来表示这个乘客实际上是VXLAN使用的数据帧。而这个VXLAN头里有一个重要的标志VNI,它是识别某个数据帧是不是应该归属自己处理的标志。而flannel中,VNI的值是1,这也是为什么宿主机的VTEP设备都叫做flannel1的原因。这个时候linux内核会把这数据帧封装一个UDP报文在转发出去。虽然k8s-m2的flannel1知道k8s-m3的flannel1的MAC地址,但是不知道k8s-m3对外网卡的MAC的地址,也就是UDP该发往那台主机,实际上flannel1还要扮演一个网桥的角色,在二层网络进行UDP转发,而在Linux内核里面,网桥设备进行转发的依据来自FDB的转发数据库。这个flannel网桥对应的FDB信息,就是flannel进程维护的,他的内容如下:
$ bridge fdb show flannel.1|grep 46:1b:96:8c:b0:cf
46:1b:96:8c:b0:cf dev flannel.1 dst 192.168.2.140 self permanent
我们可以看到发往的IP地址是192.168.2.142的主机,显然这台主机就是 k8s-m3,UDP要转发的目的也找到了。接下来就是宿主机网络封包的过程了。
下面让我们来看看,当有一个EventAdded到来时,flanneld如何进行配置,以及封包是如何在flannel网络中流动的。
借用上图所示,当主机B启动了一个flanneld的服务后,将从etcd中读取配置信息,并请求获取子网的租约。所有Node上的flanneld都依赖etcd cluster来做集中配置服务,etcd保证了所有node上flanned所看到的配置是一致的。同时每个node上的flanned监听etcd上的数据变化,实时感知集群中node的变化。flanneld一旦获取子网租约、配置后端后,会将一些信息写入/run/flannel/subnet.env
文件。它会将自己的subnet 10.1.16.0/24和Public IP 192.168.0.101写入etcd中,它还会将vtep设备flannel.1的mac地址也写入etcd中。
之后,主机A会得到EventAdded事件,并从中获取主机B添加至etcd的各种信息。这个时候,它会在本机上添加三条信息:
- 路由信息:所有通往目的地址10.1.16.0/24的封包都通过vtep设备flannel.1设备发出,发往的网关地址为10.1.16.0,即主机B中的flannel.1设备。
$ ip route list
...
10.1.16.0/24 via 10.1.16.0 dev flannel.1 onlink
...
- fdb信息:MAC地址为flannel的mac地址,发往10.1.16.0的数据包都将通过vxlan首先发往目的地址192.168.0.101,即主机B
$ ip neigh show dev flannel.1
10.1.16.0 lladdr 46:1b:96:8c:b0:cf PERMANENT
$ bridge fdb show flannel.1 | grep46:1b:96:8c:b0:cf
46:1b:96:8c:b0:cf dev flannel.1 dst 192.168.0.101 self permanent
- arp信息:网关地址10.1.16.0的MAC地址为flannel的mac地址
[root@k8s-m2 ~]# arp -v
Address HWtype HWaddress Flags Mask Iface
....
10.1.16.0 ether 46:1b:96:8c:b0:cf CM flannel.1
....
参数说明:
- Network flannel使用CIDR格式(10.244.0.0/16)的网络地址,用于为pod的配置网络功能
- SubnetLen表示每个主机分配的subnet大小,我们可以在初始化时对其指定,否则使用默认配置。在默认配置的情况下SubnetLen配置为24(表示24位子网掩码)。
- SubnetMin是集群网络地址空间中最小的可分配的subnet,可以手动指定,否则默认配置为集群网络地址空间中第一个可分配的subnet。例如对于”10.244.0.0/16″,当SubnetLen为24时,第一个可分配的subnet为”10.244.1.0/24″
- SubnetMax表示最大可分配的subnet,对于”10.244.0.0/16″,当subnetLen为24时,SubnetMax为”10.244.255.0/24″
- Backend.Type 为flannel指定使用的backend的类型,类型分三种:vxlan、host-gw、udp,如未指定,则默认为“vxlan”
- –ip-masq=true 为网桥上的IP地址开启IP伪装(代表需要为其配置SNAT)
注意:Backend为vxlan时,其中会存储vtep设备的mac地址至etcd中
抓包分析
以flannel常用的vxlan模式发送icmp包为例,ping不在同一节点上的pod。如下,default命名空间下的两个nginx pod分别位于不同的节点上。
[root@k8s-m1 k8s-total]# kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
liveness-exec-pod 1/1 Running 1 4d18h 10.244.0.10 k8s-m2 <none> <none>
my-nginx-5b8555d6b8-cdqbm 1/1 Running 1 4d18h 10.244.0.13 k8s-m2 <none> <none>
my-nginx-5b8555d6b8-vxcsj 1/1 Running 1 7d1h 10.244.2.7 k8s-m1 <none> <none>
由于只有liveness-exec-pod 这个容器自带有ping命名,我就用10.244.0.10 这个pod去ping另一个节点上的 10.244.2.7 这个ip的pod,如下:
#查看对应节点上生成的veth网卡,在如上的k8s-m2上
[root@k8s-m1 k8s-total]# kubectl exec -it liveness-exec-pod -- /bin/sh
/ # cat /sys/class/net/eth0/iflink
20
/ # route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.244.0.1 0.0.0.0 UG 0 0 0 eth0
10.244.0.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
10.244.0.0 10.244.0.1 255.255.0.0 UG 0 0 0 eth0
#注意此次的eth0是容器内部的eth0
/ # ping 10.244.2.7
PING 10.244.2.7 (10.244.2.7): 56 data bytes
64 bytes from 10.244.2.7: seq=0 ttl=62 time=0.355 ms
64 bytes from 10.244.2.7: seq=1 ttl=62 time=0.436 ms
64 bytes from 10.244.2.7: seq=2 ttl=62 time=0.309 ms
#一直ping着
到k8s-m2进行抓包,抓取对应的网卡
#先查看路由,到10.244.2.0开始是到网卡flannel.1
[root@k8s-m2 ~]# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.2.254 0.0.0.0 UG 0 0 0 ens32
10.244.0.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0
10.244.1.0 10.244.1.0 255.255.255.0 UG 0 0 0 flannel.1
10.244.2.0 10.244.2.0 255.255.255.0 UG 0 0 0 flannel.1
169.254.0.0 0.0.0.0 255.255.0.0 U 1002 0 0 ens32
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker_gwbridge
192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0 ens32
[root@k8s-m2 ~]# ip a
17: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
link/ether da:fa:5e:60:ad:7c brd ff:ff:ff:ff:ff:ff
inet 10.244.0.0/32 scope global flannel.1
valid_lft forever preferred_lft forever
.....
20: vethc1f65864@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
link/ether 66:55:12:cd:be:bb brd ff:ff:ff:ff:ff:ff link-netnsid 5
21: vethe06e914a@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
link/ether 1e:d7:3e:c5:e1:e8 brd ff:ff:ff:ff:ff:ff link-netnsid 6
22: veth0e3aa9d1@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
link/ether 1a:16:64:f3:d5:8c brd ff:ff:ff:ff:ff:ff link-netnsid 7
23: veth3c3c8697@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
link/ether 3e:5b:82:c3:c4:44 brd ff:ff:ff:ff:ff:ff link-netnsid 8
24: veth131f9e56@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
link/ether 06:af:26:b3:fa:17 brd ff:ff:ff:ff:ff:ff link-netnsid 9
[root@k8s-m2 ~]# tcpdump -i vethc1f65864 -p icmp -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on vethc1f65864, link-type EN10MB (Ethernet), capture size 262144 bytes
14:38:32.734140 IP 10.244.0.10 > 10.244.0.13: ICMP echo request, id 32555, seq 265, length 64
14:38:32.734242 IP 10.244.0.13 > 10.244.0.10: ICMP echo reply, id 32555, seq 265, length 64
14:38:33.734364 IP 10.244.0.10 > 10.244.0.13: ICMP echo request, id 32555, seq 266, length 64
14:38:33.734431 IP 10.244.0.13 > 10.244.0.10: ICMP echo reply, id 32555, seq 266, length 64
14:38:34.734583 IP 10.244.0.10 > 10.244.0.13: ICMP echo request, id 32555, seq 267, length 64
14:38:34.734681 IP 10.244.0.13 > 10.244.0.10: ICMP echo reply, id 32555, seq 267, length 64
^C
[root@k8s-m2 ~]# tcpdump -i cni0 -p icmp -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on cni0, link-type EN10MB (Ethernet), capture size 262144 bytes
15:06:25.449188 IP 10.244.0.10 > 10.244.2.7: ICMP echo request, id 3358, seq 15, length 64
15:06:25.449531 IP 10.244.2.7 > 10.244.0.10: ICMP echo reply, id 3358, seq 15, length 64
15:06:26.449342 IP 10.244.0.10 > 10.244.2.7: ICMP echo request, id 3358, seq 16, length 64
15:06:26.449604 IP 10.244.2.7 > 10.244.0.10: ICMP echo reply, id 3358, seq 16, length 64
[root@k8s-m2 ~]# tcpdump -i flannel.1 -p icmp -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on flannel.1, link-type EN10MB (Ethernet), capture size 262144 bytes
15:06:43.451706 IP 10.244.0.10 > 10.244.2.7: ICMP echo request, id 3358, seq 33, length 64
15:06:43.451957 IP 10.244.2.7 > 10.244.0.10: ICMP echo reply, id 3358, seq 33, length 64
[root@k8s-m2 ~]# tcpdump -i ens32 -p icmp -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens32, link-type EN10MB (Ethernet), capture size 262144 bytes
15:06:54.411530 IP 192.168.2.141 > 14.119.104.189: ICMP echo request, id 1, seq 21189, length 64
15:06:54.448813 IP 14.119.104.189 > 192.168.2.141: ICMP echo reply, id 1, seq 21189, length 64
在flannel的vxlan模式下不在同一节点上的pod,通过抓包分析结果如下:
- 容器内部的流量通过内部网卡eth0网桥先到达cni0网卡(上面是 cni0网卡地址是10.244.0.1)
- cni0上的流量再根据相应宿主机上的路由表,报文将送到flannel.1设备(10.244.0.0)
- flannel.1是一个tun设备,工作在三层,它接收到的是一个RAW IP包(无MAC信息)。它会查看数据包的目的ip,从flanneld获取对端隧道设备的必要信息,封装数据包并转发至eth0网卡。
- 随后eth0网卡会将发送至另一个pod所在的宿主机,该宿主机的网卡接收到数据包,发现数据包为overlay数据包,解开外层封装,并发送内层封装到flannel.1设备
- flannel.1 设备查看数据包,根据路由表匹配,将数据发送给cni0设备,然后转发给相应的pod。
处在同一节点上的pod其实是不会经过cni0和flannel的,直接通过docker0网桥进行转发。
更多关于kubernetes的知识分享,请前往博客主页。编写过程中,难免出现差错,敬请指出