Docker 容器与镜像本质

关于 docker 安装,可以参考这篇文章

什么是容器?

容器其实只是 linux 系统系统下带有特殊设置的进程。

在命令行下输入以下,看看会发生什么:

docker run -d --name=db redis:alpine

上面启动的 docker 容器内会启动一个叫 redis-server 的进程。在宿主机上,我们可以查看到 docker 容器启动的所有进程。使用下面命令查看 redis-server 进程:

ps aux | grep redis-server

docker 可以帮助我们查看进程的信息,包括进程 pid,以及 ppid。

docker ps top

这个进程的 ppid 是谁?使用命令 ps aux | grep <ppid> 查看对应的进程。没啥意外的话应该是 Containered

pstree 命令可以查看进程下所有的子进程,使用下面命令查看 dockerd 的所有子进程。

pstree -c -p -A $(pgrep dockerd)

以上命令输出:

$ ps aux | grep redis-server
999       1854  0.2  1.0  25252 10344 ?        Ssl  10:47   0:00 redis-server
root      2143  0.0  0.0  14220   960 pts/0    R+   10:51   0:00 grep --color=auto redis-server
$ docker top db
UID                 PID                 PPID                C                   STIME               TTY          TIME                CMD
999                 1854                1840                0                   10:47               ?          00:00:00            redis-server
$ ps aux | grep 1840
root      1840  0.0  0.7   8924  7620 ?        Sl   10:47   0:00 docker-containerd-shim -namespace moby -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby/55c6d2c2bf2e62eddd0d688dfc88519d116d1ee9afb917bbfffe778f8ec234ff -address /var/run/docker/containerd/docker-containerd.sock -containerd-binary /usr/bin/docker-containerd -runtime-root /var/run/docker/runtime-runc -debug
root      2243  0.0  0.0  14220  1008 pts/0    R+   10:52   0:00 grep --color=auto 1840
$ pstree -c -p -A $(pgrep dockerd)
dockerd(689)-+-docker-containe(729)-+-docker-containe(1840)-+-redis-server(1854)-+-{redis-server}(1889)
             |                      |                       |                    |-{redis-server}(1890)
             |                      |                       |                    `-{redis-server}(1891)
             |                      |                       |-{docker-containe}(1841)
             |                      |                       |-{docker-containe}(1842)
             |                      |                       |-{docker-containe}(1843)
             |                      |                       |-{docker-containe}(1844)
             |                      |                       |-{docker-containe}(1870)
             |                      |                       `-{docker-containe}(1871)
             |                      |-{docker-containe}(738)
             |                      |-{docker-containe}(739)
             |                      |-{docker-containe}(740)
             |                      |-{docker-containe}(741)
             |                      |-{docker-containe}(742)
             |                      |-{docker-containe}(760)
             |                      |-{docker-containe}(761)
             |                      `-{docker-containe}(1616)
             |-{dockerd}(710)
             |-{dockerd}(715)
             |-{dockerd}(716)
             |-{dockerd}(728)
             |-{dockerd}(731)
             |-{dockerd}(754)
             |-{dockerd}(755)
             |-{dockerd}(756)
             `-{dockerd}(1795)

进程目录

每个进程的配置目录都在 /proc 目录下,如果你知道进程的 pid,你就能找到它的配置目录。

DBPID=$(pgrep redis-server)
echo Redis is $DBPID
ls /proc
ls /proc/$DBPID
cat /proc/$DBPID/environ

命名空间

容器的一个基本部分是命名空间。 命名空间的概念是限制哪些进程可以查看和访问系统的某些部分,例如其他网络接口或进程。

启动容器时,容器运行时(如Docker)将创建新的命名空间以对该进程进行沙盒处理。 通过在它自己的Pid命名空间中运行一个进程,它看起来就像是系统上唯一的进程。

可用的命名空间有:

  • Mount (mnt)
  • Process ID (pid)
  • Network (net)
  • Interprocess Communication (ipc)
  • UTS (hostnames)
  • User ID (user)
  • Control group (cgroup)

