kubernetes网络模型

525 阅读18分钟

背景

计算、存储和网络是云时代的三大基础服务,作为新一代基础架构的 Kubernetes 也不例外。而这三者之中,网络又是一个最难掌握和最容易出问题的服务;本文通过对Kubernetes网络流量模型进行简单梳理,希望对初学者能够提供一定思路。

容器网络中涉及的几个地址:

Node Ip:物理机地址。 POD Ip:Kubernetes的最小部署单元是Pod,一个pod 可能包含一个或多个容器,简单来讲容器没有自己单独的地址,他们共享POD 的地址和端口区间。 ClusterIp:Service的Ip地址,外部网络无法ping通改地址,因为它是虚拟IP地址,没有网络设备为这个地址负责,内部实现是使用Iptables规则重新定向到其本地端口,再均衡到后端Pod;只有Kubernetes集群内部访问使用。 Public Ip :Service对象在Cluster IP range池中分配到的IP只能在内部访问,适合作为一个应用程序内部的层次。如果这个Service作为前端服务,准备为集群外的客户提供业务,我们就需要给这个服务提供公共IP。

容器网络流量模型

容器网络至少需要解决如下几种场景的通信:

①POD内容器间通信 ②同主机POD间 通信 ③跨主机POD间 通信 ④集群内Service Cluster Ip和外部访问

下面具体介绍实现方式

POD内容器间通信

Pod中的容器可以通过“localhost”来互相通信,他们使用同一个网络命名空间,对容器来说,hostname就是Pod的名称。Pod中的所有容器共享同一个IP地址和端口空间,你需要为每个需要接收连接的容器分配不同的端口。也就是说,Pod中的应用需要自己协调端口的使用。

实验如下:

首先我们创建一个Pod ,包含两个容器,容器参数如下: 查看:

可以看到容器共享Pod 的地址,那么他们是否使用同一端口资源呢,我们可以简单实验一下:首先在容器1监听一个端口: 然后在容器2查看该端口是否被占用: 可见端口也是共享的;所以简单理解,可以把Pod看做一个小系统,容器当做系统中的不同进程;

内部实现:同POD 内的容器实际共享同一个Namespace,因此使用相同的Ip和Port空间,该Namespace 是由一个叫Pause的小容器来实现,每当一个Pod被创建,那么首先创建一个pause容器, 之后这个pod里面的其他容器通过共享这个pause容器的网络栈,实现外部pod进行通信,因此对于同Pod里面的所有容器来说,他们看到的网络视图是一样的,我们在容器中看的地址,也就是Pod地址实际是Pause容器的IP地址。总体模型如下: 我们在node 节点查看之前创建的POD,可以看到该pause容器 : 这种新创建的容器和已经存在的一个容器(pause)共享一个 Network Namespace(而不是和宿主机共享) 就是我们常说的container 模式。

同主机POD间通信

每个节点上的每个Pod都有自己的namespace,同主机上的POD之间怎么通信呢?我们可以在两个POD之间建立Vet Pair进行通信,但如果有多个容器,两两建立Veth 就会非常麻烦,假如有N 个POD ,那么我们需要创建n(n-1)/2个Veth Pair,扩展性非常差,如果我们可以将这些Veth Pair 连接到一个集中的转发点,由它来统一转发就就会非常便捷,这个集中转发点就是我们常说的bridge;如下所示(简单起见,这里把pause忽略): 仍然以我们的测试环境为例,创建pod1 和pod2地址分别为:10.244.1.16、10.244.1.18,位于node1 节点 查看节点下的namespace: 这两个NS就是上述两个POD 对应的namespace,查询对应namespace 下的接口: 可以看到标红处的地址,实际就是POD 的ip地址;

NS 和对应的POD 地址都找到了,那么如何确认这两个ns 下的虚接口的另一端呢? 比较直观的确认方式为:上述接口如 3: eth0@if7,表示本端接口id 为3 ,对端接口id是7,我们看下default namespace(我们平时看的默认都在default下) 的veth口: 7: veth3b416eb5@if3 ,该接口的id 正是我们要找的id 为7的接口 ,是veth pair的另一端;

跨node通信

