docker note
- 所有容器都共用主机的 kernel。
- 新镜像从 base 镜像一层一层叠加生成的,每安装一个软件,就在现有镜像的基础上增加一层。
如果多个镜像从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像,同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享。
底层实现
命名空间
命名空间是 Linux 内核一个强大的特性。每个容器都有自己单独的命名空间,运行在其中的
应用都像是在独立的操作系统中运行一样。命名空间保证了容器之间彼此互不影响。包含pid
、net
、ipc
、mnt
、uts
、user
查看进程对应的各个namespace: ls -l /proc/[pid]/ns
控制组
cgroups
控制组可以提供对容器的内存、CPU、磁盘 IO 等资源的限制和审计管理。
联合文件系统
联合文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的
修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。
容器格式
最初,Docker 采用了 LXC
中的容器格式。从 0.7 版本以后开始去除 LXC
,转而使用自行开发的 libcontainer
,从 1.11 开始,则进一步演进为使用 runC
和 containerd
。
对更多容器格式的支持,还在进一步的发展中。
网络
镜像
docker镜像包含层数据和元数据(json文件,描述镜像的信息,包括数据间的关系和容器配置关系)。
/var/lib/docker/image/<graph_driver>/layerdb
保存了本地镜像的全部镜像层, /var/lib/docker/image/<graph_driver>/imagedb
保存了本地全部镜像的元数据。
image元数据包括了镜像架构(如 amd64)、操作系统(如 linux)、镜像默认配置、构建该镜像的容器 ID 和配置、创建时间、创建该镜像的 docker 版本、构建镜像的历史信息以及 rootfs 组成。其中构建镜像的历史信息和 rootfs 组成部分除了具有描述镜像的作用外,还将镜像和构成该镜像的镜像层关联了起来。Docker 会根据历史信息和 rootfs 中的 diff_ids 计算出构成该镜像的镜像层的存储索引 chainID 。
以下是hello-world的image数据
less imagedb/content/sha256/fce289e99eb9bca977dae136fbe2a82b6b7d4c372474c9235adc1741675f587e | jq
{
"architecture": "amd64",
"config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/hello"
],
"ArgsEscaped": true,
"Image": "sha256:a6d1aaad8ca65655449a26146699fe9d61240071f6992975be7e720f1cd42440",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"container": "8e2caa5a514bb6d8b4f2a2553e9067498d261a0fd83a96aeaaf303943dff6ff9",
"container_config": {
"Hostname": "8e2caa5a514b",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"/hello\"]"
],
"ArgsEscaped": true,
"Image": "sha256:a6d1aaad8ca65655449a26146699fe9d61240071f6992975be7e720f1cd42440",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {}
},
"created": "2019-01-01T01:29:27.650294696Z",
"docker_version": "18.06.1-ce",
"history": [
{
"created": "2019-01-01T01:29:27.416803627Z",
"created_by": "/bin/sh -c #(nop) COPY file:f77490f70ce51da25bd21bfc30cb5e1a24b2b65eb37d4af0c327ddc24f0986a6 in / "
},
{
"created": "2019-01-01T01:29:27.650294696Z",
"created_by": "/bin/sh -c #(nop) CMD [\"/hello\"]",
"empty_layer": true
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:af0b15c8625bb1938f1d7b17081031f649fd14e6b233688eea3c5483994a66a3"
]
}
}
docker如何整合镜像内容与元数据?
Docker daemon通过image的元数据得到全部layer的ID,再跟进layer的元数据梳理出顺序,最后使用联合挂载技术(AUFS联合文件系统)还原容器启动所需的rootfs和基本配置信息。运行的容器实际上就像是在这些镜像层上新建了一个动态的层。
容器
底下几层是包含镜像的只读层;再往上是只读初始化层,初始化与容器相关的环境信息,如容器主机名、主机host信息、域名服务文件等;再往上是可读写层,写时复制,数据卷的文件也会挂载到可读写层,容器进程在可读写层运行。
docker
/var/lib/docker目录
.
├── builder
│ └── fscache.db
├── buildkit
│ ├── cache.db
│ ├── content
│ │ └── ingest
│ ├── executor
│ ├── metadata.db
│ └── snapshots.db
├── containers #容器信息
│ └── e891d22a4eb4175b44685cfad639cdae716b1daaabb23fe68bb610684b71e91a #container-id
│ ├── checkpoints
│ ├── config.v2.json
│ └── hostconfig.json
├── image #镜像信息
│ └── overlay2
│ ├── distribution
│ │ ├── diffid-by-digest
│ │ │ └── sha256
│ │ │ └── 1b930d010525941c1d56ec53b97bd057a67ae1865eebf042686d2a2d18271ced
│ │ └── v2metadata-by-diffid
│ │ └── sha256
│ │ └── af0b15c8625bb1938f1d7b17081031f649fd14e6b233688eea3c5483994a66a3
│ ├── imagedb
│ │ ├── content
│ │ │ └── sha256
│ │ │ └── fce289e99eb9bca977dae136fbe2a82b6b7d4c372474c9235adc1741675f587e
│ │ └── metadata
│ │ └── sha256
│ ├── layerdb
│ │ ├── mounts
│ │ │ └── e891d22a4eb4175b44685cfad639cdae716b1daaabb23fe68bb610684b71e91a #container-id
│ │ │ ├── init-id
│ │ │ ├── mount-id
│ │ │ └── parent
│ │ ├── sha256
│ │ │ └── af0b15c8625bb1938f1d7b17081031f649fd14e6b233688eea3c5483994a66a3 #parent
│ │ │ ├── cache-id
│ │ │ ├── diff
│ │ │ ├── size
│ │ │ └── tar-split.json.gz
│ │ └── tmp
│ └── repositories.json
├── network #网络信息
│ └── files
│ └── local-kv.db
├── overlay2 #存储驱动信息
│ ├── 30565984877f9fcc71431fe65dc813122e0b5d57398cf345ce08cff57a2f3868 #mount-id
│ │ ├── diff
│ │ ├── link
│ │ ├── lower
│ │ └── work
│ │ └── work
│ ├── 30565984877f9fcc71431fe65dc813122e0b5d57398cf345ce08cff57a2f3868-init #init-id
│ │ ├── diff
│ │ │ ├── dev
│ │ │ │ ├── console
│ │ │ │ ├── pts
│ │ │ │ └── shm
│ │ │ ├── etc
│ │ │ │ ├── hostname
│ │ │ │ ├── hosts
│ │ │ │ ├── mtab -> /proc/mounts
│ │ │ │ └── resolv.conf
│ │ │ ├── proc
│ │ │ └── sys
│ │ ├── link
│ │ ├── lower
│ │ └── work
│ │ └── work
│ ├── f0f79482d9bcfd11f2c737b06b0fe87b8a9ab220ace6cb52886445fe047f9146 #parent -> cache-id, 共享
│ │ ├── diff
│ │ │ └── hello
│ │ └── link
│ └── l
│ ├── 6TDUW3SIOIHJUGF3QETWGCMT6P -> ../f0f79482d9bcfd11f2c737b06b0fe87b8a9ab220ace6cb52886445fe047f9146/diff
│ ├── 7ZJXQQQYFQ7HR4IZGR5MSIWYSW -> ../30565984877f9fcc71431fe65dc813122e0b5d57398cf345ce08cff57a2f3868/diff
│ └── NQN6NBVFBBO4TW6BCOM4KKDDS3 -> ../30565984877f9fcc71431fe65dc813122e0b5d57398cf345ce08cff57a2f3868-init/diff
├── plugins
│ ├── storage
│ │ └── blobs
│ │ └── tmp
│ └── tmp
├── runtimes
├── swarm #集群信息
├── tmp
├── trust
└── volumes #数据卷信息
└── metadata.db
image目录
layer的元数据
创建容器时,docker会为每个容器创建两个新的layer:
一个是只读的init layer,里面包含docker为容器准备的一些文件,
另一个是容器的可写mount layer,以后在容器里面对rootfs的所有增删改操作的结果都会存在这个layer中。
以上两layer的 元数据 存储在image/overlay2/layerdb/mounts/ [container-id] 下, 里面包含了 init-id (包含了init layer的cacheid), mount-id (包含了mount layer的cacheid), parent(image的最上一层layer的chainid,以sha256:cacheid的形式记录)
docker将container的layer和image的layer的 元数据 放在了不同的两个目录中(layerdb/mounts 与 layerdb/sha256)
storage-driver目录(overlay2)
layer的数据
image目录里存了layer的 元数据,layer的具体数据则存储在storage-driver目录下,每一层layer的具体数据分为
├── diff
├── link
├── lower
└── work
四个子目录与文件
diff子目录
该目录下存放着每个layer所包含的数据
以init层为例
├── a626b2c9a9eaf7695d04408d941e17fb809361aa9d094090a6367bfb50c6ef0c-init
│ ├── diff
│ │ ├── dev
│ │ │ ├── console
│ │ │ ├── pts
│ │ │ └── shm
│ │ ├── etc
│ │ │ ├── hostname
│ │ │ ├── hosts
│ │ │ ├── mtab -> /proc/mounts
│ │ │ └── resolv.conf
│ │ ├── proc
│ │ └── sys
│ ├── link
│ ├── lower
│ └── work
│ └── work
这几个文件都是Linux运行时必须的文件,如果缺少的话会导致某些程序或者库出现异常,所以docker需要为容器准备好这些文件。
以下举4例:
-
/dev/console:
在Linux主机上,该文件一般指向主机的当前控制台,有些程序会依赖该文件。在容器启动的时候,docker会为容器创建一个pts,然后通过bind mount的方式将pts绑定到容器里面的/dev/console上,这样在容器里面往这个文件里面写东西就相当于往容器的控制台上打印数据。这里创建一个空文件相当于占个坑,作为后续bind mount的目的路径。
-
hostname,hosts,resolv.conf:
对于每个容器来说,容器内的这几个文件内容都有可能不一样,这里也只是占个坑,等着docker在外面生成这几个文件,然后通过bind mount的方式将这些文件绑定到容器中的这些位置,即这些文件都会被宿主机中的文件覆盖掉。
-
/etc/mtab:
这个文件在新的Linux发行版中都指向/proc/mounts,里面包含了当前mount namespace中的所有挂载信息,很多程序和库会依赖这个文件。
注意: 这里mtab指向的路径是固定的,但内容是变化的,取决于你从哪里打开这个文件,当在宿主机上打开时,是宿主机上/proc/mounts的内容,当启动并进入容器后,在容器中打开看到的就是容器中/proc/mounts的内容。
container目录
├── containers
│ └── 77ac804c1d62331fc0460d65b5e58f5341b5955f5703925ed1f45a9a75636469
│ ├── checkpoints
│ ├── config.v2.json
│ └── hostconfig.json
checkpoints: 容器的checkpoint这个功能在当前版本还是experimental状态。
config.v2.json: 通用的配置,如容器名称,要执行的命令等
hostconfig.json: 主机相关的配置,跟操作系统平台有关,如cgroup的配置
checkpoints这个功能很强大,可以在当前node做一个checkpoint,然后再到另一个node上继续运行,相当于无缝的将一个正在运行的进程先暂停,然后迁移到另一个node上并继续运行。
问题 1:你是否知道如何修复容器中的 top 指令以及 /proc 文件系统中的信息呢?(提示:lxcfs)
其实,这个问题的答案在提示里其实已经给出了,即 lxcfs 方案。通过 lxcfs,你可以把宿主机的 /var/lib/lxcfs/proc 文件系统挂载到 Docker 容器的 /proc 目录下。使得容器中进程读取相应文件内容时,实际上会从容器对应的 Cgroups 中读取正确的资源限制。 从而得到正确的 top 命令的返回值。
问题选自第 6 篇文章《白话容器基础(二):隔离与限制》。
问题 2:既然容器的 rootfs(比如,Ubuntu 镜像),是以只读方式挂载的,那么又如何在容器里修改 Ubuntu 镜像的内容呢?(提示:Copy-on-Write)
这个问题的答案也同样出现在了提示里。简单地说,修改一个镜像里的文件的时候,联合文件系统首先会从上到下在各个层中查找有没有目标文件。如果找到,就把这个文件复制到可读写层进行修改。这个修改的结果会屏蔽掉下层的文件,这种方式就被称为 copy-on-write。