容器里装的是什么?
大家好,
这是我在 Dev.to 上的第一篇文章。
如今,大家都在使用容器化解决方案进行 CI/CD。
我一直很好奇,为什么容器能够始终作为独立的进程、CPU 和内存运行在宿主机上。
以下是我的一些发现:
流程
容器本质上就是应用了额外配置的普通 Linux 进程。启动以下 Redis 容器,以便我们了解其底层运行机制。
docker run -d --name=db redis:alpine
Docker 容器会启动一个名为 redis-server 的进程。在主机上,我们可以查看所有正在运行的进程,包括 Docker 启动的进程。
ps aux | grep redis-server
Docker 可以通过以下方式帮助我们识别有关进程的信息,包括 PID(进程 ID)和 PPID(父进程 ID):
docker top db
ps aux | grep <ppid>
查找父进程。很可能是 Containerd 进程。
命令 pstree 将列出所有子进程。使用以下命令查看 Docker 进程树。
pstree -c -p -A $(pgrep dockerd)
如您所见,从 Linux 的角度来看,这些都是标准进程,并且与我们系统上的其他进程具有相同的属性。
流程目录
Linux 只是一系列神奇的文件和内容,这使得探索和浏览以了解其底层运行机制变得很有趣,在某些情况下,还可以更改内容以查看结果。
每个进程的配置都定义在 /proc 目录中。如果您知道进程 ID,就可以找到对应的配置目录。
以下命令将列出 /proc 的所有内容,并存储 Redis PID 以供将来使用。
DBPID=$(pgrep redis-server)
echo Redis is $DBPID
ls /proc
每个进程都有自己的配置和安全设置,这些设置分别定义在不同的文件中。
ls /proc/$DBPID
cat /proc/$DBPID/environ
docker exec -it db env
命名空间
容器的基本组成部分之一是命名空间。命名空间的概念是限制哪些进程可以查看和访问系统的特定部分,例如其他网络接口或进程。
容器启动时,容器运行时(例如 Docker)会创建新的命名空间来隔离进程。通过在独立的 PID 命名空间中运行进程,它看起来就像是系统上唯一的进程。
可用的命名空间有:
挂载点 (mnt)
进程 ID (pid)
网络 (net)
进程间通信 (ipc)
UTS (hostnames)
用户 ID (user)
控制组 (cgroup)
更多信息请访问https://en.wikipedia.org/wiki/Linux_namespaces
Unshare 可以启动“独立”进程。
即使不使用 Docker 等运行时环境,进程仍然可以在其自身的命名空间内运行。Unshare 就是一个可以帮助实现这一目标的工具。
取消共享 --help
使用 unshare 命令可以启动一个进程并让它创建一个新的命名空间,例如 Pid。通过取消主机上 Pid 命名空间的共享,看起来就像 bash 提示符是机器上唯一运行的进程。
sudo unshare --fork --pid --mount-proc bash
ps
exit

共享命名空间会发生什么?
从底层来看,命名空间是磁盘上的 inode 位置。这使得进程可以共享/重用同一个命名空间,从而允许它们查看和交互。
列出所有命名空间
ls -lha /proc/$DBPID/ns/

另一个工具 NSEnter 用于将进程附加到现有命名空间。这对于调试目的非常有用。
nsenter --help
nsenter --target $DBPID --mount --uts --ipc --net --pid ps aux

使用 Docker,可以通过以下语法共享这些命名空间:。例如,以下命令会将 nginx 连接到 DB 命名空间。
docker run -d --name=web --net=container:db nginx:alpine
WEBPID=$(pgrep nginx | tail -n1)
echo nginx is $WEBPID
cat /proc/$WEBPID/cgroup
ls -lha /proc/$WEBPID/ns/
但是,这两个进程的网络命名空间都指向同一位置。
ls -lha /proc/$WEBPID/ns/ | grep net
ls -lha /proc/$DBPID/ns/ | grep net
克鲁特
容器化进程的一个重要组成部分是能够拥有独立于宿主机的不同文件。这样我们就可以基于系统上运行的不同操作系统创建不同的 Docker 镜像。
Chroot 允许进程从与父操作系统不同的根目录启动。这样,根目录中就可以出现不同的文件。
C组(对照组)
CGroups 限制进程可以消耗的资源量。这些 CGroups 值定义在 /proc 目录下的特定文件中。
要查看映射关系,请运行以下命令:
cat /proc/$DBPID/cgroup
ls /sys/fs/cgroup/

