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

Kubenenetes是怎么使用CNI的呢?作为Kubernetes集群里最小的执行单元,Pod究竟有哪些特点呢?为什么每个Pod都自带一个Pause容器呢?

Kubernetes里的CNI

通过前面几篇文章,我们对CNI有了一定的了解。现在,让我们回到作为容器编排技术的Kubernetes上,看一下它是怎么使用CNI的。

在本文以及后续几篇文章里,我们会以kubeadm-dind-cluster为例来做一些实验。这是Kubernetes SIG(Special Interest Group)下的一个子项目,它利用kubeadm,结合Docker-in-Docker技术,把Docker容器当作Kubernetes集群中的节点,可以实现在本机快速搭建起一个多节点的集群环境。有关如何使用kubeadm-dind-cluster,以及笔者对它所做的优化,可以参考Kuberntes系列的热身篇Launch multi-node Kubernetes cluster locally in one minute, and more…一文。

默认情况下,kubeadm-dind-cluster会启动一个由三个节点构成的集群,包括一个master和两个node。下面我们就选择master节点,登录到上面去看一下。因为这些节点本质上都是Docker容器,所以我们可以用docker exec命令进入到代表master节点的容器里。

$ docker exec -it kube-master bash

我们知道,Kubernetes是通过kubelet作为各个节点的agent,对节点进行管理的。通过执行ps axuw命令,可以查看到kubelet在启动时所使用的参数:

$ ps axuw | grep cni
root       348  4.9  1.4 916936 116788 ?       Ssl  23:25   1:03 /k8s/hyperkube kubelet --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --cgroup-driver=cgroupfs --network-plugin=cni --pod-infra-container-image=k8s.gcr.io/pause:3.1 --pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin --cluster-dns=10.96.0.10 --cluster-domain=cluster.local --eviction-hard=memory.available<100Mi,nodefs.available<100Mi,nodefs.inodesFree<1000 --fail-swap-on=false --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --feature-gates=DynamicKubeletConfig=true --v=4
root      9725  0.0  0.0  11108   912 pts/0    S+   23:46   0:00 grep cni

比如在我们的例子里,可以注意到kubelet的命令行参数里有如下几个参数是和CNI有关的:

  • --network-plugin:用于指明Kubernetes所使用的网络插件,如果取值为cni,就说明用的是CNI插件;
  • --cni-bin-dir:指向一个目录,用于存放所有CNI插件的二进制可执行文件,在我们的例子里,它的值为/opt/cni/bin
  • --cni-conf-dir:用于存放CNI网络配置文件的目录,Kubernetes就是根据它来配置Pod网络的,在我们的例子里,它的值为/etc/cni/net.d

如果我们查看一下/opt/cni/bin目录下的内容,就会看到很多CNI插件:

$ ls /opt/cni/bin/
bridge	dhcp  flannel  host-device  host-local	ipvlan	loopback  macvlan  portmap  ptp  sample  tuning  vlan

/etc/cni/net.d目录下,则保存了一个名叫cni.conf的网络配置文件:

$ ls /etc/cni/net.d/
cni.conf

这个配置文件是由kubeadm-dind-cluster的脚本自动生成的,它在集群中的每个节点上都存在,且内容稍有不同,这里列出的是master节点上的内容:

$ cat /etc/cni/net.d/cni.conf 
{
  "cniVersion": "0.3.1",
  "name": "dindnet",
  "type": "bridge",
  "bridge": "dind0",
  "isDefaultGateway": true,
  "hairpinMode": false,
  "ipMasq": true,
  "ipam": {
    "type": "host-local",
    "ranges": [
      [
        {
          "subnet": "10.244.1.0/24",
          "gateway": "10.244.1.1"
        }
      ]
    ],
    "routes": [
      {
        "dst": "0.0.0.0/0"
      }
    ]
  }
}

当kubelet收到请求要创建Pod的时候,它会去搜索cni-conf-dir目录下的这个配置文件,从中找到要执行的插件名称。然后去cni-bin-dir目录下执行相应的插件,为Pod配置网络环境。

可以看到,在我们的例子里,Kubernetes将会使用CNI的bridge插件,在宿主机一端生成一个名为dind0的bridge;并且它是利用IPAM插件host-local对网络进行IP地址分配的;作为Pod的网关,dind0的IP地址为10.244.1.1,所有Pod都将位于10.244.1.0/24网段内;整个集群的网络名称为dindnet

如果我们查看作为宿主机的当前节点上的网络接口:

$ ip addr show
... ...
4: dind0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
    link/ether 2e:52:45:ea:0f:ee brd ff:ff:ff:ff:ff:ff
    inet 10.244.1.1/24 scope global dind0
       valid_lft forever preferred_lft forever
... ...

可以注意到,的确有一个dind0的bridge,它的IP地址为10.244.1.1/24。那么,CNI插件在Pod一端又做了些什么呢?下面我们就一起来看一下。

关于Pod

Pod是由一个或多个容器构成的,它们彼此共享存储和网络,被共同调度,并在同一上下文里运行。位于同一Pod里的这些容器,就好比是容器技术出现之前,运行在同一物理机上的多个彼此相关的应用程序。因为Pod里的容器是共享网络的,准确地说是共享同一个network namespace,所以对外只有一个IP地址;同时,彼此之间又可以相互访问。

