与容器相关的那些事——Kubernetes网络篇

Updated on with 0 views and 0 comments

底层原理篇 中带大家从零开始搭建了一套容器网络(如下图所示),希望大家不要误以为容器网络就这点内容,真实的情况是这里面的水深得很!所以,请耐着性子,且听我娓娓道来。

ed43ed781e0a4fe49b2aaaba577f238f.png

当遇到容器连不通“外网”的时候,我们根据上图所示,可以这么做:

  • 先试试 docker0 网桥能不能ping通
  • 然后查看一下跟 docker0Veth Pair 设备(如何判断成对的veth设备请出门右转看 这里)相关的 iptables 规则是不是有异常

多说一嘴,建议大家好好学学iptables,不难的。

接下来我们一起深入聊聊“跨主机通信”这个问题。

1 Overlay Network

上面那张图中,同一个宿主机上有两个容器 Container1 和 Container2,这俩都连接docker0,也就是说,同一个宿主机上的不同容器可以通过docker0网桥进行通信。

如果对数据包的传输过程感兴趣,可以打开iptables的TRACE功能:

# 在宿主机上执行
iptables -t raw -A OUTPUT -p icmp -j TRACE
iptables -t raw -A PREROUTING -p icmp -j TRACE

然后就可以在 /var/log/syslog 中看到数据包传输的日志了。

如果容器在不同的宿主机上呢?默认情况下,不同宿主机上的docker0网桥是不连通的。既然docker0不连通,那么我们就简单粗暴点,让其连通不就可以了吗,于是诞生了整个集群公共的网桥,如下图所示:

2045e53b7dc34871ae8f9dca1a529d49.png

这就是 Overlay Network(覆盖网络),构建这种容器网络的核心在于:在已有的宿主机网络上,再通过软件构建一个覆盖在已有宿主机网络之上的、可以把所有容器连通在一起的虚拟网络。

甚至每台宿主机上都不需要有一个这种特殊的网桥,而仅仅通过某种方式配置宿主机的路由表,就能够把数据包转发到正确的宿主机上。

2 Flannel

为了解决这个容器跨主机通信的问题,社区里出现了很多容器网络方案。为了讲清楚跨主机通信的原理,我们先从最具有代表性的Flannel项目说起。

Flannel项目是CoreOS公司主推的容器网络方案,有3种代表性的实现:

  • UDP模式
  • VXLAN模式
  • host-gw模式

2.1 UDP模式

相比于两台宿主机之间的直接通信,基于 Flannel UDP 模式的容器通信多了一个额外的步骤,即 flanneld 的处理过程。而这个过程使用到了 flannel0 这个TUN设备,仅在发出IP包的过程中,就需要经过三次用户态与内核态之间的数据拷贝,如下图所示:

9142857982004cca80604eeda239c2fd.png

这里重点说明两个关键概念:

  • TUN 设备(Tunnel 设备)
    • 在Linux中,TUN设备是一种工作在三层(Network Layer)的虚拟网络设备
    • TUN设备的功能非常简单,即在操作系统内核和用户应用程序之间传递IP包
  • 子网(Subnet)
    626198a7ec9d4bb6bd2e2b7ecee475e4.png
    • 一台宿主机上的所有容器,都属于该宿主机被分配的一个子网,比方说上图中 Node1 的一个子网是 100.96.1.0/24,container-1 的IP地址是 100.96.1.2,属于这个子网
    • 子网与宿主机的对应关系保存在Etcd当中,flanneld进程可以从Etcd中找到某个子网对应的宿主机的IP地址,否则在进行UDP封装时无法知道目的容器的宿主机的IP地址

简单来说,Flannel UDP 模式提供的是一个三层的Overlay网络,即:

  • 首先对发出端的IP包进行UDP封装
  • 然后在接收端进行解封装拿到原始的IP包
  • 进而把这个IP包转发给目标容器

使用Flannel在不同宿主机上的两个容器之间打通了一条“隧道”,使得这两个容器可以直接使用IP地址进行通信,而无需关心容器和宿主机的分布情况。但是这种模式有严重的性能问题,所以已经被废弃了。

