kubernetes_network

Kubernetes Network


前言

​ 本篇文档主要介绍kubernetes网络模型实现原理。

​ 首先从docker网络模型入手,介绍同宿主机不同容器如何互通(网桥),接着分析不同宿主机的容器是如何通信的(跨主通信)。

​ 然后在有了跨主通信的基础上,转向kubernetes集群如何管理宿主机里面容器的网络(CNI插件)。

​ 总体的实现思路都是围绕着网桥的概念延伸的,在不同宿主机之间搭一个桥(搭什么样的桥?),然后让容器都上这座桥(怎么让容器上桥?),最后容器的沟通都在这桥上进行(容器怎么沟通?)。当然还要考虑,我要沟通的容器在不在这个桥上(怎么知道桥上有谁?)。

大纲

容器网络

​ 容器网络,可以直接声明使用宿主机的网络栈(-net=host),即,不开启Network Namespace:

docker run -d net=host --name nginx-host nginx

​ 这个容器启动后,直接监听宿主机的80端口。像这种直接使用宿主机网络栈的方式,虽然可以为容器提供良好的网络性能,但也会不可避免地引入共享网络资源的问题,比如端口冲突。所以在大多数情况下,希望容器进程能使用自己的Network Namespace里的网络栈,就是拥有自己的IP地址和端口。

问题1:那如何把容器网络与宿主机网络互通,或者如何让容器网络与外界网络通信?

网桥

​ 在使用Network Namespace之后,容器自己相当于是一台主机,有一套独立的“网络栈”。那问题就在于这个容器如何与其他Network Namespace沟通。我们都知道主机与主机之间通过网线,连到一台交换机上,然后进行通信。那在Linux中,能够起到虚拟交换机作用的网络设备,就是网桥(Bridge)。一个工作在数据链路层(Data Link)的设备,主要功能是根据MAC地质学习来将数据包转发到网桥的不同端口上。

​ 所以,容器网络实现的第一步,就是Docker项目会默认在宿主机上创建一个名叫docker0的网桥,凡是连到docker0网桥上的容器,就可以通过它来通信。启动容器后,通过指令ifconfig可以看到docker0这个虚拟网络设备:

在这里插入图片描述

Veth Pair虚拟设备

​ 有了网桥,那下个问题就是,如何把容器“连接”到docker0网桥上?答案就是Veth Pair虚拟设备。

​ Veth Pair虚拟设备,它被创建出来后,总是以两张虚拟网卡(Veth Peer)的形式成对出现。并且,从其中一个“网卡”发出的数据包,可以直接出现在与它对应的另一张“网卡”上,哪怕这两个“网卡”在不同的Network Namespace里。

​ 通过启动一个ubuntu容器来看下Veth Pait设备如何在容器与宿主之间体现的。

docker run -d --name redis-1 -p 6379:6379 redis:latest 

​ 进入容器,在容器内执行ifconfig指令

#如果command not found执行系列指令
$ apt-get update
$ apt install net-tools

在这里插入图片描述

​ 从上图可以看到,有张网卡叫eth0,这就是Veth Pair虚拟设备的一端。在通过指令route查看容器的路由表,可以看到eth0作为容器里的默认路由设备,对172.17.0.0/16网段的请求,也会通过eth0来处理。

在这里插入图片描述

​ 按照前面说的,Veth Pair设备有两端,一端已经在容器内了,另一端就是在宿主机上。在宿主机上,通过指令ifconfig查看。会发现,多了一个veth*******的虚拟网卡设备。

在这里插入图片描述

​ 也可以通过指令brctl show查看有哪些虚拟网卡设备连到docker0网桥上了,因为现在我虚拟机上只有一个容器,所以只会出现一个设备。之后,在这个宿主机上启动其他的容器,都会添加到这个docker0上面。

#如果指令command not found,执行下列指令
$ apt-get update
$ apt install bridge-utils

在这里插入图片描述

​ 当然,通常一台宿主机上上会有很多容器,容器会有很多veth开头的虚拟网络设备,可以通过下面方法找到你容器对应的veth虚拟网卡设备:

​ 首先在容器内打印car /sys/class/net/neth0/iflink这个序号,然后根据这个序号在宿主机上根据指令ip link上的序号,就可以找到你容器对应的虚拟网卡。

在这里插入图片描述
在这里插入图片描述

例子:2容器互ping

​ 在宿主机上再添加一个redis-2容器,然后redis-1 ping redis-2是默认可以互ping的。

​ 宿主机结构如下:

在这里插入图片描述

​ 这个⽬的IP地址会匹配到redis-1容器⾥的第⼆条路由规则。可以看到,这条路由规则的⽹关(Gateway)是0.0.0.0,这就意味着这是⼀条直连规则,即:凡是匹配到这条规则的IP包,应该经过本机的eth0⽹卡,通过⼆层⽹络直接发往⽬的主机。

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

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

​ 有了目的的MAC地址,redis-1容器的eth0网卡就可以将数据包发出去。

​ ⽽根据Veth Pair设备的原理,这个数据包会⽴刻出现在宿主机上的veth703d5b8虚拟⽹卡上。不过,此时这个veth703d5b8⽹卡的⽹络协议栈的资格已经被“剥夺”,所以这个数据包就直接流⼊到了docker0⽹桥⾥。

​ 因为是同宿主机内的容器,所以dockere0根据二层ebtables规则转发该数据包,及转发到veth3571f6b网卡。这个网卡就是redis-2的Veth Pair虚拟网络设备的另一端,所以redis-1发来的数据包就会出现在redis-2容器的eth0网卡上。

​ 这样,redis-2的网络协议栈就会对请求进行处理,返回Pong到redis-1。

跨主网络实现

​ 通过前面一节,了解到同宿主机下的容器网络通过网桥+Veth Pair的方式来实现网络通信。但是,容器不会永远只和同宿主机的容器通信。

问题2:不同宿主机上的容器如何实现网络通信?

​ 这个问题,其实就是容器的“跨主通信“问题”。在Docker的默认配置下,一台宿主机上的docker0网桥,和其他宿主机上的docker0网桥,没有任何关联,它们互相之间也没办法连通。所以,连接在这些网桥上的容器,自然也没办法进行通信。

Overlay Network

​ 在实现同宿主机下容器互通的时候,为了把不容Network Namespace下的容器“连到”同一个网络下,我们创建了docker0网桥。那在实现不同宿主机下的容器互通,我们是否可以效仿呢?答案是肯定的。

在这里插入图片描述

​ 通过上图可以看到,通过软件的方式,创建一个整个“公用”的网桥,然后把集群里的所有容器都连接到这个网桥上,那容器与容器之间是不是就可以根据IP来互通了。

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

​ 当node1的container1要访问node2上的container1时,node1上的“特殊网桥”在收到数据包之后,能够通过某种方式,把数据包转发给正确的容器,node2的container1。

​ 下面通过更详细的网络方案,Flannel,来理解容器“跨主通信”的原理。

Flannel网络插件

​ Flannel项目是CoreOS公司主推的容器网络方案。Flannel项目本身只是一个框架,真正为我们提供容器网络功能的是Flannel的后端实现。Flannel支持三种后端实现,分别是:

  • UPD

  • VXLAN

  • host-gw

    这三种方法代表了三种目前容器跨主网络的主流实现方法。

​ 在宿主机启动Flannel后(一般宿主机已经是kubernetes集群中的一个节点了),首先宿主机上会出现flannel0设备,flannel0是一个TUN设备,Tunnel设备(Network Layerd的虚拟网络设备,在操作系统内核和用户应用程序之间传递IP包)。然后,在宿主机上创建新的路由规则。以下面架构中node1为例,会增加一条转发到flannel0的路由:

$ ip route
default via 10.168.0.1 dev eth0
100.96.0.0/16 dev flannel0 proto kernel scope link src 100.96.1.0
100.96.1.0/24 dev docker0 proto kernel scope link src 100.96.1.1
10.168.0.0/24 dev eth0 proto kernel scope link src 10.168.0.2
UDP模式