进程的 CPU 统计信息是什么?
CPU 统计信息和使用情况也存储在一个文件中!
cat /sys/fs/cgroup/cpu,cpuacct/docker/$DBID/cpuacct.stat
这里还定义了CPU份额限制。
cat /sys/fs/cgroup/cpu,cpuacct/docker/$DBID/cpu.shares

容器内存配置的所有 Docker cgroup 都存储在以下位置:
ls /sys/fs/cgroup/memory/docker/
DBID=$(docker ps --no-trunc | grep 'db' | awk '{print $1}')
WEBID=$(docker ps --no-trunc | grep 'nginx' | awk '{print $1}')
ls /sys/fs/cgroup/memory/docker/$DBID

如何配置 cgroups?
Docker 的一个特性是能够控制内存限制。这可以通过 cgroup 设置来实现。
默认情况下,容器对内存没有限制。我们可以通过 docker stats 命令查看这一点。
docker stats db --no-stream

内存限制存储在名为 memory.limit_in_bytes 的文件中。
通过写入文件,我们可以更改进程的限制。
echo 8000000 >/sys/fs/cgroup/memory/docker/$DBID/memory.limit_in_bytes
如果你重新读取该文件,你会发现它已被转换为 7999488。
cat /sys/fs/cgroup/memory/docker/$DBID/memory.limit_in_bytes

再次检查 Docker 统计信息时,发现该进程的内存限制现在为 7.629M。
docker stats db --no-stream
Seccomp / AppArmor
Linux 系统中的所有操作都通过系统调用完成。内核拥有 330 个系统调用,用于执行诸如读取文件、关闭句柄和检查访问权限等操作。所有应用程序都会结合使用这些系统调用来执行所需的操作。
AppArmor 是一个应用程序定义的配置文件,用于描述进程可以访问系统的哪些部分。
可以通过以下方式查看分配给进程的当前 AppArmor 配置文件:
cat /proc/$DBPID/attr/current

Docker 的默认 AppArmor 配置文件是 docker-default(强制执行)。
在 Docker 1.13 版本之前,AppArmor Profile 存储在 /etc/apparmor.d/docker-default 文件中(该文件会在 Docker 启动时被覆盖,因此用户无法修改)。1.13 版本之后,Docker 会在 tmpfs 文件系统中生成 docker-default 文件,使用 apparmor_parser 将其加载到内核中,然后删除该文件。
Seccomp 能够限制可以进行的系统调用,阻止诸如安装内核模块或更改文件权限之类的操作。
Docker 默认允许的调用方式可在以下网址找到:https://github.com/moby/moby/blob/a575b0b1384b2ba89b79cbd7e770fbeb616758b3/profiles/seccomp/default.json
当分配给某个进程时,意味着该进程只能调用部分系统调用。如果它尝试调用被阻塞的系统调用,将会收到“操作不允许”的错误。
SecComp 的状态也是在一个文件中定义的。
cat /proc/$DBPID/status
cat /proc/$DBPID/status | grep Seccomp
能力
权限是指对进程或用户被允许执行的操作进行分组。这些权限可能涵盖多个系统调用或操作,例如更改系统时间或主机名。
状态文件中还包含 Capabilities 标志。进程可以尽可能多地移除 Capabilities 标志以确保其安全性。
cat /proc/$DBPID/status | grep ^Cap

这些标志以位掩码的形式存储,可以使用 capsh 进行解码。
capsh --decode=00000000a80425fb
希望您觉得这篇文章值得一读!
文章来源:https://dev.to/gauravratnawat/what-s-inside-a-container-5g03












