目录

Kubernetes网络模型

kubernetes 网络模型及cni插件

在Kubernetes中设计了一种网络模型,要求无论容器运行在集群中的哪个节点,所有容器都能通过一个扁平的网络平面进行通信,即在同一IP网络中。需要注意的是:在K8S集群中,IP地址分配是以Pod对象为单位,而非容器,同一Pod内的所有容器共享同一网络名称空间。

Docker 的网络模型

​ 了解Docker的朋友们都应该清楚,Docker容器的原生网络模型主要有3种:Bridge(桥接)、Host(主机)、none。

  • Bridge:借助虚拟网桥设备为容器建立网络连接。
  • Host:设置容器直接共享当前节点主机的网络名称空间。
  • none:多个容器共享同一个网络名称空间

1、使用以下命令查看docker网络

1
2
3
4
5
docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
2d636f1440a9        bridge              bridge              local
f7a1594a8e7c        host                host                local
fa3518434b75        none                null                local

2、none网络,在该网络下的容器仅有lo网卡,属于封闭式网络,通常用于对安全性要求较高并且不需要联网的应用

1
2
3
4
5
6
7
8
9
docker run -it --network=none busybox
/ # ifconfig
lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

3、host网络,共享宿主机的网络名称空间,容器网络配置和host一致,但是存在端口冲突的问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
docker run -it --network=host busybox
/ # ipaddr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast qlen 1000
    link/ether 08:00:27:a4:54:60 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.197/24 brd 192.168.10.255 scope global noprefixroute enp0s3
       valid_lft forever preferred_lft forever
    inet6 fe80::97f7:6981:54d9:5f3e/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue 
    link/ether 02:42:66:59:f3:fb brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever

4、bridge网络,Docker安装完成时会创建一个名为docker0的linux bridge,不指定网络时,创建的网络默认为桥接网络,都会桥接到docker0上

1
2
3
4
yum install bridge-utils -y
brctl show
bridge name	bridge id		STP enabled	interfaces
docker0		8000.02426659f3fb	no	
  • 运行一个centos7容器
    1
    
    docker run -it centos:7 bash
    
  • 查看宿主机网桥
    1
    2
    3
    
    brctl show
    bridge name	bridge id		STP enabled	interfaces
    docker0		8000.02426659f3fb	no		veth4086dd7
    
  • 一个新的网络接口veth3f1b114桥接到了docker0上,veth3f1b114就是新创建的容器的虚拟网卡。进入容器查看其网络配置:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    docker exec -it 781b6c8e5aa7 bash
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
      link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
      inet 127.0.0.1/8 scope host lo
         valid_lft forever preferred_lft forever
    33: eth0@if34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
      link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
      inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
         valid_lft forever preferred_lft forever
    

​从上可以看到容器内有一个网卡eth0@if34,实际上eth0@if39和veth4086dd7是一对veth pair。veth pair是一种成对出现的特殊网络设备,可以想象它们由一根虚拟的网线进行连接的一对网卡,eth0@if34在容器中,veth4086dd7挂在网桥docker0上,最终的效果就是eth0@if34也挂在了docker0上。

​桥接式网络是目前较为流行和默认的解决方案。但是这种方案的弊端是无法跨主机通信的,仅能在宿主机本地进行,而解决该问题的方法就是NAT。所有接入到该桥接设备上的容器都会被NAT隐藏,它们发往Docker主机外部的所有流量都会经过源地址转换后发出,并且默认是无法直接接受节点之外的其他主机发来的请求。当需要接入Docker主机外部流量,就需要进行目标地址转换甚至端口转换将其暴露在外部网络当中。如下图:

https://tc.ctq6.cn/tc/1349539-20190312153957757-632072688.png

容器内的属于私有地址,需要在左侧的主机上的eth0上进行源地址转换,而右侧的地址需要被访问,就需要将eth0的地址进行NAT转换。SNAT—->DNAT

​ 这样的通信方式会比较麻烦,从而需要借助第三方的网络插件实现这样的跨主机通信的网络策略。

kubernetes 网络模型

在K8S上的网络通信包含以下几类:

  • 容器间的通信:同一个Pod内的多个容器间的通信,它们之间通过lo网卡进行通信。
  • Pod之间的通信:通过Pod IP地址进行通信。
  • Pod和Service之间的通信:Pod IP地址和Service IP进行通信,两者并不属于同一网络,实现方式是通过IPVS或iptables规则转发。
  • Service和集群外部客户端的通信,实现方式:Ingress、NodePort、Loadbalance

K8S网络的实现不是集群内部自己实现,而是依赖于第三方网络插件—-CNI(Container Network Interface)

flannel、calico、canel等是目前比较流行的第三方网络插件。

​虚拟网桥、多路复用(MacVLAN)、硬件交换(SR-IOV)

​无论是上面的哪种方式在容器当中实现,都需要大量的操作步骤,而K8S支持CNI插件进行编排网络,以实现Pod和集群网络管理功能的自动化。每次Pod被初始化或删除,kubelet都会调用默认的CNI插件去创建一个虚拟设备接口附加到相关的底层网络,为Pod去配置IP地址、路由信息并映射到Pod对象的网络名称空间。

​在配置Pod网络时,kubelet会在默认的/etc/cni/net.d/目录中去查找CNI JSON配置文件,然后通过type属性到/opt/cni/bin中查找相关的插件二进制文件,如下面的"portmap"。然后CNI插件调用IPAM插件(IP地址管理插件)来配置每个接口的IP地址:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[root@localhost ~]# cat /etc/cni/net.d/10-flannel.conflist 
{
  "name": "cbr0",
  "cniVersion": "0.3.1",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}

​CNI主要是定义容器网络模型规范,链接容器管理系统和网络插件,两者主要通过上面的JSON格式文件进行通信,实现容器的网络功能。CNI的主要核心是:在创建容器时,先创建好网络名称空间(netns),然后调用CNI插件为这个netns配置网络,最后在启动容器内的进程。

​常见的CNI网络插件包含以下几种:

  • Flannel:为Kubernetes提供叠加网络的网络插件,基于TUN/TAP隧道技术,使用UDP封装IP报文进行创建叠 加网络,借助etcd维护网络的分配情况,缺点:无法支持网络策略访问控制。
  • Calico:基于BGP的三层网络插件,也支持网络策略进而实现网络的访问控制;它在每台主机上都运行一个虚拟路由,利用Linux内核转发网络数据包,并借助iptables实现防火墙功能。实际上Calico最后的实现就是将每台主机都变成了一台路由器,将各个网络进行连接起来,实现跨主机通信的功能。
  • Canal:由Flannel和Calico联合发布的一个统一网络插件,提供CNI网络插件,并支持网络策略实现。
  • 其他的还包括Weave Net、Contiv、OpenContrail、Romana、NSX-T、kube-router等等。而Flannel和Calico是目前最流行的选择方案.

