k8s CNI之Flannel的"套路"

779

导言

  有docker使用经验的人应该了解,docker的网络栈被"困在了"本地的namspace中,从而导致了跨机器docker间网络无法通信的问题,那么作为容器调度平台的k8s是如何解决这个问题的呢?

pod网络模型

  k8s官方并没有亲自去实现pod网络通信方案,而是以插件的形式兼容并蓄市面上各种解决方案,比如flannel、calico、vpc-cni等,但无论何种方案,都必须满足它的要求:

  • Pod 之间无需设置 NAT 即可实现相互的网络通讯
  • 每个 Pod 都会看到自己和其他 Pod 一样可看到自身的 IP 地址
  • 运行 Kubernetes 集群的节点,可支持物理机或者虚拟机,或任何支持运行 Kubernetes 的环境,这些节点也可无需设置 NAT,便可实现同其他 Pod 节点的相互通讯。

  为什么有这样的要求?这就不得不提传统的docker端口暴露方式的限制了

  有docker使用经验的人,应该了解 docker端口是如何暴露的吧,通过iptables将宿主机端口映射到docker端口即可。例如,将宿主机的80端口映射到nginx docker的80端口,这样调用方就能通过宿主机的ip+80端口访问到nginx docker;但如果宿主机上有多个nginx docker呢?

  so easy,将多个nginx docker分别与宿主机的不同端口通过NAT方式一一映射不就可以了吗?相信绝大多数人会不假思索的采用此种方法;但是,细品深究,如果k8s也采用这样的方式,会不会遇到些问题呢?

  • 宿主机应该按照何种规则将自身端口映射给众多的docker呢?
  • 调用方调用多个nginx,需不需要区分目标端口,会不会给调用方造成难度和困惑?
  • 在微服务注册与发现场景下,位于同一宿主机上的不同服务的IP在经过NAT后,最终注册到注册中心的地址将会是同一个ip,即宿主机ip,这样,必然会造成服务注册与发现的失效

  显而易见,NAT端口映射的方案是不能简单的套用在K8S上的;因此,也就不难理解k8s官方提出的pod网络模型了;从该网络模型可以看出,k8s想要实现的是扁平的网络,即容器不需要借助宿主机的IP进行NAT,只需自身ip也能实现相互通信;容器间相互通信看到的ip也应该是彼此真正的ip

Flannel

Flannel 是 CoreOs 公司 为 K8s 设计的通过 Overlay Network 方式实现的架构,所谓的 overlay, 其实就是 tcp 的嵌套,在 tcp 协议中再封装一层 tcp/udp 协议。下图便是 vxlan overlay 包格式

image.png 依据封装协议的不同,flannel 还可以分为多种 backend

  • udp   udp 模式的解封装是在用户态进行的,比较耗费性能
  • host-gw   只适用于二层网络,无法跨越三层
  • vxlan   vxlan 解封装在内核态进行,性能高,能跨域三层,这也是业界普遍使用的方案

Flannel vxlan 模式的组件主要包括 ectd、Flanneld 等

ETCD

  如果有使用过单机版Docker,细心的你可能会发现 docker 分配到的网段是 172.17.0.0/16, 再启动一台宿主机,你发现 docker 分配到的还是同样的网段,那不同宿主机上的docker ip 岂不会存在重复的可能?这时就需要一个中心节点来保证 docker ip 的全局唯一性,etcd 担当的正是这样一个角色,它存储着所有宿主机分配给容器的网段;当然,它其实只是 Flannel 元信息的搬运工,真正负责划分网段,注册上报 subnet vtep 等信息的是 Flanneld

Flanneld

可以简单的认为flanned 作为 ectcd 的 agent 运行在宿主机节点上,它主要有以下功能:

  • 从 etcd 中获取 network 的配置信息
  • 划分 subnet,并在 etcd 中进行注册
  • 将子网信息记录到宿主机的/run/flannel/subnet.env
  • 当然最最重要的功能是封解装vxlan数据包
root@ip-172-25-34-198:~#  cat /run/flannel/subnet.envFLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=192.168.2.1/24
FLANNEL_MTU=8951
FLANNEL_IPMASQ=true
ubuntu@ip-172-25-33-13:~$ cat /run/flannel/subnet.envFLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=192.168.1.1/24
FLANNEL_MTU=8951
FLANNEL_IPMASQ=true

如上所示,两个节点分配到了不同的容器 subnet, 这些信息会上报到 ectcd, 并被注入到 docker 启动参数中,从而保证了容器ip 在k8s集群中的唯一性

vxlan 封装流程

ip 已经唯一了,那么跨节点的 docker 究竟是如何通信的?

便于理解, 基于 vxlan 模式分析下 跨节点 docker 192.168.1.4 —> 192.168.2.2 间的数据转发流程

image.png

  1. 数据一开始从 ip 为 192.168.1.4 的 docker 出发,根据路由表,目的地址为 192.168.2.2 的数据包匹配到了默认路由, 于是报文从容器的 eth0 网络接口发送出去
