Kubernetes

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 也不会影响运行中的容器。

留言

您的电子邮箱地址不会被公开。