虽然我们目前还没有部署任何新的Pod,但是Kubernetes自身提供的服务也是通过Pod来部署的,它们通常都在kube-system名字空间下。利用kubectl get pod命令,我们可以列出集群中目前包含的Pod:

$ get pod -n kube-system -o=custom-columns=NAME:.metadata.name,NODE:.spec.nodeName,IP:.status.podIP
NAME                                  NODE          IP
coredns-fb8b8dccf-vxxt5               kube-node-1   10.244.2.2
etcd-kube-master                      kube-master   10.192.0.2
kube-apiserver-kube-master            kube-master   10.192.0.2
kube-controller-manager-kube-master   kube-master   10.192.0.2
kube-proxy-f2d74                      kube-master   10.192.0.2
kube-proxy-px4g4                      kube-node-2   10.192.0.4
kube-proxy-qkvwj                      kube-node-1   10.192.0.3
kube-scheduler-kube-master            kube-master   10.192.0.2

这里,我们利用-o参数对数据结果做了定制,目的是为了让输出结果在排版上更加紧凑。可以看到,这里列出了每一个Pod所在的节点,以及对应的IP地址。

我们知道,虽然Pod对容器进行了“包装”,但Pod里的容器实际上还是跑在Pod所在的节点机器上的。因此,我们仍然可以在节点上执行docker ps命令来查看跑在这个节点上的所有容器。

比如,我们以etcd-kube-master这个Pod为例,它就跑在kube-master这个节点上,也就是当前集群里的master节点。在master节点上执行docker ps命令查看相应的Docker容器:

$ docker ps | grep etcd
d4fedb6f3e8f        2c4adeb21b4f           "etcd --advertise-cl…"   3 hours ago         Up 3 hours                              k8s_etcd_etcd-kube-master_kube-system_b3df8d6088284257da0011ac71ef453c_1
dac475b9907b        k8s.gcr.io/pause:3.1   "/pause"                 3 hours ago         Up 3 hours                              k8s_POD_etcd-kube-master_kube-system_b3df8d6088284257da0011ac71ef453c_1

可以看到,除了etcd本身对应的容器以外,这里还有一个叫“pause”的容器,那这个pause又是干什么用的呢?

Pause容器

当我们用docker inspect命令查看etcd容器时:

$ docker inspect d4fedb6f3e8f|grep NetworkMode
            "NetworkMode": "container:dac475b9907b82ec5ab0c227789df1221fba8e9cf53341aff1e4a34ab569390c",

会发现,NetworkMode的值遵循container:<name|id>这样一种格式。这里面的nameid是指某个容器的名称或Id。按照Docker的文档,这种格式的意思,是要求容器在启动以后加入到另一个容器的所在网络里,也就是network namespace。在我们的例子里,etcd启动后加入的,就是container:后面的Id值所对应的那个容器。对照前面docker ps的输出结果会发现,这个被要求加入的容器正是pause。也就是说,etcd加入了pause的网络。前面我们提到Pod里所有容器共享同一网络,实际上指的就是pause容器的网络。

那么,为什么选择pause容器的网络作为Pod的网络呢?试想一下,假设我们选择某个应用程序的容器来承担这个角色,一旦这个容器因为某些原因而崩溃,那么Pod里其他容器的网络就无法连通了,新的容器也无法再加入到Pod的网络里来。

这一现象可以通过实验来验证。假设我们在当前机器上启动两个容器,分别是:busybox1和busybox2,并让busybox2加入到busybox1的网络里:

$ docker run -dit --name busybox1 busybox
e4202f80580d1940d6e6b22bf7019bce5a9c11cc186e6a0d4e5263a7721261a0
$ docker run -dit --name busybox2 --net=container:busybox1 busybox
0201b9edebb50b65be342e2b2014174ded2ac15c7fb01486379f7b4762d1aa40

这个时候,如果我们分别对两个容器调用ip addr show命令:

$ docker exec busybox1 ip addr show
$ docker exec busybox2 ip addr show

会发现它们的输出结果是完全一样的:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0
3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop qlen 1
    link/tunnel6 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 brd 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

这说明busybox1和busybox2是共享网络的,它们用了同一个网络接口eth0,IP地址为172.17.0.2。这个地址从宿主机一端是可以ping通的:

$ ping 172.17.0.2 -c 3           
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.161 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=64 time=0.086 ms
64 bytes from 172.17.0.2: icmp_seq=3 ttl=64 time=0.082 ms

--- 172.17.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2087ms
rtt min/avg/max/mdev = 0.082/0.109/0.161/0.038 ms

现在,假如我们把busybox1停掉:

$ docker stop busybox1
busybox1

然后再看busybox2的网络配置:

$ docker exec busybox2 ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0
3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop qlen 1
    link/tunnel6 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 brd 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00

会发现,输出结果里已经找不到eth0了。这个时候,虽然busybox2还处于运行状态,但是由于没有eth0,网络是没有办法连通的。

正是由于这样的原因,对于承担Pod网络环境的容器而言,就提出了一定的要求:它必须是简单小巧,而不会出错的!

再来看一看我们的pause容器,有兴趣的同学可以读一下它在GitHub上的源代码,后面有机会我会单独写一篇文章来介绍它的逻辑。我们会发现,pause的代码非常简单,它的核心逻辑就是循环调用Linux提供的pause()函数,让当前进程进入休眠,直到有外部信号进来。这也是pause容器名称的由来。正是pause的特点,决定了它非常适合为Pod提供网络。

留下评论

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

正在加载...