以 ls 命令为例,它也是个管理工具,可以帮助我们得知:是否可以访问某个文件、任意目录下有哪些文件可用、每个文件的功能(例如,一个文件是否是可执行的)。
"一切皆为文件"是 Linux 独有的。例如,/proc 目录包含正在运行的进程的实时信息,在许多方面可以像管理一个文件目录一样管理它。而 Windows 的开发则需要与 Windows APIs、注册表、PowerShell 甚至 GUI 打交道,因此不太可能仅凭读写文件来管理整个 Windows 操作系统。
一切皆为文件(或文件描述符)
Linux 原语几乎总是在做一些事情来操纵、移动或提供某种文件的抽象,这是因为用 Kubernetes 构建的一切最初都是为了在 Linux 上工作,而 Linux 从设计上就是使用文件抽象作为控制原语。
"一切皆为文件"的几个例子:
标准输出:向标准输出文件写入后,它会魔法般地把内容显示在终端
目录:一个包含其他文件名的文件
设备:例如以太网设备文件会被附加到容器内部
套接字(Socket)和管道(Pipe):被用于本地进程间通信的文件。CSI 会大量利用这一抽象概念来定义 kubelet 与 Pod 存储实例的通信方式
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b55055a19d9d kindest/node:v1.24.0 "/usr/local/bin/entr…" 3 minutes ago Up 3 minutes 127.0.0.1:35003->6443/tcp kind-control-plane
将上述内容保存至 pod.yaml,创建该 Pod 的命令是 kubectl create -f pod.yaml。
我们想知道由 Pod 启动带起进程对操作系统的可见性,这可以在控制平面容器中借助 ps -ax 命令来列出系统中的所有进程(-x flag 允许我们看到系统级进程)进行观察。
root@kind-control-plane:/# ps -ax | wc -l # 在控制平面容器中计算起初运行了多少个进程
35
$ kubectl create -f pod.yml # 在外部创建一个 Pod
pod/core-k8s created
root@kind-control-plane:/# ps -ax | wc -l # 再回到容器中计算 Pod 创建后有多少运行中的进程
37
探索 Pod 的 Linux 依赖
运行一个 Pod 和运行别的程序对用户来说没什么区别,同样在输入输出、消耗计算和网络资源,以及依赖共享库和操作系统底层工具等。而 kubelet 做了很多类似 Linux 管理员的工作,比如在程序执行之前为其创建了一个隔离的目录、CPU、内存、网络、namespace 限制和其他资源,在程序退出之后再清理掉这些资源。
图 3.2 展示的是 kubelet/Pod 生命周期,它是最底层的控制循环之一。
这时执行 kubectl get pods -o yaml 我们可以看见正在运行的 Pod 对象的全部定义与状态,它在先前提交的 YAML 文件内容之上扩充了更多内容,除了有些是默认值或表示 Pod 的状态,在某些组织内可能会存在一个拦截器,它可以在我们的 YAML 定义被提交至 API 服务器之前对其进行检查与值修改,例如可以在上面附加 CPU 与内存限制。我们可以使用 JSONPath 来筛选出我们感兴趣的内容:
$ kubectl get pods -o=jsonpath='{.items[0].status.phase}' # Pod 状态
Running
$ kubectl get pods -o=jsonpath='{.items[0].status.podIP}' # Pod IP
10.244.0.7
$ kubectl get pods -o=jsonpath='{.items[0].status.hostIP}' # Pod 所在主机的 IP
172.18.0.2
除了状态,我们还可以进一步了解 Pod 上挂载了哪些内容。例如在输出的 YAML 上我们找到 spec 下面的 volumes 字段,里面只有一项内容,表示的是 Kubernetes 集群为 Pod 赋予的一个允许访问 API 服务器的证书。
# /bin/bash
# Makes our box with the bin and lib directories as dependencies for our Bash program
mkdir -p /home/namespace/box/bin
mkdir -p /home/namespace/box/lib
mkdir -p /home/namespace/box/lib64
mkdir -p /home/namespace/box/proc
mkdir -p /home/namespace/box/data
# Copies all the programs from our base OS into this box so we can run Bash in our root directory
cp -v /bin/bash /home/namespace/box/bin
cp -v /bin/ip /home/namespace/box/bin
cp -v /bin/ls /home/namespace/box/bin
cp -v /bin/mount /home/namespace/box/bin
cp -v /bin/umount /home/namespace/box/bin
cp -v /bin/kill /home/namespace/box/bin
cp -v /bin/ps /home/namespace/box/bin
cp -v /bin/curl /home/namespace/box/bin
cp -v /bin/pidof /home/namespace/box/bin
cp -v /bin/expr /home/namespace/box/bin
cp -v /bin/rm /home/namespace/box/bin
# Copies the library dependencies of these programs into the lib/ directories
cp -r /lib/* /home/namespace/box/lib/
cp -r /lib64/* /home/namespace/box/lib64/
# Mounts the /proc and /tmp directory to this location
mount -t proc proc /home/namespace/box/proc
mount --bind -v /tmp/ /home/namespace/box/data
# This is the important part: we start our isolated Bash process in a sandboxed directory.
chroot /home/namespace/box /bin/bash
因为我们上面的 unshare 命令添加了 flag --net,所以我们的隔离“容器”处于一个新的网络环境。
bash-5.1# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
link/sit 0.0.0.0 brd 0.0.0.0
而我们回到最初的终端(运行 kind 的 Linux 终端),查看任意一个 Pod 的网络设备,我们可以发现一个显著的区别,我们的“容器”与真正的 Pod 相比少了一个 eth0 设备。这说明 chroot 虽然帮我们实现了简易版容器化,但我们仍缺少这个重要的网络设备。我们同样可以在控制平面容器找到一个 eth0 设备。
$ kubectl exec -t -i core-k8s ip a # 查看 core-k8s 这个 Pod 的网络设备
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: sit0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1000
link/sit 0.0.0.0 brd 0.0.0.0
3: eth0@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue
link/ether 86:d4:53:ed:1c:e8 brd ff:ff:ff:ff:ff:ff
inet 10.244.0.2/24 brd 10.244.0.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::84d4:53ff:feed:1ce8/64 scope link
valid_lft forever preferred_lft forever
我们的“容器”不但没有 eth0 这个设备,而且失去了路由和 IP 信息(这些信息是从 hostNetwork namespace 继承的)。为了验证这一点,由于我们先前也拷贝了 cURL 工具,所以可以试着先获取 google.com 的一个 IP 地址,然后在“容器”内部和外部分别对该 IP 使用 curl 命令看看有什么区别:
bash-5.1# curl 142.250.189.206 # 用 ping 得到的 google.com 的一个 IP 地址,我们在容器内部 curl 它。
curl: (7) Couldn't connect to server
bash-5.1# exit # 来到容器外部
exit
root@kind-control-plane:/# curl 142.250.189.206 # 我们在控制平面 curl 它。
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>
不出意外,我们的“容器”嗷的一声挂了,因为我们只为他分配了 10 个字节的内存大小,甚至连执行 ls 命令都会 OOM。
让我们回顾一下:我们已经创建了自己的“容器”,它与主机上其他文件隔离但也可以通过挂载向其提供数据(文件),资源占用有限制,并且运行在一个隔离的进程空间使其认为自己是整个世界上唯一的进程。这是 Kubernetes 集群中 Pod 的自然状态。
在现实世界中使用我们的 Pod
联网问题
图 3.4 描述的是一个真实的容器与 Kubernetes 集群的交互。我们在上一节手工制作的“容器”与之相比没有网卡,也没有一个集群中独立的 IP,因此面对微服务常常需要与其他服务进行沟通的场景,我们自制的“容器”是无法做到的,更不用说借助 DNS 进行服务发现、注入证书之类的能力了。
Pod 联网问题可以细化为三个场景:
入流量:接收来自集群内其它 Pod 的流量
出流量:向其它 Pod 或互联网发送的流量
负载均衡:凭静态 IP 作为一个端点,来接收由 Service 负载均衡传递过来的流量
为了实现这些操作,API 服务器会将 Pod 的元数据(metadata)发布到 Kubernetes 的其他部分;kubelet 会对其状态进行监视,并随着时间的推移更新这些状态。因此,Pod 不仅仅是一个容器命令和一个 Docker 镜像,它不但有元数据(例如 labels),而且它还应该有规范(spec)。规范是指 Pod 有明确定义的状态、重启逻辑,以及在集群内拥有的 IP 地址的可达性的保证。
利用 iptables 来了解 kube-proxy 如何实现 Kubernetes 服务
iptables 是 Linux 上常用的防火墙软件。下面是一个在 Kubernetes 之外的传统环境中配置 iptables 的例子:
$ iptables -A INPUT -s 10.1.2.3 -j DROP
$ iptables-save | grep 10.1.2.3
-A INPUT -s 10.1.2.3/32 -j DROP
$ kubectl get service kube-dns -n kube-system -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 22m k8s-app=kube-dns
$ kubectl get pod -l k8s-app=kube-dns -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-6d4b75cb6d-ff52n 1/1 Running 0 22m
kube-system coredns-6d4b75cb6d-zn55v 1/1 Running 0 22m
从名字和端口号 53 我们可以得知这是一个 DNS 服务。当集群内的 Pod 之间需要在通信前向该服务查询 DNS 时,kube-dns 会把流量转发到两个 coredns Pod 的其中一个。
下面我们来验证一下,我们先查询出两个 coredns Pod 的集群内 IP 地址:
$ kubectl get pod coredns-6d4b75cb6d-zn55v -n kube-system -o=jsonpath='{.status.podIP}'
10.244.0.3
$ kubectl get pod coredns-6d4b75cb6d-ff52n -n kube-system -o=jsonpath='{.status.podIP}'
10.244.0.4
现在我们再次进入控制平面容器,使用 ip route 命令查看控制平面节点的路由信息:
root@kind-control-plane:/# ip route
default via 172.18.0.1 dev eth0
10.244.0.2 dev vethf6e1facb scope host
10.244.0.3 dev veth365aab0b scope host
10.244.0.4 dev vethb5e9a03d scope host
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.2
我们发现在输出的路由表里也存在 10.244.0.3 和 10.244.0.4 这两个 IP,Pod 的 IP 地址和路由规则都是由 CNI 插件提供的。路由表的描述是:当目的地址是 10.244.0.3 或 10.244.0.4 时,将流量转发至特定的 veth 设备。这些设备也是由 CNI 插件制作的,我们可以用 ip a 命令查看控制平面节点的网络设备:
root@kind-control-plane:/# ip a
...
3: vethf6e1facb@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 46:68:a9:a7:b6:c2 brd ff:ff:ff:ff:ff:ff link-netns cni-ac9a3988-a855-101b-de1b-4c48546e32ae
inet 10.244.0.1/32 scope global vethf6e1facb
valid_lft forever preferred_lft forever
4: veth365aab0b@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 2a:50:35:d0:13:60 brd ff:ff:ff:ff:ff:ff link-netns cni-955eeeb6-ecaf-db34-deb7-a65264ade2f8
inet 10.244.0.1/32 scope global veth365aab0b
valid_lft forever preferred_lft forever
5: vethb5e9a03d@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether ba:17:4e:74:7b:e9 brd ff:ff:ff:ff:ff:ff link-netns cni-66de6450-7376-4ebd-d4e8-8b7f0cc2e994
inet 10.244.0.1/32 scope global vethb5e9a03d
valid_lft forever preferred_lft forever
...
调度:我们先前讨论过 Pod 的调度本身是复杂的,例如如果我们在设置 cgroup 的时候把内存设定过高,或者我们在没有足够的内存的节点上以 request 字段上定义的内存大小来启动一个 Pod,那么最终可能会令节点崩溃。我们也简介过 Kubernetes 的 Pod 调度策略,它会根据亲和力(affinity)、CPU、内存、存储可用性、数据中心拓扑 结构等参数,为 Pod 挑选简单、以容器为中心、可预测的节点