Unshare 可以启动 “contained” 进程

如果不适用 docker 运行时,也可以用 unshare 之类的工具构造只在自己命名空间内运行的进程。

unshare --help

通过 unshare,可以启动进程并创建一个新的命名空间,例如Pid。 通过从主机取消共享Pid命名空间,看起来bash提示符是计算机上运行的唯一进程。

sudo unshare --fork --pid --mount-proc bash  # 使用新的命名空间启动 bash
ps  # 在上面的 bash 环境下查看进程,此时只能看到 bash 和 ps 进程
exit  # 退出上面启动的 bash 进程

当我们共享命名空间时,发生了什么?

命名空间实际就是磁盘上的 inode。这使得我们可以共享/重用命名空间,以及允许对它们查看并交互。

查看进程的命名空间列表:

$ ls -lha /proc/$DBPID/ns/
total 0
dr-x--x--x 2 999 packer 0 Sep  2 12:21 .
dr-xr-xr-x 9 999 packer 0 Sep  2 12:21 ..
lrwxrwxrwx 1 999 packer 0 Sep  2 12:22 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 999 packer 0 Sep  2 12:21 ipc -> ipc:[4026532157]
lrwxrwxrwx 1 999 packer 0 Sep  2 12:21 mnt -> mnt:[4026532155]
lrwxrwxrwx 1 999 packer 0 Sep  2 12:21 net -> net:[4026532160]
lrwxrwxrwx 1 999 packer 0 Sep  2 12:21 pid -> pid:[4026532158]
lrwxrwxrwx 1 999 packer 0 Sep  2 12:21 user -> user:[4026531837]
lrwxrwxrwx 1 999 packer 0 Sep  2 12:21 uts -> uts:[4026532156]

使用Docker,可以使用语法容器共享这些命名空间:<container-name>。 例如,下面的命令将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

查看 web容器的命名空间,发现有些跟 db容器的命名空间一致:

$ ls -lha /proc/$WEBPID/ns/
total 0
dr-x--x--x 2 systemd-network systemd-journal 0 Sep  2 12:24 .
dr-xr-xr-x 9 systemd-network systemd-journal 0 Sep  2 12:23 ..
lrwxrwxrwx 1 systemd-network systemd-journal 0 Sep  2 12:24 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Sep  2 12:24 ipc -> ipc:[4026532225]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Sep  2 12:24 mnt -> mnt:[4026532223]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Sep  2 12:24 net -> net:[4026532160]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Sep  2 12:24 pid -> pid:[4026532226]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Sep  2 12:24 user -> user:[4026531837]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Sep  2 12:24 uts -> uts:[4026532224]
$ ls -lha /proc/$DBPID/ns/ | grep net:
lrwxrwxrwx 1 999 packer 0 Sep  2 12:21 net -> net:[4026532160]
$ ls -lha /proc/$WEBPID/ns/ | grep net:
lrwxrwxrwx 1 systemd-network systemd-journal 0 Sep  2 12:24 net -> net:[4026532160]

Chroot

容器进程的一个重要部分是能够拥有独立于主机的不同文件。 这就是我们如何根据我们系统上运行的不同操作系统获得不同的Docker镜像。

Chroot为进程提供了从父OS的不同根目录开始的能力。 这允许不同的文件出现在根目录中。

Cgroups (Control Groups)

CGroup限制进程可以使用的资源量。 这些cgroup是在/ proc目录中的特定文件中定义的值。

要查看映射,请运行以下命令:

cat /proc/$DBPID/cgroup

这些映射到磁盘上的其他cgroup目录

ls /sys/fs/cgroup/

什么是进程的 CPU 统计信息(stats)

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/

每个目录都根据Docker分配的容器ID进行分组。

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 Stats时,进程的内存限制现在为7.629M.

docker stats db --no-stream

Seccomp / AppArmor