flannel 网络插件

在各节点上的Docker主机在docker0上默认使用同一个子网,不同节点的容器都有可能会获取到相同的地址,那么在跨节点通信时就会出现如下问题:

  • 地址冲突的问题。
  • 并且在多个节点上的docker0使用不同的子网,也会因为没有准确的路由信息导致无法准确送达报文。

针对以上两个问题flannel的解决办法如下: 1、预留一个使用网络,如10.244.0.0/16,然后自动为每个节点的Docker容器引擎分配一个子网,如10.244.1.0/24和10.244.2.0/24,并将分配信息保存在etcd持久存储。 2、Flannel采用不同类型的后端网络模型进行处理

  • VxLAN:使用内核中的VxLAN模块进行封装报文。也是flannel推荐的方式,其报文格式如下 https://tc.ctq6.cn/tc/1349539-20190312153144413-1844597225.png
  • host-gw:即Host GateWay,通过在节点上创建目标容器地址的路由直接完成报文转发,要求各节点必须在同一个2层网络,对报文转发性能要求较高的场景使用。
  • UDP:使用普通的UDP报文封装完成隧道转发

由于官方已经不推荐使用 UDP 模式,性能相对较差。这里不做介绍,只介绍上面其他的两种网络模型

Vxlan

​VxLAN(Virtual extensible Local Area Network)虚拟可扩展局域网,采用MAC in UDP封装方式,具体的实现方式为:

1、将虚拟网络的数据帧添加到VxLAN首部,封装在物理网络的UDP报文中 2、以传统网络的通信方式传送该UDP报文 3、到达目的主机后,去掉物理网络报文的头部信息以及VxLAN首部,并交付给目的终端

跨节点的Pod之间的通信就是以上的一个过程,整个过程中通信双方对物理网络是没有感知的。如下网络图 https://tc.ctq6.cn/tc/1349539-20190312153127876-1058842025.png

VxLan设计原理如下:

在现有的三层网络之上,“覆盖”一层虚拟的、由内核VxLAN模块负责维护的二层网络,使得连接在这个VxLAN二层网络上的“主机”(虚拟机或容器都可以),可以像在同一个局域网(LAN)里那样自由通信。 为了能够在二层网络上打通“隧道”,VxLAN会在宿主机上设置一个特殊的网络设备作为“隧道”的两端,叫VTEP:VxLAN Tunnel End Point(虚拟隧道端点),flannel.1设备,就是VxLAN的VTEP,即有IP地址。为了能够将“原始IP包”封装并发送到正常的主机,VxLAN需要找到隧道的出口:宿主机的VTEP设备,这个设备信息,由宿主机的flanneld进程维护。

VTEP设备之间通过二层数据桢进行通信,源VTEP设备收到原始IP包后,在上面加上一个目的MAC地址,封装成一个L2数据桢,发送给目的VTEP设备(获取 MAC地址需要通过三层IP地址查询,这是ARP表的功能)

https://tc.ctq6.cn/tc/662544-20191022103148437-452134686.png

封装过程只是加了一个二层头,不会改变“原始IP包”的内容 这些VTEP设备的MAC地址,对宿主机网络来说没什么实际意义,称为内部数据桢,并不能在宿主机的二层网络传输,Linux内核还需要把它进一步封装成为宿主机的一个普通的数据桢,好让它带着“内部数据桢”通过宿主机的eth0进行传输,Linux会在内部数据桢前面,加上一个VxLAN头,VxLAN头里有一个重要的标志叫VNI,它是VTEP识别某个数据桢是不是应该归自己处理的重要标识。 在Flannel中,VNI的默认值是1,这也是为什么宿主机的VTEP设备都叫flannel.1的原因

一个flannel.1设备只知道另一端flannel.1设备的MAC地址,却不知道对应的宿主机地址是什么。 在linux内核里面,网络设备进行转发的依据,来自FDB的转发数据库,这个flannel.1网桥对应的FDB信息,是由flanneld进程维护的,linux内核再在IP包前面加上二层数据桢头,把Node2的MAC地址填进去。这个MAC地址本身,是Node1的ARP表要学习的,需 Flannel维护,这时候Linux封装的“外部数据桢”的格式如下

https://tc.ctq6.cn/tc/662544-20191022103125394-40388131.png

然后Node1的flannel.1设备就可以把这个数据桢从eth0发出去,再经过宿主机网络来到Node2的eth0,Node2的内核网络栈会发现这个数据桢有VxLAN Header,并且VNI为1,Linux内核会对它进行拆包,拿到内部数据桢,根据VNI的值,所它交给Node2的flannel.1设备

flannel 部署

​VxLAN的部署可以直接在官方上找到其YAML文件,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
kca -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
Warning: policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
podsecuritypolicy.policy/psp.flannel.unprivileged created
clusterrole.rbac.authorization.k8s.io/flannel created
clusterrolebinding.rbac.authorization.k8s.io/flannel created
serviceaccount/flannel created
configmap/kube-flannel-cfg created
daemonset.apps/kube-flannel-ds created
# 查看flannel 运行状态
[root@localhost ~]# kubectl get daemonset -n kube-system
NAME              DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
kube-flannel-ds   1         1         1       1            1           <none>                   67m
kube-proxy        1         1         1       1            1           kubernetes.io/os=linux   104m

运行正常后,flanneld会在宿主机的/etc/cni/net.d目录下生成自已的配置文件,kubelet将会调用它。

网络插件运行成功后,Node状态才Ready

1
2
3
4
root@k8s01 ~]# kubectl get node
NAME    STATUS   ROLES    AGE    VERSION
k8s01   Ready    master   35m    v1.19.8
k8s02   Ready    <none>   4m5s   v1.19.8

flannel运行后,在各Node宿主机多了一个网络接口:

1
2
3
4
5
6
7
8
9
[root@localhost ~]# 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
        inet6 fe80::fc50:86ff:fe05:94f4  prefixlen 64  scopeid 0x20<link>
        ether fe:50:86:05:94:f4  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 8 overruns 0  carrier 0  collisions 0

从上面的结果可以知道:

  • flannel默认就是VXLAN模式,即Overlay Network。
  • flanneld创建了一个flannel.1接口,它是专门用来封装隧道协议的,默认分给集群的Pod网段为10.244.0.0/16。
  • flannel给k8s-master节点配置的Pod网络为10.244.0.0段,给k8s-node01节点配置的Pod网络为10.244.1.0段,给k8s-node01节点配置的Pod网络为10.244.2.0段,如果有更多的节点,以此类推

