Docker&k8s(六)

容器网络

​ 所谓“网络栈”,就包括了:网卡(Network Interface)、回环设备(Loopback Device)、路由表(Routing Table)和 iptables 规则。对于一个进程来说,这些要素,其实就构成了它发起和响应网络请求的基本环境。在大多数情况下,我们都希望容器进程能使用自己 Network Namespace 里的网络栈,即:拥有属于自己的 IP 地址和端口。

​ 多台主机间的通信需要通过交换机,在linux中,能够起到虚拟交换机作用的是网桥。工作在数据链路层的设备,主要功能是根据 MAC 地址学习来将数据包转发到网桥的不同端口(Port)上。Docker 项目会默认在宿主机上创建一个名叫 docker0 的网桥,凡是连接在 docker0 网桥上的容器,就可以通过它来进行通信。

​ 需要通过Veth Pair的虚拟设备把容器连接到docker0网桥上。Veth Pair 设备的特点是:它被创建出来后,总是以两张虚拟网卡(Veth Peer)的形式成对出现的。并且,从其中一个“网卡”发出的数据包,可以直接出现在与它对应的另一张“网卡”上,哪怕这两个“网卡”在不同的 Network Namespace 里。使得 Veth Pair 常常被用作连接不同 Network Namespace 的“网线”。

原理:

​ 当你在 nginx-1 容器里访问 nginx-2 容器的 IP 地址(比如 ping 172.17.0.3)的时候,这个目的 IP 地址会匹配到 nginx-1 容器里的第二条路由规则。可以看到,这条路由规则的网关(Gateway)是 0.0.0.0,这就意味着这是一条直连规则,即:凡是匹配到这条规则的 IP 包,应该经过本机的 eth0 网卡,通过二层网络直接发往目的主机。

​ 而要通过二层网络到达 nginx-2 容器,就需要有 172.17.0.3 这个 IP 地址对应的 MAC 地址。所以 nginx-1 容器的网络协议栈,就需要通过 eth0 网卡发送一个 ARP 广播,来通过 IP 地址查找对应的 MAC 地址。

备注:ARP(Address Resolution Protocol),是通过三层的 IP 地址找到对应的二层 MAC 地址的协议。

​ 当一张虚拟网卡被插在网桥上,就会变成从设备,降级成一个端口,只能接受流入的数据包,然后把这些数据包的管理操作全部交给对应的网桥。

​ 在收到这些 ARP 请求之后,docker0 网桥就会扮演二层交换机的角色,把 ARP 广播转发到其他被“插”在 docker0 上的虚拟网卡上。这样,同样连接在 docker0 上的 nginx-2 容器的网络协议栈就会收到这个 ARP 请求,从而将 172.17.0.3 所对应的 MAC 地址回复给 nginx-1 容器。

​ 有了这个目的 MAC 地址,nginx-1 容器的 eth0 网卡就可以将数据包发出去。这个数据包会由docker0网桥处理转发,根据数据包的目的MAC地址,在它的CAM表(交换机通过 MAC 地址学习维护的端口和 MAC 地址的对应表)里查到对应的端口(Port)为:vethb4963f3,然后把数据包发往这个端口(正是 nginx-2 容器“插”在 docker0 网桥上的另一块虚拟网卡)。

image-20200814161809829

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

​ 当你在一台宿主机上,访问该宿主机上的容器的 IP 地址时,这个请求的数据包,也是先根据路由规则到达 docker0 网桥,然后被转发到对应的 Veth Pair 设备,最后出现在容器里。

image-20200814162055282

​ 当一个容器连接到另一个宿主机时,会先经过docker0网桥出现在宿主机上,然后根据宿主机的路由表的路由规则,交给宿主机的eth0处理,然后通过网卡转发到对应的宿主机上。

image-20200814164347932

当你遇到容器连不通“外网”的时候,你都应该先试试 docker0 网桥能不能 ping 通,然后查看一下跟 docker0 和 Veth Pair 设备相关的 iptables 规则是不是有异常,往往就能够找到问题的答案了。

​ 如果是不同宿主机上的容器通信,而两边的docker0网桥没有关联无法连通,我们则需要一个集群公用的网桥,把所有容器都连接到这个网桥上。

image-20200814164548549

​ 我们需要在已有的宿主机网络上,再通过软件构建一个覆盖在已有宿主机网络之上的、可以把所有容器连通在一起的虚拟网络。所以,这种技术就被称为:Overlay Network(覆盖网络)。

跨主网络的实现原理之——UDP