​ UDP模式最直接,但是性能最差,目前已经被弃用。不过,我们可以通过UDP模式,来了解容器跨主网络的实现原理,然后再通过某些技术去解决UDP模式的问题,形成新的解决方案。

​ 通过下面这架构来理解UDP的跨主网络:

在这里插入图片描述

先看一下这个架构,2个node,分别有2个容器:

  • node1,etch0网卡IP为10.168.0.2,flannel0设备IP为100.16.1.0,docker0设备IP为100.96.1.1,container1的IP为100.96.1.12

  • node2,etch0网卡IP为10.168.0.3,flannel0设备IP为100.16.2.0,docker0设备IP为100.96.2.1,container1的IP为100.96.2.3

    我们将通过node1发送一个包给node2,通过这个发包整个过程,了解flannel UDP模式下的运作原理。

发包过程

  1. 容器发包

    node1的container1里的进程发起IP包,源IP为100.96.1.12,目的IP为100.96.2.1。这里步骤在上一节的容器网络有介绍,容器的包如何出现在docker0上,这里就不在赘述。因此,容器发出的IP包就会出现在docker0网桥上。由于目的IP 100.96.2.3不在node1的docker0的网段上,所以IP包的下一个目的地,就取决于宿主机上的路由规则。

  2. IP包发送至flannel设备

    在前面我们讲到flannel会事先在宿主机上添加一条路由规则,在ip route结果里:

    100.96.0.0/16 dev flannel0 proto kernel scope link src 100.96.1.0
    

    可以看到这个容器发起的IP包匹配这条路由规则,从而进入到flannel0这个设备里。前面介绍过,这个设备是在操作系统内核和用户应用程序之间传递IP包。当IP包发送给flannel0设备后,flannel0就会把这个IP包,交个创建这个设备的应用程序,也就是Flannel进程。这个一个从内核态向用户态的流动方向。flanneld(即Flannel进程)收到IP包后,根据目的IP 100.96.2.3,就把这个IP包发到node2宿主机。

    问题3:flanneld进程如何知道IP的转发路径?

    这里就不得不提Flannel中一个很重要的概念:子网(subnet)

    在由Flannel管理的容器网络里,一台宿主机上的所有容器,都属于该宿主机被分配的一个“子网”。在我们的例子中,node1的子网是100.96.1.0/24,node1的container1 IP为100.96.1.12。node2的子网是100.96.2.0/24,node2的container1 IP为100.96.2.3。那这些对应关系存在哪呢?怎么让不同宿主机的flanneld进程都知道呢?别忘了,我们现在kubernetes的环境下,kubernetes通过Etcd来存储整个集群的元数据。同样的,子网与宿主机的对应关系,也是保存在Etcd当中。可以通过etcdctl查看:

    $ etcdctl ls /coreos.com/network/subnets
    /coreos.com/network/subnets/100.96.1.0-24
    /coreos.com/network/subnets/100.96.2.0-24
    

    所以,flanneld进程在处理由flannel0传入的IP包时,就可以根据目的IP的地址,匹配到对应的子网,然后再从Etcd中找到这个子网对应的宿主机的IP是10.168.0.3。

    $ etcdctl get /coreos.com/network/subnets/100.96.2.0-24
    {"PublicIP":"10.168.0.3"}
    
  3. 封装UDP

    flanneld在收到IP包后,就会把这个IP包直接封装在一个UDP包里,然后发送给node2。源IP为10.168.0.2,即node1的IP地址。目的IP为10.168.0.3,即node2的IP地址。当然,这个发送请求可以成功的原因是,每台宿主机上的flanneld都监听这个一个8285的端口,所以flanneld只要把UDP包发往Node2的8285端口即可。

  4. flanneld把包通过eth0发到node2上,通过宿主网络

  5. 解封装UDP

    node2上监听8285端口的进程也是flanneld,所以,flanneld就可以从这个UDP包解析出封装在里面的、node1的container1的IP包。接下来,flanneld会直接把这个IP包发送给它所管理的TUN设备,即flannel0设备。

  6. flannel设备转发至docker0

    根据前面讲的,这是一个从用户态向内核态的流动方向,所以Linux内核网络栈就会负责处理这个IP包,具体通过本机的路由表来寻找这个IP包下一步要转发到哪里。根据node2的ip route:

    $ ip route
    default via 10.168.0.1 dev eth0
    100.96.0.0/16 dev flannel0 proto kernel scope link src 100.96.2.0
    100.96.2.0/24 dev docker0 proto kernel scope link src 100.96.2.1
    10.168.0.0/24 dev eth0 proto kernel scope link src 10.168.0.3
    

    这个IP包的目的IP是100.96.2.3,匹配第三条,所以这个IP包就会转发至docker0网桥

  7. 容器接收IP包

    第七步,就和容器网络里面的接收是一样的,docker0网桥会扮演二层交换机的角色,将数据包发送给正确的端口,进而通过Veth Pair设备进入到node2的container1的Network Namespace里。