验证flannel 网络模型

1、启动一个容器

1
for i in `seq 1 8`;do kubectl run busybox-$i --image=busybox --command sleep 10000000;done

2、查看pod

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[root@k8s01 ~]# kubectl get pods -o wide
NAME        READY   STATUS    RESTARTS   AGE     IP            NODE    NOMINATED NODE   READINESS GATES
busybox-1   1/1     Running   0          2m26s   10.244.1.22   k8s02   <none>           <none>
busybox-2   1/1     Running   0          2m26s   10.244.1.23   k8s02   <none>           <none>
busybox-3   1/1     Running   0          2m26s   10.244.1.24   k8s02   <none>           <none>
busybox-4   1/1     Running   0          2m26s   10.244.1.25   k8s02   <none>           <none>
busybox-5   1/1     Running   0          2m26s   10.244.1.26   k8s02   <none>           <none>
busybox-6   1/1     Running   0          2m26s   10.244.1.27   k8s02   <none>           <none>
busybox-7   1/1     Running   0          2m25s   10.244.0.11   k8s01   <none>           <none>
busybox-8   1/1     Running   0          2m25s   10.244.0.12   k8s01   <none>           <none>

3、可以看到,6个Pod都分别运行在node节点之上,2个pod运行在master上,在node节点上查看网络接口可以发现在各个节点上多了一个虚拟接口cni0,其ip地址为10.244.1.1。它是由flanneld创建的一个虚拟网桥叫cni0,在Pod本地通信使用。 这里需要注意的是,cni0虚拟网桥,仅作用于本地通信!!!!

