注: 本文采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。知识共享许可协议

Kubernetes的网络是构建在CNI基础之上的。为什么我们需要CNI?它是如何工作的呢?本文将通过对CNI的bridge插件的使用,并结合network namespace,来演示它的工作原理。

在搭建和配置Kubernetes集群环境的时候,可能你会注意到,有时我们会通过配置CNI来为Kubernetes选择不同的网络,比如像常见的bridge network,还有像flannel,weave,calico等。

CNI的全称是Container Network Interface,它为容器提供了一种基于插件结构的标准化网络解决方案。以往,容器的网络层是和具体的底层网络环境高度相关的,不同的网络服务提供商有不同的实现。CNI从网络服务里抽象出了一套标准接口,从而屏蔽了上层网络和底层网络提供商的网络实现之间的差异。并且,通过插件结构,它让容器在网络层的具体实现变得可插拔了,所以非常灵活。

CNI隶属于CNCF(Cloud Native Computing Foundation),在GitHub上有两个项目。其中,cni项目包含了它的规范和一个用Go语言编写的库。我们可以利用这个库编写自己的CNI插件对容器网络进行配置。另一个plugins项目,包含了一系列作为参考实现的标准插件。这些插件彼此独立,但根据需要也可以组合起来使用,比如:flannel插件底层就是调用的bridge插件来完成bridge和veth的创建的。当然,还有很多第三方开发的插件,它们不在这个项目里。

接下来,我们将利用CNI标准插件中的bridge插件来进行相关的实验,通过它来理解CNI的工作原理。

准备工作

我们依然沿用Kubernetes网络篇——从docker0开始中所使用的实验环境。如果在这之前我们已经在实验环境里做过一些实验,并且希望从一个干净的环境重新开始,可以先退出实验环境(即:lab-dind容器),然后执行如下命令:

$ docker-compose stop dind
Stopping lab-kubernetes_dind_1_6de26708a911 ... done

$ docker-compose rm -f dind
Going to remove lab-kubernetes_dind_1_6de26708a911
Removing lab-kubernetes_dind_1_6de26708a911 ... done

关于启动实验环境的步骤,可以参考Kubernetes网络篇——从docker0开始一文里的“实验环境”部分。

接下来,我们进入实验环境,在当前用户的home目录下新建一个子目录叫test-cni,并进入到该目录。我们后面的所有实验都将在这个目录下进行:

$ cd
$ mkdir test-cni
$ cd test-cni/
$ pwd
/root/test-cni

现在,让我们在test-cni目录下新建一个子目录叫plugins,然后在该目录下把最新的CNI插件包下载并解压。CNI的plugins项目把所有标准插件的二进制可执行文件打成了一个包,可以从它的GitHub库下载到。比如:

$ mkdir plugins
$ cd plugins/
$ pwd
/root/test-cni/plugins

$ curl -OL https://github.com/containernetworking/plugins/releases/download/v0.8.1/cni-plugins-linux-amd64-v0.8.1.tgz
$ tar -xzvf cni-plugins-linux-amd64-v0.8.1.tgz

我们在解压后的目录里可以看到很多可执行文件,分别对应不同的CNI插件。本文,我们关注的是bridge插件。

因为我们在本文中将使用network namespace来模拟容器,所以在开始真正实验之前,我们还需要创建一个network namespace:

$ ip netns add lab-ns
$ ip netns list
lab-ns

配置CNI插件

在运行CNI插件对容器网络进行配置的时候,我们首先需要告诉插件有关网络的定义信息。CNI对网络的定义是以JSON文件的格式保存的。接下来,我们在cni目录下再新建一个conf子目录:

$ mkdir conf
$ cd conf
$ pwd
/root/test-cni/conf

并把JSON格式的网络定义文件保存在这个目录下:

$ cat > lab-br0.conf <<"EOF"
{
    "cniVersion": "0.4.0",
    "name": "lab-br0",
    "type": "bridge",
    "bridge": "lab-br0",
    "isGateway": true,
    "ipMasq": true,
    "ipam": {
        "type": "host-local",
        "subnet": "10.15.10.0/24",
        "routes": [
            { "dst": "0.0.0.0/0" },
        ],
        "rangeStart": "10.15.10.100",
        "rangeEnd": "10.15.10.200",
        "gateway": "10.15.10.99"
    }
}
EOF

接下来,我们对这个文件里的每个配置项一一进行解释:

cniVersion

代表CNI规范所用的版本。截止本文撰写期间,CNI规范的最新版本是0.4.0。

name

目标网络的名称。

type

所用插件的类型。在我们的例子,用的是bridge插件。

bridge和isGateway

这两个都是和bridge插件相关的特定参数:

  • bridge:我们通过它告诉bridge插件,将要创建的bridge(网桥)名称。
  • isGateway:为true就是告诉插件,作为网关,给我们的bridge指定一个IP地址。这样,连接到bridge的容器就可以拿它当网关来用了。

ipMasq

为目标网络配上Outbound Masquerade(地址伪装),即:由容器内部通过网关向外发送数据包时,对数据包的源IP地址进行修改。

当我们的容器以宿主机作为网关时,这个参数是必须要设置的。否则,从容器内部发出的数据包就没有办法通过网关路由到其他网段。因为容器内部的IP地址无法被目标网段识别,所以这些数据包最终会被丢弃掉。

ipam

IPAM(IP Adderss Management)即IP地址管理,提供了一系列方法用于对IP和路由进行管理。实际上,它对应的是由CNI提供的一组标准IPAM插件,比如像host-local,dhcp,static等。其他插件,比如本文中用到的bridge插件,会调用我们所指定的IPAM插件,实现对网络设备IP地址的分配和管理。

以host-local插件为例,只要我们为它提供配置信息,定义好期望的子网与网关信息,以及允许的IP地址范围(可选),插件就会帮我们自动在目标网段里分配好IP地址。为了保证把IP地址不冲突,它把IP地址的分配信息保存在了宿主机的本地文件系统里,这样可以确保在同一台宿主机上运行的所有容器,IP地址一定都是彼此唯一的。

另一个插件dhcp,则会在宿主机上启动一个DHCP daemon守护进程。跑在宿主机上的容器,可以通过它向网络上的DHCP服务器发送请求,以获得相应的IP地址。

下面我们来看一下具体的参数配置:

  • type:指定所用IPAM插件的名称,在我们的例子里,用的是host-local。
  • subnet:为目标网络分配网段,包括网络ID和子网掩码,以CIDR形式标记。在我们的例子里为10.15.10.0/24,也就是目标网段为10.15.10.0,子网掩码为255.255.255.0
  • routes:用于指定路由规则,插件会为我们在容器的路由表里生成相应的规则。其中,dst表示希望到达的目标网段,以CIDR形式标记。gw对应网关的IP地址,也就是要到达目标网段所要经过的“next hop(下一跳)”。如果省略gw的话,那么插件会自动帮我们选择默认网关。在我们的例子里,gw选择的是默认网关,而dst为0.0.0.0/0则代表“任何网络”,表示数据包将通过默认网关发往任何网络。实际上,这对应的是一条默认路由规则,即:当所有其他路由规则都不匹配时,将选择该路由。
  • rangeStart:允许分配的IP地址范围的起始值
  • rangeEnd:允许分配的IP地址范围的结束值
  • gateway:为网关(也就是我们将要在宿主机上创建的bridge)指定的IP地址。如果省略的话,那么插件会自动从允许分配的IP地址范围内选择起始值作为网关的IP地址。

运行CNI插件

定义好网络配置文件以后,接下来就该运行CNI插件了。我们的bridge插件将会在宿主机上为我们创建bridge,然后把容器(或者说network namespace)通过veth pair连接到bridge上。

在开始运行插件之前,还有一件事情要做。我们要告诉bridge插件有关容器的一些信息,这些信息是通过环境变量传递给插件的。

CNI_COMMAND