​ 至此,就是跨主网络flannel UDP模式通信的完整例子。这个例子有个前提,就是docker0网桥的地址范围必须是Flannel为宿主机分配的子网。可以通过下面方式实现:

$ FLANNEL_SUBNET=100.96.1.1/24
$ dockerd --bip=$FLANNEL_SUBNET ...
#在kubernetes里可以通过kubelet的参数--pod-network-cidr设定

​ 可以看到,Flannel UDP模式提供的其实是⼀个三层的Overlay⽹络,即:它⾸先对发出端的IP包进⾏UDP封装,然后在接收端进⾏解封装拿到原始的IP包,进⽽把这个IP包转发给⽬标容器。这就好⽐,Flannel在不同宿主机上的两个容器之间打通了⼀条“隧道”,使得这两个容器可以直接使⽤IP地址进⾏通信,⽽⽆需关⼼容器和宿主机的分布情况。

性能不足分析

​ 在一开始的时候,我们提到UDP模式虽然直接,但是性能不好,现在我们分析下为何性能不好。

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

在这里插入图片描述
​ 我们可以看到:
​ 第⼀次:⽤户态的容器进程发出的IP包经过docker0⽹桥进⼊内核态;
​ 第⼆次:IP包根据路由表进⼊TUN(flannel0)设备,从⽽回到⽤户态的flanneld进程;
​ 第三次:flanneld进⾏UDP封包之后重新进⼊内核态,将UDP包通过宿主机的eth0发出去。
​ 此外,我们还可以看到,Flannel进⾏UDP封装(Encapsulation)和解封装(Decapsulation)的过程,也都是在⽤户态完成的。在Linux操作系统中,上述这些上下⽂切换和⽤户态操作的代价其实是⽐较⾼的,这也正是造成Flannel UDP模式性能不好的主要原因。
​ 所以说,我们在进⾏系统级编程的时候,有⼀个⾮常重要的优化原则,就是要减少⽤户态到内核态的切换次数,并且把核⼼的处理逻辑都放在内核态进⾏。

VXLAN模式

​ 从上面UDP性能分析来看,UDP模式主要问题是在有多次的用户态转内核态,内核态转用户态的操作。因此,Flannel后来提出了VXLAN模式,也是主流的容器网络方案。

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

​ VXLAN的覆盖⽹络的设计思想是:在现有的三层⽹络之上,“覆盖”⼀层虚拟的、由内核VXLAN模块负责维护的⼆层⽹络,使得连接在这个VXLAN⼆层⽹络上的“主机”(虚拟机或者容器都可以)之间,可以像在同⼀个局域⽹(LAN)⾥那样⾃由通信。当然,实际上,这些“主机”可能分布在不同的宿主机上,甚⾄是分布在不同的物理机房⾥。
​ ⽽为了能够在⼆层⽹络上打通“隧道”,VXLAN会在宿主机上设置⼀个特殊的⽹络设备作为“隧道”的两端。这个设备就叫作VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)。VTEP设备的作用,其实跟前面的flanneld进程非常相似。只不过,它进行封装和解封装的对象,是二层数据帧(Ethernet Frame),并且整个工作的执行流程,全部都是在内核里完成的,因为VXLAN本身就是Linux内核中的一个模块。