跨node通信就涉及到了Container Network Interface的实现,也就是容器网络的API,它有一定的要求,简要下来有三点

  1. 运行在一个节点当中的Pod能在不经过NAT的情况下跟集群中所有的Pod进行通信
  2. 节点当中的客户端(system daemon、kubelet)能跟该节点当中的所有Pod进行通信
  3. 以host network模式运行在一个节点上的Pod能跟集群中所有的Pod进行通信

针对Kubernetes网络模型也涌现出了许多的实现方案,例如Calico、Flannel、Weave等等,虽然实现原理各有千秋,但都围绕着同一个问题即如何实现Kubernetes当中的扁平网络进行展开。Kubernetes只需要负责编排调度相关的事情,修桥铺路的事情交给相应的网络插件即可,我们会以Flannel为例进行讲解。

Flannel

不过多讲述部署过程,在安装部署完成之后应该能看到在各个节点上通过DaemonSet的方式运行了一个Flannel的Pod。

[root@10-10-88-192 ~]# kubectl get daemonset -n kube-system -l app=flannel
NAME              DESIRED   CURRENT   READY     UP-TO-DATE   AVAILABLE   NODE SELECTOR                   AGE
kube-flannel-ds   3         3         3         3            3           beta.kubernetes.io/arch=amd64   135d
[root@10-10-88-192 ~]#

[root@10-10-88-192 ~]#
[root@10-10-88-192 ~]# kubectl get pod -n kube-system -o wide -l app=flannel
NAME                    READY     STATUS    RESTARTS   AGE       IP               NODE
kube-flannel-ds-npcxv   1/1       Running   0          2h        172.16.130.164   10-10-88-170
kube-flannel-ds-rv8wv   1/1       Running   0          2h        172.16.130.244   10-10-88-192
kube-flannel-ds-t5zlv   1/1       Running   0          2h        172.16.130.140   10-10-88-195
[root@10-10-88-192 ~]#

每一个Flannel的Pod当中都运行了一个flanneld进程,且flanneld的配置文件以ConfigMap的形式挂载到容器内的/etc/kube-flannel/目录供flanneld使用。

[root@10-10-88-192 ~]# kubectl get cm -n kube-system -l app=flannel
NAME               DATA      AGE
kube-flannel-cfg   2         137d
[root@10-10-88-192 ~]#

Flannel Backend Flannel通过在每一个节点上启动一个叫flanneld的进程,负责每一个节点上的子网划分,并将相关的配置信息如各个节点的子网网段、外部IP等保存到etcd当中,而具体的网络包转发交给具体的Backend来实现。 flanneld可以在启动的时候通过配置文件来指定不同的Backend来进行网络通信,目前比较成熟的Backend有VXLAN、host-gw以及UDP三种方式,也已经有诸如AWS,GCE and AliVPC这些还在实验阶段的Backend。VXLAN是目前官方最推崇的一种Backend实现方式,host-gw一般用于对网络性能要求比较高的场景,但需要基础架构本身的支持,UDP则一般用于Debug和一些比较老的不支持VXLAN的Linux内核。 这里只简单介绍一下两种Backend网络通信实现流程:

  • UDP
  • VXLAN

UDP

由于UDP模式相对容易理解,故这里先采用UDP这种Backend模式进行举例说明然后再对其他Backend模式进行展开讲解。 采用UDP模式时需要在flanneld的配置文件当中指定Backend type为UDP,可以通过直接修改flanneld的ConfigMap的方式实现,配置修改完成之后如下:

[root@10-10-88-192 ~]# kubectl get cm -n kube-system -o yaml kube-flannel-cfg
apiVersion: v1
data:
  cni-conf.json: |
    {
      "name": "cbr0",
      "type": "flannel",
      "delegate": {
        "isDefaultGateway": true
      }
    }
  net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "udp"
      }
    }
kind: ConfigMap
metadata:
  creationTimestamp: 2018-10-30T08:34:01Z
  labels:
    app: flannel
    tier: node
  name: kube-flannel-cfg
  namespace: kube-system
  resourceVersion: "33718154"
  selfLink: /api/v1/namespaces/kube-system/configmaps/kube-flannel-cfg
  uid: 8d981eff-dc1e-11e8-8103-fa900126bc00