2.2 VXLAN模式

VXLAN,即 Virtual Extensible LAN(虚拟可扩展局域网),是Linux内核支持的一种网络虚似化技术。

VXLAN的覆盖网络的设计思想是:在现有的三层网络之上,“覆盖”一层虚拟的、由内核VXLAN模块负责维护的二层网络,使得连接在这个VXLAN二层网络上的“主机”(虚拟机或者容器都可以)之间,可以像在同一个局域网里那样自由通信。

VXLAN具体实现方式为:将虚拟网络的数据帧添加VXLAN首部后,封装在物理网络中的UDP报文中,然后以传统网络的通信方式传送该UDP报文,到达目的主机后,去掉物理网络报文的头部信息以及VXLAN首部,将报文交付给目的终端。如下图所示:

f595c27fb39a43ff83d4eabd575bdcc8.png

为了能够在二层网络上打通“隧道”,上图中用到了一个特殊的网络设备 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)。

VTEP设备的作用其实跟前面的flanneld进程非常相似,只不过它进行封装和解封装的对象是二层数据帧(Ethernet frame),而且这个工作的执行流程,全部是在内核里完成的。

简单来说,VXLAN可以完全在内核态实现上述封装和解封装的工作,从而实现与UDP模式类似的“隧道”机制,构建出覆盖网络(Overlay Network)。

2.3 host-gw模式

host-gw模式的工作原理是将每个Flannel子网的“下一跳”设置成该子网对应的宿主机的IP地址,在通信过程中就减少了封包和解包的性能损耗。

0402e8599fad43979687434f1800a992.png

如上图所示,Node1上的Infra-container-1要访问Node2上的Infra-container-2,过程如下:

  • 首先,Node1根据它的路由表设置下一跳地址(next-hop)是 10.168.0.3
  • 而Node2的内核网络栈从二层数据帧里拿到IP包后,根据Node2上的路由表,该目的地址会匹配到第二条路由规则(也就是 10.244.1.0/24 对应的路由规则)进入 cni0 网桥
  • 最后进入到Infra-container-2当中

host-gw模式相比上面提到的两种模式有性能优势,不过需要注意的是:

  • host-gw模式要求集群内部所有主机二层网络必须是连通的
  • 采用host-gw模式后,flanneld的唯一作用就是负责主机上路由表的动态更新,在大规模集群路由表的动态更新存在一定压力

3 Calico

Calico项目提供的网络解决方案,与Flannel的host-gw模式几乎是完全一样的,主要有两点不同:

  • Flannel通过Etcd和宿主机上的flanneld来维护路由信息,而Calico项目使用BGP来自动地在整个集群中分发路由信息
  • Calico项目与Flannel的host-gw模式的另一个不同之处,就是它不会在宿主机上创建任何网桥设备

3.1 BGP

BGP的全称是 Border Gateway Protocol,即边界网关协议。它是一个Linux内核原生就支持的、专门用在大规模数据中心里维护不同的“自治系统”之间路由信息的、无中心的路由协议。

简单来说,BGP就是在大规模网络中实现节点路由信息共享的一种协议。

4e81f04c768a4e4298f5826ef62da92c.png

在使用了BGP之后,我们可以简单地想象一下,在每个边界网关上都会运行着一个小程序,它们会将各自的路由表信息,通过TCP传输给其他的边界网关。而其他边界网关上的这个小程序,则会对收到的这些数据进行分析,然后将需要的信息添加到自己的路由表里。

3.2 Calico项目架构

8b73b6f159b14f8eb07803c9903b3dc7.png

Calico项目的架构主要由三个部分组成:

  • CNI插件,这是Calico与Kubernetes对接的部分,关于CNI插件的工作原理下面会讲到
  • Felix,是一个DaemonSet,负责在宿主机上插入路由规则,即写入Linux内核的 FDB 转发信息库,以及维护Calico所需的网络设备等工作
    b3338039c8d141d198f8cedb8a26abf3.png
  • BIRD,是BGP的客户端,专门负责在集群里分发路由规则信息