​ 和UDP模式一样,在启动flannel VXLAN模式后,会在宿主机上创建一个flannel.1的虚拟网络设备,也就是VTEP设备,这个设备既有IP地址也有MAC地址。如果这个Flannel网络有别的宿主机,就会添加一条路由到其他宿主机上。例如,node2是后来添加到Flannel网络的,那node的路由规则就会添加下面这条:

Destination Gateway    Genmask        Flags  Metric Ref   Use  Iface
100.96.2.0  100.96.2.0 255.255.255.0  UG     0      0     0    flannel.1

VXLAN的架构如下,和UDP模式的很类似:

在这里插入图片描述

​ 我们也通过node1的container1发包到node2的container1来了解VXLAN的工作模式,主要封包封装的变化:

  1. 容器发包

    实现方式和UDP模式的第一个步骤一样,不做赘述。

  2. IP包转发至flannel.1

    实现方式和UDP模式的第二个步骤类似,不做赘述。

    到这里的包头如下:

在这里插入图片描述

  1. 封装成为UDP后转发至node2

    此时IP包已经到了VTEP设备,即flannel.1。这些VTEP设备之间,就需要想办法组成一个虚拟的二层网络,即通过二层数据帧进行通信。所以,“源VTEP设备”(node1的flannel.1)收到IP包后,就要把IP包加上一个目的MAC地址,封装成一个二层数据帧,然后发送给“目的VTEP设备”(node2的flannel.1)。

    目的VTEP设备的MAC地址,早就在node2添加到Flannel网络的时候,自动添加到node1上的ARP记录里。可以用指令ip neigh show dev flannel.1查看

    $ ip neigh show dev flannel.1
    100.96.2.0 lladdr 5e:f8:4f:00:e3:37 PERMANENT
    

    有了这个目的VTEP设备的MAC地址,Linux内核就可以开始二层封包工作。Linux内核会把目的VTEP设备的MAC地址,填在Inner Ethernet Header字段,得到一个二层数据帧,此时包头如下:

在这里插入图片描述

但是添加的目的VTEP设备MAC地址相对于宿主机来说没有实际意义,因为现在封装好的数据帧,并不能在宿主机的二层网络里传输,也被成为内部数据帧(Inner Ethernet Frame)。为了让内部数据帧转换为外部数据帧,成为宿主机网络里的一个普通数据帧,Linux内核还需要做如下动作。

Linux内核会在“内部数据帧”前面,加一个特殊的VXLAN头,用来表示这是实际上是一个VXLAN要使用的数据帧,此时包头如下:

在这里插入图片描述

然后,Linux内核会把这个数据帧封装进一个UDP包发出去。跟UDP模式类似,在宿主机看来,它会以为⾃⼰的flannel.1设备只是在向另外⼀台宿主机的flannel.1设备,发起了⼀次普通的UDP链接。它哪⾥会知道,其实这个UDP包⾥⾯,其实是⼀个完整的⼆层数据帧:

在这里插入图片描述

到目前为止,外部数据帧还剩下IP层和数据层还没有封装。从前面得知,flannel.1设备只知道另一端flannel.1设备的MAC地址,却不知道对应的宿主机地址是什么,所以我们要先找到这个UDP包应该发给哪个宿主机。

flannel.1设备实际上要扮演一个“网桥”的角色,在二层网络进行UDP包的转发。而在Linux内核里面,“⽹桥”设备进⾏转发的依据,来⾃于⼀个叫作FDB(Forwarding Database)的转发数据库。因此,flannel.1对应的FDB信息可以通过bridge 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

找到目的宿主机的IP之后,就是一个正常的、宿主机网络上的封包工作,外部数据帧的包头如下:

在这里插入图片描述

这样封包工作就结束了。

  1. node2收到数据包

    node2的内核网络栈发现这个数据帧里有VXLAN Header,所以Linux内核会对它进行拆包,拿到里面的内部数据帧,交给node2的flannel.1设备。

  2. flannel.1设备转发到docker0

    flannel.1设备会继续拆包,取出“原始IP包”。根据取得的IP,转发至docker0设备。

  3. 容器接收IP包

    第六步,就和容器网络里面的接收是一样的,docker0网桥会扮演二层交换机的角色,将数据包发送给正确的端口,进而通过Veth Pair设备进入到node2的container1的Network Namespace里。

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

