容器本可以成为轻量级虚拟机的替代品。然而,最广泛使用的容器形式(由 Docker 普及并随后由 OCI 标准化)鼓励您只拥有一个过程每个容器的服务。这种方法有一些显着的优点- 增强的隔离性、简化的水平扩展、更高的可重用性等。但是,这种设计也有一个主要缺点 - 在现实世界中,虚拟机很少只运行一项服务。因此,容器抽象对于功能齐全的虚拟机替换来说通常可能过于有限。
当 Docker 试图提供创建多服务容器的解决方法时,Kubernetes 迈出了更大胆的一步,选择了一组称为Pod的内聚容器作为其最小的可部署单元,而不是单个单元。
对于具有 VM 或裸机经验的工程师来说,应该相对容易掌握 Pod 的概念,或者看起来是这样...... 🙈
开始使用 Kubernetes 时首先要了解的事情之一是为每个 Pod 分配一个唯一的 IP 地址和主机名。此外,Pod 内的容器可以通过 localhost 相互通信。因此,很快就会发现 Pod 类似于服务器的缩影。
但过了一段时间,您就会意识到 Pod 中的每个容器都有一个独立的文件系统,并且在一个容器内部,您看不到同一 Pod 中其他容器的文件和进程。那么,也许 Pod 并不是一个很小的服务器,而只是一组具有共享网络堆栈的容器?
但随后您就会了解到,一个 Pod 中的容器可以通过共享内存和其他典型的 Linux IPC 方式进行通信!所以,网络命名空间可能不是唯一共享的东西......
我决定深入研究并亲眼看看:
- Pod 是如何在底层实现的。
- Pod 和 Container 之间的实际区别是什么。
- 使用标准 Docker 命令创建 Pod 需要什么。
听起来不错?那么就和我一起踏上旅程吧!至少,它可以帮助您巩固 Linux、Docker 和 Kubernetes 技能。
检查容器
让我们启动我们的容器:
css
docker run --name foo --rm -d --memory='512MB' --cpus='0.5' nginx:alpine
检查容器的名称空间
首先,了解容器启动时创建了哪些隔离原语会很有趣。
以下是查找容器名称空间的方法:
bash
NGINX_PID=$(pgrep nginx | sort | head -n 1)
sudo lsns -p ${NGINX_PID}
arduino
NS TYPE NPROCS PID USER COMMAND
...
4026532253 mnt 3 1269 root nginx: master process nginx -g daemon off;
4026532254 uts 3 1269 root nginx: master process nginx -g daemon off;
4026532255 ipc 3 1269 root nginx: master process nginx -g daemon off;
4026532256 pid 3 1269 root nginx: master process nginx -g daemon off;
4026532258 net 3 1269 root nginx: master process nginx -g daemon off;
4026532319 cgroup 3 1269 root nginx: master process nginx -g daemon off;
用于隔离 nginx 容器的命名空间是:
- mnt ( Mount ) - 容器有一个独立的安装表。
- uts (UNIX 分时)- 容器能够拥有自己的主机名和域名。
- ipc (进程间通信)-容器内的进程只能通过系统级 IPC 与同一容器内的进程进行通信。
- pid (进程 ID ) - 容器内的进程只能看到同一容器内或同一 pid 命名空间内的其他进程。
- net ( Network ) - 容器拥有自己的网络堆栈。
- cgroup ( Cgroup ) - 容器有自己的cgroup_虚拟化视图_(不要与 cgroup 本身混淆)。
请注意_User_命名空间没有被使用!OCI 运行时规范提到了用户 命名空间支持。然而,虽然 Docker 可以为其容器使用此命名空间,但由于固有的限制和可能增加的额外操作复杂性,默认情况下它不会这样做。因此,root
容器中的用户可能是root
主机系统中的用户。谨防!
列表中另一个需要特殊标注的命名空间是cgroup
。我花了一段时间才明白cgroup命名空间与cgroups机制是不一样的。Cgroup 命名空间只是为容器提供了 cgroup 伪文件系统的隔离视图(将在下面讨论)。
检查容器的 cgroup
Linux 命名空间使容器内的进程认为它们在专用机器上运行。然而,看不到任何邻居并不意味着完全受到保护。一些饥饿的邻居可能会意外消耗主机资源的不公平份额。
Cgroups 来救援!
给定进程的 cgroup 限制可以通过检查 cgroup 伪文件系统 ( cgroupfs
) 中的节点来检查,该节点通常安装在/sys/fs/cgroup
。但首先,我们需要找出感兴趣进程的 cgroupfs 子树的路径:
css
sudo systemd-cgls --no-pager
yaml
Control group /:
-.slice
...
│
└─system.slice
...
│
├─docker-866191e4377b052886c3b85fc771d9825ebf2be06f84d0bea53bc39425f753b6.scope ...
│ ├─1269 nginx: master process nginx -g daemon off;
│ └─1314 nginx: worker process
...
然后列出 cgroupfs 子树:
bash
ls -l /sys/fs/cgroup/system.slice/docker-866191e4377b052886c3b85fc771d9825ebf2be06f84d0bea53bc39425f753b6.scope/
diff
...
-rw-r--r-- 1 root root 0 Sep 27 11:12 cpu.max
-r--r--r-- 1 root root 0 Sep 27 11:12 cpu.stat
-rw-r--r-- 1 root root 0 Sep 27 11:12 cpu.weight
...
-rw-r--r-- 1 root root 0 Sep 27 11:51 io.max
-r--r--r-- 1 root root 0 Sep 27 11:12 io.stat
...
-rw-r--r-- 1 root root 0 Sep 27 11:12 memory.high
-rw-r--r-- 1 root root 0 Sep 27 11:12 memory.low
-rw-r--r-- 1 root root 0 Sep 27 11:12 memory.max
-rw-r--r-- 1 root root 0 Sep 27 11:12 memory.min
...
-r--r--r-- 1 root root 0 Sep 27 11:42 pids.current
-r--r--r-- 1 root root 0 Sep 27 11:51 pids.events
-rw-r--r-- 1 root root 0 Sep 27 11:12 pids.max
要查看具体的内存限制,需要读取文件中的值memory.max
:
bash
cat /sys/fs/cgroup/system.slice/docker-866191e4377b052886c3b85fc771d9825ebf2be06f84d0bea53bc39425f753b6.scope/memory.max
bash
536870912 # Exactly 512MB that were requested at the container start.
有趣的是,在没有显式设置任何资源限制的情况下启动容器无论如何都会为其配置一个 cgroup 切片。我还没有真正检查过,但我的猜测是,虽然默认情况下 CPU 和 RAM 消耗不受限制,但 cgroups 可能用于限制容器内部的一些其他资源消耗和设备访问(例如,/dev/sda
和)。/dev/mem
以下是根据上述发现如何可视化容器的方法:
检查 Pod
现在,让我们看一下 Kubernetes Pod。为了保持容器与 Pod 比较的公平性,Pod 检查将在 Kubernetes 集群上进行,该集群使用与 Docker 相同的底层容器运行时 - containerd/runc。
与容器非常相似,Pod 的实现可能会有所不同。例如,当Kata 容器用作CRI 运行时(通过runtimeClassName
在 Pod 规范上设置属性)时, Pod 就成为真正的虚拟机!预计,基于 VM 的 Pod 在实现和功能上将不同于使用传统 Linux 容器实现的 Pod。
将检查以下两个容器 Pod:
pod.yaml
yaml
apiVersion: v1
kind: Pod
metadata:
name: foo
spec:
containers:
- name: app
image: nginx:alpine
ports:
- containerPort: 80
resources:
limits:
memory: "256Mi"
- name: sidecar
image: curlimages/curl:8.3.0
command: ["/bin/sleep", "3650d"]
resources:
limits:
memory: "128Mi"
您可以使用以下命令启动 Pod:
kubectl apply -f pod.yaml
检查 Pod 的容器
Pod 检查应在运行该 Pod 的 Kubernetes 集群节点上完成。使用kube-01
右侧的选项卡,让我们尝试查找 Pod 的进程:
ps auxf
yaml
PID USER ELAPSED CMD
...
1748 root 01:16 /var/lib/rancher/k3s/data/ab2055bc72380bad965b219e8
1769 65535 01:15 \_ /pause
1801 root 01:15 \_ nginx: master process nginx -g daemon off;
1846 message+ 01:15 | \_ nginx: worker process
1847 message+ 01:15 | \_ nginx: worker process
1859 _apt 01:15 \_ /bin/sleep 3650d
基于正常运行时和公共父进程的相似性,上述三个(顶级)进程很可能是在 Pod 启动期间创建的。这很有趣,因为在清单中只请求了两个容器nginx
和sleep
.
可以使用名为 的 containerd 命令行客户端ctr
交叉检查上述发现:
ini
sudo ctr --namespace=k8s.io containers ls
lua
CONTAINER IMAGE RUNTIME
1c0d4c94188aa docker.io/library/nginx:alpine io.containerd.runc.v2
3f2b45521b479 docker.io/curlimages/curl:8.3.0 io.containerd.runc.v2
fe99217fab7c5 docker.io/rancher/mirrored-pause:3.6 io.containerd.runc.v2
...
事实上,Kubernetes 容器运行时创建了三个新容器 - nginx
、sleep
和pause
。与此同时,另一个与任何 Kubernetes CRI 运行时兼容的命令行客户端仅显示两个容器:
sudo crictl ps
CONTAINER IMAGE ... NAME POD ID POD
d74ad720df223 ead0a4a53df89 coredns bf8d6b16f6c10 coredns-6799fbcd5-6xksq
3f2b45521b479 99f2927cb384d sidecar fe99217fab7c5 foo
1c0d4c94188aa 433dbc17191a7 app fe99217fab7c5 foo
但请注意上面的字段如何与 的输出中的容器 IDPOD ID
相匹配!嗯,看来 Pod 有一个辅助容器。那么,它是做什么用的呢?
我不知道 Pod 是否有任何等效的 OCI 运行时规范。因此,当我对Kubernetes API 规范提供的信息不满意时,我通常会直接转到Kubernetes 容器运行时接口 (CRI) protobuf 文件:
scss
// kubelet expects any compatible container runtime
// to implement the following gRPC methods:
service RuntimeService {
...
rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}
rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}
rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {}
rpc ReopenContainerLog(ReopenContainerLogRequest) returns (ReopenContainerLogResponse) {}
// ...
}
message CreateContainerRequest {
// ID of the PodSandbox in which the container should be created.
string pod_sandbox_id = 1;
// Config of the container.
ContainerConfig config = 2;
// Config of the PodSandbox. This is the same config that was passed
// to RunPodSandboxRequest to create the PodSandbox. It is passed again
// here just for easy reference. The PodSandboxConfig is immutable and
// remains the same throughout the lifetime of the Pod.
PodSandboxConfig sandbox_config = 3;
}
因此,Pod 实际上是根据沙箱
和可以在这些沙箱中启动的容器
来定义的。沙箱管理一些所有 Pod 容器共有的资源,pause
容器在调用过程中启动RunPodSandbox()
。上网一查,发现这个容器里面只有一个空闲进程
检查 Pod 的命名空间
以下是(相关)命名空间在集群节点上的样子:
sudo lsns
yaml
NS TYPE NPROCS PID USER COMMAND
...
4026532333 net 5 1769 65535 /pause
4026532397 mnt 1 1769 65535 /pause
4026532398 uts 5 1769 65535 /pause
4026532399 ipc 5 1769 65535 /pause
4026532400 pid 1 1769 65535 /pause
4026532401 mnt 3 1801 root nginx: master process nginx -g daemon off;
4026532402 pid 3 1801 root nginx: master process nginx -g daemon off;
4026532403 cgroup 3 1801 root nginx: master process nginx -g daemon off;
4026532404 mnt 1 1859 _apt /bin/sleep 3650d
4026532405 pid 1 1859 _apt /bin/sleep 3650d
4026532406 cgroup 1 1859 _apt /bin/sleep 3650d
与第一部分中的 Docker 容器非常相似,该pause
容器有五个命名空间 - net 、mnt 、uts 、ipc 和pid 。但显然,nginx
容器sleep
仅由三个命名空间组成:mnt 、pid 和cgroup。
事实证明,lsns
可能不是检查进程名称空间的最佳工具。相反,要检查某个进程使用的名称空间,/proc/${PID}/ns
可以引用该路径:
rust
# 1801 is the PID of the nginx container
sudo ls -l /proc/1801/ns
...
lrwxrwxrwx 1 root root 0 sep 28 12:00 cgroup -> 'cgroup:[4026532403]'
lrwxrwxrwx 1 root root 0 Sep 28 12:00 ipc -> 'ipc:[4026532399]'
lrwxrwxrwx 1 root root 0 Sep 28 12:00 mnt -> 'mnt:[4026532401]'
lrwxrwxrwx 1 root root 0 Sep 28 12:00 net -> 'net:[4026532333]'
lrwxrwxrwx 1 root root 0 Sep 28 12:00 pid -> 'pid:[4026532402]'
lrwxrwxrwx 1 root root 0 Sep 28 12:00 uts -> 'uts:[4026532398]'
...
rust
# 1859 is the PID of the sleep container
sudo ls -l /proc/1859/ns
...
lrwxrwxrwx 1 _apt messagebus 0 Sep 28 12:00 cgroup -> 'cgroup:[4026532406]'
lrwxrwxrwx 1 _apt messagebus 0 Sep 28 12:00 ipc -> 'ipc:[4026532399]'
lrwxrwxrwx 1 _apt messagebus 0 Sep 28 12:00 mnt -> 'mnt:[4026532404]'
lrwxrwxrwx 1 _apt messagebus 0 Sep 28 12:00 net -> 'net:[4026532333]'
lrwxrwxrwx 1 _apt messagebus 0 Sep 28 12:00 pid -> 'pid:[4026532405]'
lrwxrwxrwx 1 _apt messagebus 0 Sep 28 12:00 uts -> 'uts:[4026532398]'
...
虽然可能很难注意到,但nginx
和sleep
容器实际上重用了容器的net 、uts 和ipc 命名空间pause
!
同样,这可以通过以下方式进行交叉检查crictl
:
bash
# 1c0d4c94188aa is the ID of the nginx container
sudo crictl inspect 1c0d4c94188aa | jq .info.runtimeSpec.linux.namespaces
[
{
"type": "pid"
},
{
"type": "ipc",
"path": "/proc/1769/ns/ipc"
},
{
"type": "uts",
"path": "/proc/1769/ns/uts"
},
{
"type": "mount"
},
{
"type": "network",
"path": "/proc/1769/ns/net"
},
{
"type": "cgroup"
}
]
bash
# 3f2b45521b479 is the ID of the sleep container
sudo crictl inspect 3f2b45521b479 | jq .info.runtimeSpec.linux.namespaces
[
{
"type": "pid"
},
{
"type": "ipc",
"path": "/proc/1769/ns/ipc"
},
{
"type": "uts",
"path": "/proc/1769/ns/uts"
},
{
"type": "mount"
},
{
"type": "network",
"path": "/proc/1769/ns/net"
},
{
"type": "cgroup"
}
]
我认为上述发现完美地解释了同一个 Pod 中容器的能力:
- 互相通信
- 通过和/或本地主机
- 使用IPC手段(共享内存、消息队列等)
- 拥有共享域和主机名。
然而,在看到所有这些命名空间如何在容器之间自由重用后,我开始怀疑默认边界可以被打破。事实上,对Pod API 规范的更彻底的阅读表明,将shareProcessNamespace
标志设置为true
Pod 的容器将具有四个公共命名空间,而不是默认的三个。但还有一个更令人震惊的发现------ hostIPC
、hostNetwork
、 和hostPID
flags 可以让容器使用相应主机的命名空间!
有趣的是,CRI API 规范似乎更加灵活。至少在语法上,它允许将net 、pid 和ipc命名空间的范围限定为CONTAINER、POD或NODE。因此,假设可以构建一个容器无法通过 localhost 相互通信的 Pod 🙈
检查 Pod 的 cgroup
好的,Pod 的 cgroup 怎么样了?systemd-cgls
可以很好地可视化 cgroups 层次结构:
python
sudo systemd-cgls --no-pager
Control group /:
-.slice
...
└─kubepods.slice
└─kubepods-burstable.slice
...
└─kubepods-burstable-pode88e5109_51f9_4f39_8d99_ada6fb281137.slice
├─cri-containerd-1c0d4c94188aa402db8751db1301de1d3adcc6739ee2ca78c6738273ee8251a7.scope ...
│ ├─1801 nginx: master process nginx -g daemon off;
│ ├─1846 nginx: worker process
│ └─1847 nginx: worker process
├─cri-containerd-fe99217fab7c597c40f10b8086dfa2978394e9942b53d66569441b5b7d7d4ea0.scope ...
│ └─1769 /pause
└─cri-containerd-3f2b45521b479bc1c07a8eebf9409e244d5fbf9eb431156d250a37f492599604.scope ...
└─1859 /bin/sleep 3650d
看起来 Pod 本身就有一个父节点,并且每个容器也可以单独调整。这符合我的预期,因为在 Pod 清单中,可以为 Pod 中的每个容器单独设置资源限制。
此时此刻,我脑海中的 Pod 看起来是这样的:
使用 Docker 实现 Pod
如果 Pod 底层被实现为一堆具有公共 cgroup 父级的半融合容器,是否可以使用 Docker 重现类似 Pod 的构造?
最近我已经尝试做类似的事情来让多个容器监听同一个套接字,并且我知道 Docker 允许创建一个使用语法重用现有网络命名空间的容器docker run --network container:<other-container-name>
。但我也知道 OCI 运行时规范仅定义create
和start
命令。因此,当您在现有容器内执行命令时docker exec <existing-container> <command>
,您实际上run
(即create
then)start
是一个完全新鲜的容器,它恰好重用了目标容器的所有名称空间。这让我非常有信心可以使用标准 Docker 命令来重现 Pod。
首先,需要配置父cgroup条目。 幸运的是,现在,为了简洁起见,我将仅配置cpu和内存的cgroup 控制器:
ini
sudo cat <<EOF > /etc/systemd/system/mypod.slice
[Unit]
Description=My Pod Slice
[Slice]
MemoryLimit=512M
CPUQuota=50%
EOF
python
sudo systemctl daemon-reload
sudo systemctl start mypod.slice
检查切片是否实际创建:
python
sudo systemd-cgls --no-pager --all
Control group /:
-.slice
...
├─mypod.slice
...
其次,需要创建一个沙箱容器:
css
docker run -d --rm \
--name mypod_sandbox \
--cgroup-parent mypod.slice \
--ipc 'shareable' \
alpine sleep infinity
最后,我们需要重用沙箱容器的命名空间来启动有效负载容器:
lua
# app (nginx)
docker run -d --rm \
--name app \
--cgroup-parent mypod.slice \
--network container:mypod_sandbox \
--ipc container:mypod_sandbox \
nginx:alpine
lua
# sidecar (sleep)
docker run -d --rm \
--name sidecar \
--cgroup-parent mypod.slice \
--network container:mypod_sandbox \
--ipc container:mypod_sandbox \
curlimages/curl sleep 365d
您注意到我省略了哪个名称空间吗?是的,我无法在容器之间共享uts命名空间。目前命令中似乎没有公开这种可能性docker run
。嗯,这当然很遗憾。但除了uts命名空间之外,它是成功的!
cgroup 看起来很像 Kubernetes 本身创建的 cgroup:
css
sudo systemd-cgls --no-pager --all
这次,切片将列出几个活动进程:
bash
Control group /:
-.slice
...
├─mypod.slice
│ ├─docker-575fd1bbc28340fbd37c35374dd4ef8a91d796cf4abc2e97eaac42981ae2058a.scope ...
│ │ └─1480 sleep infinity
│ ├─docker-c36d2f83cf53ebe5354f7a6f60770b8728772e6c788979d8a35338da102a2fd6.scope ...
│ │ └─1312 sleep infinity
│ ├─docker-48dff78e59361aea6876385aa0677c1ad949b0951cb97b9cf7d1e8fba991dc3e.scope ...
│ │ └─1669 sleep 365d
│ └─docker-85b436943b55fdb4666d384711ad3577f41c0d03e58987c639633a35a37bacf4.scope ...
│ ├─1599 nginx: master process nginx -g daemon off;
│ └─1635 nginx: worker process
命名空间的全局列表看起来也很熟悉:
yaml
NS TYPE NPROCS PID USER COMMAND
...
4026532322 mnt 1 1480 root sleep infinity
4026532323 uts 1 1480 root sleep infinity
4026532324 ipc 4 1480 root sleep infinity
4026532325 pid 1 1480 root sleep infinity
4026532327 net 4 1480 root sleep infinity
4026532385 cgroup 1 1480 root sleep infinity
4026532386 mnt 2 1599 root nginx: master process nginx -g daemon off;
4026532387 uts 2 1599 root nginx: master process nginx -g daemon off;
4026532388 pid 2 1599 root nginx: master process nginx -g daemon off;
4026532389 cgroup 2 1599 root nginx: master process nginx -g daemon off;
4026532390 mnt 1 1669 _apt sleep 365d
4026532391 uts 1 1669 _apt sleep 365d
4026532392 pid 1 1669 _apt sleep 365d
4026532393 cgroup 1 1669 _apt sleep 365d
并且app ( nginx
) 和sidecar( curl
) 容器似乎共享ipc 和net命名空间:
bash
# app container
sudo ls -l /proc/1599/ns
rust
lrwxrwxrwx 1 root root 0 Sep 28 13:09 cgroup -> 'cgroup:[4026532389]'
lrwxrwxrwx 1 root root 0 Sep 28 13:09 ipc -> 'ipc:[4026532324]'
lrwxrwxrwx 1 root root 0 Sep 28 13:09 mnt -> 'mnt:[4026532386]'
lrwxrwxrwx 1 root root 0 Sep 28 13:09 net -> 'net:[4026532327]'
lrwxrwxrwx 1 root root 0 Sep 28 13:09 pid -> 'pid:[4026532388]'
lrwxrwxrwx 1 root root 0 Sep 28 13:09 uts -> 'uts:[4026532387]'
...
rust
# sidecar container
sudo ls -l /proc/1669/ns
lrwxrwxrwx 1 _apt messagebus 0 Sep 28 13:09 cgroup -> 'cgroup:[4026532393]'
lrwxrwxrwx 1 _apt messagebus 0 Sep 28 13:09 ipc -> 'ipc:[4026532324]'
lrwxrwxrwx 1 _apt messagebus 0 Sep 28 13:09 mnt -> 'mnt:[4026532390]'
lrwxrwxrwx 1 _apt messagebus 0 Sep 28 13:09 net -> 'net:[4026532327]'
lrwxrwxrwx 1 _apt messagebus 0 Sep 28 13:09 pid -> 'pid:[4026532392]'
lrwxrwxrwx 1 _apt messagebus 0 Sep 28 13:09 uts -> 'uts:[4026532391]'
...
耶!我们刚刚(几乎)仅使用标准docker run
命令创建了一个 Pod 🎉
总结
容器和 Pod 很相似。在幕后,他们严重依赖 Linux 命名空间和 cgroup。然而,Pod 不仅仅是容器组。Pod 是一个自给自足的高级构造。所有Pod的容器运行在同一台机器(集群节点)上,生命周期同步,弱化相互隔离,简化容器间通信。这使得 Pod 更接近传统 VM,带回了熟悉的部署模式,例如sidecar或客户端服务代理