需要注意的是,Calico默认使用 Node-to-Node Mesh 模式,在这种模式下,每台宿主机上的 BGP Client 都需要跟其他所有节点的 BGP Client 进行通信以便交换路由信息。

也就是说,随着节点数量N的增加,这些连接的数量就会以N的平方规模快速增长,因此会给集群本身的网络带来巨大的压力。

所以,Node-to-Node Mesh 模式一般推荐用在少于100个节点的集群里。而在更大规模的集群中,需要用到 Route Reflector 模式,简单来说,Calico会指定一个或者几个专门的节点(即中间代理),来负责跟所有节点建立BGP连接从而学习到全局的路由规则。

3.3 IPIP模式

实际上,Calico也要求集群宿主机之间是二层连通的,为了解决这一问题,引入了IPIP模式,其通信的原理如下图所示:

f0a28fa3708b45eaa4e66fb0e9d74bf5.png

在IPIP模式下,Felix进程会在节点上添加如下路由规则:

851e4efc6b4741aca2a4ad40a6a93e85.png

上面的 tunl0 设备不同于 Flannel UDP 模式使用的TUN设备,其本质是一个IP隧道(IP tunnel)设备。IP包进入IP隧道设备之后,就会被Linux内核的IPIP驱动接管,IPIP驱动会将原IP包直接封装在一个宿主机网络的IP包中,作为新IP包的 Payload

拿上面的图来举例,从Node1上的Container1发往Node2的Container3的IP包,过程如下:

  • 首先经过Node1上的IP隧道,被伪装成了一个从Node1到Node2的IP包
  • 这个IP包在离开Node1之后,就可以经过路由器,最终“跳”到Node2上
  • Node2的网络内核栈会使用IPIP驱动进行解包,从而拿到原始的IP包
  • 拿到了原始IP包,后续过程就不赘述了

4 CNI网络插件

看完上面介绍的容器跨主机网络通信方案,我们再来说说容器网络插件。简单概括一下,网络插件在宿主机上会创建一个特殊的设备,然后通过某种方法,把不同宿主机上的特殊设备连通,从而达到容器跨主机通信的目的。这其实就是Kubernetes对容器网络的主要处理方法。

只不过,Kubernetes是通过一个叫作 CNI 的接口,维护了一个单独的 cni0 网桥来代替 docker0

在部署Kubernetes的时候,有一个步骤是安装 kubernetes-cni 包,它的目的就是在宿主机上安装CNI插件所需的基础可执行文件,这些可执行文件放在 /opt/cni/bin 目录下:

ac9cc74437574cb78313a978073265d7.png

要实现一个Kubernetes能用的容器网络方案,需要做两部分工作,以Flannel项目为例:

  • 首先,实现这个网络方案本身
    • 这一部分需要编写的,其实就是flanneld进程里的主要逻辑
    • 比如,创建和配置 flannel.1 设备、配置宿主机路由、配置ARP和FDB表里的信息等等
  • 然后,实现该网络方案对应的CNI插件
    • 这一部分主要做的就是配置 Infra 容器里面的网络栈,并把它连接在CNI网桥上
    • 网络栈包括:网卡(Network Interface)、回环设备(Loopback Device)、路由表(Routing Table)和iptables规则
    • 实际上,CNI插件需要实现ADD和DEL两个操作,这里就不展开讲了

另外,需要注意以下两点:

  • 在Kubernetes中,处理容器网络相关的逻辑并不会在kubelet主干代码里执行,而是在具体的 CRIContainer Runtime Interface,容器运行时接口)实现里完成的,比如当底层使用的是Docker,对应的CRI实现叫作 dockershim
  • 在默认情况下,网桥设备是不允许一个数据包从一个端口进来后,再从这个端口发出去的,需要开启 Hairpin Mode 才可以
    • 比方说,做了这样一个端口映射 docker run -p 8080:80,然后在容器里面访问宿主机的8080端口

5 NetworkPolicy

在Kubernetes里,对网络隔离能力的定义是依靠一种专门的API对象来描述的,即NetworkPolicy。

