Atlantis
GitHub 切换暗/亮/自动模式 切换暗/亮/自动模式 切换暗/亮/自动模式 返回首页

2. Cgroups与Namespaces

Created At 2024-10-09 Updated on 2025-10-25

1. 前言

翻开 Docker 的发展史,我们可以看到它最初是基于 LXC 开发的,而 LXC 的核心是 Cgroups(2008 年引入于 Linux 2.6.24)与 Namespaces(2004 年引入于 Linux 2.6.0),这两个技术的引入为后来的容器技术发展奠定了基础。

日常使用 Linux 时,systemd 和容器运行时封装好了一切,屏蔽了底层细节,我们很少需要直接操作 Cgroups 与 Namespaces,下面会搭建测试环境,探索下 Linux 以及容器是如何使用他们的。

2. Cgroups

2.1 什么是Cgroups

Cgroups(Linux control groups)是 Linux 中用于限制、监控和隔离进程组资源使用的技术,它允许系统管理员和开发者对系统资源进行细粒度控制,以提高系统的性能和稳定性。在容器技术中,主要使用到 Cgroups 的资源限制和监控的功能。

Cgroups 子系统是 Linux 内核中用于资源管理的模块,每个子系统提供特定的资源控制和监控功能,从 man 手册可以查到目前存在以下 13 个子系统:

子系统 描述 引入版本
cpu 限制和监控 CPU 使用 2.6.24
cpuacct 监控 CPU 使用情况 2.6.24
cpuset 将进程绑定到指定的 CPU 和 NUMA 节点 2.6.24
memory 报告和限制进程、内核和交换内存的使用 2.6.25
devices 控制进程对设备的访问权限 2.6.26
freezer 暂停和恢复 Cgroup 中的所有进程 2.6.28
net_cls 为网络数据包指定类标识,用于流量控制 2.6.29
blkio 控制和限制对指定块设备的访问 2.6.33
perf_event 允许对 Cgroup 中的一组进程进行性能监控 2.6.39
net_prio 为 Cgroup 指定每个网络接口的优先级 3.3
hugetlb 限制 Cgroup 中大页的使用 3.5
pids 限制 Cgroup 中及其后代的进程数量 4.3
rdma 限制 per Cgroup 使用 RDMA/IB 特定资源 4.11

一个典型的 Docker 容器会用到以下子系统:cpu、cpuacct、memory、blkio、devices、freezer、pids、net_cls 和 net_prio。

Cgroups 有两个主要版本 v1 和 v2,不过 v2 只实现了 v1 的部分控制器,未做到完全向后兼容。

下面是 Debian 10 系统上的 Cgroups,可以看到 v1 与 v2 版本共存:

debian@debian:~$ uname -sr
Linux 4.19.0-27-amd64
debian@debian:~$ mount|grep -i cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)

下面是 Debian 12 系统上的 Cgroups,可以看到只有 v2 版本,/sys/fs/cgroup 的类型为 cgroup2,v2 引入了统一的层级结构,所有子系统共享同一个层级,减少了配置的复杂性:

➜  ~ uname -sr
Linux 6.1.0-17-amd64
➜  ~ mount|grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)

我们可以通过 systemd 来使用 Cgroups,屏蔽 v1 和 v2 的差异,以 Docker 为例:

  1. systemd 不存在:会直接使用 cgroupfs 作为 Cgroup Driver
  2. systemd 存在:
    1. 若系统默认启用 Cgroups v2,则默认使用 systemd 作为 Cgroup Driver
    2. 其他情况,如系统只支持 Cgroups v1 或者 v1、v2 共存,但默认启用 v1,则继续使用 cgroupfs 作为 Cgroup Driver,但可以修改 dockerd 启动参数来使用 systemd

systemd 提供了一个统一接口来管理 Cgroups,可以让容器更好地与系统服务整合,翻看 K8s 的文档也能发现,自 v1.22 及以后版本开始,当使用 kubeadm 创建集群时,默认使用 systemd 作为 Cgroup Driver。

接下来看看容器是如何使用 Cgroups 的。

2.2 Docker

使用 Debian 12 操作系统,内核版本为 6.1.x,默认启用 Cgroups v2。

首先安装 docker-ce,启动后能看到它使用 systemd 作为 Cgroup Driver:

➜  ~ docker info|grep Cgroup
 Cgroup Driver: systemd
 Cgroup Version: 2