告诉CNI插件要执行的命令,允许的命令有ADD,DEL,CHECK,VERSION。我们的例子里用的是ADD,就是告诉插件把容器加入到bridge所对应的网络里。

对于支持CNI规范的容器系统而言,当容器启动的时候,系统就会自动调用相应的CNI插件,并设置CNI_COMMAND为ADD。相应地,DEL是在容器被销毁时调用的,用于清除在执行ADD阶段分配的网络资源。

CHECK用于检查容器网络是否正常。VERSION则用来显示插件的版本。

CNI_CONTAINERID

告诉CNI插件,将要加入目标网络的容器所对应的network namespace的ID。在我们的例子里,就是lab-ns0。

CNI_NETNS

容器对应的network namespace在宿主机上的文件路径。可以在/var/run/netns目录下找到我们刚才手工创建的network namespace:

$ ls /var/run/netns
lab-ns

CNI_IFNAME

作为veth pair在容器一端的网络接口,我们所期望的名称。在我们的例子里,这个网络接口的名称是eth0。

CNI_PATH

CNI插件的所在路径。在我们的例子里,因为是手工运行插件,所以我们直接去相应的路径下执行命令就可以了。但通常情况下,对CNI插件的调用是由容器系统来完成的,比如像Kubernetes,这个时候就需要通过CNI_PATH告诉系统去哪里找插件了。

最后,我们切换到plugins目录来运行bridge插件,把网络定义文件通过标准输入设备(STDIN)传给插件,同时指定相应的环境变量:

$ pwd
/root/test-cni/plugins

$ CNI_COMMAND=ADD CNI_CONTAINERID=lab-ns CNI_NETNS=/var/run/netns/lab-ns CNI_IFNAME=eth0 CNI_PATH=`pwd` ./bridge <../conf/lab-br0.conf
{
    "cniVersion": "0.4.0",
    "interfaces": [
        {
            "name": "lab-br0",
            "mac": "be:54:02:8c:4b:87"
        },
        {
            "name": "veth8ec19086",
            "mac": "76:52:41:98:f1:56"
        },
        {
            "name": "eth0",
            "mac": "42:2d:af:d7:0b:f0",
            "sandbox": "/var/run/netns/lab-ns"
        }
    ],
    "ips": [
        {
            "version": "4",
            "interface": 2,
            "address": "10.15.10.100/24",
            "gateway": "10.15.10.99"
        }
    ],
    "routes": [
        {
            "dst": "0.0.0.0/0"
        }
    ],
    "dns": {}
}

从输出结果可以看到,根据我们刚才定义的目标网络,bridge插件为lab-br0分配的IP地址是10.15.10.99,为lab-ns分配的IP地址是10.15.10.100。下面我们就来进一步验证这一结果。

验证

我们来看一下宿主机的网络接口:

$ ip addr show
... ...
5: lab-br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether be:54:02:8c:4b:87 brd ff:ff:ff:ff:ff:ff
    inet 10.15.10.99/24 brd 10.15.10.255 scope global lab-br0
       valid_lft forever preferred_lft forever
6: veth8ec19086@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master lab-br0 state UP group default 
    link/ether 76:52:41:98:f1:56 brd ff:ff:ff:ff:ff:ff link-netns lab-ns
... ...

可以看到,bridge插件为我们新建的网桥lab-br0,IP地址正是10.15.10.99。还有veth pair在宿主机一端的网络接口veth8ec19086@if5。

查看lab-ns的网络接口:

$ ip netns exec lab-ns ip addr show
... ...
5: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 42:2d:af:d7:0b:f0 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.15.10.100/24 brd 10.15.10.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::402d:afff:fed7:bf0/64 scope link 
       valid_lft forever preferred_lft forever

可以看到,veth pair位于lab-ns一端的网络接口,eth0@if6。bridge插件为它分配的IP地址是10.15.10.100,就在我们之前所指定的IP地址范围内。

再来看一下宿主机上的iptables:

$ iptables-save | grep lab-br
-A POSTROUTING -s 10.15.10.100/32 -m comment --comment "name: \"lab-br0\" id: \"lab-ns\"" -j CNI-95c521ba458df43df4d8c523
-A CNI-95c521ba458df43df4d8c523 -d 10.15.10.0/24 -m comment --comment "name: \"lab-br0\" id: \"lab-ns\"" -j ACCEPT
-A CNI-95c521ba458df43df4d8c523 ! -d 224.0.0.0/4 -m comment --comment "name: \"lab-br0\" id: \"lab-ns\"" -j MASQUERADE

可以看到,因为我们之前配了IP地址伪装,所以bridge插件自动为我们生成了相应的规则。当数据包里的源地址为10.15.10.100/32时,上面三条规则中的第一条就会匹配;然后就会跳到第二条规则,如果这个时候数据包里的目标地址为10.15.10.0/24网段,那么这条规则也会匹配,并跳到第三条规则;最后一条规则是为了排除目标地址为224.0.0.0/4的数据包,因为这种类型的数据包是为了在当前子网内进行多播(Multicast)用的,因此不应该被转发出去。当所有这些条件都满足时,我们指定系统为其进行IP地址伪装。

接着来看一下lab-ns里的路由规则:

$ ip netns exec lab-ns ip route
default via 10.15.10.99 dev eth0 
10.15.10.0/24 dev eth0 proto kernel scope link src 10.15.10.100 

这里的第一条规则,就是前面提到的默认路由规则。第二条规则告诉我们,所有来自lab-ns(IP地址为10.15.10.100)的数据包都将发往10.15.10.0/24网段。而所有目标地址为其他网段的数据包,则将匹配默认路由规则,通过网关lab-br0(IP地址为10.15.10.99)进行转发。

另外,如果我们查看/var/lib/cni/目录:

$ ls /var/lib/cni/networks
lab-br0

会发现这里有一个子目录,目录名就是我们指定的位于宿主机上的网桥lab-br0。作为IPAM插件的host-local,就是把IP地址的配置信息保存在这个目录下的:

$ ls -l /var/lib/cni/networks/lab-br0/
total 8
-rw-r--r--    1 root     root            12 Jun 15 23:14 10.15.10.100
-rw-r--r--    1 root     root            12 Jun 15 23:14 last_reserved_ip.0
-rwxr-x---    1 root     root             0 Jun 15 23:14 lock

可以看到,这里的10.15.10.100文件,其文件名就是我们分配给lab-ns的IP地址,而文件内容则对应network namespace的Id:

$ cat /var/lib/cni/networks/lab-br0/10.15.10.100
lab-ns

至于last_reserved_ip.0文件,它保存了上次用过的IP地址,从而保证了host-local在下次为容器分配IP地址时,不会和网段里的其他设备产生冲突:

$ cat /var/lib/cni/networks/lab-br0/last_reserved_ip.0 
10.15.10.100

最后,让我们启动loopback网络接口:

$ ip netns exec lab-ns ip link set dev lo up

然后,在lab-ns内部测试一下和网关的连通性:

$ ip netns exec lab-ns ping 10.15.10.99 -c 3
PING 10.15.10.99 (10.15.10.99): 56 data bytes
64 bytes from 10.15.10.99: seq=0 ttl=64 time=0.132 ms
64 bytes from 10.15.10.99: seq=1 ttl=64 time=0.164 ms
64 bytes from 10.15.10.99: seq=2 ttl=64 time=0.167 ms

--- 10.15.10.99 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.132/0.154/0.167 ms

还有和lab-ns里eth0的连通性:

$ ip netns exec lab-ns ping 10.15.10.100 -c 3
PING 10.15.10.100 (10.15.10.100): 56 data bytes
64 bytes from 10.15.10.100: seq=0 ttl=64 time=0.088 ms
64 bytes from 10.15.10.100: seq=1 ttl=64 time=0.132 ms
64 bytes from 10.15.10.100: seq=2 ttl=64 time=0.131 ms

--- 10.15.10.100 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.088/0.117/0.132 ms

留下评论

您的电子邮箱地址并不会被展示。请填写标记为必须的字段。 *

正在加载...