​ IP包寻找下一个目的地时,根据宿主机的路由规则匹配不到docker0的路由规则,只能匹配到flannel0的设备,这是一个Tunnel设备(工作在三层的虚拟网络设备,在操作系统内核和用户应用程序之间传递IP包),也就是IP包发送给flannel0。然后交给Flannel进程。

​ 当 IP 包从容器经过 docker0 出现在宿主机,然后又根据路由表进入 flannel0 设备后,宿主机上的 flanneld 进程(Flannel 项目在每个宿主机上的主进程),就会收到这个 IP 包。然后,flanneld 看到了这个 IP 包的目的地址,就把它发送给了目的地 宿主机。

flanneld 又是如何知道这个 IP 地址对应的容器,是在的呢——子网(Subnet)

​ 在由 Flannel 管理的容器网络里,一台宿主机上的所有容器,都属于该宿主机被分配的一个“子网”,而这些子网与宿主机的对应关系,正是保存在 Etcd 当中

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

​ 然后flanneld就可以把IP包封装在UDP包里发送给目的宿主机上flanneld进程了,然后解析UDP包,取出原IP包,发送给flannel0设备。

​ 然后通过本机的路由表来找到docker0网桥,把数据包发送给正确的端口,通过Veth Pair设备进入到容器的Network Namespace里。

image-20200814172136041

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

  1. 用户态的容器进程发出的 IP 包经过 docker0 网桥进入内核态;
  2. IP 包根据路由表进入 TUN(flannel0)设备,从而回到用户态的 flanneld 进程;
  3. flanneld 进行 UDP 封包之后重新进入内核态,将 UDP 包通过宿主机的 eth0 发出去。

​ 且UDP的封装和解封都是在用户态(Flannel进程)完成的,操作代价很高。我们在进行系统级编程的时候,有一个非常重要的优化原则,就是要减少用户态到内核态的切换次数,并且把核心的处理逻辑都放在内核态进行。这也是为什么,Flannel 后来支持的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 内核中的一个模块)。

image-20200814173347610

​ 当 Node 2 启动并加入 Flannel 网络之后,在 Node 1(以及所有其他节点)上,flanneld 就会添加一条路由规则,凡是发往 10.1.16.0/24 网段的 IP 包,都需要经过 flannel.1 设备发出,并且,它最后被发往的网关地址是:10.1.16.0(也就是Node2的IP地址)

​ VTEP 设备之间,就需要想办法组成一个虚拟的二层网络,即:通过二层数据帧进行通信。就需要知道目的VTEP设备的MAC地址

可以看到,最新版本的 Flannel 并不依赖 L3 MISS 事件和 ARP 学习,而会在每台节点启动时把它的 VTEP 设备对应的 ARP 记录,直接下放到其他每台宿主机上。

​ 接下来,Linux 内核还需要再把“内部数据帧”进一步封装成为宿主机网络里的一个普通的数据帧,好让它“载着内部数据帧”,通过宿主机的 eth0 网卡进行传输。并加上一个特殊的VXLAN头,标识他实际上是一个VXLAN要使用的数据帧。最后又封装在UDP包里发出去。目的宿主机的IP通过Forwarding Database来找到。接下来的流程,就是一个正常的、宿主机网络上的封包工作。

image-20200814175528561

​ 接下来,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 包”。接下来就回到了我在上一篇文章中分享的单机容器网络的处理流程。最终,IP 包就进入到了 container-2 容器的 Network Namespace 里。

Kubernetes 网络模型与 CNI 网络插件

​ UDP 和 VXLAN的共性是:用户的容器都连接在 docker0 网桥上。而网络插件则在宿主机上创建了一个特殊的设备(UDP 模式创建的是 TUN 设备,VXLAN 模式创建的则是 VTEP 设备),docker0 与这个设备之间,通过 IP 转发(路由表)进行协作。

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

image-20200814175838475

Kubernetes 之所以要设置这样一个与 docker0 网桥功能几乎一样的 CNI 网桥,主要原因包括两个方面:

  • 一方面,Kubernetes 项目并没有使用 Docker 的网络模型(CNM),所以它并不希望、也不具备配置 docker0 网桥的能力;

  • 另一方面,这还与 Kubernetes 如何配置 Pod,也就是 Infra 容器的 Network Namespace 密切相关。

​ Kubernetes 创建一个 Pod 的第一步,就是创建并启动一个 Infra 容器,用来“hold”住这个 Pod 的 Network Namespace。所以,CNI 的设计思想,就是:Kubernetes 在启动 Infra 容器之后,就可以直接调用 CNI 网络插件,为这个 Infra 容器的 Network Namespace,配置符合预期的网络栈。

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