启动一个运行 nginx 的 容器,这里将 nginx 进程绑定到 0 和 1 号核心,限制最多使用 1 个 CPU,内存最大不超过 512MB:

docker run -d --name nginx --cpus 1 --cpuset-cpus 0,1 --memory=512m -p 80:80 nginx:alpine

如果已经安装了 cgroup-tools,可以使用 lscgroup 命令检查容器的 Cgroup:

➜  ~ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                               NAMES
faa9e4f179cb   nginx:alpine   "/docker-entrypoint.…"   6 seconds ago   Up 6 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp   nginx
➜  ~ lscgroup|grep faa9e4f179cb
cpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/system.slice/docker-faa9e4f179cb19501d718adf356e01dc02b41fde1147fc61c6ddcaa12d0a44f1.scope

我们也可以通过容器的主进程检查它所属的 Cgroup:

➜  ~ docker inspect -f '{{.State.Pid}}' faa9e4f179cb
2386
➜  ~ cat /proc/2386/cgroup
0::/system.slice/docker-faa9e4f179cb19501d718adf356e01dc02b41fde1147fc61c6ddcaa12d0a44f1.scope

可以看到这个容器的 Cgroup 存放在 system.slice 下,对应的文件目录是:

/sys/fs/cgroup/system.slice/docker-faa9e4f179cb19501d718adf356e01dc02b41fde1147fc61c6ddcaa12d0a44f1.scope

进入这个目录后检查 CPU 和内存参数,可以看到与创建容器时设置的一致:

➜  docker-faa9e4f179cb19501d718adf356e01dc02b41fde1147fc61c6ddcaa12d0a44f1.scope cat cpu.max
100000 100000
➜  docker-faa9e4f179cb19501d718adf356e01dc02b41fde1147fc61c6ddcaa12d0a44f1.scope cat cpuset.cpus
0-1
➜  docker-faa9e4f179cb19501d718adf356e01dc02b41fde1147fc61c6ddcaa12d0a44f1.scope cat memory.max
536870912

2.3 K8s

这里使用 K8s v1.31 与 Containerd v1.6.36,同样使用 systemd 作为 Cgroup Driver。

启动一个运行 nginx 的 容器组:

kubectl run ngx --image=nginx:alpine --port=80

上面未设置 requests 和 limits,容器组的 QoS 级别为 BestEffort,然后检查它的 Cgroup:

➜  ~ kubectl get pod ngx -o yaml|grep containerd
  - containerID: containerd://05d1e160f9d30479f79c3d24868401618d0da9bc15d72a8baa63f9a39120421e
➜  ~ lscgroup|grep 05d1e160f9d3
cpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod88a39d19_76de_4f79_b978_e4bd3ff54404.slice/cri-containerd-05d1e160f9d30479f79c3d24868401618d0da9bc15d72a8baa63f9a39120421e.scope
➜  ~ cat /sys/fs/cgroup/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod88a39d19_76de_4f79_b978_e4bd3ff54404.slice/cri-containerd-05d1e160f9d30479f79c3d24868401618d0da9bc15d72a8baa63f9a39120421e.scope/cgroup.procs
6343
6379
6380
6381
6382
➜  ~ nerdctl -n k8s.io inspect -f '{{.State.Pid}}' 05d1e160f9d3
6343
➜  ~ cat /proc/6343/cgroup
0::/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod88a39d19_76de_4f79_b978_e4bd3ff54404.slice/cri-containerd-05d1e160f9d30479f79c3d24868401618d0da9bc15d72a8baa63f9a39120421e.scope
➜  ~ kubectl get pod ngx -o yaml|grep uid
  uid: 88a39d19-76de-4f79-b978-e4bd3ff54404
