深入剖析 Kubernetes-5 容器网络

深入剖析 Kubernetes-5 容器网络

1 浅谈容器网络

1.1 Veth Pair与Docker网桥

容器要想跟外界进行通信,它发出的 IP 包就必须从它的 Network Namespace 里出来,来到宿主机上。

Docker为容器创建一个一端在容器里充当默认网卡、另一端在宿主机上的 Veth Pair 设备。根据 Veth Pair 设备的原理,发送到容器中 Veth 的数据包会立刻出现在宿主机的 Veth 设备上。宿主机的一端 Veth 被插在 docker0 网桥上,网桥会扮演二层交换机的角色,根据数据包的目的 MAC 地址,在它的CAM表(即交换机通过 MAC 地址学习维护的端口和 MAC 地址的对应表),查找到对应端口(即某个Veth设备),然后把数据包发往这个端口。而这个端口正是另外一个容器插在 docker0 网桥上的一块虚拟网卡,这样,数据包就进入到目标容器的Network Namespace里。

一旦一张虚拟网卡被“插”在网桥上,它就会变成该网桥的“从设备”。从设备会被“剥夺”调用网络协议栈处理数据包的资格,从而“降级”成为网桥上的一个端口。而这个端口唯一的作用,就是接收流入的数据包,然后把这些数据包的“生杀大权”(比如转发或者丢弃),全部交给对应的网桥。

image-20230108190535852

1.2 总结

默认情况下,被限制在 Network Namespace 里的容器进程,实际上是通过 Veth Pair 设备 + 宿主机网桥的方式,实现了跟同其他容器的数据交换。

同一宿主机同一子网内的容器之间是二层网络直连的。

docker网桥充当了容器网络的默认网关,容器访问宿主机或宿主机访问容器,都会经过docker网桥,然后根据宿主机上的iptables规则转发到目的宿主机。

2 深入解析容器跨主机网络

2.1 Flannel

Flannel 项目是 CoreOS 公司主推的容器网络方案。

目前,Flannel 支持三种后端实现,分别是:

  • VXLAN
  • host-gw
  • UDP

2.1.1 UDP模式

在 Linux 中,TUN 设备是一种工作在三层(Network Layer)的虚拟网络设备。TUN 设备的功能非常简单,即:在操作系统内核和用户应用程序之间传递 IP 包。

基于 Flannel UDP 模式的跨主通信的基本原理:

image-20230108212622728

以Node1上的container-1访问Node2上的container-2为例:

  • container-1到Node1

container-1 容器里的进程发起的 IP 包,其源地址就是 100.96.1.2,目的地址就是 100.96.2.3。由于目的地址 100.96.2.3 并不在 Node 1 的 docker0 网桥的网段里,所以这个 IP 包会被交给默认路由规则,通过容器的网关进入 docker0 网桥(如果是同一台宿主机上的容器间通信,走的是直连规则),从而出现在宿主机上。

  • Node1 flannel0处理

这时候,这个 IP 包的下一个目的地,就取决于宿主机上的路由规则了。此时,Flannel 已经在宿主机上创建出了一系列的路由规则,指定了访问container-2的网段进入到一个叫做flannel0的设备中。

100.96.0.0/16 dev flannel0  proto kernel  scope link  src 100.96.1.0

flannel0 设备:TUN 设备(Tunnel 设备),当操作系统将一个 IP 包发送给 flannel0 设备之后,flannel0 就会把这个 IP 包,交给创建这个设备的应用程序,也就是 Flannel 进程。这是一个从内核态(Linux 操作系统)向用户态(Flannel 进程)的流动方向。

  • Node1 flanneld 进程找到 Node2 IP地址,并封装为UDP包发往Node2

宿主机上的 flanneld 进程(Flannel 项目在每个宿主机上的主进程),就会收到这个 IP 包。flanneld 进程在处理由 flannel0 传入的 IP 包时,就可以根据目的 IP 的地址(比如 100.96.2.3),匹配到对应的子网(比如 100.96.2.0/24),从 Etcd 中找到这个子网对应的宿主机的 IP 地址是 10.168.0.3。

由 Flannel 管理的容器网络里,一台宿主机上的所有容器,都属于该宿主机被分配的一个“子网”。在我们的例子中,Node 1 的子网是 100.96.1.0/24,container-1 的 IP 地址是 100.96.1.2。Node 2 的子网是 100.96.2.0/24,container-2 的 IP 地址是 100.96.2.3。子网与宿主机的对应关系,正是保存在 Etcd 当中。

