- 使用Docker学习Docker
- 一、搭建演示环境
- 二、关联基础
- docker image 文件系统
- 什么是image layer
- Dockerfile VOLUME(数据卷) 指令
- Volume命令的使用
- 什么是container-diff
- 使用
- 三、演示时间
- 探索 docker create 命令
- 探索 docker start 命令
- 探索 docker stop 命令
- 探索 docker exec 命令
- 四、总结
Learning Docker with Docker - Toying With DinD For Fun And Profit
您是否曾经注意到,一半的容器管理命令看起来像进程管理操作,而另一半则看起来像文件管理操作?
这里有一个小练习来加深你对容器的理解… 通过玩转它们,我们的目标是证明容器不仅仅是 Linux 进程,它也是 Linux文件!
这个想法很简单——用一台安装了 Docker 守护进程的 Linux 机器,在上面运行一系列常用的命令,比如 Docker create | start | exec | … 密切关注机器的文件系统,希望能有一些有趣的发现。
一、搭建演示环境首先,简单介绍一下临时演示环境。基本上,任何运行 Docker 的干净 Linux 机器都可以。但是由于我们想要跟踪主机文件系统上 created/deleted/modified的文件,我们应该在每个 docker
这听起来确实像是 Docker-in-Docker 的一个很好的用例。如果实验的Docker 守护进程本身运行在容器中,我们可以随时使用标准 docker commit
但是官方的 docker:dind容器镜像 不能用,因为它包含一个特定的 VOLUME 指令。该指令存在的理由很充分——将 /var/lib/docker文件夹移出容器的(slow and expensive)联合文件系统。然而,正如你马上会看到的,这个文件夹将是我们实验中最热门的位置之一。因此,我们需要确保将/var/lib/docker提交到快照镜像,不需要volumes。
$ git clone https://github.com/docker-library/docker.git $ cd docker/20.10/dind $ sed -i '/VOLUME/d' ./Dockerfile $ docker build -t my-dind:origin .
另一个重要的要求是保持环境的足够小和可控性。为此,我们需要使用一个超小的测试容器镜像,其中包含我们在对实验Docker 守护进程(guinea-pig Docker daemon)进行实验时所需要的内容。一个从头开始的镜像,里面有一个简单的 Go 二进制文件,听起来是一个不错的选择。但是,这个镜像应该在实例构建容器镜像之外构建,因为这涉及到运行临时容器,因此可能会破坏临时环境。
There are many ways to build and distribute images, but for our experiment, I ended up with probably the simplest possible setup:
有许多方法可以构建和发布镜像,但是对于我们的实验,最终得到了可能是最简单的设置:
以下是如何在运行 Docker 的Linux计算机上运行上述设置:
# 1. Prepare config to make Docker trust the local registry: $ cat > daemon.json <{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' my-registry) $ echo "$REGISTRY_IP my-registry" | sudo tee --append /etc/hosts # 7. Run the guinea-pig Docker daemon using the patched DinD image: $ docker run --detach --privileged --network skynet --volume `pwd`/daemon.json:/etc/docker/daemon.json --name my-dind my-dind:origin # 8. Commit the initial state of the guinea-pig Docker system # as the starting point for further comparisons: $ docker commit my-dind my-dind:just-started # [Optional] See what changes have been made to my-dind # container's filesystem during the container's startup: $ container-diff diff --type=file daemon://my-dind:origin daemon://my-dind:just-started
现在,准备测试应用程序——一个简单的 HTTP 程序和一个sleep的cli工具:
# syntax=docker/dockerfile:1.4 # ---=== This part is just for building ===--- FROM golang:1.18 as builder WORKDIR / COPY <Last but not least:
docker buildx build -t my-registry:5000/my-app . docker push my-registry:5000/my-app二、关联基础 docker image 文件系统一图看尽 Docker 容器文件系统
参考URL: https://zhuanlan.zhihu.com/p/362132467docker 容器启动就是一个文件系统的启动。在docker中,每一层镜像都具备一些文件。
构建镜像的时候,从一个最基本的操作系统开始,每个构建的操作都相当于做一层的修改,增加了一层文件系统。一层层往上叠加,上层的修改会覆盖底层该位置的可见性,这也很容易理解,就像上层把底层遮住了一样。当你使用的时候,你只会看到一个完全的整体,你不知道里面有几层,也不清楚每一层所做的修改是什么。
从基本的看起,一个典型的Linux文件系统由bootfs和rootfs两部分组成,bootfs(boot file system)主要包含bootloader和kernel,bootloader主要用于引导加载kernel,当kernel被加载到内存中后bootfs会被umount掉。rootfs (root file system)包含的就是典型Linux系统中的/dev,/proc,/bin,/etc等标准目录和文件。不同的linux发行版(如ubuntu和CentOS )在rootfs这一层会有所区别,体现发行版本的差异性。
但Docker在bootfs自检完毕之后并不会把rootfs的read-only改为read-write,而是利用union mount(UnionFS的一种挂载机制)将image中的其他的layer加载到之前的read-only的rootfs层之上,每一层layer都是rootfs的结构,并且是read-only的。所以,我们是无法修改一个已有镜像里面的layer的!只有当我们创建一个容器,也就是将Docker镜像进行实例化,系统会分配一层空的read-write的rootfs,用于保存我们做的修改。一层layer所保存的修改是增量式的,就像git一样。
综上,image其实就是一个文件系统,它与宿主机的内核一起为程序提供一个虚拟的linux环境。在启动docker container时,依据image,docker会为container构建出一个虚拟的linux环境。
Docker目前支持五种镜像层次的存储driver:aufs、device mapper、btrfs、vfs、overlay。
其中最常用的就是aufs了,但随着linux内核3.18把overlay纳入其中,overlay的地位变得更重目前docker默认的存储类型就是overlay2,docker版本是1.8,如下
什么是image layer
docker默认的存储目录是/var/lib/docker,我们只关心image和overlay2,image:主要存放镜像中layer层的元数据和overlay2:各层的具体信息。Dockerfile由多条指令构成,Dockerfile中的每一条指令都会对应于Docker镜像中的一层。
如下Dockerfile
FROM ubuntu:14.04 ADD run.sh / VOLUME /data CMD ["./run.sh"]通过docker build以上Dockerfile的时候,会在Ubuntu:14.04镜像基础上,添加三层独立的镜像,依次对应于三条不同的命令。镜像示意图如下:
Dockerfile VOLUME(数据卷) 指令
Dockerfile中命令与镜像层一一对应,那么是否意味着docker build完毕之后,镜像的总大小=每一层镜像的大小总和呢?答案是肯定的。依然以上图为例:如果ubuntu:14.04镜像的大小为200MB,而run.sh的大小为5MB,那么以上三层镜像从上到下,每层大小依次为0、0以及5MB,那么最终构建出的镜像大小的确为0+0+5+200=205MB。有状态容器都有数据持久化需求。Docker 采用 AFUS 分层文件系统时,文件系统的改动都是发生在最上面的容器层。在容器的生命周期内,它是持续的,包括容器在被停止后。但是,当容器被删除后,该数据层也随之被删除了。因此,Docker 采用 volume (卷)的形式来向容器提供持久化存储。
想要了解Docker Volume,首先我们需要知道Docker的文件系统是如何工作的。Docker镜像是由多个文件系统(只读层)叠加而成。当我们启动一个容器的时候,Docker会加载只读镜像层并在其上(译者注:镜像栈顶部)添加一个读写层。如果运行中的容器修改了现有的一个已经存在的文件,那该文件将会从读写层下面的只读层复制到读写层,该文件的只读版本仍然存在,只是已经被读写层中该文件的副本所隐藏。当删除Docker容器,并通过该镜像重新启动时,之前的更改将会丢失。在Docker中,只读层及在顶部的读写层的组合被称为Union File System(联合文件系统)。
为了能够保存(持久化)数据以及共享容器间的数据,Docker提出了Volume的概念。简单来说,Volume就是目录或者文件,它可以绕过默认的联合文件系统,而以正常的文件或者目录的形式存在于宿主机上。
Volume:即数据卷。
- Docker Volume命令能让容器从宿主主机中读取文件,或从容器中持久化数据到宿主主机内,让容器与容器产生的数据分离开来,一个容器可以挂载多个不同的目录。
- Volume的生命周期是独立于容器的生命周期之外的,即使容器删除了,volume(数据卷)也会被保留下来,Docker也不会因为这个volume(数据卷)没有被容器使用而回收。
- 在容器中,添加或修改这个文件夹里的文件也不会影响容器的联合文件系统。
作用:创建一个匿名数据卷挂载点
格式:VOLUME ["/data"]详解:运行容器时可以从本地主机或其他容器挂载数据卷,一般用来存放数据库和需要保持的数据等
这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化
Volume命令的使用(1)创建数据卷
命令:docker volume create 自定义名称root@cka-k8s-master:~# docker volume create myVolume myVolume root@cka-k8s-master:~#每创建一个Volume,Docker默认会在宿主机的/var/lib/docker/volumes/目录下创建一个子目录,默认情况下目录名是一串UUID。
如果指定了名称,则目录名是Volume名称(例如上面的myVolume)。Volume里的数据都存储在这个子目录的_data目录下。
之后我们可以把这个数据卷挂载到一个新的容器中,例如Nginx容器。(2)查看本地数据卷列表
命令:docker volume lsroot@cka-k8s-master:~# docker volume ls
DRIVER VOLUME NAME
local myVolume
root@cka-k8s-master:~#(3)打印myVolume数据卷的详细信息
命令:docker volume inspect 一个或多个Volume名称root@cka-k8s-master:~# docker volume inspect myVolume [ { "CreatedAt": "2022-04-27T10:21:53Z", "Driver": "local", "Labels": {}, "Mountpoint": "/data/docker/volumes/myVolume/_data", "Name": "myVolume", "Options": {}, "Scope": "local" } ] root@cka-k8s-master:~#
每创建一个Volume,Docker默认会在宿主机的/var/lib/docker/volumes/目录下创建一个子目录,默认情况下目录名是一串UUID。
如果指定了名称,则目录名是Volume名称(例如上面的myVolume)。Volume里的数据都存储在这个子目录的_data目录下。
具名挂载和匿名挂载
(1)匿名挂载
匿名挂载格式:-v /容器内路径或者-v /宿主机路径:/容器内路径
(2)具名挂载
指令:docker run -d -P --name nginx02 -v juming-nginx:/etc/nginx nginx注意:这里 -v juming-nginx: 代表直接给定名字,但是没有指定路径
什么是container-diff
如图,查看数据卷一栏增加了刚刚添加的数据卷。官方github: https://github.com/GoogleContainerTools/container-diff
container-diff 是 Google 开源的一个分析和比较容器镜像的工具,可用来分析 Docker 镜像之间的差异。
container-diff 可通过几个不同的标准(角度)来检查镜像,包括:
使用
- Docker 镜像历史
- 镜像文件系统
- Apt 包管理器
- pip 包管理器
- npm 包管理器
分析单个Docker镜像
container-diff analyze对比两个Docker镜像
container-diff diff如果不指定type,默认分析/对比的是镜像大小,即–type=size
可以通过指定type,分析/对比特定维度container-diff analyze--type= container-diff diff --type= type类型支持如下:
- history:镜像构建历史
- file:镜像文件
- size:镜像大小
- rpm:rpm包管理器
- pip:pip包管理器
- apt:apt包管理器
- node:node包管理器
通过设置--type=file和--filename=/path/file,可以比较比较两个docker镜像中某目录或文件的区别,例如:
container-diff diff nginx:v1 nginx:v2 --type=file --filename=/etc/复制代码通过设置-j,可以使用json格式输出结果。
三、演示时间
通过设置-w ,可以将结果输入到文件。For the experiment itself, I’ll use two terminals simultaneously:
对于实验本身,我将同时使用两个终端:如下,我们进入到 my-dind 容器中,使用docker ps,在容器中我们看到其他容器- -!这就是容器中的容器!
# Terminal 1 - DinD $ docker exec -it my-dind sh $ docker ps探索 docker create 命令# Terminal 2 - host system $ docker ps CONTAINER ID IMAGE COMMAND ... STATUS NAMES d07b66a353b0 my-dind:origin "dockerd-entrypoint.…" ... Up 25 minutes my-dind 9a6addf796f6 registry:2 "/entrypoint.sh /etc…" ... Up 26 minutes my-registry 第一个实验——使用 DinD Docker 实例创建一个 my-app 容器,然后看看会创建哪些文件:
使用两个终端进行实验-DinD 在左边,主机在右边。# Terminal 1 (DinD) # Create container: $ docker create --name my-app my-registry:5000/my-app # List existing containers: $ docker ps -a CONTAINER ID IMAGE COMMAND ... STATUS NAMES 2c23a0da2b19 my-registry:5000/my-app "/server" Created my-app # List running processes: $ ps auxf PID USER TIME COMMAND 1 root 0:00 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0. 61 root 0:00 dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsv 70 root 0:21 containerd --config /var/run/docker/containerd/containerd.toml --log-level 176 root 0:00 sh 232 root 0:00 ps auxf发现 # 1: docker create 创建了一个容器,但是没有创建任何新的进程!
# Terminal 2 (Host) # Snapshot the DinD container filesystem: $ docker commit my-dind my-dind:cont-created # Compare the current state with the previous one: $ container-diff diff --type=file daemon://my-dind:just-started daemon://my-dind:cont-created -----File----- These entries have been added to my-dind:just-started: # <-- looks like a bug in container-diff output FILE ... # This group of files seems important /var/lib/docker/buildkit/cache.db /var/lib/docker/buildkit/containerdmeta.db /var/lib/docker/buildkit/content /var/lib/docker/buildkit/content/ingest /var/lib/docker/buildkit/executor /var/lib/docker/buildkit/metadata_v2.db /var/lib/docker/buildkit/snapshots.db # So... This is where containers live on disk! /var/lib/docker/containers//var/lib/docker/containers/ /checkpoints /var/lib/docker/containers/ /config.v2.json /var/lib/docker/containers/ /hostconfig.json # And this is where images live /var/lib/docker/image ... # Look ma', our `app` image files! /var/lib/docker/vfs/dir/ /var/lib/docker/vfs/dir/ /server /var/lib/docker/vfs/dir/ /var/lib/docker/vfs/dir/ /server /var/lib/docker/vfs/dir/ /sleep ... These entries have been deleted from my-dind:just-started: None These entries have been changed between my-dind:just-started and my-dind:cont-created: FILE /certs/... 发现 # 2: /var/lib/docker/containers/
——这是我们的容器在磁盘存储的文件。 发现 # 3: docker create 似乎与 runc create 非常不同——到目前为止还没有创建任何运行时绑定!
Finding #4: Container logs aren’t a thing at this stage yet. The container-diff output above is abridged, but if you’re performing this exercise while reading the article, try searching for files containing the log word in their name - you won’t find anything.
探索 docker start 命令发现 # 4: 在此阶段,容器日志还找不到。上面的container-diff输出是删减的,但是如果您在阅读文章时执行此练习,请尝试搜索包含其名称的日志单词的文件 - 您找不到任何东西。
是时候启动 my-app 容器了:
# Terminal 1 (DinD) $ docker start my-app $ docker ps -a CONTAINER ID IMAGE COMMAND ... STATUS NAMES 435edb948b83 my-registry:5000/my-app "/server" Up 21 seconds my-app $ ps auxf PID USER TIME COMMAND 1 root 0:00 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert /certs/server/ca.pem --tlscert /certs/server/cert.pem --tlskey /certs/server/key.pem 60 root 0:00 dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert /certs/server/ca.pem --tlscert /certs/server/cert.pem --tlskey /certs/server/key.pem 69 root 0:07 containerd --config /var/run/docker/containerd/containerd.toml --log-level info 210 root 0:00 sh 265 root 0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace moby -id 435edb948b8360ffcbae5452fd6fc0451b5c17daf6940f63db6795c099958357 -address /var/run/docker/containerd/containerd.sock 284 root 0:00 /server 320 root 0:00 ps auxf那么,已经创建了哪些文件?
# Terminal 2 (Host) $ docker commit my-dind my-dind:cont-started # Compare the current state with the previous one: $ container-diff diff --type=file daemon://my-dind:cont-created daemon://my-dind:cont-started -----File----- These entries have been added to my-dind:cont-created: # <-- rather in cont-started... FILE ... # Here it goes - the OCI runtime bundle! /run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby//run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/ /address /run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/ /config.json /run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/ /init.pid /run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/ /log.json /run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/ /options.json /run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/ /rootfs /run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/ /runtime /run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/ /work ... 发现 # 5: OCI runtime bundle 是在容器启动时创建的,而不是在容器创建时创建的。
发现 # 6: bundle 是在临时文件系统上创建的! 我现在很好奇——它总是这样吗?
发现 #7: Container logs finally appeared, and it’s just a plain file on disk (depends on the log driver, though):
最终出现了容器日志,它只是磁盘上的一个普通文件(不过取决于日志驱动程序): # Terminal 1 (DinD) cat /var/lib/docker/containers/探索 docker stop 命令/ -json.log {"log":"2022/04/26 05:21:59 Starting HTTP server...n","stream":"stderr","time":"2022-04-26T05:21:59.3588249Z"} 那么,当您停止容器时会发生什么? 它的文件会被删除吗?
# Terminal 1 (DinD) $ docker stop my-app $ docker ps -a CONTAINER ID IMAGE COMMAND ... STATUS NAMES 435edb948b83 my-registry:5000/my-app "/server" Exited (2) 2 seconds ago my-app $ ps auxf PID USER TIME COMMAND 1 root 0:00 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert /certs/server/ca.pem --tlscert /certs/server/cert.pem --tlskey /certs/server/key.pem 60 root 0:00 dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert /certs/server/ca.pem --tlscert /certs/server/cert.pem --tlskey /certs/server/key.pem 69 root 0:10 containerd --config /var/run/docker/containerd/containerd.toml --log-level info 210 root 0:00 sh 372 root 0:00 ps auxf探索 docker exec 命令发现 # 8: 停止容器会删除 OCI runtime bundle,但不会删除容器在/var/lib/docker/
的状态(除非 --rm 在创建步骤中被使用)。所以,重新开始是可能的! 让我们重复这个实验,但是这一次试着捕捉当你执行到一个运行的容器中时会发生什么:
# Terminal 1 (DinD) $ docker start my-app # ...jump to the second terminal for a second # Terminal 2 (Host) $ docker commit my-dind my-dind:cont-restarted # ...back to the DinD terminal # Terminal 1 (DinD) $ docker exec -it my-app /sleep 2022/04/21 19:19:52 Zzz... 2022/04/21 19:19:53 Zzz... 2022/04/21 19:19:54 Zzz... 2022/04/21 19:19:55 Zzz... 2022/04/21 19:19:56 Zzz... # Terminal 3 (also DinD) $ ps auxf PID USER TIME COMMAND 1 root 0:00 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert /certs/server/ca.pem --tlscert /certs/server/cert.pem --tlskey /certs/server/key.pem 60 root 0:00 dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert /certs/server/ca.pem --tlscert /certs/server/cert.pem --tlskey /certs/server/key.pem 69 root 0:07 containerd --config /var/run/docker/containerd/containerd.toml --log-level info 210 root 0:00 sh 265 root 0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace moby -id 435edb948b8360ffcbae5452fd6fc0451b5c17daf6940f63db6795c099958357 -address /var/run/docker/containerd/containerd.sock 284 root 0:00 /server 320 root 0:00 ps auxf 363 root 0:00 /sleep但是在实际执行期间究竟发生了什么?
# Terminal 2 (Host) $ docker commit my-dind my-dind:cont-execed $ container-diff diff --type=file daemon://my-dind:cont-restarted daemon://my-dind:cont-execed -----File----- These entries have been added to my-dind:cont-restarted: FILE /run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby// .pid These entries have been deleted from my-dind:cont-restarted: None These entries have been changed between my-dind:cont-restarted and my-dind:cont-execed: None 好吧,这是令人惊讶的!我期待着为 exec 会话创建另一个临时(和匿名)容器,因为 exec 不是真正的 OCI 运行时命令,而只是由 Docker 和类似的容器管理器实现的一个方便的高级操作,它重用 OCI 运行时 start 命令。但是实际的实现似乎非常轻量级,并且使用与主容器相同的 OCI runtime bundle。
四、总结发现 # 9: docker exec 几乎没有文件系统足迹!
好吧,我希望这是一个有趣的练习至少对我来说,现在我明白了为什么一半的容器管理命令看起来像文件管理操作——容器与文件和进程一样重要。