第一类,叫作 Main 插件,它是用来创建具体网络设备的二进制文件。比如,bridge(网桥设备)、ipvlan、loopback(lo 设备)、macvlan、ptp(Veth Pair 设备),以及 vlan。前面提到过的 Flannel、Weave 等项目,都属于“网桥”类型的 CNI 插件。所以在具体的实现中,它们往往会调用 bridge 这个二进制文件。

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

第三类,是由 CNI 社区维护的内置 CNI 插件。比如:flannel,就是专门为 Flannel 项目提供的 CNI 插件;tuning,是一个通过 sysctl 调整网络设备参数的二进制文件;portmap,是一个通过 iptables 配置端口映射的二进制文件;bandwidth,是一个使用 Token Bucket Filter (TBF) 来进行限流的二进制文件。从这些二进制文件中,我们可以看到,如果要实现一个给 Kubernetes 用的容器网络方案,其实需要做两部分工作,以

Flannel 项目为例:

首先,实现这个网络方案本身。这一部分需要编写的,其实就是 flanneld 进程里的主要逻辑。比如,创建和配置 flannel.1 设备、配置宿主机路由、配置 ARP 和 FDB 表里的信息等等。

然后,实现该网络方案对应的 CNI 插件。这一部分主要需要做的,就是配置 Infra 容器里面的网络栈,并把它连接在 CNI 网桥上。

CNI 插件的工作原理:

​ 启动Infra容器,然后执行SetUpPod方法,为CNI插件准备参数。然后调用CNI插件,调用所需的参数分为:

  1. 是由 dockershim 设置的一组 CNI 环境变量。CNI_COMMAND。它的取值只有两种:ADD 和 DEL。这个 ADD 和 DEL 操作,就是 CNI 插件唯一需要实现的两个方法。其中 ADD 操作的含义是:把容器添加到 CNI 网络里;DEL 操作的含义则是:把容器从 CNI 网络里移除掉。意味着把容器以 Veth Pair 的方式“插”到 CNI 网桥上,或者从网桥上“拔”掉。
  2. 是 dockershim 从 CNI 配置文件里加载到的、默认插件的配置信息。

​ 有了上面2部分信息之后,dockershim就会把这个信息给flannel插件,插件会对第二部分信息做补充,然后这个插件来做ADD操作,而这个操作是由CNI bridge这个插件来完成的。这个bridge使用全部的参数信息来执行把容器加入到CNI网络的操作了。它会做如下内容:

  1. 检查是否有CNI网桥,如果没有就创建并UP这个网桥,相当于在宿主机上执行ip link add cni0 type bridgeip link set cni0 up命令
  2. 接下来bridge会通过传递过来的Infra容器的网络名称空间文件进入这个网络名称空间中,然后创建Veth Pair设备,然后UP容器的这一端eth0,然后将另外一端放到宿主机名称空间里并UP这个设备。
  3. bridge把宿主机的一端连接到网桥cni0上
  4. bridge会调用IPAM插件为容器的eht0分配IP地址,并设置默认路由。
  5. bridge会为CNI网桥添加IP地址,如果是第一次的话。
  6. 最后CNI插件会把容器的IP地址等信息返回给dockershim,然后被kubelet添加到POD的status字段中。
  1. 所有容器都可以直接使用 IP 地址与其他容器通信,而无需使用 NAT。
  2. 所有宿主机都可以直接使用 IP 地址与所有容器通信,而无需使用 NAT。反之亦然。
  3. 容器自己“看到”的自己的 IP 地址,和别人(宿主机或者容器)看到的地址是完全一样的。

Flannel 的 host-gw 模式

image-20200814182408190

当设置 Flannel 使用 host-gw 模式之后,flanneld 会在宿主机上创建这样一条规则,以 Node 1 为例:

$ ip route
...
10.244.1.0/24 via 10.168.0.3 dev eth0

这条路由规则的含义是:

​ 目的 IP 地址属于 10.244.1.0/24 网段的 IP 包,应该经过本机的 eth0 设备发出去(即:dev eth0);并且,它下一跳地址(next-hop)是 10.168.0.3(即:via 10.168.0.3)。所谓下一跳地址就是:如果 IP 包从主机 A 发到主机 B,需要经过路由设备 X 的中转。那么 X 的 IP 地址就应该配置为主机 A 的下一跳地址(目的宿主机)。