flanneld 会把这个 IP 包直接封装在一个 UDP 包里,然后发送给 Node 2,这个 UDP 包的源地址,就是 flanneld 所在的 Node 1 的地址,而目的地址,则是 container-2 所在的宿主机 Node 2 的地址。

这个请求得以完成的原因是,每台宿主机上的 flanneld,都监听着一个 8285 端口,所以 flanneld 只要把 UDP 包发往 Node 2 的 8285 端口即可。

  • Node2 flanneld 解析 UDP 包,并发往 flannel0 设备

Node2 上的flanneld 可以从这个 UDP 包里解析出封装在里面的、container-1 发来的原 IP 包,把这个 IP 包发送给它所管理的 TUN 设备,即 flannel0 设备。

  • Node2到container-2

这是一个从用户态向内核态的流动方向,Linux 内核网络栈就会负责处理这个 IP 包,具体的处理方法,就是通过本机的路由表来寻找这个 IP 包的下一步流向,Linux 内核会按照路由规则,把这个 IP 包转发给 docker0 网桥,然后通过网桥二层交换机发送到对应的Veth,通过Veth Pair进入到container-2 的 Network Namespace 里。

100.96.2.0/24 dev docker0  proto kernel  scope link  src 100.96.2.1

总结

  • Flannel UDP 模式提供的其实是一个三层的 Overlay 网络,即:它首先对发出端的 IP 包进行 UDP 封装,然后在接收端进行解封装拿到原始的 IP 包,进而把这个 IP 包转发给目标容器。这就好比,Flannel 在不同宿主机上的两个容器之间打通了一条“隧道”,使得这两个容器可以直接使用 IP 地址进行通信,而无需关心容器和宿主机的分布情况。
  • UDP 模式有严重的性能问题,已经被废弃了,原因时 flanneld 的处理过程,使用了 flannel0 这个 TUN 设备,需要经过多次用户态和内核态之间的数据拷贝。且Flannel 进行 UDP 封装(Encapsulation)和解封装(Decapsulation)的过程,也都是在用户态完成的。

image-20230108214828094

  • 我们在进行系统级编程的时候,有一个非常重要的优化原则,就是要减少用户态到内核态的切换次数,并且把核心的处理逻辑都放在内核态进行

2.1.2 VXLAN

原理

VXLAN,即 Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络虚似化技术。VXLAN 可以完全在内核态实现上述封装和解封装的工作,从而通过与前面相似的“隧道”机制,构建出覆盖网络(Overlay Network)。

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

为了能够在二层网络上打通“隧道”,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)。VTEP 设备的作用,其实跟前面的 flanneld 进程非常相似。只不过,它进行封装和解封装的对象,是二层数据帧(Ethernet frame);而且这个工作的执行流程,全部是在内核里完成的(因为 VXLAN 本身就是 Linux 内核中的一个模块)。

每台宿主机上名叫 flannel.1 的设备,是 VXLAN 所需的 VTEP 设备,它既有 IP 地址,也有 MAC 地址。

image-20230108220932569

以Node1上的container-1访问Node2上的container-2为例:

  • Node1访问container-2经由flannel.1设备,封装二层包内部数据帧

Node1上的路由表确定了访问 container-2 所在网段经由 flannel.1 设备发出,发出的网关地址是 Node2 的VTEP设备的IP地址。

有了目的 VTEP 设备的 IP 地址,还需要 MAC 地址,而flanneld 进程在节点启动时,会自动将 VTEP设备的 MAC 地址添加到ARP表中。

# 在 Node 1 上
$ ip neigh show dev flannel.1
10.1.16.0 lladdr 5e:f8:4f:00:e3:37 PERMANENT

有了目的 VTEP 设备的 MAC 地址后,Linux 内核就可以开始二层封包工作了。封包过程只是加一个二层头,不会改变“原始 IP 包”的内容。Inner IP Header 字段,依然是 container-2 的 IP 地址,即 10.1.16.3。

  • Node1获取目的VTEP设备所在宿主机IP,封装外部数据帧,发送UDP包

封装好二层头后,并不能在宿主机二层网络里传输,我们称它为”内部数据帧“,Linux 内核还需要再把“内部数据帧”进一步封装成为宿主机网络里的一个普通的数据帧,好让它“载着”“内部数据帧”,通过宿主机的 eth0 网卡进行传输。

Linux 内核会在“内部数据帧”前面,加上一个特殊的 VXLAN 头,用来表示这个“乘客”实际上是一个 VXLAN 要使用的数据帧。VXLAN 头里有一个重要的标志叫作VNI,它是 VTEP 设备识别某个数据帧是不是应该归自己处理的重要标识。而在 Flannel 中,VNI 的默认值是 1。

