注: 本文采用知识共享署名-相同方式共享 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>
这样一种格式。这里面的name
或id
是指某个容器的名称或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提供网络。
留下评论
您的电子邮箱地址并不会被展示。请填写标记为必须的字段。 *