​ 当 IP 包从网络层进入链路层封装成帧的时候,eth0 设备就会使用下一跳地址对应的 MAC 地址,作为该数据帧的目的 MAC 地址。显然,这个 MAC 地址,正是 Node 2 的 MAC 地址。 Node 2 的内核网络栈从二层数据帧里拿到 IP 包后,会“看到”这个 IP 包的目的 IP 地址是Infra-container-2 的 IP 地址。这时候,根据 Node 2 上的路由表,从而进入 cni0 网桥,进而进入到 Infra-container-2 当中。

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

​ 而在这种模式下,容器通信的过程就免除了额外的封包和解包带来的性能损耗。根据实际的测试,host-gw 的性能损失大约在 10% 左右,而其他所有基于 VXLAN“隧道”机制的网络方案,性能损失都在 20%~30% 左右。核心,就在于 IP 包在封装成帧发送出去的时候,会使用路由表里的“下一跳”来设置目的 MAC 地址。这样,它就会经过二层网络到达目的宿主机。

Calico

​ Calico 也会在每台宿主机上,添加一个格式如下所示的路由规则:

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

​ 网关的 IP 地址,正是目的容器所在宿主机的 IP 地址。不同于 Flannel 通过 Etcd 和宿主机上的 flanneld 来维护路由信息的做法,Calico 项目使用了一个“重型武器”来自动地在整个集群中分发路由信息。——Border Gateway Protocol(边界网关协议)。它是一个 Linux 内核原生就支持的、专门用在大规模数据中心里维护不同的“自治系统”之间路由信息的、无中心的路由协议。

image-20200814183326635

​ AS1和AS2是两个自治系统,自治系统指的是一个组织关下下的所有IP网络和路由器的全体,类比全体主机和路由器,正常不会有来往。但是如果要让他们用IP地址直接通向,需要使用路由器把两个自治系统连接起来。量少时可通过人工维护,如果网络拓扑结构复杂,则需要BGP——Border Gateway Protocol

​ 可以认为每个边界网关上都会运行着一个小程序,会将各自的路由表信息通过TCP传输给其他的边界网关,其他的边界网关会将需要的信息添加到自己的路由表里。所谓 BGP,就是在大规模网络中实现节点路由信息共享的一种协议。类似zk的数据同步。

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

  1. Calico 的 CNI 插件。这是 Calico 与 Kubernetes 对接的部分。我已经在上一篇文章中,和你详细分享了 CNI 插件的工作原理,这里就不再赘述了。
  2. Felix。它是一个 DaemonSet,负责在宿主机上插入路由规则(即:写入 Linux 内核的 FIB 转发信息库),以及维护 Calico 所需的网络设备等工作。
  3. BIRD。它就是 BGP 的客户端,专门负责在集群里分发路由规则信息。

除了对路由信息的维护方式之外,Calico 项目与 Flannel 的 host-gw 模式的另一个不同之处,就是它不会在宿主机上创建任何网桥设备。这时候,Calico 的工作方式,可以用一幅示意图来描述:

image-20200814183918656

​ Calico 的 CNI 插件会为每个容器设置一个 Veth Pair 设备,然后把其中的一端放置在宿主机上(它的名字以 cali 前缀开头)。由于 Calico 没有使用 CNI 的网桥模式,Calico 的 CNI 插件还需要在宿主机上为每个容器的 Veth Pair 设备配置一条路由规则,用于接收传入的 IP 包。

​ 有了这样的 Veth Pair 设备之后,容器发出的 IP 包就会经过 Veth Pair 设备出现在宿主机上。然后,宿主机网络栈就会根据路由规则的下一跳 IP 地址,把它们转发给正确的网关。接下来的流程就跟 Flannel host-gw 模式完全一致了。

如果宿主机在不同的子网,则没办法通过下一跳把IP包发到下一个地址。则需要为Calico打开IPIP模式

image-20200814184733912

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

10.233.2.0/24 via 192.168.2.2 tunl0

​ 可以看到,尽管这条规则的下一跳地址仍然是 Node 2 的 IP 地址,但这一次,要负责将 IP 包发出去的设备,变成了 tunl0。注意,是 T-U-N-L-0,而不是 Flannel UDP 模式使用的 T-U-N-0(tun0),这两种设备的功能是完全不一样的,是一个IP隧道设备。

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

​ 经过封装后的新的 IP 包的目的地址,正是原 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 项目才能够覆盖更多的场景,并为你提供更加可靠的组网方案和架构思路。

你可能感兴趣的:(Docker&k8s(六))