Linux 内核会把这个数据帧封装进一个 UDP 包里发出去,但是此时只知道目的flannel.1 设备的 MAC 地址,却不知道对应的宿主机地址是什么。

在这种场景下,flannel.1 设备实际上要扮演一个“网桥”的角色,在二层网络进行 UDP 包的转发。而在 Linux 内核里面,“网桥”设备进行转发的依据,来自于一个叫作 FDB(Forwarding Database)的转发数据库。这个 flannel.1“网桥”对应的 FDB 信息,也是 flanneld 进程负责维护的。

# 在 Node 1 上,使用“目的 VTEP 设备”的 MAC 地址进行查询
$ bridge fdb show flannel.1 | grep 5e:f8:4f:00:e3:37
5e:f8:4f:00:e3:37 dev flannel.1 dst 10.168.0.3 self permanent

现在UDP包要访问的目的地址找到后,接下来的流程,就是一个正常的、宿主机网络上的封包工作。

image-20230108223150206

  • Node2解析UDP包并拿到内部数据帧,发送给目的VTEP设备,进而获取到原始IP包

Node 1 上的 flannel.1 设备就可以把这个数据帧从 Node 1 的 eth0 网卡发出去。显然,这个帧会经过宿主机网络来到 Node 2 的 eth0 网卡。Node 2 的内核网络栈会发现这个数据帧里有 VXLAN Header,并且 VNI=1。所以 Linux 内核会对它进行拆包,拿到里面的内部数据帧,然后根据 VNI 的值,把它交给 Node 2 上的 flannel.1 设备。而 flannel.1 设备则会进一步拆包,取出“原始 IP 包”。

总结

VXLAN 模式组建的覆盖网络,其实就是一个由不同宿主机上的 VTEP 设备,也就是 flannel.1 设备组成的虚拟二层网络。对于 VTEP 设备来说,它发出的“内部数据帧”就仿佛是一直在这个虚拟的二层网络上流动。这,也正是覆盖(Overlay )网络的含义。

2.1.3 host-gw

纯三层(Pure Layer 3)网络。

Kubernetes 是通过一个叫作 CNI 的接口,维护了一个单独的网桥来代替 docker0。这个网桥的名字就叫作:CNI 网桥,它在宿主机上的设备名称默认是:cni0。

image-20230109215615796

以Node1上的container-1访问Node2上的container-2为例:

  • 宿主机Node1上flanneld创建iptables规则,确定访问Node2上容器经由eth0,下一跳为Node2的eth0
$ ip route
...
10.244.1.0/24 via 10.168.0.3 dev eth0

配置了下一跳地址,当 IP 包从网络层进入链路层封装成帧的时候,eth0 设备就会使用下一跳地址对应的 MAC 地址,作为该数据帧的目的 MAC 地址,即 Node2 的 MAC 地址。数据帧就会从 Node 1 通过宿主机的二层网络顺利到达 Node 2 上。

  • 宿主机Node2上内核网络栈根据目标IP,通过iptables规则,进入到cni0网桥

Node 2 的内核网络栈从二层数据帧里拿到 IP 包后,看到目标 IP 地址为 Infra-container-2 的 IP 地址,进入到 cni0 网桥,进而进入到 Infra-container-2 中。

原理

host-gw 模式的工作原理,其实就是将每个 Flannel 子网(Flannel Subnet,比如:10.244.1.0/24)的“下一跳”,设置成了该子网对应的宿主机的 IP 地址。

Flannel 子网和主机的信息,都是保存在 Etcd 当中的。flanneld 只需要 WACTH 这些数据的变化,然后实时更新路由表即可。

在这种模式下,容器通信的过程就免除了额外的封包和解包带来的性能损耗,性能比VXLAN”隧道“机制好。

Flannel host-gw 模式必须要求集群宿主机之间是二层连通的。

2.2 Calico

Calico 项目提供的网络解决方案,与 Flannel 的 host-gw 模式,几乎是完全一样的。也就是说,Calico 也会在每台宿主机上,添加一个格式如下所示的路由规则:

< 目的容器 IP 地址段 > via < 网关的 IP 地址 > dev eth0

这个三层网络方案得以正常工作的核心,是为每个容器的 IP 地址,找到它所对应的、“下一跳”的网关

不同于 Flannel 通过 Etcd 和宿主机上的 flanneld 来维护路由信息的做法,Calico 项目使用了BGP来自动地在整个集群中分发路由信息。

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

Calico 项目由三个部分组成:

  • Calico 的 CNI 插件。这是 Calico 与 Kubernetes 对接的部分。
  • Felix。它是一个 DaemonSet,负责在宿主机上插入路由规则(即:写入 Linux 内核的 FIB 转发信息库),以及维护 Calico 所需的网络设备等工作。
  • BIRD。它就是 BGP 的客户端,专门负责在集群里分发路由规则信息。