[root@10-10-88-192 ~]#

关键字段为Backend当中的Type字段,采用UDP模式时Backend Port默认为8285,即flanneld的监听端口。

flanneld的ConfigMap更新完成之后delete flannel pod进行配置更新:

[root@10-10-88-192 ~]# kubectl delete pod -n kube-system -l app=flannel
pod "kube-flannel-ds-npcxv" deleted
pod "kube-flannel-ds-rv8wv" deleted
pod "kube-flannel-ds-t5zlv" deleted
[root@10-10-88-192 ~]#

当采用UDP模式时,flanneld进程在启动时会通过打开/dev/net/tun的方式生成一个TUN设备,TUN设备可以简单理解为Linux当中提供的一种内核网络与用户空间(应用程序)通信的一种机制,即应用可以通过直接读写tun设备的方式收发RAW IP包。

flanneld进程启动后通过ip a命令可以发现节点当中已经多了一个叫flannel0的网络接口:

[root@10-10-88-192 ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    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: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether fa:90:01:26:bc:00 brd ff:ff:ff:ff:ff:ff
    inet 10.10.88.192/24 brd 10.10.88.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::f890:1ff:fe26:bc00/64 scope link
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether fa:86:b8:79:70:01 brd ff:ff:ff:ff:ff:ff
    inet 172.16.130.244/24 brd 172.16.130.255 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::f886:b8ff:fe79:7001/64 scope link
       valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN
    link/ether 02:42:ae:dd:19:83 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 scope global docker0
       valid_lft forever preferred_lft forever
5: flannel0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1472 qdisc pfifo_fast state UNKNOWN qlen 500
    link/none
    inet 10.244.0.0/16 scope global flannel0
       valid_lft forever preferred_lft forever
    inet6 fe80::969a:a8eb:e4da:308b/64 scope link flags 800
       valid_lft forever preferred_lft forever
6: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue state UP qlen 1000
    link/ether 0a:58:0a:f4:00:01 brd ff:ff:ff:ff:ff:ff
    inet 10.244.0.1/24 scope global cni0
       valid_lft forever preferred_lft forever
    inet6 fe80::3428:a4ff:fe6c:bb77/64 scope link
       valid_lft forever preferred_lft forever

细心的同学就会发现此时flannel0这个网络接口上的MTU为1472,相比Kubernetes集群网络接口eth1小了28个字节,为什么呢?

通过可以ip -d link show flannel0可以看到这是一个tun设备:

[root@10-10-88-192 ~]# ip -d link show flannel0
5: flannel0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1472 qdisc pfifo_fast state UNKNOWN mode DEFAULT qlen 500
    link/none  promiscuity 0
    tun
[root@10-10-88-192 ~]#

通过netstat -ulnp命令可以看到此时flanneld进程监听在8285端口:

[root@10-10-88-192 ~]# netstat -ulnp | grep flanneld
udp        0      0 172.16.130.140:8285     0.0.0.0:*                           2373/flanneld
[root@10-10-88-192 ~]#

容器跨节点通信实现流程:

假设在节点A上有容器A(10.244.1.96),在节点B上有容器B(10.244.2.194),此时容器A向容器发送一个ICMP请求报文(ping),我们来逐步分析一下ICMP报文是如何从容器A到达容器B的。

1、容器A当中发出ICMP请求报文,通过IP封装后形式为:10.244.1.96 -> 10.244.2.194,此时通过容器A内的路由表匹配到应该将IP包发送到网关10.244.1.1(cni0网桥)。

完整的帧格式为:

2、此时到达cni0的IP包目的地IP 10.244.2.194匹配到节点A上第一条路由规则(10.244.0.0),内核将RAW IP包发送给flannel0接口。 3、flannel0为tun设备,发送给flannel0接口的RAW IP包(无MAC信息)将被flanneld进程接收到,flanneld进程接收到RAW IP包后在原有的基础上进行UDP封包,UDP封包的形式为:172.16.130.140:src port -> 172.16.130.164:8285。 这里有一个问题就是flanneld怎么知道10.244.2.194这个容器到底是在哪个节点上呢? flanneld在启动时会将该节点的网络信息通过api-server保存到etcd当中,故在发送报文时可以通过查询etcd得到10.244.2.194这个容器的IP属于host B,且host B的IP为172.16.130.164。 RAW IP包示例: 4、flanneld将封装好的UDP报文经eth1发出,从这里可以看出网络包在通过eth1发出前先是加上了UDP头(8个字节),再然后加上了IP头(20个字节)进行封装,这也是为什么flannel0的MTU要比eth1的MTU小28个字节的原因(防止封装后的以太网帧超过eth1的MTU而在经过eth1时被丢弃)。 此时完整的以太网帧格式为: 5、网络包经节点A和节点B之间的网络连接到达host B。 6、host B收到UDP报文后经Linux内核通过UDP端口号8285将包交给正在监听的应用flanneld。 7、运行在host B当中的flanneld将UDP包解包后得到RAW IP包:10.244.1.96 -> 10.244.2.194。 8、解封后的RAW IP包匹配到host B上的路由规则(10.244.2.0),内核将RAW IP包发送到cni0。 此时的完整的以太网帧格式为: 9、cni0将IP包转发给连接在cni0网桥上的container B,而flanneld在整个过程中主要主要负责两个工作:

  • UDP封包解包
  • 节点上的路由表的动态更新 从上面虚线部分就可以看到container A和container B虽然在物理网络上并没有直接相连,但在逻辑上就好像是处于同一个三层网络当中,这种基于底下的物理网络设备通过Flannel等软件定义网络技术实现的网络我们称之为Overlay网络。 那么上面通过UDP这种Backend实现的网络传输过程有没有问题呢?最明显的问题就是,网络数据包先是通过tun设备从内核当中复制到用户态的应用,然后再由用户态的应用复制到内核,仅一次网络传输就进行了两次用户态和内核态的切换,显然这种效率是不会很高的。那么有没有高效一点的办法呢?当然,最简单的方式就是把封包解包这些事情都交给内核去干好了,事实上Linux内核本身也提供了比较成熟的网络封包解包(隧道传输)实现方案VXLAN,下面我们就来看看通过内核的VXLAN跟flanneld自己通过UDP封装网络包在实现上有什么差别。

VXLAN

VXLAN全称Virtual Extensible LAN,是一种虚拟化隧道通信技术,主要是为了突破VLAN的最多4096个子网的数量限制,以满足大规模云计算数据中心的需求。VLAN技术的缺陷是VLAN Header预留的长度只有12 bit,故最多只能支持2的12次方即4096个子网的划分,无法满足云计算场景下主机数量日益增长的需求。当前VXLAN的报文Header内有24 bit,可以支持2的24次方个子网,并通过VNI(Virtual Network Identifier)来区分不同的子网,相当于VLAN当中的VLAN ID。 不同于其他隧道协议,VXLAN是一个一对多的网络,并不仅仅是一对一的隧道协议。一个VXLAN设备能通过像网桥一样的学习方式学习到其他对端的IP地址,也可以直接配置静态转发表。 VXLAN包格式: 从VXLAN的包格式就可以看到原本的二层以太网帧被放在VXLAN包头里进行封装,VXLAN实际实现的是一个二层网络的隧道,通过VXLAN让处于同一个VXLAN网络(VNI相同则为同一个VXLAN网络)当中的机器看似处在同一个二层网络当中(逻辑上处于同一个二层网络),而网络包转发的方式也类似二层网络当中的交换机(这样虽然不是很准确,但更便于理解)。 当采用VXLAN模式时,flanneld在启动时会通过Netlink机制与Linux内核通信,建立一个VTEP(Virtual Tunnel Access End Point)设备flannel.1 (命名规则为flannel.[VNI],VNI默认为1),类似于交换机当中的一个网口。 可以通过ip -d link查看VTEP设备flannel.1的配置信息,从以下输出可以看到,VTEP的local IP为172.16.130.244,destination port为8472。

[root@10-10-88-192 ~]# ip -d link show flannel.1
5: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT
    link/ether a2:5e:b0:43:09:a7 brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local 172.16.130.244 dev eth1 srcport 0 0 dstport 8472 nolearning ageing 300 addrgenmode eui64
[root@10-10-88-192 ~]#

在UDP模式下由flanneld进程进行网络包的封包和解包的工作,而在VXLAN模式下解封包的事情交由内核处理,那么此时FlannnelD的作用是什么呢?带着这个疑问我们先来简单看一下VXLAN Backend是如何工作的。

Flannel当中对VXLAN Backend的实现经过了几个版本的改进之后目前最新版本的flanneld当中的处理流程为:

当flanneld启动时将创建VTEP设备(默认为flannel.1,若已经创建则跳过),并将VTEP设备的相关信息上报到etcd当中,而当在Flannel网络中有新的节点发现时,各个节点上的flanneld将依次执行以下流程:

在节点当中创建一条该节点所属网段的路由表,主要是能让Pod当中的流量路由到flannel.1接口。

通过route -n可以查看到节点当中已经有两条flannel.1接口的路由:

[root@10-10-88-192 ~]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.10.88.254    0.0.0.0         UG    0      0        0 eth0
10.10.88.0      0.0.0.0         255.255.255.0   U     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
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 eth0
169.254.0.0     0.0.0.0         255.255.0.0     U     1003   0        0 eth1
172.16.130.0    0.0.0.0         255.255.255.0   U     0      0        0 eth1
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
[root@10-10-88-192 ~]#

在节点当中添加一条该节点的IP以及VTEP设备的静态ARP缓存。 可通过arp -n命令查看到master节点当中已经缓存了另外两个节点以及VTEP的ARP信息(已删除无关ARP缓存信息)。

[root@10-10-88-192 ~]# arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
10.244.2.0               ether   42:7f:69:c7:cd:37   CM                    flannel.1
10.244.1.0               ether   7a:2c:d0:7f:48:3f   CM                    flannel.1
172.16.130.140           ether   fa:89:cf:03:e3:01   C                     eth1
172.16.130.164           ether   fa:88:2a:44:2b:01   C                     eth1

在节点当中添加一条该节点的转发表。 通过bridge命令查看节点上的VXLAN转发表(FDB entry),MAC为对端VTEP设备即flannel.1的MAC,IP为VTEP对应的对外IP(可通过flanneld的启动参数–iface=eth1指定,若不指定则按默认网关查找网络接口对应的IP),可以看到已经有两条转发表。

[root@10-10-88-192 ~]# bridge fdb show dev flannel.1
42:7f:69:c7:cd:37 dst 172.16.130.164 self permanent
7a:2c:d0:7f:48:3f dst 172.16.130.140 self permanent
[root@10-10-88-192 ~]#

从UDP更改到VXLAN的过程不多赘述,修改后同样可以通过netstat -ulnp命令查看VXLAN监听的端口:

[root@10-10-88-192 ~]# netstat -ulnp | grep 8472
udp        0      0 0.0.0.0:8472            0.0.0.0:*                           -
[root@10-10-88-192 ~]#

但跟UDP模式下查看flanneld监听的端口的区别为,最后一栏显示的不是进程的ID和名称,而是一个破折号“-”,这说明UDP的8472端口不是由用户态的进程在监听的,也证实了VXLAN模块工作在内核态模式下。

此时容器跨节点网络通信实现流程为:

  • 同UDP Backend模式,容器A当中的IP包通过容器A内的路由表被发送到cni0
  • 到达cni0当中的IP包通过匹配host A当中的路由表发现通往10.244.2.194的IP包应该交给flannel.1接口
  • flannel.1作为一个VTEP设备,收到报文后将按照VTEP的配置进行封包,首先通过etcd得知10.244.2.194属于节点B,并得到节点B的IP,通过节点A当中的转发表得到节点B对应的VTEP的MAC,根据flannel.1设备创建时的设置的参数(VNI、local IP、Port)进行VXLAN封包
  • 通过host A跟host B之间的网络连接,VXLAN包到达host B的eth1接口
  • 通过端口8472,VXLAN包被转发给VTEP设备flannel.1进行解包
  • 解封装后的IP包匹配host B当中的路由表(10.244.2.0),内核将IP包转发给cni0
  • cni0将IP包转发给连接在cni0上的容器B

参考资料