​ 相较于UDP模式,VXLAN少了flanneld的封装动作,都在Linux内核完成了。

Kubernetes网络实现

问题4:在前面两节介绍了容器在同宿主机,不同宿主机之间的网络通信,那放到kubernetes上是如何做的呢?

​ 网络插件真正要做的事情,则是通过某种方法,把不同宿主机上的特殊设备联通个,从而达到容器跨主机通信的目的。

CNI插件

​ 实际上,kubernetes对容器网络的主要处理方法。kubernets是通过CNI的接口,维护了一个单独的网桥来代替docker0。这个网桥叫CNI网桥,它在宿主机上的设备名称默认是:cni0。在kubernetes环境里,它的工作方式跟上两节的工作方式没有不同,只不过docker0网桥被替换成CNI网桥

架构基于Flannel VXLAN模式,如下:

在这里插入图片描述

在这⾥,Kubernetes为Flannel分配的⼦⽹范围是10.244.0.0/16。可以在部署的时候指定:

$ kubeadm init --pod-network-cidr=10.244.0.0/16
创建理由

​ kubernetes之所以要设置这样⼀个与docker0⽹桥功能⼏乎⼀样的CNI⽹桥,主要原因包括两个方面:

  • Kubernetes项⽬并没有使⽤Docker的⽹络模型(CNM),所以它并不希望、也不具备配置docker0⽹桥的能⼒;

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

设计思想

​ kubernetes在启动Infra容器之后,就可以直接调⽤CNI⽹络插件,为这个Infra容器的Network Namespace,配置符合预期的⽹络栈。

管理对象

注意:CNI网桥只接管所有CNI插件负责的、即kubernetes创建的容器。如果用docker run单独启动的一个容器,那么Docker项目还是会把这个容器连接到docker0网桥上。这个容器的IP也应该是docker0网桥的网段。

部署与实现

问题5:CNI如何配置容器的网络栈?

  • 部署

    在部署Kubernetes的时候,有⼀个步骤是安装kubernetes-cni包,⽬的就是在宿主机上安装CNI插件所需的基础可执行文件。安装完后,可以通过ls -al /opt/cni/bin查看相关的执行文件:

在这里插入图片描述

上图的文件可以分为这几个类:

main插件:用来创建具体的网络设备的二进制文件,有bridge,ipvlan,lookback,macvlan,ptp,vlan

IPAM(IP Address Management)插件:负责分配IP地址的二进制文件,dhcp,host-local

由CNI社区维护的CNI插件:flannel,tuning,portmap,bandwidth。注意,flannel CNI插件内置,Weave,Calico等网络插件需要把对应的CNI插件放到/opt/cni/bin下

  • 实现

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

    ⾸先,实现这个网络方案本身。这⼀部分需要编写的,其实就是flanneld进程⾥的主要逻辑。⽐如,创建和配置flannel.1设备、配置宿主机路由、配置ARP和FDB表⾥的信息等等。
    然后,实现该⽹络⽅案对应的CNI插件。这⼀部分主要需要做的,就是配置Infra容器⾥⾯的⽹络栈,并把它连接在CNI⽹桥上。

    接下来,你就需要在宿主机上安装flanneld(网络方案本身)。⽽在这个过程中,flanneld启动后会在每台宿主机上⽣成它对应的CNI配置⽂件(它其实是⼀个ConfigMap),从⽽告诉Kubernetes,这个集群要使⽤Flannel作为容器⽹络⽅案。加载CNI配置文件的进程是dockershim,它是kubernetes CRI的实现(在kubernetes中,处理容器网络相关的逻辑并不会在kubelet主干代码里执行,而是会在具体的CRI实现里完成)。

    /etc/cni/net.d/10-flannel.conflist为例:

在这里插入图片描述