除了对路由信息的维护方式之外,Calico 项目与 Flannel 的 host-gw 模式的另一个不同之处,就是它不会在宿主机上创建任何网桥设备

image-20230109222655542

绿色实线标出的路径,就是一个 IP 包从 Node 1 上的 Container 1,到达 Node 2 上的 Container 4 的完整路径。

容器发出的 IP 包会经过 Veth Pair 设备出现在宿主机上。然后,宿主机网络栈就会根据路由规则的下一跳 IP 地址,把它们转发给正确的网关。

由于 Calico 没有使用 CNI 的网桥模式,Calico 的 CNI 插件还需要在宿主机上为每个容器的 Veth Pair 设备配置一条路由规则,用于接收传入的 IP 包。比如,宿主机 Node 2 上的 Container 4 对应的路由规则,如下所示:

10.233.2.3 dev cali5863f3 scope link

即:发往 10.233.2.3 的 IP 包,应该进入 cali5863f3 设备。

2.2.1 Node-to-Node Mesh

Calico 维护的网络在默认配置下,是一个被称为“Node-to-Node Mesh”的模式。这时候,每台宿主机上的 BGP Client 都需要跟其他所有节点的 BGP Client 进行通信以便交换路由信息。但是,随着节点数量 N 的增加,这些连接的数量就会以 N²的规模快速增长,从而给集群本身的网络带来巨大的压力。

2.2.2 Route Reflector

在这种模式下,Calico 会指定一个或者几个专门的节点,来负责跟所有节点建立 BGP 连接从而学习到全局的路由规则。而其他节点,只需要跟这几个专门的节点交换路由信息,就可以获得整个集群的路由规则信息了。

这些专门的节点,就是所谓的 Route Reflector 节点,它们实际上扮演了“中间代理”的角色,从而把 BGP 连接的规模控制在 N 的数量级上。

2.2.3 IPIP

如果集群中两个宿主机不在同一个子网中,二层不是连通的,那么就没办法通过在宿主机上的iptables规则,直接设置目标容器的ip下一跳为目标宿主机的ip,没办法通过二层网络把 IP 包发送到下一跳地址。

10.233.2.0/16 via 192.168.2.2 eth0

在这种情况下,就需要为 Calico 打开 IPIP 模式。

image-20230109223435163

在 Calico 的 IPIP 模式下,Felix 进程在 Node 1 上添加的路由规则,会稍微不同,如下所示:

10.233.2.0/24 via 192.168.2.2 tunl0

尽管这条规则的下一跳地址仍然是 Node 2 的 IP 地址,但这一次,要负责将 IP 包发出去的设备,变成了 tunl0。

Calico 使用的这个 tunl0 设备,是一个 IP 隧道(IP tunnel)设备。

IP 包进入 IP 隧道设备之后,就会被 Linux 内核的 IPIP 驱动接管。IPIP 驱动会将这个 IP 包直接封装在一个宿主机网络的 IP 包中,如下所示:

image-20230109224458590

经过封装后的新的 IP 包的目的地址(图中的 Outer IP Header 部分),正是原 IP 包的下一跳地址,即 Node 2 的 IP 地址:192.168.2.2。而原 IP 包本身,则会被直接封装成新 IP 包的 Payload。这样,原先从容器到 Node 2 的 IP 包,就被伪装成了一个从 Node 1 到 Node 2 的 IP 包。

由于宿主机之间已经使用路由器配置了三层转发,也就是设置了宿主机之间的“下一跳”。所以这个 IP 包在离开 Node 1 之后,就可以经过路由器,最终“跳”到 Node 2 上。

这时,Node 2 的网络内核栈会使用 IPIP 驱动进行解包,从而拿到原始的 IP 包。然后,原始 IP 包就会经过路由规则和 Veth Pair 设备到达目的容器内部。

当 Calico 使用 IPIP 模式的时候,集群的网络性能会因为额外的封包和解包工作而下降。在实际测试中,Calico IPIP 模式与 Flannel VXLAN 模式的性能大致相当。所以,在实际使用时,如非硬性需求,我建议你将所有宿主机节点放在一个子网里,避免使用 IPIP。

2.2.4 BGP Peer

通过 IPIP 模式需要额外封包和解包,通过设置BGP Perr,设置宿主机和边界路由器的下一跳地址,也可以实现类似Flannel host-gw模式。不过这种情况更适用于私有部署环境,公有云环境下,宿主机之间的网关,肯定不会允许用户进行干预和设置。