1
2
3
4
5
6
7
8
9
[root@k8s02 ~]# ifconfig cni0
cni0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
        inet 10.244.1.1  netmask 255.255.255.0  broadcast 10.244.1.255
        inet6 fe80::2c60:e5ff:fe48:506b  prefixlen 64  scopeid 0x20<link>
        ether 2e:60:e5:48:50:6b  txqueuelen 1000  (Ethernet)
        RX packets 4  bytes 112 (112.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 5  bytes 446 (446.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

4、flanneld为每个Pod创建一对veth虚拟设备,一端放在容器接口上,一端放在cni0桥上。 使用brctl查看该网桥:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@k8s02 ~]# brctl show cni0
bridge name	bridge id		STP enabled	interfaces
cni0		8000.2e60e548506b	no		veth2cffb815
							veth30af0abc
							veth3e3daf57
							veth648e5bfd
							veth94d7d5fe
							vethbeeb4a05


#宿主机上测试和pod通信情况
[root@k8s01 ~]# ping 10.244.1.22
PING 10.244.1.22 (10.244.1.22) 56(84) bytes of data.
64 bytes from 10.244.1.22: icmp_seq=1 ttl=63 time=0.592 ms
64 bytes from 10.244.1.22: icmp_seq=2 ttl=63 time=0.388 ms
64 bytes from 10.244.1.22: icmp_seq=3 ttl=63 time=0.395 ms
64 bytes from 10.244.1.22: icmp_seq=4 ttl=63 time=0.381 ms
64 bytes from 10.244.1.22: icmp_seq=5 ttl=63 time=0.384 ms
64 bytes from 10.244.1.22: icmp_seq=6 ttl=63 time=0.382 ms
64 bytes from 10.244.1.22: icmp_seq=7 ttl=63 time=0.382 ms
64 bytes from 10.244.1.22: icmp_seq=8 ttl=63 time=0.351 ms
64 bytes from 10.244.1.22: icmp_seq=9 ttl=63 time=0.358 ms

5、在现有的Flannel VxLAN网络中,主机上的Pod间通信,也是正常的,如master节点上的Pod访问另外一个pod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[root@k8s01 ~]# kexec -it   busybox-7 -n default sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
/ # ping 10.244.1.22
PING 10.244.1.22 (10.244.1.22): 56 data bytes
64 bytes from 10.244.1.22: seq=0 ttl=62 time=0.647 ms
64 bytes from 10.244.1.22: seq=1 ttl=62 time=0.458 ms
64 bytes from 10.244.1.22: seq=2 ttl=62 time=0.482 ms
64 bytes from 10.244.1.22: seq=3 ttl=62 time=0.428 ms
64 bytes from 10.244.1.22: seq=4 ttl=62 time=0.415 ms
^C
--- 10.244.1.22 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.415/0.486/0.647 ms

6、可以看到是可以正常通信的,master上查看路由表信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 查看设备接口
[root@k8s01 ~]# ip r
default via 172.28.95.253 dev eth0 
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1 
10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink 
169.254.0.0/16 dev eth0 scope link metric 1002 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 
172.28.80.0/20 dev eth0 proto kernel scope link src 172.28.81.7 
# 查看路由表
[root@k8s01 ~]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.28.95.253   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 cni0
10.244.1.0      10.244.1.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 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
172.28.80.0     0.0.0.0         255.255.240.0   U     0      0        0 eth0

7、抓包分析

  • 单只有一个结点时: 发送到10.244.0.0/24网段的数据报文不会经过flannel网卡,而是直接发给本机的cni0接口,发送数据到对应veth接口上。使用tcpdump进行 抓一下包,如下:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    [root@k8s01 ~]# tcpdump -i flannel.1 -nn host 10.244.0.12
    tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
    listening on flannel.1, link-type EN10MB (Ethernet), capture size 262144 bytes
    ^C
    0 packets captured
    0 packets received by filter
    0 packets dropped by kernel
    [root@k8s01 ~]# tcpdump -i cni0 -nn host 10.244.0.12
    tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
    listening on cni0, link-type EN10MB (Ethernet), capture size 262144 bytes
    18:09:29.380824 IP 10.244.0.1 > 10.244.0.12: ICMP echo request, id 17565, seq 19, length 64
    18:09:29.380858 IP 10.244.0.12 > 10.244.0.1: ICMP echo reply, id 17565, seq 19, length 64
    18:09:30.380828 IP 10.244.0.1 > 10.244.0.12: ICMP echo request, id 17565, seq 20, length 64
    18:09:30.380863 IP 10.244.0.12 > 10.244.0.1: ICMP echo reply, id 17565, seq 20, length 64
    ^C
    12 packets captured
    12 packets received by filter
    0 packets dropped by kernel
    
  • 当有多个节点时 发送到10.244.1.0/24的网段的数据报文发给本机的flannel接口,即进入二层隧道,然后对数据报文进行封装(封装VxLAN首部–>UDP首部–>IP首部–>以太网首部),到达目标Node节点后,由目标Node上的flannel.1进行解封装。使用tcpdump进行 抓一下包,如下:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    [root@k8s01 ~]# tcpdump -i flannel.1 -nn host 10.244.1.22
    tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
    listening on flannel.1, link-type EN10MB (Ethernet), capture size 262144 bytes
    18:18:44.586179 IP 10.244.0.11 > 10.244.1.22: ICMP echo request, id 20, seq 20, length 64
    18:18:44.586572 IP 10.244.1.22 > 10.244.0.11: ICMP echo reply, id 20, seq 20, length 64
    18:18:45.586322 IP 10.244.0.11 > 10.244.1.22: ICMP echo request, id 20, seq 21, length 64
    18:18:45.586688 IP 10.244.1.22 > 10.244.0.11: ICMP echo reply, id 20, seq 21, length 64
    
    ^C
    20 packets captured
    20 packets received by filter
    0 packets dropped by kernel
    

节点内通信

显然,节点内的容器间通信通过 cni0 网桥就能完成,不涉及任何VXLAN报文的封包解包。例如在下面的图例中,Node1的子网为10.244.0.1/24, PodA 10.244.0.11 和 PodB 10.224.0.12通过 cni0 网桥实现互通。

https://tc.ctq6.cn/tc/20220512215810.png

跨节点通信

不同node上的pod通信流程如下:

  • 1、pod中产生数据,根据pod的路由信息,将数据发送到Cni0
  • 2、Cni0 根据节点的路由表,将数据发送到隧道设备flannel.1
  • 3、Flannel.1查看数据包的目的ip,从flanneld获得对端隧道设备的必要信息,封装数据包。
  • 4、Flannel.1将数据包发送到对端设备。对端节点的网卡接收到数据包,发现数据包为overlay数据包,解开外层封装,并发送内层封装到flannel.1设备。
  • 5、Flannel.1设备查看数据包,根据路由表匹配,将数据发送给Cni0设备。
  • 6、Cni0匹配路由表,发送数据给网桥上对应的端口

下面重点分析一下跨节点的容器通信过程。假设有两个节点Node1和Node2,其中Node1的PodA要跟Node2的PodB通信,则它们之间的通信过程如下图所示:

https://tc.ctq6.cn/tc/20220512220737.png

大概总结一下整个过程:

  • 发送端:在PodA中发起 ping 10.244.1.22 ,ICMP 报文经过 cni0 网桥后交由 flannel.1 设备处理。 flannel.1 设备是一个VXLAN类型的设备,负责VXLAN封包解包。 因此,在发送端,flannel.1 将原始L2报文封装成VXLAN UDP报文,然后从 eth0 发送。
  • 接收端:Node2收到UDP报文,发现是一个VXLAN类型报文,交由 flannel.1 进行解包。根据解包后得到的原始报文中的目的IP,将原始报文经由 cni0 网桥发送给PodB

Flannel 封包过程

flanneld 从 etcd 中可以获取所有节点的子网情况,以此为依据为各节点配置路由,将属于非本节点的子网IP都路由到 flannel.1 处理,本节点的子网路由到 cni0 网桥处理,如果节点信息有变化, flanneld 也会同步的对路由信息做修改。

VXLAN的封包是将二层以太网帧封装到四层UDP报文中的过程

原始L2帧

先来看一下vxlan的报文形式

https://tc.ctq6.cn/tc/20220512233939.png

https://tc.ctq6.cn/tc/20220512234113.png

要生成原始的L2帧, flannel.1 需要得知:

  • 内层源/目的IP地址
  • 内层源/目的MAC地址

内层的源/目的IP地址是已知的,即为PodA/PodB的PodIP,在图例中,分别为10.224.0.11和10.224.1.23。 内层源/目的MAC地址要结合路由表和ARP表来获取。根据路由表得知:

1、下一跳地址是10.224.1.0,关联ARP表,得到下一跳的MAC地址,也就是目的MAC地址:Node2_flannel.1_MAC; 2、报文要从 flannel.1 虚拟网卡发出,因此源MAC地址为 flannel.1 的MAC地址。

要注意的是,这里ARP表的表项并不是通过ARP学习得到的,而是 flanneld 预先为每个节点设置好的,由 flanneld负责维护,没有过期时间。

1
2
[root@k8s01 ~]#  ip n |grep flannel
10.244.1.0 dev flannel.1 lladdr 66:b0:1e:b3:71:e6 PERMANENT

有了上面的信息,flannel.1 就可以构造出内层的2层以太网帧:

https://tc.ctq6.cn/tc/20220512222126.png

外层VXLAN UDP报文

要将原始L2帧封装成VXLAN UDP报文,flannel.1 还需要填充源/目的IP地址。前面提到,VTEP是VXLAN隧道的起点或终点。因此,目的IP地址即为对端VTEP的IP地址,通过FDB表获取。在FDB表中,dest字段表示的即为VXLAN隧道目的端点(对端VTEP)的IP地址,也就是VXLAN UDP报文的目的IP地址。FDB表也是由 flanneld 在每个节点上预设并负责维护的。

FDB表(Forwarding database)用于保存二层设备中MAC地址和端口的关联关系,就像交换机中的MAC地址表一样。在二层设备转发二层以太网帧时,根据FDB表项来找到对应的端口。例如cni0网桥上连接了很多veth pair网卡,当网桥要将以太网帧转发给Pod时,FDB表根据Pod网卡的MAC地址查询FDB表,就能找到其对应的veth网卡,从而实现联通

可以使用 bridge fdb show 查看FDB表:

1
2
[root@k8s01 ~]# bridge fdb show | grep flannel.1
66:b0:1e:b3:71:e6 dev flannel.1 dst 172.28.81.9 self permanent

源IP地址信息来自于 flannel.1 网卡设置本身,根据 local 192.168.50.2 可以得知源IP地址为192.168.50.2。

1
2
3
4
5
6
7
8
[root@k8s01 ~]# ip -d a show flannel.1
4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default 
    link/ether 66:c0:b5:49:b0:ce brd ff:ff:ff:ff:ff:ff promiscuity 0 
    vxlan id 1 local 172.28.81.7 dev eth0 srcport 0 0 dstport 8472 nolearning ageing 300 noudpcsum noudp6zerocsumtx noudp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 
    inet 10.244.0.0/32 scope global flannel.1
       valid_lft forever preferred_lft forever
    inet6 fe80::64c0:b5ff:fe49:b0ce/64 scope link 
       valid_lft forever preferred_lft forever

至此, flannel.1 已经得到了所有完成VXLAN封包所需的信息,最终通过 eth0 发送一个VXLAN UDP报文:

https://tc.ctq6.cn/tc/20220512223330.png

Flannel的VXLAN模式通过静态配置路由表,ARP表和FDB表的信息,结合VXLAN虚拟网卡 flannel.1 ,实现了一个所有Pod同属一个大二层网络的VXLAN网络模型。

​VXLAN是Linux内核本身支持的一种网络虚拟化技术,是内核的一个模块,在内核态实现封装解封装,构建出覆盖网络,其实就是一个由各宿主机上的Flannel.1设备组成的虚拟二层网络。

​由于VXLAN由于额外的封包解包,导致其性能较差,所以Flannel就有了host-gw模式,即把宿主机当作网关,除了本地路由之外没有额外开销,性能和calico差不多,由于没有叠加来实现报文转发,这样会导致路由表庞大。因为一个节点对应一个网络,也就对应一条路由条目。

​host-gw虽然VXLAN网络性能要强很多,但是种方式有个缺陷: 要求各物理节点必须在同一个二层网络中。物理节点必须在同一网段中。这样会使得一个网段中的主机量会非常多,万一发一个广播报文就会产生干扰。在私有云场景下,宿主机不在同一网段是很常见的状态,所以就不能使用host-gw了。

​VXLAN还有另外一种功能,VXLAN也支持类似host-gw的玩法,如果两个节点在同一网段时使用host-gw通信,如果不在同一网段中,即 当前pod所在节点与目标pod所在节点中间有路由器,就使用VXLAN这种方式,使用叠加网络。 结合了Host-gw和VXLAN,这就是VXLAN的Direct routing模式

VxLAN的Direct routing模式

修改kube-flannel.yml文件,将flannel的configmap对象改为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[root@k8s01 ~]# vim kube-flannel.yml
  net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "vxlan",
        "Directrouting": true
      }
    }