Linux的所有操作都是通过系统调用完成的。内核有330个系统调用,执行读取文件,关闭句柄和检查访问权限等操作。所有应用程序都使用这些系统调用的组合来执行所需的操作。

AppArmor 是一个应用程序定义的配置文件,描述了进程可以访问的系统部分。

可以通过 cat /proc/$DBPID/attr/current 查看分配给进程的当前 AppArmor 配置文件

Docker的默认AppArmor配置文件是docker-default(enforce)。

在Docker 1.13之前,它将AppArmor配置文件存储在 /etc/apparmor.d/docker-default 中(当Docker启动时被覆盖,因此用户无法修改它。在v1.13之后,Docker现在在tmpfs中生成docker-default ,使用apparmor_parser将其加载到内核中,然后删除该文件。

该模板可以在https://github.com/moby/moby/...

Seccomp提供限制可以进行哪些系统调用的功能,阻止诸如安装内核模块或更改文件权限等方面。

可以在https://github.com/moby/moby/...

分配给进程时,意味着进程将仅限于能力系统调用的子集。如果它试图呼叫被阻止的系统呼叫,则会收到错误“Operation Not Allowed”。

SecComp的状态也在文件中定义。

cat /proc/$DBPID/status

cat /proc/$DBPID/status | grep Seccomp

标志含义为:0:禁用1:严格2:过滤

Capabilities

Capabilities 是关于进程或用户有权执行的操作的分组。
这些 Capabilities 可能涵盖多个系统调用或操作,例如更改系统时间或主机名。状态文件还容纳 Capabilities 标志。
进程可以尽可能多地降低 Capabilities 以确保其安全。

cat /proc/$DBPID/status | grep ^ Cap

标志存储为可以使用capsh解码的位掩码

capsh --decode = 00000000a80425fb

容器镜像

容器镜像是包含tar文件的tar文件。 每个tar文件都是一个层。 将所有tar文件提取到同一位置后,您就拥有了容器的文件系统。

可以通过Docker进行探索。将镜像拉到本地系统上。

docker pull redis:3.2.11-alpine

将图像导出为原始tar格式。

docker save redis:3.2.11-alpine> redis.tar

提取到磁盘

tar -xvf redis.tar

现在可以查看所有镜像tar文件。

ls

该图像还包括有关图像的元数据,例如版本信息和标签名称。

cat repositories
cat manifest.json

提取镜像将显示该镜像提供的文件。

tar -xvf da2a73e79c2ccb87834d7ce3e43d274a750177fe6527ea3f8492d08d3bb0123c / layer.tar

创建空镜像

由于镜像只是一个tar文件,因此可以使用以下命令创建空图像。

tar cv --files-from /dev/null | docker import  - empty

通过导入tar,将创建其他元数据。

docker images

但是,由于容器不包含任何内容,因此无法启动进程。

不使用 Dockerfile 创建映像

可以扩展先前导入 tar 文件的想法,从头开始创建整个镜像。

首先,我们将使用 BusyBox 作为基础。这将为我们提供基础linux命令。这被定义为rootfs。

Docker 提供了一个下载 BusyBox rootfs 的脚本:https://github.com/moby/moby/blob/a575b0b1384b2ba89b79cbd7e770fbeb616758b3/contrib/mkimage/busybox-static

curl -LO https://raw.githubusercontent.com/moby/moby/a575b0b1384b2ba89b79cbd7e770fbeb616758b3/contrib/mkimage/busybox-static&& chmod + x busybox-static

./busybox-static busybox

运行该脚本将下载rootfs和主二进制文件。

ls -lha busybox

默认的Busybox rootfs不包含任何版本信息,所以让我们创建一个文件。

echo KatacodaPrivateBuild> busybox / release

和以前一样,目录可以转换为tar,并作为镜像自动导入Docker。

tar -C busybox -c。 | docker import  -  busybox

现在可以作为容器启动。

docker run busybox cat /release

内容来源:https://www.katacoda.com/courses/container-runtimes
翻译:google 翻译
校验:xx

相关推荐