docker containerd runc ctr circtl cgroups namespaces 原理及生产运维
背景
相信刚开始使用 docker 或 cri-o / containerd 的童鞋都应该经常碰到过这样一个问题,如想查看某个容器的 IP 或运行的进程等资源,发现进入容器后都是 command not found,因为通常生产环境为了减少镜像尺寸都会使用如 alpine 这种裁剪版linux很多命令都被移除,那么如何优雅的查看容器里的资源呢?
1. Linux namespaces 原理及与容器操作
1.1 Linux namespaces 基础
最初是由 Google Borg 系统在linux上限制资源的需求而设计的的机制,发展到如今 linux kernel 标准的 CGroups subsystem 机制,以下关于 namespaces 相关基础:
Namespaces Type | 系统调用参数 | 支持的内核版本 |
---|---|---|
Mount namespaces | CLONE_NEWNS | Linux 2.4.19+ |
UTS namespaces | CLONE_NEWUTS | Linux 2.4.19+ |
IPC namespaces | CLONE_NEWIPC | Linux 2.4.19+ |
PID namespaces | CLONE_NEWPID | Linux 2.6.24+ |
Network namespaces | CLONE_NEWNET | Linux 2.4.29+ |
User namespaces | CLONE_NEWUSER | Linux 3.8+ |
- Mount: mount表空间隔离,配合chroot系统调用,使程序有自己的文件系统
- UTS: hostname(本地主机名)和doaminname隔离,使应用有自己独立的主机名
假设分配UTS1、UTS2给2个进程P1、P2,那么P1看到的本地主机名H1,P2看到的本地主机名H2。放佛P1、P2是运行在2个不同的机器上一样。 - IPC: 隔离进程间通讯
- PID: 使程序包括子程序构造一个独立的程序集,最先创建的程序为1号pid
- Network: 网络空间隔离,支行在此空间的程序拥有独立的网络栈。
一个进程属于什么Network命名空间,决定了运行在进程里的应用程序能看见什么网络端口。每个网络接口属于一个命名空间,但是可以从一个命名空间转移到另一个。每个容器都使用属于它自己的网络命名空间,因此每个容器技能看到它自己的一组网络接口。 - User: 此空间下有独立的 uid/gid 系统,目前较新版本的 docker 貌似是使用与宿主机的 uid/gid 数字映射.
因此一个容器拥有一组命名空间, 即: Mount1、UTS1、IPC1、PID1、Network1、User1 组合。
Namespaces 操作可以通过三个系统调用完成:clone(),unshare(),setns()
, 默认情况下子进程继承新父进程的 namespaces。
1.2 如何从宿主机内核层面查看某个容器的网络、主机等信息?
因无 ifconfig
等命令,我们可以直接进入容器所属相应的 namespace 来查看资源情况:(以查看 docker 中 wordpress 容器的 IP 和 Hostname 为例)
1.3 查看容器 IP (net 命名空间) 和 Hostname (UTS 命名空间)
# 失败示例:尝试在 wordpress 容器中使用 ifconfig 命令查看 ip
sudo docker exec -it wordpress bash -c "ifconfig"
bash: ifconfig: command not found
# 直接通过 namespace 查看
# 先找到 wordpress 容器的 pid
sudo docker inspect wordpress | grep -i pid
"Pid": 1609,
"PidMode": "",
"PidsLimit": null,
# 查看所属 net-namespace 的 IP
sudo nsenter -t 1609 -n ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.2 netmask 255.255.0.0 broadcast 172.17.255.255
ether 02:42:ac:11:00:02 txqueuelen 0 (Ethernet)
RX packets 97251 bytes 217684487 (207.6 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 107996 bytes 51322784 (48.9 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
# 查看容器主机名 (uts 命名空间)
sudo nsenter -t 1609 -u hostname
wordpress-1
使用
nsenter
命令等价于进入容器执行命令, 即docker exec -it canal-server bash -c "ifconfig"
, 以上操作只进入命名空间,但不使用该命名空间的 net 和 mnt 命名空间 (不使用 mnt 命名空间的好处是可以使用宿主机的命令行工具)
1.4 (扩展知识) 接上面 1.3
通过下面例子理解一下其实 container 相比 VM 实现更加轻量的原因就是 共享 kernel,本质上不同的容器就只是不同的父子 PID 而已。
# 在宿主机上查看该容器的 PID (该容器 NetworkMode 为: bridge)
ps -ef|grep 1609
root 1609 1588 0 Sep30 ? 00:00:24 apache2 -DFOREGROUND
33 1691 1609 0 Sep30 ? 00:00:13 apache2 -DFOREGROUND
33 1692 1609 0 Sep30 ? 00:00:15 apache2 -DFOREGROUND
33 1693 1609 0 Sep30 ? 00:00:13 apache2 -DFOREGROUND
33 1694 1609 0 Sep30 ? 00:00:13 apache2 -DFOREGROUND
33 1695 1609 0 Sep30 ? 00:00:14 apache2 -DFOREGROUND
33 11955 1609 0 Sep30 ? 00:00:13 apache2 -DFOREGROUND
root 771644 764660 0 17:06 pts/0 00:00:00 grep --color=auto 1609
33 1964767 1609 0 Oct08 ? 00:00:06 apache2 -DFOREGROUND
33 2316703 1609 0 Oct03 ? 00:00:10 apache2 -DFOREGROUND
33 2890263 1609 0 Oct04 ? 00:00:10 apache2 -DFOREGROUND
33 3562943 1609 0 Oct10 ? 00:00:04 apache2 -DFOREGROUND
# 再在宿主机查看父进程
ps -ef|grep 1588
root 1588 1 0 Sep30 ? 00:00:30 /usr/lib/docker-current/containerd-shim-runc-v2 -namespace moby -id aefcf5a28fb3db7057ca1b59b489255024e333eedb497058e5163a109ea2cbc9 -address /run/containerd/containerd.sock
root 1609 1588 0 Sep30 ? 00:00:24 apache2 -DFOREGROUND
root 771682 764660 0 17:06 pts/0 00:00:00 grep --color=auto 1588
通过以上操作,发现了什么?
- a. 本质上不同的容器就只是不同的父子 PID 而已。
- b. 解释了有些同学:升级 docker 会影响正在运行的容器吗? --- 不会。原因:是因为调用过程是 docker --(rest)--> dockerd --(grpc)--> containerd --(ipc)--> containerd-shim-runc-v2 --(ipc)--> runc,其中页可以看到 1588 的父进程是 1。
1.5 查看容器里所有 java 进程 (以容器 root 权限执行),然后找到某进程监听的端口
# a. 找到容器在宿主机上的 PID
docker inspect canal-admin | grep -i pid
"Pid": 6196,
"PidMode": "",
"PidsLimit": null,
# b. 进入 6196 的 pid-namespace,然后以 root 用户执行 jps 命令
sudo nsenter -t 6196 --pid -r jps
99 CanalAdminApplication
1508 Jps
# c. 查看该容器的 net-namespace ,其中进程号为 99 的监听端口
sudo nsenter -t 6196 --net -r ss -nlpt | grep 99
LISTEN 0 100 *:8089 *:* users:(("java",99,106))
LISTEN 0 50 *:9990 *:*
1.6 关于 lsns
基本操作
- 查看所有 ns
sudo lsns
NS TYPE NPROCS PID USER COMMAND
4026531836 pid 317 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531837 user 318 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531838 uts 317 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531839 ipc 317 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531856 mnt 1 91 root kdevtmpfs
4026531956 net 317 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026532215 mnt 1 5127 chrony /usr/sbin/chronyd
4026532216 mnt 2 5120 root /usr/sbin/NetworkManager --no-daemon
4026532219 mnt 1 6264 root /usr/bin/simple
....
- 查看 proc 文件系统查看 ns
sudo ls -l /proc/*/ns/ |awk '{print $11}' |sort -u
ipc:[4026531839]
ipc:[4026532221]
mnt:[4026531840]
mnt:[4026531856]
mnt:[4026532215]
mnt:[4026532216]
mnt:[4026532219]
mnt:[4026532281]
net:[4026531956]
net:[4026532224]
....
1.7 查看某容器的 namespaces
# 查询容器对应在宿主机上的 pid
sudo docker inspect -f "{{.State.Pid}}" 34b48c1d3093
10427
sudo ls -l /proc/10427/ns
lrwxrwxrwx 1 root root 0 Dec 11 16:21 ipc -> ipc:[4026533018]
lrwxrwxrwx 1 root root 0 Dec 11 16:21 mnt -> mnt:[4026533016]
lrwxrwxrwx 1 root root 0 Dec 11 16:21 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 Dec 11 16:21 pid -> pid:[4026533019]
lrwxrwxrwx 1 root root 0 Dec 11 16:21 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Dec 11 16:21 uts -> uts:[4026533017]
- 首先查找到容器 id
- 根据容器id查找容器对应的 pid
- 根据 pid,查找 /proc/{pid}/ns 下的命名空间
从结果看该容器对应6个命名空间, 类型不重复, 正好验证了我们之前的结论 “一个容器拥有一组命名空间, 即 Mount1、UTS1、IPC1、PID1、Network1、User1组合”。
1.8 调试容器的文件系统
-
方式1: 常规方法
docker exec -it <containerId> cat /etc/hosts
-
方式2: 直接操作 kernel 虚拟 fs 来访问容器进程的文件
cat /proc/<containerPid>/root/etc/hosts
- 或:
echo '127.0.0.1 example.com' >> /proc/<containerPid>/root/etc/hosts
-
方式3: 基于 docker 默认使用
overlay2FS
原理,可获取容器的MergedDir
(即对应宿主机上的实际目录).docker ps -q | xargs docker inspect --format '{{.State.Pid}}, {{.Id}}, {{.Name}}, {{.GraphDriver.Data.MergedDir}}'
2. Docker 与 containerd 及 ctr、runc 关联
2.1 命令行区别
docker | ctr | crictl |
---|---|---|
docker version | ctr version | crictl version |
docker images | ctr -n k8s.io images ls | crictl img |
docker pull nginx:latest | ctr i pull docker.io/library/nginx:latest | crictl pull docker.io/library/redis |
docker push registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/pause:3.2 | ctr -n k8s.io images push --user myuser1:mypassword1 registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/pause:3.2 k8s.gcr.io/pause:3.2 | |
docker run -d –name nginx-name nginx:latest | ctr run -d docker.io/library/nginx:latest nginx-name | crictl run [command options] container-config.[json yaml] pod-config.[json yaml] |
docker ps | ctr c ls / ctr t ls | crictl ps |
docker inspect nginx-name | ctr c info nginx-name | crictl inspect |
docker stop nginx-name | ctr t kill nginx-name | crictl stop |
docker start nginx-name | crictl start | |
docker rm nginx-name | ctr c rm nginx-name | |
docker exec -it nginx-name bash | ctr t exec -t –exec-id=”foo” nginx-name sh | crictl exec -i -t [containerId] ls |
docker logs -f --tail 10 wordpress | crictl logs --tail=10 [containerId] |
2.2 查看 docker / containerd / runc 等调用关系
-
docker --(rest)--> dockerd --(grpc/ipc)--> containerd --(ipc)--> containerd-shim-runc-v2- --(ipc)--> runc (真正管理 namespaces/cgroups 等)
-
验证:
# I. 在宿主机上查看相关进程
> ps -ef | grep container
root 954 1 0 Sep30 ? 00:05:22 /usr/bin/containerd
root 1188 1 0 Sep30 ? 00:01:42 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root 1572 1188 0 Sep30 ? 00:00:01 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 10080 -container-ip 172.17.0.2 -container-port 80
root 1577 1188 0 Sep30 ? 00:00:01 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 10080 -container-ip 172.17.0.2 -container-port 80
root 1588 1 0 Sep30 ? 00:00:33 /usr/lib/docker-current/containerd-shim-runc-v2 -namespace moby -id aefcf5a28fb3db7057ca1b59b489255024e333eedb497058e5163a109ea2cbc9 -address /run/containerd/containerd.sock
root 1685964 1675561 0 18:19 pts/0 00:00:00 grep --color=auto container
# II. 使用 docker 查看容器列表
> docker ps | head
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
aefcf5a28fb3 wordpress:latest "docker-entrypoint.s…" 5 months ago Up 12 days 0.0.0.0:10080->80/tcp, :::10080->80/tcp wordpress
# III. 使用 ctr 查看容器列表
> ctr -n moby c ls | head
CONTAINER IMAGE RUNTIME
aefcf5a28fb3db7057ca1b59b489255024e333eedb497058e5163a109ea2cbc9 - io.containerd.runc.v2
# IV. 使用 runc 查看容器列表
> runc --root /run/docker/runtime-runc/moby/ list | head
ID PID STATUS BUNDLE CREATED OWNER
aefcf5a28fb3db7057ca1b59b489255024e333eedb497058e5163a109ea2cbc9 1609 running /run/containerd/io.containerd.runtime.v2.task/moby/aefcf5a28fb3db7057ca1b59b489255024e333eedb497058e5163a109ea2cbc9 2021-09-30T09:34:23.492183284Z root
# V. 使用 docker 查看镜像列表
> docker images | head
REPOSITORY TAG IMAGE ID CREATED SIZE
sonatype/nexus3 latest a0d390a200d2 7 weeks ago 655MB
k8s.gcr.io/kube-apiserver v1.21.3 3d174f00aa39 2 months ago 126MB
k8s.gcr.io/kube-scheduler v1.21.3 6be0dc1302e3 2 months ago 50.6MB
k8s.gcr.io/kube-proxy v1.21.3 adb2816ea823 2 months ago 103MB
k8s.gcr.io/kube-controller-manager v1.21.3 bc2bb319a703 2 months ago 120MB
gcr.io/istio-testing/build-tools release-1.9-2021-07-13T16-45-56 82ed67ca3328 3 months ago 3.24GB
wordpress latest c01290f258b3 5 months ago 550MB
nextcloud latest 6d746b3f79cd 6 months ago 868MB
gitlab/gitlab-ce latest 9b80fc55d250 6 months ago 2.2GB
# VI. 使用 ctr 查看镜像列表
# 注: 默认这里显示不了docker镜像,但如果通过 ctr 导入 docker 导出的镜像是可以的,原因是他们存放镜像的目录不同
> ctr -n moby i ls
REF TYPE DIGEST SIZE PLATFORMS LABELS
# VII. 使用 find 找到 docker 中 k8s.gcr.io/kube-apiserver 镜像
> find /var/lib/docker/image/overlay2/imagedb/content/sha256/ | grep 3d174f00aa39
/var/lib/docker/image/overlay2/imagedb/content/sha256/3d174f00aa39eb8552a9596610d87ae90e0ad51ad5282bd5dae421ca7d4a0b80
# VII. 查看 containerd 相关进程树
> systemctl status | grep -A 5 -B 3 containerd
...
├─init.scope
│ └─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 18
└─system.slice
├─rngd.service
...
│ └─755 /usr/bin/lsmd -d
├─coredns.service
│ └─925 /opt/apps/coredns-1.7.0/coredns_linux_amd64_v1.7.0 -conf /opt/apps/coredns-1.7.0/Corefile
├─containerd.service
│ ├─ 954 /usr/bin/containerd
│ └─1588 /usr/lib/docker-current/containerd-shim-runc-v2 -namespace moby -id aefcf5a28fb3db7057ca1b59b489255024e333eedb497058e5163a109ea2cbc9 -address /run/containerd/containerd.sock
├─nginx.service
...
│ ├─1259 nginx: worker process
│ └─1260 nginx: worker process
├─docker.service
│ ├─1188 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
│ ├─1572 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 10080 -container-ip 172.17.0.2 -container-port 80
│ └─1577 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 10080 -container-ip 172.17.0.2 -container-port 80
...
....
通过以上分析可知,docker 的容器最终是由 containerd 调用的 containerd-shim-runc-v2 再调用的 runc,而 containerd-shim-runc-v2 的父进程为 1,所以可以在线升级 containerd 也不会影响运行中的容器。