dockershim会加载上述的CNI配置文件。注意,kubernetes⽬前不⽀持多个CNI插件混⽤。如果你在CNI配置⽬录(/etc/cni/net.d)⾥放置了多个CNI配置⽂件的话,dockershim只会加载按字⺟顺序排序的第⼀个插件。

CNI允许你在一个CNI配置文件里,通过plugins字段,可以定义多个插件进行协作。以上述的CNI配置文件,dockershim会把这个CNI配置文件加载起来,并且把列表里的第一个插件,也就是flannel插件,设置为默认插件。在后⾯的执⾏过程中,flannel和portmap插件会按照定义顺序被调⽤,从⽽依次完成“配置容器⽹络”和“配置端⼝映射”这两步操作。

注意到上面配置文件里面有一个字段叫delegate。Delegate字段的意思是,这个CNI插件并不会自己做事,⽽是会调⽤Delegate指定的某种CNI内置插件来完成。对于Flannel来说,它调⽤的Delegate插件,就是前⾯介绍到的CNI bridge插件。所以说,dockershim对Flannel CNI插件的调⽤,其实就是⾛了个过场。Flannel CNI插件唯⼀需要做的,就是对dockershim传来的Network Configuration进⾏补充。

工作原理

​ 当kubelet组件需要创建Pod的时候,它第⼀个创建的⼀定是Infra容器。所以在这⼀步,dockershim就会先调⽤Docker API创建并启动Infra容器,紧接着执⾏⼀个叫作SetUpPod的⽅法。这个⽅法的作⽤就是:为CNI插件准备参数,然后调⽤CNI插件为Infra容器配置⽹络。

​ 以上面的例子,就是会调用/opt/cni/bin/flannel插件,它需要两部分参数

  1. 由dockershim设置的一组CNI环境变量

    环境变量参数: CNI_COMMAND,值为ADD和DEL。ADD和DEL也是CNI插件唯一需要实现的两个方法。字面理解就是ADD把容器添加到CNI网络里,DEL把容器从CNI网络里移除。

    对于⽹桥类型的CNI插件来说,这两个操作意味着把容器以Veth Pair的⽅式“插”到CNI⽹桥上,或者从⽹桥上“拔”掉。

  2. dockershim从CNI配置文件里加载到的、默认插件的配置信息

    这个配置信息在CNI中被叫作Network Configuration,dockershim会把Network Configuration以JSON数据的格式,通过标准输入的方式传给Flannel CNI插件。