➜  ~ lscgroup|grep 88a39d19
cpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod88a39d19_76de_4f79_b978_e4bd3ff54404.slice
cpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod88a39d19_76de_4f79_b978_e4bd3ff54404.slice/cri-containerd-040ab2b7c8ab3bc2964e461581f1444a2a29df541a950532a776842421fb9add.scope
cpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod88a39d19_76de_4f79_b978_e4bd3ff54404.slice/cri-containerd-05d1e160f9d30479f79c3d24868401618d0da9bc15d72a8baa63f9a39120421e.scope
➜  ~ systemd-cgls /kubepods.slice
Control group /kubepods.slice:
├─kubepods-burstable.slice (#3239)
│ → user.invocation_id: fe43e4ed5b12463c94591015ab037f23
│ → trusted.invocation_id: fe43e4ed5b12463c94591015ab037f23
│ ├─kubepods-burstable-pod07e0f3f8_4a27_4fbd_814c_28a997d6e151.slice (#4263)
│ │ → user.invocation_id: c8e6f2aac8b042fe9b82552ac0d02b2b
│ │ → trusted.invocation_id: c8e6f2aac8b042fe9b82552ac0d02b2b
│ │ ├─cri-containerd-00a0dc915f4c7d197be9ad290a2d1d5cd15c23d4e71a6cb1f60c2209959355fb.scope … (#5191)
│ │ │ → user.invocation_id: da69c31d05f3483685dc016c89defb58
│ │ │ → trusted.invocation_id: da69c31d05f3483685dc016c89defb58
│ │ │ → user.delegate: 1
│ │ │ → trusted.delegate: 1
│ │ │ └─2242 /coredns -conf /etc/coredns/Corefile
│ │ └─cri-containerd-799a6b24f236c62b22ecc2b79652969e71a6ae38de3f7fec356576750c8fc156.scope … (#5120)
│ │   → user.invocation_id: 8a8e325216e744beaec17b59df74cee5
│ │   → trusted.invocation_id: 8a8e325216e744beaec17b59df74cee5
│ │   → user.delegate: 1
│ │   → trusted.delegate: 1
│ │   └─2212 /pause
...

在上面的操作中,我们首先找到 nginx 容器,然后查看它归属的 Cgroup 以及 Pod 归属的 Cgroup,可知:

  1. nginx 容器关联的 Cgroup 下存在多个进程,其中 6343 为主进程,都绑定到了同一个 Cgroup
  2. K8s 的顶层 Cgroup 为 kubepods.slice,然后根据 QoS 划分出 BestEffort、Burstable 与 Guarantee 三种类型的二层 Cgroup,其中 Guarantee 类型的 Pod 直接会创建在 kubepods.slice 下
  3. 容器组根据 QoS 类型创建在对应父级 Cgroup 下,然后在里面创建容器的 Cgroup
  4. 我们创建的 Pod 中存在两个容器:pause 容器与 nginx 容器

Cgroups 的资源限制是具有继承性的,利用层级结构可以让子 Cgroup 继承父 Cgroup 的限制,但子 Cgroup 只能进一步限制资源,不能增加资源,比如父 Cgroup 限制 CPU 使用为 50%,子 Cgroup 即使设置了 100% 限制,也最多只能使用 50%。

假如我们在 default 命名空间创建了以下的 LimitRange:

apiVersion: v1
kind: LimitRange
metadata:
  name: limit-range
spec:
  limits:
  - type: Container
    default:
      cpu: 2000m
      memory: 4Gi
      ephemeral-storage: 2Gi
    defaultRequest:
      cpu: 200m
      memory: 256Mi
      ephemeral-storage: 500Mi
    max:
      cpu: "4"
      memory: 8Gi
      ephemeral-storage: 4Gi
    min:
      cpu: 100m
      memory: 128Mi
      ephemeral-storage: 250Mi
  - type: Pod
    max:
      cpu: "4"
      memory: 8Gi
      ephemeral-storage: 4Gi
    min:
      cpu: 200m
      memory: 256Mi
      ephemeral-storage: 500Mi

那么创建容器组后,可以查看到 requests 和 limits 如下:

...
spec:
  containers:
  - image: nginx:alpine
    imagePullPolicy: IfNotPresent
    name: ngx
    ports:
    - containerPort: 80
      protocol: TCP
    resources:
      limits:
        cpu: "2"
        ephemeral-storage: 2Gi
        memory: 4Gi
      requests:
        cpu: 200m
        ephemeral-storage: 500Mi
        memory: 256Mi
...

但查看 nginx 容器的 CPU 限制,就会发现数值为:200000 100000,对应 Container 中 min 设置的 100m 与 default 设置的 2000m,也体现了上面提到的 子 Cgroup 只能进一步限制资源,不能增加资源 的规则。

如果我们将 Container 中的几个限制条件参数设置为一致,那么创建出的容器组 QoS 全部都是 Guarantee,将 LimitRange 的 CPU 和内存固定为 1 和 2Gi 后,创建一个新的 Pod,如下:

apiVersion: v1
kind: LimitRange
metadata:
  name: limit-range
spec:
  limits:
  - type: Container
    default:
      cpu: "1"
      memory: 2Gi
    defaultRequest:
      cpu: "1"
      memory: 2Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: two-containers
spec:
  containers:
  - name: nginx
    image: nginx:alpine
  - name: redis
    image: redis:7

应用 yaml 后能看到容器组的 Cgroup 位于 /kubepods.slice 下:

➜  ~ kubectl get pod two-containers -o yaml|grep uid
  uid: ceba48fa-8212-4c49-b7a4-41a726e1d799
➜  ~ lscgroup|grep ceba48fa
cpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/kubepods.slice/kubepods-podceba48fa_8212_4c49_b7a4_41a726e1d799.slice
cpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/kubepods.slice/kubepods-podceba48fa_8212_4c49_b7a4_41a726e1d799.slice/cri-containerd-220c3e153f38c6e0bd967c58392837c1d8d076edd860895034924caf04b07027.scope
cpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/kubepods.slice/kubepods-podceba48fa_8212_4c49_b7a4_41a726e1d799.slice/cri-containerd-a9ed8eebe202b5bd2cc14a2adfd8400aed064c1ed30f8226cd382570203ef4a6.scope
cpuset,cpu,io,memory,hugetlb,pids,rdma,misc:/kubepods.slice/kubepods-podceba48fa_8212_4c49_b7a4_41a726e1d799.slice/cri-containerd-6f790eead5fadeee3405deca799fff96f6878369074e4096e75fa74c6b98370d.scope
➜  ~ systemd-cgls /kubepods.slice/kubepods-podceba48fa_8212_4c49_b7a4_41a726e1d799.slice
Control group /kubepods.slice/kubepods-podceba48fa_8212_4c49_b7a4_41a726e1d799.slice:
├─cri-containerd-220c3e153f38c6e0bd967c58392837c1d8d076edd860895034924caf04b07027.scope … (#7776)
│ → user.invocation_id: 0b05d6c2da6b4d1f9592ea6363408461
│ → trusted.invocation_id: 0b05d6c2da6b4d1f9592ea6363408461
│ → user.delegate: 1
│ → trusted.delegate: 1
│ ├─28941 nginx: master process nginx -g daemon off;
│ ├─28988 nginx: worker process
│ ├─28989 nginx: worker process
│ ├─28990 nginx: worker process
│ └─28991 nginx: worker process
├─cri-containerd-a9ed8eebe202b5bd2cc14a2adfd8400aed064c1ed30f8226cd382570203ef4a6.scope … (#7847)
│ → user.invocation_id: 9fa29ad400714872b171ef4ebec872a9
│ → trusted.invocation_id: 9fa29ad400714872b171ef4ebec872a9
│ → user.delegate: 1
│ → trusted.delegate: 1
│ └─28998 redis-server *:6379
└─cri-containerd-6f790eead5fadeee3405deca799fff96f6878369074e4096e75fa74c6b98370d.scope … (#7705)
  → user.invocation_id: 50743862d69446b6b87b348fd5353f1f
  → trusted.invocation_id: 50743862d69446b6b87b348fd5353f1f
  → user.delegate: 1
  → trusted.delegate: 1
  └─28911 /pause

3. Namespaces

3.1 什么是Namespaces

Namespaces 是 Linux 内核的一项重要特性,我们可以使用它为一组进程创建一个独立的资源视图,让不同进程可以看到不同的系统资源视图,从而实现了资源的隔离。

Linux 目前实现了 8 种类型的 Namespaces,如下:

Namespace 类型 引入版本 包含资源
Mount 2.4.19 (2002) 文件系统挂载点
UTS 2.6.19 (2006) 主机名和 NIS 域名
IPC 2.6.19 (2006) System V IPC 对象,POSIX 消息队列
PID 2.6.24 (2008) 进程 ID
Network 2.6.29 (2009) 网络设备、IP 地址、IP 路由表、防火墙规则、/proc/net 目录、端口号
User 3.8 (2013) 用户 ID、组 ID
Cgroup 4.6 (2016) Cgroup 根目录
Time 5.6 (2020) 系统和进程的时间

一个典型的 Docker 容器会用到上述的所有 Namespaces,但我觉得最核心的命名空间还是 Mount,它提供了完整的文件系统隔离,实现了容器镜像的可移植性。在运行容器时,除了容器根目录外,其余资源都可以与宿主机共享。

Namespaces 的功能看似简单,实际上直到 Linux 3.8 版本引入完整的 User Namespace 支持后,才为容器革命铺平道路,接下来会分别在宿主机、Docker 和 K8s 中探索下 Namespaces 的用法。

3.2 常用工具

工欲善其事,必先利其器。

3.2.1 unshare

unshare 命令允许进程脱离共享的命名空间,并在新的命名空间中运行程序,下面利用一个 alpine 3.20 系统的 rootfs 模拟 Docker 使用宿主机网络运行 alpine 的场景:

wget -c https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-minirootfs-3.20.3-x86_64.tar.gz
mkdir alpine-3.20 && tar -zxf alpine-minirootfs-3.20.3-x86_64.tar.gz -C alpine-3.20
cp -f /etc/resolv.conf alpine-3.20/etc/resolv.conf
unshare --mount --uts --ipc --pid --mount-proc --fork --root=$PWD/alpine-3.20 --wd=/root /bin/sh

然后在隔离出的环境中安装 neofetch 打印当前系统信息,如下:

alt text

3.2.2 lsns

lsns 用于列出系统中的 namespace,我们新开一个终端执行 lsns,就可以看到上面创建的进程,如下:

➜  ~ lsns
        NS TYPE   NPROCS   PID USER             COMMAND
4026531834 time      144     1 root             /sbin/init
4026531835 cgroup    144     1 root             /sbin/init
4026531836 pid       143     1 root             /sbin/init
4026531837 user      144     1 root             /sbin/init
4026531838 uts       139     1 root             /sbin/init
4026531839 ipc       142     1 root             /sbin/init
4026531840 net       144     1 root             /sbin/init
4026531841 mnt       138     1 root             /sbin/init
4026532370 mnt         1   510 root             ├─/lib/systemd/systemd-udevd
4026532371 uts         1   510 root             ├─/lib/systemd/systemd-udevd
4026532468 mnt         1   757 systemd-timesync ├─/lib/systemd/systemd-timesyncd
4026532469 uts         1   757 systemd-timesync ├─/lib/systemd/systemd-timesyncd
4026532543 uts         1   863 root             ├─/lib/systemd/systemd-logind
4026532544 mnt         1   863 root             └─/lib/systemd/systemd-logind
4026531862 mnt         1    62 root             kdevtmpfs
4026532504 mnt         2  3657 root             unshare --mount --uts --ipc --pid --mount-proc --fork --root=/root/tmp/alpine-3.20 --wd=/root /bin/sh
4026532505 uts         2  3657 root             unshare --mount --uts --ipc --pid --mount-proc --fork --root=/root/tmp/alpine-3.20 --wd=/root /bin/sh
4026532506 ipc         2  3657 root             unshare --mount --uts --ipc --pid --mount-proc --fork --root=/root/tmp/alpine-3.20 --wd=/root /bin/sh
4026532507 pid         1  3658 root             └─/bin/sh

3.2.3 nsenter

nsenter 用于进入现有的 namespace 并在其中执行命令,以上面 unshare 创建的为例,执行以下命令进入 sh 进程所在的全部命名空间:

➜  tmp nsenter -t 3658 --all --root=$PWD/alpine-3.20 --wd=/root /bin/sh
 # cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.20.3
PRETTY_NAME="Alpine Linux v3.20"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"

利用 unshare 创建出的进程与 Docker 容器中的进程还是存在一些差异,下面是用 Docker 运行一个 alpine 容器,然后使用 nsenter 进入其中执行命令,如下:

➜  ~ docker run -d --name alpine alpine:3.20 tail -f /dev/null
22711ef0ada28991aac78d607ae73d023f0aa085224938d7183fe166d71aa950
➜  ~ docker inspect -f '{{.State.Pid}}' alpine
4582
➜  ~ nsenter -t 4582 --all /bin/sh
/ # apk add neofetch
fetch https://dl-cdn.alpinelinux.org/alpine/v3.20/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.20/community/x86_64/APKINDEX.tar.gz
(1/5) Installing ncurses-terminfo-base (6.4_p20240420-r1)
(2/5) Installing libncursesw (6.4_p20240420-r1)
(3/5) Installing readline (8.2.10-r0)
(4/5) Installing bash (5.2.26-r0)
Executing bash-5.2.26-r0.post-install
(5/5) Installing neofetch (7.1.0-r1)
Executing busybox-1.36.1-r29.trigger
OK: 10 MiB in 19 packages
/ # neofetch
       .hddddddddddddddddddddddh.          root@22711ef0ada2
      :dddddddddddddddddddddddddd:         -----------------
     /dddddddddddddddddddddddddddd/        OS: Alpine Linux v3.20 x86_64
    +dddddddddddddddddddddddddddddd+       Host: KVM/QEMU (Standard PC (Q35 + ICH9, 2009) pc-q35-9.0)
  `sdddddddddddddddddddddddddddddddds`     Kernel: 6.1.0-18-amd64
 `ydddddddddddd++hdddddddddddddddddddy`    Uptime: 8 hours, 8 mins
.hddddddddddd+`  `+ddddh:-sdddddddddddh.   Packages: 19 (apk)
hdddddddddd+`      `+y:    .sddddddddddh   Shell: zsh /usr/bin/neofetch: line 1669: /usr/bin/zsh: No such file or directory
ddddddddh+`   `//`   `.`     -sddddddddd   Resolution: 1280x800
ddddddh+`   `/hddh/`   `:s-    -sddddddd   Terminal: /dev/pts/0
ddddh+`   `/+/dddddh/`   `+s-    -sddddd   CPU: Intel Xeon E5-2676 v4 (8) @ 2.394GHz
ddd+`   `/o` :dddddddh/`   `oy-    .yddd   Memory: 243MiB / 32058MiB
hdddyo+ohddyosdddddddddho+oydddy++ohdddh
.hddddddddddddddddddddddddddddddddddddh.
 `yddddddddddddddddddddddddddddddddddy`
  `sdddddddddddddddddddddddddddddddds`
    +dddddddddddddddddddddddddddddd+
     /dddddddddddddddddddddddddddd/
      :dddddddddddddddddddddddddd:
       .hddddddddddddddddddddddh.

/ #

可以看到与我们执行 docker exec 的效果类似,能够完全使用目标进程的所有命名空间。

如果我们运行的是一个特权容器并与宿主机共享 PID 命名空间,则可以通过在容器中执行 nsenter -t 1 --all /bin/sh 登录宿主机环境,下面是用一个 Debian 12 容器做测试:

➜  ~ docker run -d --name debian --pid=host --privileged=true debian:12 tail -f /dev/null
3f15aa52e4e19063e72ad602be12cc34a6bb9beeec07c5cf47e9db8adf9fee4a
➜  ~ docker exec -it debian /bin/bash
root@3f15aa52e4e1:/# nsenter -t 1 --all /bin/zsh
➜  / docker ps
CONTAINER ID   IMAGE       COMMAND               CREATED              STATUS              PORTS     NAMES
3f15aa52e4e1   debian:12   "tail -f /dev/null"   About a minute ago   Up About a minute             debian

可以看到我们首先进入了容器内,然后使用 nsenter 进入 1 号进程的所有命名空间执行 zsh,切换到宿主机环境,最后执行 docker ps 查看到刚才创建的容器。

在 K8s 中就可以用这个办法通过 kubectl 登录任意节点,我们也可以制作一个命令工具齐全的容器镜像,然后以特权模式运行并共享宿主机命名空间,这样即使宿主机上缺失调试工具,也可以通过特权容器调试目标容器。

3.3 Docker

首先使用 Docker 运行一个 Nginx 容器,并获取到进程 ID:

➜  ~ docker run -d --name nginx -p 80:80 nginx:alpine
fcc566035e9cdca819f62a982016574c7738d2d5a223236795a791b5752f006d
➜  ~ docker inspect -f '{{.State.Pid}}' nginx
5670

拿到进程 ID 后,可以在宿主机上检查容器使用的命名空间,如下:

# 访问 proc 目录查看 ns
➜  ~ ls -lh /proc/5670/ns
total 0
lrwxrwxrwx 1 root root 0 Oct 11 22:27 cgroup -> 'cgroup:[4026532591]'
lrwxrwxrwx 1 root root 0 Oct 11 22:27 ipc -> 'ipc:[4026532509]'
lrwxrwxrwx 1 root root 0 Oct 11 22:27 mnt -> 'mnt:[4026532506]'
lrwxrwxrwx 1 root root 0 Oct 11 22:26 net -> 'net:[4026532511]'
lrwxrwxrwx 1 root root 0 Oct 11 22:27 pid -> 'pid:[4026532510]'
lrwxrwxrwx 1 root root 0 Oct 11 22:28 pid_for_children -> 'pid:[4026532510]'
lrwxrwxrwx 1 root root 0 Oct 11 22:27 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Oct 11 22:28 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Oct 11 22:27 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Oct 11 22:27 uts -> 'uts:[4026532507]'
# 使用 lsns 列出命名空间,可以看到这里共享了宿主机的 time 和 user 命名空间
➜  ~ lsns -p 5670
        NS TYPE   NPROCS   PID USER COMMAND
4026531834 time      152     1 root /sbin/init
4026531837 user      152     1 root /sbin/init
4026532506 mnt         9  5670 root nginx: master process nginx -g daemon off;
4026532507 uts         9  5670 root nginx: master process nginx -g daemon off;
4026532509 ipc         9  5670 root nginx: master process nginx -g daemon off;
4026532510 pid         9  5670 root nginx: master process nginx -g daemon off;
4026532511 net         9  5670 root nginx: master process nginx -g daemon off;
4026532591 cgroup      9  5670 root nginx: master process nginx -g daemon off;

接下来可以使用 nsenter 进入目标进程的指定命名空间执行命令,比如我们需要使用宿主机命令行工具,在容器网络内执行一些操作,就可以执行以下命令:

➜  ~ nsenter -t 5670 --net /bin/zsh
➜  ~ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
16: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
➜  ~ ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.2

这里只切换了网络命名空间,其余命名空间涉及的资源沿用宿主机。

3.4 K8s

K8s 中容器的情况与 Docker 容器类似,不过 Pod 内的命名空间共享情况会更复杂。

接下来实践下前面提到的:使用一个工具期权的特权容器远程调试集群内的应用容器。

测试使用到的 yaml如下:

apiVersion: v1
kind: Pod
metadata:
  name: two-containers
spec:
  containers:
  - name: nginx
    image: nginx:alpine
  - name: redis
    image: redis:7
---
apiVersion: v1
kind: Pod
metadata:
  name: debian
spec:
  hostNetwork: true
  hostPID: true
  hostIPC: true
  containers:
  - name: debian
    image: debian:12
    command: ["tail", "-f", "/dev/null"]
    securityContext:
      privileged: true
    volumeMounts:
    - name: containerd-sock
      mountPath: /run/containerd/containerd.sock
  volumes:
  - name: containerd-sock
    hostPath:
      path: /run/containerd/containerd.sock
      type: Socket

我们会在一个双节点集群的 Master上执行所有操作,下面准备测试环境:

# 暂时设置 master-0 不可调度
➜  ~ kubectl cordon master-0
node/master-0 cordoned
# 检查节点
➜  ~ kubectl get node
NAME       STATUS                     ROLES           AGE   VERSION
master-0   Ready,SchedulingDisabled   control-plane   32h   v1.31.1
worker-0   Ready                      <none>          32h   v1.31.1
# 创建一个应用 Pod与特权 Pod,调度到 worker 上
➜  ~ kubectl apply -f pods.yaml
pod/two-containers created
pod/debian created
➜  ~ kubectl get pod -o wide
NAME             READY   STATUS    RESTARTS   AGE   IP              NODE       NOMINATED NODE   READINESS GATES
debian           1/1     Running   0          4s    192.168.123.6   worker-0   <none>           <none>
two-containers   2/2     Running   0          4s    10.244.1.27     worker-0   <none>           <none>

然后执行 kubectl 登录 debian 容器,执行测试:

# 远程登录容器
➜  ~ kubectl exec -it debian -- /bin/bash
root@worker-0:/# 
...
# 下载 nerdctl、安装常用工具
...
# 在 debian 容器内访问 containerd 列出所有容器
root@worker-0:~# nerdctl -n k8s.io ps
CONTAINER ID    IMAGE                                 COMMAND                   CREATED          STATUS    PORTS    NAMES
03499be06661    docker.io/library/debian:12           "tail -f /dev/null"       4 minutes ago    Up                 k8s://default/debian/debian
0bd2ccc1284e    registry.k8s.io/kube-proxy:v1.31.1    "/usr/local/bin/kube…"    9 hours ago      Up                 k8s://kube-system/kube-proxy-vkrd9/kube-proxy
17109bd0841e    docker.io/library/nginx:alpine        "/docker-entrypoint.…"    4 minutes ago    Up                 k8s://default/two-containers/nginx
2aa5b308785a    registry.k8s.io/pause:3.6             "/pause"                  4 minutes ago    Up                 k8s://default/debian
3058a5a5ea1b    docker.io/flannel/flannel:v0.25.7     "/opt/bin/flanneld -…"    9 hours ago      Up                 k8s://kube-flannel/kube-flannel-ds-4l4ch/kube-flannel
394a1b4f0750    registry.k8s.io/pause:3.6             "/pause"                  4 minutes ago    Up                 k8s://default/two-containers
8b8c6d91b6ae    registry.k8s.io/pause:3.6             "/pause"                  9 hours ago      Up                 k8s://kube-flannel/kube-flannel-ds-4l4ch
f8d17b8a586e    registry.k8s.io/pause:3.6             "/pause"                  9 hours ago      Up                 k8s://kube-system/kube-proxy-vkrd9
f9d4aa198255    docker.io/library/redis:7             "docker-entrypoint.s…"    4 minutes ago    Up                 k8s://default/two-containers/redis
# 切换到 1 号进程命名空间执行 zsh
root@worker-0:~# nsenter -t 1 --all /bin/zsh
➜  / cd /root
# 使用 nerdctl 获取容器 ID,检查每个进程的命名空间使用情况
➜  ~ nerdctl -n k8s.io ps|grep two-containers
17109bd0841e    docker.io/library/nginx:alpine        "/docker-entrypoint.…"    11 minutes ago    Up                 k8s://default/two-containers/nginx
394a1b4f0750    registry.k8s.io/pause:3.6             "/pause"                  11 minutes ago    Up                 k8s://default/two-containers
f9d4aa198255    docker.io/library/redis:7             "docker-entrypoint.s…"    11 minutes ago    Up                 k8s://default/two-containers/redis
# 逐一检查进程 ID、获取命名空间共享情况
➜  ~ nerdctl -n k8s.io inspect -f '{{.State.Pid}}' 394a1b4f0750
130866
➜  ~ lsns -p 130866
        NS TYPE   NPROCS    PID USER  COMMAND
4026531834 time      152      1 root  /sbin/init
4026531835 cgroup    141      1 root  /sbin/init
4026531837 user      152      1 root  /sbin/init
4026532439 net        11 130866 65535 /pause
4026532503 mnt         1 130866 65535 /pause
4026532504 uts        11 130866 65535 /pause
4026532505 ipc        11 130866 65535 /pause
4026532506 pid         1 130866 65535 /pause
➜  ~ nerdctl -n k8s.io inspect -f '{{.State.Pid}}' 17109bd0841e
130925
➜  ~ lsns -p 130925
        NS TYPE   NPROCS    PID USER  COMMAND
4026531834 time      152      1 root  /sbin/init
4026531837 user      152      1 root  /sbin/init
4026532439 net        11 130866 65535 /pause
4026532504 uts        11 130866 65535 /pause
4026532505 ipc        11 130866 65535 /pause
4026532508 mnt         9 130925 root  nginx: master process nginx -g daemon off;
4026532509 pid         9 130925 root  nginx: master process nginx -g daemon off;
4026532510 cgroup      9 130925 root  nginx: master process nginx -g daemon off;
➜  ~ nerdctl -n k8s.io inspect -f '{{.State.Pid}}' f9d4aa198255
130994
➜  ~ lsns -p 130994
        NS TYPE   NPROCS    PID USER  COMMAND
4026531834 time      152      1 root  /sbin/init
4026531837 user      152      1 root  /sbin/init
4026532439 net        11 130866 65535 /pause
4026532504 uts        11 130866 65535 /pause
4026532505 ipc        11 130866 65535 /pause
4026532511 mnt         1 130994 999   redis-server *:6379
4026532512 pid         1 130994 999   redis-server *:6379
4026532513 cgroup      1 130994 999   redis-server *:6379

分析以上输出,我们可以知道:

  1. pause 进程共享宿主机的 time、cgroup 和 user 命名空间
  2. nginx 与 redis 共享 pause 的 net、uts、ipc 命名空间以及宿主机的 time、user 命名空间
  3. nginx 与 redis 的 mnt、pid 与 cgroup 命名空间都是独立的,对于每个容器来说,使用各自的容器镜像创建挂载点、容器内看到的主进程 ID 都为 1、容器资源占用独立统计