在具体实现上,凡是支持NetworkPolicy的CNI网络插件,都维护着一个 NetworkPolicy Controller,通过控制循环的方式对NetworkPolicy对象的增删改查做出响应,然后在宿主机上完成iptables规则的配置工作。

为了方便大家理解,可以把NetworkPolicy定义的规则当作“白名单”,涉及到3个关键字段 podSelectornamespaceSelectoripBlock,具体配置看 官方文档

在CNI网络插件中,网络隔离主要通过设置下面两组iptables规则来实现:

  • 第一组规则,负责“拦截”对被隔离Pod的访问请求
    3bf1b0e06c234be2a375b245c7311158.png
  • 第二组规则,根据白名单判断是允许还是拒绝
    d0f5256441b54996b0d3d41e744f84ea.png

6 Service

Kubernetes之所以需要Service,一方面是因为Pod的IP不是固定的,另一方面则是因为一组Pod实例之间总会有负载均衡的需求。

一般情况下,Service是由 kube-proxy 组件加上iptables来共同实现的。但是,过多的iptables规则,制约着Kubernetes项目承载更多的Pod。

针对这个问题,一个行之有效的方法是使用IPVS模式的Service。IPVS模式的工作原理如下:

  • 当创建好Service后,kube-proxy首先会在宿主机上创建一个虚拟网卡 kube-ipvs0,并为它分配 Service VIP 作为IP地址
  • 接下来,kube-proxy会通过Linux的IPVS模块,为这个IP地址设置IPVS虚拟主机,并设置好负载均衡策略,我们可以通过 ipvsadm 查看到这个设置,如下图所示:
    f73dfb667d4a4133a5b5e0de59245109.png

需要注意的是,IPVS模块只负责上述的负载均衡和代理功能,而一个完整的Service流程正常工作所需要的包过滤、SNAT等操作,还是要靠iptables来实现,只不过这些辅助性的iptables规则数量有限。

最后我们一起看看与Service相关的问题:

  • 无法通过DNS访问到Service
    • 检查Kubernetes的Master节点的 Service DNS 是否正常
      42946e9ee188470eacd0c80dabff3ef3.png
    • 如果上面访问 kubernetes.default 返回的值都有问题,就需要检查 kube-dns 的运行状态和日志
    • 否则,应该去检查Service定义是不是有问题
  • 无法通过ClusterIP访问到Service
    • 首先应该检查的是这个Service是否有Endpoints
      d04891414a04496fa40181487199a993.png
    • 如果Endpoints正常,那就查看kube-proxy输出的日志,看是否有异常
    • 如果kube-proxy一切正常,那就应该仔细查看宿主机上的iptables
      • KUBE-SERVICES 或者 KUBE-NODEPORTS 规则对应的Service的入口链,这个规则应该与VIP和Service端口一一对应
      • KUBE-SEP-(hash) 规则对应的DNAT链,这些规则应该与Endpoints 一一对应
      • KUBE-SVC-(hash) 规则对应的负载均衡链,这些规则的数目应该与Endpoints数目一致
      • 如果是 NodePort 模式,还有POSTROUTING处的SNAT链要检查
  • Pod无法通过Service访问到自己
    • 只需要确保将kubelet的 hairpin-mode 设置为 hairpin-veth 或者 promiscuous-bridge 即可

7 Ingress

Ingress实际上是Kubernetes对“反向代理”的抽象。

a1dec942e0834b75a4c2acb6ca741146.png

Kubernetes提出Ingress概念的原因其实也非常容易理解,有了Ingress这个抽象,用户就可以根据自己的需求来自由选择 Ingress Controller,如果没有合适的还可以实现一个自定义的。

更多关于Ingress的内容请看 官方文档

8 附录

最后附上一张DHCP流程图,希望骚年的你能够体会到里面的精髓:

image.png


标题:与容器相关的那些事——Kubernetes网络篇
作者:yanghao
地址:http://solo.fancydigital.com.cn/articles/2022/05/14/1652525347210.html