这种方案,是使用一个或多个独立组件负责搜集整个集群里的所有路由信息,然后通过 BGP 协议同步给网关。而我们前面提到,在大规模集群中,Calico 本身就推荐使用 Route Reflector 节点的方式进行组网。所以,这里负责跟宿主机网关进行沟通的独立组件,直接由 Route Reflector 兼任即可。

这种情况下网关的 BGP Peer 个数是有限并且固定的。所以我们就可以直接把这些独立组件配置成路由器的 BGP Peer,而无需 Dynamic Neighbors 的支持,它们只需要 WATCH Etcd 里的宿主机和对应网段的变化信息,然后把这些信息通过 BGP 协议分发给网关即可。

2.3 推荐配置

  • 在公有云上,由于宿主机网络本身比较“直白”,一般会推荐更加简单的 Flannel host-gw 模式。

  • 在私有部署环境中,Calico 项目能够覆盖更多的场景,更受推荐。

3 NetworkPolicy

3.1 简介

Kubernetes 里的 Pod 默认都是“允许所有”(Accept All)的,即:Pod 可以接收来自任何发送方的请求;或者,向任何接收方发送请求。如果你要对这个情况作出限制,就必须通过 NetworkPolicy 对象来指定。

一旦 Pod 被 NetworkPolicy 选中,那么这个 Pod 就会进入“拒绝所有”(Deny All)的状态,即:这个 Pod 既不允许被外界访问,也不允许对外界发起访问。

NetworkPolicy 定义的规则,其实就是“白名单”。

3.2 原理

Kubernetes 网络插件对 Pod 进行隔离,其实是靠在宿主机上生成 NetworkPolicy 对应的 iptable 规则来实现的。

iptables 只是一个操作 Linux 内核 Netfilter 子系统的“界面”。Netfilter 子系统的作用,是 Linux 内核里挡在“网卡”和“用户态进程”之间的一道“防火墙”。

image-20230110230128263

IP 包“一进一出”的两条路径上,有几个关键的“检查点”,它们正是 Netfilter 设置“防火墙”的地方。在 iptables 中,这些“检查点”被称为:链(Chain)。这是因为这些“检查点”对应的 iptables 规则,是按照定义顺序依次进行匹配的。

Kubernetes 为 Pod 配置 NetworkPolicy 后,会生成两组 iptables 规则。

  • 第一组规则,负责“拦截”对被隔离 Pod 的访问请求
iptables -A FORWARD -d $podIP -m physdev --physdev-is-bridged -j KUBE-POD-SPECIFIC-FW-CHAIN
iptables -A FORWARD -d $podIP -j KUBE-POD-SPECIFIC-FW-CHAIN

第一条 FORWARD 链“拦截”的是一种特殊情况:它对应的是同一台宿主机上容器之间经过 CNI 网桥进行通信的流入数据包。其中,–physdev-is-bridged 的意思就是,这个 FORWARD 链匹配的是,通过本机上的网桥设备,发往目的地址是 podIP 的 IP 包。

第二条 FORWARD 链“拦截”的则是最普遍的情况,即:容器跨主通信。这时候,流入容器的数据包都是经过路由转发(FORWARD 检查点)来的。

这些规则最后都跳转(即:-j)到了名叫 KUBE-POD-SPECIFIC-FW-CHAIN 的规则上。它正是网络插件为 NetworkPolicy 设置的第二组规则。

  • 第二组规则,针对被隔离 Pod 的访问请求做出“允许”或“拒绝”的判断
iptables -A KUBE-POD-SPECIFIC-FW-CHAIN -j KUBE-NWPLCY-CHAIN
iptables -A KUBE-POD-SPECIFIC-FW-CHAIN -j REJECT --reject-with icmp-port-unreachable

首先在第一条规则里,我们会把 IP 包转交给前面定义的 KUBE-NWPLCY-CHAIN 规则去进行匹配。按照我们之前的讲述,如果匹配成功,那么 IP 包就会被“允许通过”。

而如果匹配失败,IP 包就会来到第二条规则上。可以看到,它是一条 REJECT 规则。通过这条规则,不满足 NetworkPolicy 定义的请求就会被拒绝掉,从而实现了对该容器的“隔离”。

4 Service

4.1 Service原理

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

Service 是由 kube-proxy 组件,加上 iptables 来共同实现的。

4.1.1 iptables模式

kube-proxy 可以通过 Service 的 Informer 感知到这样一个 Service 对象的添加,作为对这个事件的响应,它会在宿主机上创建这样一条 iptables 规则(你可以通过 iptables-save 看到它),如下所示:

-A KUBE-SERVICES -d 10.0.1.175/32 -p tcp -m comment --comment "default/hostnames: cluster IP" -m tcp --dport 80 -j KUBE-SVC-NWV5X2332I4OT4T3

-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-WNBA2IHDGP2BOBGZ
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-X3P2623AGDH6CDF3
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -j KUBE-SEP-57KPRZ3JQVENLNBR

访问Service的VIP(10.0.1.175)将跳转到KUBE-SVC-NWV5X2332I4OT4T3规则,它是一组规则的集合,实际上是一组随机模式(–mode random)的 iptables 链,并且概率均等,随机转发的目的地正是这个 Service 代理的三个 Pod。

-A KUBE-SEP-57KPRZ3JQVENLNBR -s 10.244.3.6/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-57KPRZ3JQVENLNBR -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.3.6:9376
 
-A KUBE-SEP-WNBA2IHDGP2BOBGZ -s 10.244.1.7/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-WNBA2IHDGP2BOBGZ -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.1.7:9376
 
-A KUBE-SEP-X3P2623AGDH6CDF3 -s 10.244.2.3/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-X3P2623AGDH6CDF3 -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.2.3:9376

这三条链,其实是三条 DNAT 规则,在 PREROUTING 检查点之前,也就是在路由之前,将流入 IP 包的目的地址和端口,改成–to-destination 所指定的新的目的地址和端口,即被代理 Pod 的 IP 地址和端口。

4.1.2 ipvs模式

kube-proxy 设置–proxy-mode=ipvs

kube-proxy 通过 iptables 处理 Service 的过程,其实需要在宿主机上设置相当多的 iptables 规则。而且,kube-proxy 还需要在控制循环里不断地刷新这些规则来确保它们始终是正确的。当宿主机上有大量 Pod 的时候,成百上千条 iptables 规则不断地被刷新,会大量占用该宿主机的 CPU 资源,甚至会让宿主机“卡”在这个过程中。

一直以来,基于 iptables 的 Service 实现,都是制约 Kubernetes 项目承载更多量级的 Pod 的主要障碍。

IPVS 模式的工作原理,其实跟 iptables 模式类似。当我们创建了前面的 Service 之后,kube-proxy 首先会在宿主机上创建一个虚拟网卡(叫作:kube-ipvs0),并为它分配 Service VIP 作为 IP 地址,如下所示:

# ip addr
  ...
  73:kube-ipvs0:<BROADCAST,NOARP>  mtu 1500 qdisc noop state DOWN qlen 1000
  link/ether  1a:ce:f5:5f:c1:4d brd ff:ff:ff:ff:ff:ff
  inet 10.0.1.175/32  scope global kube-ipvs0
  valid_lft forever  preferred_lft forever

接下来,kube-proxy 就会通过 Linux 的 IPVS 模块,为这个 IP 地址设置三个 IPVS 虚拟主机,并设置这三个虚拟主机之间使用轮询模式 (rr) 来作为负载均衡策略。我们可以通过 ipvsadm 查看到这个设置,如下所示:

# ipvsadm -ln
 IP Virtual Server version 1.2.1 (size=4096)
  Prot LocalAddress:Port Scheduler Flags
    ->  RemoteAddress:Port           Forward  Weight ActiveConn InActConn     
  TCP  10.102.128.4:80 rr
    ->  10.244.3.6:9376    Masq    1       0          0         
    ->  10.244.1.7:9376    Masq    1       0          0
    ->  10.244.2.3:9376    Masq    1       0          0

这三个 IPVS 虚拟主机的 IP 地址和端口,对应的正是三个被代理的 Pod。

这时候,任何发往 10.102.128.4:80 的请求,就都会被 IPVS 模块转发到某一个后端 Pod 上了。

4.1.3 ipvs对比iptables

  • iptables规则复杂零乱。

  • iptables规则多了之后性能下降,这是因为iptables规则是基于链表实现,查找复杂度为O(n),当规模非常大时,查找和处理的开销就特别大。

  • iptables主要是专门用来做主机防火墙的,而不是专长做负载均衡的。虽然通过iptables的statistic模块以及DNAT能够实现最简单的只支持概率轮询的负载均衡,但是往往我们还需要更多更灵活的算法,比如基于最少连接算法、源地址HASH算法等。

  • ipvs却是专门做负载均衡的,配置简单,基于散列查找O(1)复杂度性能好,支持数十种调度算法。

  • ipvs 只负责负载均衡和代理功能,而一个完整的 Service 流程正常工作所需要的包过滤、SNAT 等操作,还是要靠 iptables 来实现。只不过,这些辅助性的 iptables 规则数量有限,也不会随着 Pod 数量的增加而增加。