[root@k8s01 ~]# kca -f kube-flannel.yml 
podsecuritypolicy.policy/psp.flannel.unprivileged configured
clusterrole.rbac.authorization.k8s.io/flannel unchanged
clusterrolebinding.rbac.authorization.k8s.io/flannel unchanged
serviceaccount/flannel unchanged
configmap/kube-flannel-cfg configured
daemonset.apps/kube-flannel-ds unchanged

# 查看路由信息
[root@k8s01 ~]# ip route
default via 172.28.95.253 dev eth0 
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1 
10.244.1.0/24 via 172.28.81.9 dev eth0 
169.254.0.0/16 dev eth0 scope link metric 1002 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 
172.28.80.0/20 dev eth0 proto kernel scope link src 172.28.81.7

[root@k8s02 ~]# ip route
default via 172.28.95.253 dev eth0 
10.244.0.0/24 via 172.28.81.7 dev eth0 
10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1 
169.254.0.0/16 dev eth0 scope link metric 1002 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 
172.28.80.0/20 dev eth0 proto kernel scope link src 172.28.81.9

​从上面的结果可以看到,发往10.244.1.0/24和10.244.1.0/24的包都是直接经过eth0网络接口直接发出去的,这就是Directrouting。如果两个节点是跨网段的,则flannel自动降级为VxLAN模式。

​此时,在各个集群节点上执行“iptables -nL”命令 可以 看到, iptables filter 表 的 FORWARD 链 上 由其 生成了如下两条转发规则, 它显式放行了10.244.0.0/16网络进出 的所有报文,用于确保由物理接口接收或发送的目标地址或源地址为10.244.0.0/16网络的所有报文均能够正常通行。这些是Direct Routing模式得以实现的必要条件:

1
2
ACCEPT     all  --  10.244.0.0/16        0.0.0.0/0            /* flanneld forward */
ACCEPT     all  --  0.0.0.0/0            10.244.0.0/16        /* flanneld forward */

​再在此之前创建的Pod和宿主机上进行ping测试,可以看到在flannel.1接口上已经抓不到包了,在eth0上可以用抓到ICMP的包,如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[root@k8s01 ~]# tcpdump -i flannel.1 -nn host 10.244.1.22
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on flannel.1, link-type EN10MB (Ethernet), capture size 262144 bytes
^C
0 packets captured
0 packets received by filter
0 packets dropped by kernel

[root@k8s01 ~]# tcpdump -i eth0 -nn host 10.244.1.22
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
23:19:27.475984 IP 10.244.0.11 > 10.244.1.22: ICMP echo request, id 59, seq 56, length 64
23:19:28.476118 IP 10.244.0.11 > 10.244.1.22: ICMP echo request, id 59, seq 57, length 64
23:19:29.476269 IP 10.244.0.11 > 10.244.1.22: ICMP echo request, id 59, seq 58, length 64
23:19:30.476465 IP 10.244.0.11 > 10.244.1.22: ICMP echo request, id 59, seq 59, length 64
23:19:31.476611 IP 10.244.0.11 > 10.244.1.22: ICMP echo request, id 59, seq 60, length 64
23:19:32.476748 IP 10.244.0.11 > 10.244.1.22: ICMP echo request, id 59, seq 61, length 64
^C
6 packets captured
14 packets received by filter
0 packets dropped by kernel

host-gw

​Flannel除了上面2种数据传输的方式以外,还有一种是host-gw的方式,host-gw后端是通过添加必要的路由信息使用节点的二层网络直接发送Pod的通信报文。它的工作方式类似于Directrouting的功能,但是其并不具备VxLan的隧道转发能力。

编辑kube-flannel的配置清单,将ConfigMap资源kube-flannel-cfg的data字段中网络配置进行修改,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[root@k8s01 ~]# vim kube-flannel.yml
  net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "host-gw"
      }
    }
[root@k8s01 ~]# kca -f kube-flannel.yml 
podsecuritypolicy.policy/psp.flannel.unprivileged configured
clusterrole.rbac.authorization.k8s.io/flannel unchanged
clusterrolebinding.rbac.authorization.k8s.io/flannel unchanged
serviceaccount/flannel unchanged
configmap/kube-flannel-cfg configured
daemonset.apps/kube-flannel-ds unchanged

配置完成后,各节点会生成类似directrouting一样的 路由和iptables规则,用于实现二层转发Pod网络的通信报文,省去了隧道转发模式的额外开销。但是存在的问题点是,对于不在同一个二层网络的报文转发,host-gw是无法实现的。延续上面的例子,进行抓包查看:

master上的Pod:10.244.0.11向node01节点上的Pod:10.244.1.22发送ICMP报文

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[root@k8s01 ~]# ip route
default via 172.28.95.253 dev eth0 
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1 
10.244.1.0/24 via 172.28.81.9 dev eth0 
169.254.0.0/16 dev eth0 scope link metric 1002 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 
172.28.80.0/20 dev eth0 proto kernel scope link src 172.28.81.7

[root@k8s02 ~]# ip route
default via 172.28.95.253 dev eth0 
10.244.0.0/24 via 172.28.81.7 dev eth0 
10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1 
169.254.0.0/16 dev eth0 scope link metric 1002 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 
172.28.80.0/20 dev eth0 proto kernel scope link src 172.28.81.9

#进行ping包测试
[root@k8s01 ~]# ping 10.244.1.22

#在eth0上进行抓包
[root@k8s01 ~]# tcpdump -i eth0 -nn host 10.244.1.22
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
23:25:52.985827 IP 172.28.81.7 > 10.244.1.22: ICMP echo request, id 1, seq 71, length 64
23:25:53.985836 IP 172.28.81.7 > 10.244.1.22: ICMP echo request, id 1, seq 72, length 64
23:25:54.985828 IP 172.28.81.7 > 10.244.1.22: ICMP echo request, id 1, seq 73, length 64
23:25:55.985827 IP 172.28.81.7 > 10.244.1.22: ICMP echo request, id 1, seq 74, length 64
23:25:56.985838 IP 172.28.81.7 > 10.244.1.22: ICMP echo request, id 1, seq 75, length 64
23:25:57.985847 IP 172.28.81.7 > 10.244.1.22: ICMP echo request, id 1, seq 76, length 64
^C
6 packets captured
17 packets received by filter
0 packets dropped by kernel

该模式下,报文转发的相关流程如下:

  • 1、Pod(10.244.0.17)向Pod(10.244.1.146)发送报文,查看到报文中的目的地址为:10.244.1.146,并非本地网段,会直接发送到网关(192.168.56.11);

  • 2、网关发现该目标地址为10.244.1.146,要到达10.244.1.0/24网段,需要送达到node2 的物理网卡,node2接收以后发现该报文的目标地址属于本机上的另一个虚拟网卡,然后转发到相对应的Pod(10.244.1.146)

工作模式流程图如下:

https://tc.ctq6.cn/tc/20220512233646.png

在host-gw模式下,由于不涉及VXLAN的封包解包,不再需要flannel.1虚机网卡。 flanneld 负责为各节点设置路由 ,将对应节点Pod子网的下一跳地址指向对应的节点的IP,如图中路由表所示。

由于没有封包解包带来的消耗,host-gw是性能最好的。不过一般在云环境下,都不支持使用host-gw的模式,在私有化部署的场景下,可以考虑

​以上就是Flannel网络模型的三种工作模式,但是flannel自身并不具备为Pod网络实现网络策略和网络通信隔离的功能,为此只能借助于Calico联合统一的项目Calnal项目进行构建网络策略的功能。

calico 插件

Calico 是一种容器之间互通的网络方案。在虚拟化平台中,比如 OpenStack、Docker 等都需要实现 workloads 之间互连,但同时也需要对容器做隔离控制,就像在 Internet 中的服务仅开放80端口、公有云的多租户一样,提供隔离和管控机制。而在多数的虚拟化平台实现中,通常都使用二层隔离技术来实现容器的网络,这些二层的技术有一些弊端,比如需要依赖 VLAN、bridge 和隧道等技术,其中 bridge 带来了复杂性,vlan 隔离和 tunnel 隧道则消耗更多的资源并对物理环境有要求,随着网络规模的增大,整体会变得越加复杂。我们尝试把 Host 当作 Internet 中的路由器,同样使用 BGP 同步路由,并使用 iptables 来做安全访问策略,最终设计出了 Calico 方案。

优势

  • 更优的资源利用 二层网络通讯需要依赖广播消息机制,广播消息的开销与 host 的数量呈指数级增长,Calico 使用的三层路由方法,则完全抑制了二层广播,减少了资源开销。

    另外,二层网络使用 VLAN 隔离技术,天生有 4096 个规格限制,即便可以使用 vxlan 解决,但 vxlan 又带来了隧道开销的新问题。而 Calico 不使用 vlan 或 vxlan 技术,使资源利用率更高。

  • 可扩展性 Calico 使用与 Internet 类似的方案,Internet 的网络比任何数据中心都大,Calico 同样天然具有可扩展性。

  • 简单而更容易 debug

    因为没有隧道,意味着 workloads 之间路径更短更简单,配置更少,在 host 上更容易进行 debug 调试。

  • 更少的依赖 Calico 仅依赖三层路由可达。

  • 可适配性 Calico 较少的依赖性使它能适配所有 VM、Container、白盒或者混合环境场景。

架构

https://tc.ctq6.cn/tc/1060878-20190413152300545-538840176.png

  • Felix:运行在每一台 Host 的 agent 进程,主要负责网络接口管理和监听、路由、ARP 管理、ACL 管理和同步、状态上报等。

  • etcd:分布式键值存储,主要负责网络元数据一致性,确保Calico网络状态的准确性,可以与kubernetes共用;

  • BGP Client(BIRD):Calico 为每一台 Host 部署一个 BGP Client,使用 BIRD 实现,BIRD 是一个单独的持续发展的项目,实现了众多动态路由协议比如 BGP、OSPF、RIP 等。在 Calico 的角色是监听 Host 上由 Felix 注入的路由信息,然后通过 BGP 协议广播告诉剩余 Host 节点,从而实现网络互通。

  • BGP Route Reflector:在大型网络规模中,如果仅仅使用 BGP client 形成 mesh 全网互联的方案就会导致规模限制,因为所有节点之间俩俩互联,需要 N^2 个连接,为了解决这个规模问题,可以采用 BGP 的 Router Reflector 的方法,使所有 BGP Client 仅与特定 RR 节点互联并做路由同步,从而大大减少连接数。

工作原理

Calico把每个操作系统的协议栈认为是一个路由器,然后把所有的容器认为是连在这个路由器上的网络终端,在路由器之间跑标准的路由协议——BGP的协议,然后让它们自己去学习这个网络拓扑该如何转发。所以Calico方案其实是一个纯三层的方案,也就是说让每台机器的协议栈的三层去确保两个容器,跨主机容器之间的三层连通性。

对于控制平面,它每个节点上会运行两个主要的程序,一个是Felix,它会监听ECTD中心的存储,从它获取事件,比如说用户在这台机器上加了一个IP,或者是分配了一个容器等。接着会在这台机器上创建出一个容器,并将其网卡、IP、MAC都设置好,然后在内核的路由表里面写一条,注明这个IP应该到这张网卡。另外一部分是一个标准的路由程序,它会从内核里面获取哪一些IP的路由发生了变化,然后通过标准BGP的路由协议扩散到整个其他的宿主机上,让外界都知道这个IP在这里,你们路由的时候得到这里来。