接下去我们通过这两部分参数实现的ADD操作,来看一下CNI插件如何把一个Infra容器加到CNI网络里。

  1. 补充配置文件

    在上面讲到因为配置文件中的Delegate字段,所以Flannel CNI会对dockershim传来的Network Configuration会先进行补充,⽐如,将Delegate的Type字段设置为bridge,将Delegate的IPAM字段设置为host-local等:

    {
    	"hairpinMode":true,
    	"ipMasq":false,
    	#读取⾃Flannel在宿主机上⽣成的Flannel配置⽂件,即:宿主机上的/run/flannel/subnet.env⽂件。
    	"ipam":{ 
    		"routes":[
    			{
    				"dst":"10.244.0.0/16"
    			}
    		],
    		"subnet":"10.244.1.0/24",
    		"type":"host-local"
    	},
    	"isDefaultGateway":true,
    	"isGateway":true,
    	"mtu":1410,
    	"name":"cbr0",
    	"type":"bridge"
    }
    

    这就是补充完之后的Delegate字段。

  2. 调用CNI bridge插件

    Flannel CNI插件就会调用CNI bridge插件,也就是/opt/cni/bin/bridge二进制文件。

    调⽤CNI bridge插件需要的两部分参数:

    • 第⼀部分、也就是CNI环境变量,CNI_COMMAND = ADD
    • 第⼆部分Network Configration,正是上⾯补充好的Delegate字段。Flannel CNI插件会把Delegate字段的内容以标准输⼊(stdin)的⽅式传递给CNI bridge插件。

    有了这两部分参数,接下来CNI bridge插件就可以“代表”Flannel,进⾏“将容器加⼊到CNI⽹络⾥”这⼀步操作了 。

  3. 检查CNI网桥是否存在

    如果没有会创建一个CNI网桥,这个步骤在宿主机上执行:

    $ ip link add cni0 type bridge
    $ ip link set cni0 up
    
  4. 创建Veth Pair设备

    CNI bridge插件会通过Infra容器的Network Namespace⽂件,进⼊到这个Network Namespace⾥⾯,然后创建⼀对Veth Pair设备。并且把其中一个Veth Pair“移动”到宿主机上:

    #创建⼀对Veth Pair设备。其中⼀个叫作eth0,另⼀个叫作vethb4963f3
    $ ip link add eth0 type veth peer name vethb4963f3
    
    # 启动eth0设备
    $ ip link set eth0 up
    
    # 将Veth Pair设备的另⼀端(也就是vethb4963f3设备)放到宿主机(也就是Host Namespace)⾥
    $ ip link set vethb4963f3 netns $HOST_NS
    
    # 通过Host Namespace,启动宿主机上的vethb4963f3设备
    $ ip netns exec $HOST_NS ip link set vethb4963f3 up
    

    这样Veth Pair设备,容器端的eth0,宿主机端的vethb4963f3就创建好了。

  5. Veth Pair连至CNI网桥

    在宿主机上,CNI bridge插件要把宿主机端的Veth Pair设备连到CNI网桥上:

    $ ip link set vethb4963f3 master cni0
    
  6. 设置Hairpin Mode(发夹模式)

    在vethb4963f3连至CNI网桥后,CNI bridge插件会为他设置Hairpin Mode。这是因为,在默认情况下,⽹桥设备是不允许⼀个数据包从⼀个端⼝进来后,再从这个端⼝发出去的。但是,它允许你为这个端⼝开启HairpinMode,从⽽取消这个限制。这个特性,主要⽤在容器需要通过NAT(即:端⼝映射)的⽅式,“⾃⼰访问⾃⼰”的场景下。

  7. 调用CNI ipam插件

    CNI bridge插件会调⽤CNI ipam插件,从ipam.subnet字段规定的⽹段⾥为容器分配⼀个可⽤的IP地址。然后,CNI bridge插件就会把这个IP地址添加在容器的eth0⽹卡上,同时为容器设置默认路由,在容器里:

    $ ip addr add 10.244.0.2/24 dev eth0
    $ ip route add default via 10.244.0.1 dev eth0
    
  8. 为CNI网桥添加IP

    最后,CNI bridge插件会为CNI网桥添加IP地址,在宿主上:

    $ ip addr add 10.244.0.1/24 dev cni0
    

在执⾏完上述操作之后,CNI插件会把容器的IP地址等信息返回给dockershim,然后被kubelet添加到Pod的Status字段。至此,网桥类型CNI插件的ADD方法实现流程就结束了(非网桥类型的CNI插件会有不同,需要注意)。


总结

​ 通过从docker角度看网络模型,一步一步了解,到从kubernetes角度看网络模型的实现。

​ 容器首先通过网桥与宿主机搭了一座桥,docker0。再创建Veth Pair设备上桥,这样容器的网络和宿主机的网络就可以串在一起了,容器的数据包可以出现在宿主机上进行下一步的转发。之后,为了让不同宿主机之间的容器可以通过容器IP互通,引入了跨主通信的覆盖网络,overlay network。Overlay Network的设计思想也是像网桥一样,只不过搭的桥连通的是不同宿主机上的容器。再通过Flannel项目的UDP模式和VXLAN模式更深入了解跨主通信这座桥是怎么搭的,怎么通的。由于是不同的宿主机,为了能够让桥上的容器都知道这座桥有谁,把这个桥上的容器信息放在etcd上,供查阅。

​ 最后kubernetes集群的网络模型,由于kubernetes的最小调度单位是Pod,而不是docker容器。所以虽然kubernetes也搭桥,但是是用CNI插件搭了一个CNI网桥。也通过Flannel项目的例子,来描述了kubernetes的Pod如何上这个CNI网桥。

​ 网络插件还有很多,但是基本原理都是通过,建一座桥,然后找个管桥的,最后再找个车夫接人上桥。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值