root@ip-172-25-33-13:/etc/cni/net.d# docker exec bd69 ip addr1: 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 forever3: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 8951 qdisc noqueue state UP
    link/ether 96:53:14:4a:4e:bd brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.4/24 scope global eth0
       valid_lft forever preferred_lft forever
root@ip-172-25-33-13:/etc/cni/net.d# docker exec bd69 ip route
default via 192.168.1.1 dev eth01
0.244.0.0/16 via 192.168.1.1 dev eth0
192.168.1.0/24 dev eth0  proto kernel  scope link  src 192.168.1.4
  1. 从 docker 出来的数据会发送到哪里? 理论上,docker 和宿主机位于两个不同的network namespace中,网络是隔离不通的,但是,vethpair的出现,让事情变得柳暗花明,vethpair是连接两个namespaces 的”网线”,串联起 docker eth0 接口和宿主机的 cni0 接口,于是数据包会被再次转发到宿主机的 cni0

  2. 宿主机上的路由表,有一条目的地址为 192.168.2.0/24 的路由,目标设备为flannel.1,于是颠沛流离的目标数据包又再次被转发到 flannel.1 端口

root@ip-172-25-33-13:/etc/cni/net.d# ip route
default via 172.25.32.1 dev eth0
172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1 linkdown
172.25.32.0/20 dev eth0  proto kernel  scope link  src 172.25.33.13
192.168.0.0/24 via 192.168.0.0 dev flannel.1 onlink
192.168.1.0/24 dev cni0  proto kernel  scope link  src 192.168.1.1
192.168.2.0/24 via 192.168.2.0 dev flannel.1 onlink

4.flannel.1 是一个虚拟 VTEP(Vxlan tunnel endpoint) 设备,负责 vxlan 协议报文的封解包,一旦数据包发送到 flannel.1,就会进入 vxlan 协议的套路

 当数据报文来到 flannel.1 时,需要根据 vxlan 协议封装数据包,此时的dst ip 为 192.168.2.2,flannel.1 需要知道 192.168.2.2 对应的mac地址

 不同于传统的二层寻址,flannel.1 不会发送 arp 请求去获得 192.168.2.2 的 mac 地址,而是由 Linux kernel 将一个“L3 Miss”事件请求发送的用户空间的 flanned 程序。Flanned 程序收到内核的请求事件之后,从 etcd 查找能够匹配目标地址的子网的 flannel.1 设备的 mac 地址,即发往的 pod 所在 host 中 flannel.1 设备的 mac 地址, 这样,就形成了如下完整的 vxlan 内层数据封包格式

image.png

  1. 按照 TCP 协议栈从上向下的封装工序,要形成完整的能够传输的数据帧,还需要目标 ip 和目标 mac 地址,那么这两者如何找寻呢?   对于目标 ip,flannel.1 根据对端 vtep 的 mac 地址,从 fdb(forwarding database) 中查找 mac 地址 26:1c:b0:2b:17:31 对应的 ip 地址,如下所示,该 mac 地址对应的 ip 为 172.25.34.198
root@ip-172-25-33-13:~# bridge fdb show dev flannel.1
22:5b:16:5a:1b:fc dst 172.25.42.118 self permanent
26:1c:b0:2b:17:31 dst 172.25.34.198 self permanent

再通过 arp 协议即可找到 172.25.34.198 对应的 mac 地址,于是,一个完整的数据帧便呈现如下:

image.png 最终,目标 ip 为 172.25.34.198 目标端口为 8472 的数据帧便从宿主机的 eth0 端口发送出去

  1. 数据帧通过二层网络顺利转发到达对端 172.25.34.198 宿主机的 8472 端口,此端口正是 Flanneld 的监听端口,Flanneld 解封 vxlan 数据包, 发现数据包目标 ip 为 192.168.2.2, 匹配系统明细路由,将数据包转发给了 cni0 端口
root@ip-172-25-34-198:~# ip route
default via 172.25.32.1 dev eth0172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1 linkdown
172.25.32.0/20 dev eth0  proto kernel  scope link  src 172.25.34.198
192.168.0.0/24 via 192.168.0.0 dev flannel.1 onlink
192.168.1.0/24 via 192.168.1.0 dev flannel.1 onlink
192.168.2.0/24 dev cni0  proto kernel  scope link  src 192.168.2.1
  1. 前文提及,cni0 端口是与 docker namespace 直连的,于是通过 vethpair 将数据转发到了 docker192.168.2.1中
    历经千辛万苦,尝尽无数套路,数据包最终实现了跨主机 docke 间的通信

快递包模型

  在网络模型中,Flannel这种模式更专业的叫法应该叫overlay网络模型,但为了便于理解,我将它称之为快递包模型。

  在Flannel模式中,数据包犹如一个嵌套快递包:大快递包里藏着小快递包,而数据的流转犹如快递包在物流网中的分发一般,首先按照小快递包的地址单进行收集,上高速前在集散地进行二次包装,封装在一个大集装箱内,然后按照集装箱的目的地址进行物流分发,到达目标集散地后,拆分出小快递包,再次按照小快递包的地址单进行分发,最终实现了跨节点数据的传递

文章均为原创,关注公众号云猿生\color{green} {云猿生} 获取更多知识

image.png