由于Calico是一种纯三层的实现,因此可以避免与二层方案相关的数据包封装的操作,中间没有任何的NAT,没有任何的overlay,所以它的转发效率可能是所有方案中最高的,因为它的包直接走原生TCP/IP的协议栈,它的隔离也因为这个栈而变得好做。因为TCP/IP的协议栈提供了一整套的防火墙的规则,所以它可以通过IPTABLES的规则达到比较复杂的隔离逻辑。

两种网络模式

1、IPIP

从字面来理解,就是把一个IP数据包又套在一个IP包里,即把 IP 层封装到 IP 层的一个 tunnel。它的作用其实基本上就相当于一个基于IP层的网桥!一般来说,普通的网桥是基于mac层的,根本不需 IP,而这个 ipip 则是通过两端的路由做一个 tunnel,把两个本来不通的网络通过点对点连接起来。

2、BGP

BGP(border gateway protocol)是外部路由协议(边界网关路由协议),用来在AS之间传递路由信息是一种增强的距离矢量路由协议(应用场景),基本功能是在自治系统间自动交换无环路的路由信息,通过交换带有自治系统号序列属性的路径可达信息,来构造自治系统的拓扑图,从而消除路由环路并实施用户配置的路由策略。

边界网关协议(Border Gateway Protocol, BGP)是互联网上一个核心的去中心化自治路由协议。它通过维护IP路由表或‘前缀’表来实现自治系统(AS)之间的可达性,属于矢量路由协议。BGP不使用传统的内部网关协议(IGP)的指标,而使用基于路径、网络策略或规则集来决定路由。因此,它更适合被称为矢量性协议,而不是路由协议。BGP,通俗的讲就是讲接入到机房的多条线路(如电信、联通、移动等)融合为一体,实现多线单IP,BGP 机房的优点:服务器只需要设置一个IP地址,最佳访问路由是由网络上的骨干路由器根据路由跳数与其它技术指标来确定的,不会占用服务器的任何系统。

实际上,Calico 项目提供的 BGP 网络解决方案,与 Flannel 的 host-gw 模式几乎一样。也就是说,Calico也是基于路由表实现容器数据包转发,但不同于Flannel使用flanneld进程来维护路由信息的做法,而Calico项目使用BGP协议来自动维护整个集群的路由信息

部署calico组件

1、下载calico安装文件,修改网段

1
curl -LO https://docs.projectcalico.org/archive/v3.15/manifests/calico.yaml

将配置文件中CALICO_IPV4POOL_CIDR注释取消,并将值修改为: 10.244.0.0/16

2、安装calico

1
kca -f calico.yaml

3、查看是否正常

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[root@localhost ~]# kubectl get ds -n kube-system
NAME          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
calico-node   1         1         1       1            1           kubernetes.io/os=linux   13m
kube-proxy    1         1         1       1            1           kubernetes.io/os=linux   43m