4.2 Service暴露

Service 的访问信息在 Kubernetes 集群之外,其实是无效的。

所谓 Service 的访问入口,其实就是每台宿主机上由 kube-proxy 生成的 iptables 规则,以及 kube-dns 生成的 DNS 记录。而一旦离开了这个集群,这些信息对用户来说,也就自然没有作用了。

如何从外部(Kubernetes 集群之外),访问到 Kubernetes 里创建的 Service?

4.2.1 NodePort

NodePort模式会为Service在每台宿主机上开辟一个端口,外界连通时只要访问集群内任一宿主机ip:port即可访问Service。

原理

  • DNAT

kube-proxy 要做的,就是在每台宿主机上生成这样一条 iptables 规则:

-A KUBE-NODEPORTS -p tcp -m comment --comment "default/my-nginx: nodePort" -m tcp --dport 8080 -j KUBE-SVC-67RL4FN6JRUPOJYM

KUBE-SVC-67RL4FN6JRUPOJYM 其实就是一组随机模式的 iptables 规则,将对Service的访问转发到后端Pod。

  • SNAT

需要注意的是,在 NodePort 方式下,Kubernetes 会在 IP 包离开宿主机发往目的 Pod 时,对这个 IP 包做一次 SNAT 操作,如下所示:

-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE

这条规则设置在 POSTROUTING 检查点,也就是说,它给即将离开这台主机的 IP 包,进行了一次 SNAT 操作,将这个 IP 包的源地址替换成了这台宿主机上的 CNI 网桥地址,或者宿主机本身的 IP 地址(如果 CNI 网桥不存在的话)。

这个 SNAT 操作只需要对 Service 转发出来的 IP 包进行(否则普通的 IP 包就被影响了)。而 iptables 做这个判断的依据,就是查看该 IP 包是否有一个“0x4000”的“标志”。你应该还记得,这个标志正是在 IP 包被执行 DNAT 操作之前被打上去的。

对流出的包做 SNAT 操作的原因

client通过node2访问service时,可能会把ip包转发给node1上的pod,当node1上的pod处理完成后,如果没有做snat操作,ip包的源地址就是client的ip地址,此时pod会直接将回复发给client。对于 client 来说,它的请求明明发给了 node 2,收到的回复却来自 node 1,这个 client 很可能会报错。

           client
             \ ^
              \ \
               v \
   node 1 <--- node 2
    | ^   SNAT
    | |   --->
    v |
 endpoint

4.2.2 LoadBalancer

LoadBalancer 类型的 Service 适用于公有云上的 Kubernetes 服务。

在公有云提供的 Kubernetes 服务里,都使用了一个叫作 CloudProvider 的转接层,来跟公有云本身的 API 进行对接。所以,在上述 LoadBalancer 类型的 Service 被提交后,Kubernetes 就会调用 CloudProvider 在公有云上为你创建一个负载均衡服务,并且把被代理的 Pod 的 IP 地址配置给负载均衡服务做后端。

4.2.3 ExternalName

ExternalName 类型的 Service,其实是在 kube-dns 里为你添加了一条 CNAME 记录。

指定externalName后,通过 Service 的 DNS 名字访问它,Kubernetes 为你返回的就是 externalName,访问 Service 的 DNS 域名和访问 externalName 是一个效果。

4.2.4 ExternalIPs

Kubernetes 的 Service 还允许你为 Service 分配公有 IP 地址,但它必须是至少能够路由到一个 Kubernetes 的节点。

5 Ingress

所谓 Ingress 对象,其实就是 Kubernetes 项目对“反向代理”的一种抽象。

一个 Ingress 对象的主要内容,实际上就是一个“反向代理”服务(比如:Nginx)的配置文件的描述。而这个代理服务对应的转发规则,就是 IngressRule。

在实际的使用中,你只需要从社区里选择一个具体的 Ingress Controller,把它部署在 Kubernetes 集群里即可。

这个 Ingress Controller 会根据你定义的 Ingress 对象,提供对应的代理能力。目前,业界常用的各种反向代理项目,比如 Nginx、HAProxy、Envoy、Traefik 等,都已经为 Kubernetes 专门维护了对应的 Ingress Controller。

Nginx Ingress Controller 为你提供的服务,其实是一个可以根据 Ingress 对象和被代理后端 Service 的变化,来自动进行更新的 Nginx 负载均衡器。