# 生成对应的tul0网桥和对应veth pair对
[root@localhost ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:a4:54:60 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.118/24 brd 192.168.10.255 scope global noprefixroute enp0s3
       valid_lft forever preferred_lft forever
    inet6 fe80::97f7:6981:54d9:5f3e/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:1b:a1:4b:4e brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
4: cali788093a9dfd@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP group default 
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link 
       valid_lft forever preferred_lft forever
5: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
    inet 10.244.73.64/32 brd 10.244.73.64 scope global tunl0
       valid_lft forever preferred_lft forever
6: cali01dd37f65d2@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP group default 
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link 
       valid_lft forever preferred_lft forever
7: calida0b2d07b71@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP group default 
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link 
       valid_lft forever preferred_lft forever

IPIP 工作模式

1、启动一个DomeSet资源,配置文件如下(nginx-des.yaml):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: nginx-daemonset
  labels:
    app: nginx-daemonset
spec:
  selector:
    matchLabels:
      app: nginx-daemonset
  template:
    metadata:
      labels:
        app: nginx-daemonset
    spec:
      containers:
      - name: nginx-daemonset
        image: nginx:alpine

2、查看pod运行情况

1
2
3
[root@iZj6cdsbgjsfiq4jznz7ziZ ~]# kubectl get po -n kube-system -o wide|grep nginx
nginx-daemonset-4cdfj                     1/1     Running   0          118s    10.244.73.68     k8s01   <none>           <none>
nginx-daemonset-djlj9                     1/1     Running   0          2m31s   10.244.236.129   k8s02   <none>           <none>

3、在master节点上的podA(10.244.73.68)测试与node(10.244.236.129)节点上的podB连通性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[root@iZj6cdsbgjsfiq4jznz7ziZ ~]# kexec -it nginx-daemonset-4cdfj -n kube-system sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
/ # ping 10.244.236.129
PING 10.244.236.129 (10.244.236.129): 56 data bytes
64 bytes from 10.244.236.129: seq=0 ttl=62 time=0.627 ms
64 bytes from 10.244.236.129: seq=1 ttl=62 time=0.431 ms
64 bytes from 10.244.236.129: seq=2 ttl=62 time=0.419 ms
64 bytes from 10.244.236.129: seq=3 ttl=62 time=0.513 ms
64 bytes from 10.244.236.129: seq=4 ttl=62 time=0.455 ms
64 bytes from 10.244.236.129: seq=5 ttl=62 time=0.410 ms
^C
--- 10.244.236.129 ping statistics ---
6 packets transmitted, 6 packets received, 0% packet loss
round-trip min/avg/max = 0.410/0.475/0.627 ms
跨节点访问过程

PodA 访问 PodB 流程如下:

  • 数据包从 PodA 出到达Veth Pair另一端(宿主机上,以cali前缀开头)。
  • 进入IP隧道设备(tunl0),由Linux内核IPIP驱动封装,把源容器ip换成源宿主机ip,目的容器ip换成目的主机ip,这样就封装成 k8s01 到 k8s02 的数据包。
    1
    2
    3
    4
    5
    6
    7
    8
    
    此时包的类型:
      原始IP包:
      源IP:10.244.73.68
      目的IP:10.244.236.129
    
      TCP:
      源IP: 172.28.81.11
      目的iP:172.28.81.12
    
  • 数据包经过路由器三层转发到 Node2。
  • k8s02 收到数据包后,网络协议栈会使用IPIP驱动进行解包,从中拿到原始IP包。
  • 然后根据路由规则,将数据包转发给cali设备,从而到达 PodB

https://tc.ctq6.cn/tc/20220513184238.png

实际流程如下:

1、进入master上的podA容器查看路由表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/ # route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         169.254.1.1     0.0.0.0         UG    0      0        0 eth0
169.254.1.1     0.0.0.0         255.255.255.255 UH    0      0        0 eth0

/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1440 qdisc noqueue state UP 
    link/ether 1e:b1:8f:36:03:43 brd ff:ff:ff:ff:ff:ff
    inet 10.244.73.68/32 scope global eth0
       valid_lft forever preferred_lft forever

eth0@if8 为在宿主机上创建的虚拟网桥的一端,另一端为 cali23efcac8988

1
2
[root@iZj6cdsbgjsfiq4jznz7ziZ ~]# route -n |grep 10.244.73.68
10.244.73.68    0.0.0.0         255.255.255.255 UH    0      0        0 cali23efcac8988

2、去往podB的数据包,由podA经过网桥设备转发到podA宿主机上,宿主机上查询路由表

1
2
[root@iZj6cdsbgjsfiq4jznz7ziZ ~]# route -n|grep 10.244.236
10.244.236.128  172.28.81.12    255.255.255.192 UG    0      0        0 tunl0

可以看到 podB (pod-ip 10.244.236.129) 的下一跳是172.28.81.12 ,也就是 podB 所在的宿主机ip地址,网络接口是 tunl0

tunl0 设备是一个ip隧道(ip tunnel)设备,技术原理是IPIP,ip包进入ip隧道后会被linux内核的IPIP驱动接管并重新封装(伪造)成去宿主机网络的ip包,然后发送出去。这样原始ip包经过封装后下一跳地址就是 172.28.81.12

3、数据包到达 podB 的宿主机 172.28.81.12 后,经过解包查找 10.244.236.129 的路由为网桥设备 calid733d03af9b

1
2
[root@k8s02 ~]# route -n |grep 10.244.236.129
10.244.236.129  0.0.0.0         255.255.255.255 UH    0      0        0 calid733d03af9b

4、最终 10.244.236.129 经过网桥设备 cali0393db3e4a6 被转发到容器 podB 内部,返回的数据包路径也是一样。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1440 qdisc noqueue state UP 
    link/ether 26:19:7d:29:38:d7 brd ff:ff:ff:ff:ff:ff
    inet 10.244.236.129/32 scope global eth0
       valid_lft forever preferred_lft forever

BGP 工作模式

全互联模式(node-to-node mesh)

全互联模式 每一个BGP Speaker都需要和其他BGP Speaker建立BGP连接,这样BGP连接总数就是N^2,如果数量过大会消耗大量连接。如果集群数量超过100台官方不建议使用此种模式

路由反射模式Router Reflection(RR)

RR模式 中会指定一个或多个BGP Speaker为RouterReflection,它与网络中其他Speaker建立连接,每个Speaker只要与Router Reflection建立BGP就可以获得全网的路由信息。在calico中可以通过Global Peer实现RR模式

修改calico 模式为BGP

1、修改calico配置文件, 配置如下内容

1
2
3
4
- name: CALICO_IPV4POOL_IPIP
value: Always
- name: CALICO_AUTODETECTION_METHOD
value: interface=eth0

2、重新安装calico服务

1
kca -f calico.yaml
跨节点访问过程如下

PodA 访问 PodB 流程如下

  • 数据包从 PodA 出到达Veth Pair另一端(宿主机上,以cali前缀开头)
  • 宿主机根据路由规则,将数据包转发给下一跳(网关)
  • 到达 Node2,根据路由规则将数据包转发给 cali 设备,从而到达 Pod2。

https://tc.ctq6.cn/tc/20220513185903.png

其中,这里最核心的 下一跳 路由规则,就是由 Calico 的 Felix 进程负责维护的。这些路由规则信息,则是通过 BGP Client 中 BIRD 组件,使用 BGP 协议来传输。

不难发现,Calico 项目实际上将集群里的所有节点,都当作是边界路由器来处理,它们一起组成了一个全连通的网络,互相之间通过 BGP 协议交换路由规则。这些节点,我们称为 BGP Peer。

而 Flannel host-gw 和 Calico 的唯一不一样的地方就是当数据包下一跳到达node2节点容器时发生变化,并且出数据包也发生变化,知道它是从veth的设备流出,容器里面的数据包到达宿主机上,这个数据包到达k8s02之后,它又根据一个特殊的路由规则,这个会记录目的通信地址的cni网络,然后通过cali设备进去容器,这个就跟网线一样,数据包通过这个网线发到容器中,这也是一个二层的网络互通才能实现

Route Reflector 模式(RR)(路由反射)

Calico 维护的网络在默认是 (Node-to-Node Mesh)全互联模式,Calico集群中的节点之间都会相互建立连接,用于路由交换。但是随着集群规模的扩大,mesh模式将形成一个巨大服务网格,连接数成倍增加。这时就需要使用 Route Reflector(路由器反射)模式解决这个问题。确定一个或多个Calico节点充当路由反射器,让其他节点从这个RR节点获取路由信息。

在BGP中可以通过calicoctl node status看到启动是 node-to-node mesh 网格的形式,这种形式是一个全互联的模式,默认的BGP在k8s的每个节点担任了一个BGP的一个喇叭,一直吆喝着扩散到其他节点,随着集群节点的数量的增加,那么上百台节点就要构建上百台链接,就是全互联的方式,都要来回建立连接来保证网络的互通性,那么增加一个节点就要成倍的增加这种链接保证网络的互通性,这样的话就会使用大量的网络消耗,所以这时就需要使用Route reflector,也就是找几个大的节点,让他们去这个大的节点建立连接,也叫RR,也就是公司的员工没有微信群的时候,找每个人沟通都很麻烦,那么建个群,里面的人都能收到,所以要找节点或着多个节点充当路由反射器,建议至少是2到3个,一个做备用,一个在维护的时候不影响其他的使用。

Calico 优势 与 劣势

优势

  • Calico BGP模式下没有封包和解包过程,完全基于两端宿主机的路由表进行转发。
  • 可以配合使用 Network Policy 做 pod 和 pod 之前的访问控制。

劣势

  • 要求宿主机处于同一个2层网络下,也就是连在一台交换机上
  • 路由的数目与容器数目相同,非常容易超过路由器、三层交换、甚至node的处理能力,从而限制了整个网络的扩张。(可以使用大规模方式解决)
  • 每个node上会设置大量(海量)的iptables规则、路由,运维、排障难度大。
  • 原理决定了它不可能支持VPC,容器只能从calico设置的网段中获取ip。