为了让用户能够用到这个 Nginx,我们就需要创建一个 Service 来把 Nginx Ingress Controller 管理的 Nginx 服务暴露出去,可以使用NodePort或Loadbalancer的方式。访问这个 Service,便可以访问到根据 Nginx Ingress Controller,进而根据Ingress转发规则,访问到对应的后端 Service 及 Pod。

如果我的请求没有匹配到任何一条 IngressRule,Ingress Controller 也允许你通过 Pod 启动命令里的–default-backend-service 参数,设置一条默认规则,比如:–default-backend-service=nginx-default-backend。

目前,Ingress 只能工作在七层,而 Service 只能工作在四层。所以当你想要在 Kubernetes 里为应用进行 TLS 配置等 HTTP 相关的操作时,都必须通过 Ingress 来进行。

6 CNI网络插件

CNI 全称是 Container Network Interface,即容器网络的 API 接口。

CNI 是 K8s 中标准的一个调用网络实现的接口。Kubelet 通过这个标准的 API 来调用不同的网络插件以实现不同的网络配置方式,实现了这个接口的就是 CNI 插件,它实现了一系列的 CNI API 接口。常见的 CNI 插件包括 Calico、flannel、Terway、Weave Net 以及 Contiv。

CNI 插件负责将网络接口插入容器网络命名空间(例如,veth 对的一端),并在主机上进行任何必要的改变(例如将 veth 的另一端连接到网桥)。然后将 IP 分配给接口,并通过调用适当的 IPAM 插件来设置与 “IP 地址管理” 部分一致的路由。

6.1 Kubernetes 中如何使用 CNI

CNI插件基本操作:

  • 将容器添加到网络
  • 从网络中删除容器
  • IP分配

K8s 通过 CNI 配置文件来决定使用什么 CNI,基本的使用方法为:

  • 每个结点上配置 CNI 配置文件(/etc/cni/net.d/xxnet.conf),其中 xxnet.conf 是某一个网络配置文件的名称。
  • 安装 CNI 配置文件中所对应的二进制插件。
  • 在节点上创建 Pod 之后,Kubelet 就会根据 CNI 配置文件执行前两步所安装的 CNI 插件,包括创建cni 网桥、veth pair、调用ipam分配容器ip,设置容器接口ip、设置容器默认路由、设置cni网桥ip及发卡模式等操作。
  • 在执行完上述操作之后,CNI 插件会把容器的 IP 地址等信息返回给容器运行时(例如dockershim),然后被 kubelet 添加到 Pod 的 Status 字段。

6.2 CNI 插件所需的基础可执行文件

安装 kubernetes-cni 包完成后,在宿主机的 /opt/cni/bin 目录下看到它们,如下图所示:

$ ls -al /opt/cni/bin/
total 73088
-rwxr-xr-x 1 root root  3890407 Aug 17  2017 bridge
-rwxr-xr-x 1 root root  9921982 Aug 17  2017 dhcp
-rwxr-xr-x 1 root root  2814104 Aug 17  2017 flannel
-rwxr-xr-x 1 root root  2991965 Aug 17  2017 host-local
-rwxr-xr-x 1 root root  3475802 Aug 17  2017 ipvlan
-rwxr-xr-x 1 root root  3026388 Aug 17  2017 loopback
-rwxr-xr-x 1 root root  3520724 Aug 17  2017 macvlan
-rwxr-xr-x 1 root root  3470464 Aug 17  2017 portmap
-rwxr-xr-x 1 root root  3877986 Aug 17  2017 ptp
-rwxr-xr-x 1 root root  2605279 Aug 17  2017 sample
-rwxr-xr-x 1 root root  2808402 Aug 17  2017 tuning
-rwxr-xr-x 1 root root  3475750 Aug 17  2017 vlan

这些 CNI 的基础可执行文件,按照功能可以分为三类:

第一类,叫作 Main 插件,它是用来创建具体网络设备的二进制文件。比如,bridge(网桥设备)、ipvlan、loopback(lo 设备)、macvlan、ptp(Veth Pair 设备),以及 vlan。

第二类,叫作 IPAM(IP Address Management)插件,它是负责分配 IP 地址的二进制文件。比如,dhcp,这个文件会向 DHCP 服务器发起请求;host-local,则会使用预先配置的 IP 地址段来进行分配。

第三类,是由 CNI 社区维护的内置 CNI 插件。比如:flannel,就是专门为 Flannel 项目提供的 CNI 插件;tuning,是一个通过 sysctl 调整网络设备参数的二进制文件;portmap,是一个通过 iptables 配置端口映射的二进制文件;bandwidth,是一个使用 Token Bucket Filter (TBF) 来进行限流的二进制文件。

posted @ 2023-01-08 22:57  hunter-w  阅读(141)  评论(0编辑  收藏  举报