docker镜像构建
- 文件系统
- 镜像构建
-
- 容器层操作细节
- 镜像构建
-
- docker commit
- docker export
- docker save
- docker build
- docker history
- 容器生命周期
-
- docker RUN
- docker CMD
- docker ENTRYPOINT
- docker exec
- docker attach
- docker logs
- docker 启停
-
- docker start
- docker stop
-
- 使用docker stop myredis_exec命令停止exec模式启动的myredis2容器。
- docker stop命令停止shell命令启动的myredis容器。
- 孤儿进程和僵尸进程
- 总结:
- docker kill
文件系统
linux操作系统由内核空间和用户空间组成。其中内核空间是kernel,对应的文件系统时bootfs,linux在刚启动的时候会加载bootfs文件系统,在启动完成以后会卸载bootfs。用户空间是rootfs文件系统,包括我们常用/etc、/proc、/bin、/dev等,如下图所示:

一般镜像结构如下:
在Image上还会有很多的Image。只有bootfs上面一层那个Image是base Image。(rootfs)
对于任何docker镜像,其文件系统都是与宿主机共用kernel,共用bootfs,但是不共用rootfs。容器的rootfs由容器自己提供。
对于一个镜像,如果其base镜像(bootfs上面一层的那个镜像层)不是操作系统镜像,而是自己写的一个镜像,那么该镜像的文件系统就是自己拷贝过去的文件目录。
比如,自己写了一个helloworld的代码,使用dockerfile构建docker镜像。
FROM scratch 白手起家从0构建一个镜像
COPY hello / 将文件"hello"复制到镜像的根目录,镜像根目录是/
CMD ["hello"] 容器启动时,执行/hello
base镜像一般时底层,同时也能被其他镜像将其作为基础进行扩展。
镜像构建
Docker Daemon在构建dockerfile的过程中,对dockerfile中的每一条命令(FROM命令除外)都会构建一个新的image。
例如用如下命令构建镜像:
构建过程如下:
构建出来的镜像层,在容器中都只是可读,不可写,也就是说,在容器运行过程中,对镜像进行修改操作时不会修改镜像的,只会修改镜像层以上的容器层。
当容器启动时,会在镜像层上创建一个新的容器层,该层是可读可写层,容器启动以后的结构如下所示:
镜像层的数量可能有很多,所有的镜像层联合在一起组成了镜像的文件系统。如果在不同层中,有一个相同路径的文件,那么在容器中,用户只能访问到在上层的文件,不能访问到在下层的该文件。相当于历史层无法访问到。
容器层操作细节
1) 添加文件。在容器中添加文件时,是将该文件添加到容器层中。
2) 读取文件。在容器中读取文件时,docker从文件系统顶端往下依次每层查找此文件,一旦找到该文件,则打开并读取到内存中。因此不会读取到下层的同目录文件。
3) 修改文件。在容器中读取文件时,docker从文件系统顶端往下依次每层查找此文件,一旦找到该文件,则将其复制到容器层中,然后对其进行修改。
4) 删除文件。在容器中删除文件时,docker从文件系统顶端往下依次每层查找此文件,一旦找到该文件,容器则记录下删除操作,并不会真的去删除镜像层的文件。
可以看到,容器在运行过程中,只有当需要修改的时候,才会去复制一份数据,这种特性称为Copy-on-Write。
镜像构建
docker commit
docker commit操作时保存容器层文件系统以及容器层文件系统以下的镜像层文件系统,但是该方式不会保存具体的修改步骤,通过这种方式保存的镜像,无法知道容器层到底进行了哪些修改。
docker export
docker export操作则是只保存最新版的容器层文件系统,并且清除容器层之前的层,也就是清除之前的所有底层镜像,将这个最新的容器层作为新的base镜像。
docker save
docker save方式则是保存容器层以下的镜像层文件系统,相当于修改没有奏效。
docker build
docker build -t ImageName:TagName Dir
-t
—— 给镜像加一个tag
ImageName
—— 给镜像起的名字
TagName
—— 给镜像起的标签名
Dir
—— build context目录
Docker默认从build context中查找dockerfile文件。也可以通过-f参数指定dockerfile文件目录。
需要注意的是,dockerfile中的ADD、COPY等命令可以将build context中的文件添加到镜像中。(会将build context目录中的所有文件都发送给docker daemon)。
如下命令的执行顺序:
执行RUN,将Ubuntu作为base镜像
执行RUN,安装vim
启动一个临时容器,在该容器中通过apt-get
vim安装vim
将该容器保存为镜像(docker commit)
删除容器
镜像构建成功
在整个过程中,实际上的保存容器的方法是docker commit。
dockerfile构建过程:
1) 从base镜像运行一个容器
2) 执行一条指令,对容器进行修改
3) 执行docker commit操作,生成一个新的容器镜像层
4) docker再基于刚刚提交的镜像,运行一个新容器
5) 重复2~4步,直到dockerfile中的所有指令全部执行完毕。in
docker history
docker history ImageName
ImageName
—— 镜像名
docker history命令可以查看镜像的历史信息,可以看到每一层镜像是通过什么命令来对镜像进行修改的。
输出结果中的
missing
表示无法获取image id,通常从docker hub下载的镜像会有这个问题。
容器生命周期
docker RUN
docker RUN命令在当前镜像的顶部执行命令,并创建新的层。docker RUN命令常常用于镜像修改。
docker CMD
docker CMD命令是用来设置docker启动时的CMD默认命令。
docker CMD命令会被启动docker时的docker run命令覆盖掉,如果docker run命令后面有执行命令,则不会执行docker CMD命令。
docker ENTRYPOINT
docker ENTRYPOINT命令不会被覆盖掉,不会被docker run命令覆盖,始终都会执行。
容器的生命周期取决于启动时运行的命令,只要该命令不结束,容器就不会退出。
如上所示的命令中,while死循环,命令不会结束,该容器永不退出。
docker exec
docker exec在容器中打开新的终端,并且可以启动新的进程。
docker exec
进入的容器,新建的终端属于daemon的子进程。
例如:
这里,docker exec进入到的则是bash终端,不是容器启动时创建的/bin/bash终端。
docker attach
docker attach直接进入容器启动命令创建的终端中,不会启动新的进程。
例如:使用docker attach命令则会进入容器创建时的/bin/bash终端。
docker logs
docker logs -f +容器id
可以查看/bin/bash终端的输出。
docker 启停
docker镜像的构建使用CMD命令时,有两种方式,一种时shell方式,一种是exec方式。如下所示:
shell方式
dockerfile
构建如下,利用shell方式构建容器。
FROM ubuntu:14.04
RUN apt-get update && apt-get -y install redis-server && rm -rf /var/lib/apt/lists/*
EXPOSE 6379
CMD "/usr/bin/redis-server"
exec方式 dockerfile
构建如下,利用exec方式构建容器。
FROM ubuntu:14.04
RUN apt-get update && apt-get -y install redis-server && rm -rf /var/lib/apt/lists/*
EXPOSE 6379
CMD ["/usr/bin/redis-server"]
然后基于它们构建两个镜像
myredis:shell
和
myredis:exec
docker build -t myredis:shell -f Dockerfile_shell .
docker build -t myredis:exec -f Dockerfile_exec .
运行
myredis:shell
镜像,我们可以发现它的启动进程(PID1)是
/bin/sh -c "/usr/bin/redis-server"
并且它创建了一个子进程
/usr/bin/redis-server *:6379
[email protected]:~# docker run -d --name myredis_shell myredis:shell
128fdc38cdccc222234ee2c2bc3a4c84355bc97a33e66ddf53c580b86cfe298e
[email protected]:~# docker exec myredis_shell ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:23 ? 00:00:00 /bin/sh -c "/usr/bin/redis-server"
root 7 1 0 14:23 ? 00:00:00 /usr/bin/redis-server *:6379
root 10 0 1 14:23 ? 00:00:00 ps -ef
下面运行
myredis:exec
镜像,我们可以发现它的启动进程是
/usr/bin/redis-server *:6379
,并没有其他子进程存在。
[email protected]:~# docker run -d --name myredis_exec myredis:exec
ffd77fef180740ef199281747e372c33d6e2df06d9edbf11a03c0d30caf61fa3
[email protected]:~# docker exec myredis_exec ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:27 ? 00:00:00 /usr/bin/redis-server *:6379
root 9 0 0 14:27 ? 00:00:00 ps -ef
docker start
docker stop
docker stop命令是向容器发送一个SIGTERM信号。
docker每个container都是docker daemon的子进程。
当创建一个docker容器的时候,就会新建一个PID命名空间,容器启动进程在该命名空间内PID=1。
当PID1的进程结束以后,容器会销毁对应的的PID命名空间,并向容器内其他子进程发送SIGKILL信号。
使用docker stop myredis_exec命令停止exec模式启动的myredis2容器。
[email protected]:~# docker stop myredis_exec
myredis_exec
[email protected]:~# docker logs myredis_exec
[1] 09 Sep 14:27:17.175 # Warning: no config file specified, using the default config. In order to specify a config file use /usr/bin/redis-server /path/to/redis.conf
[1] 09 Sep 14:27:17.175 * Max number of open files set to 10032
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 2.8.4 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in stand alone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 1
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
[1] 09 Sep 14:27:17.175 # Server started, Redis version 2.8.4
[1] 09 Sep 14:27:17.175 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
[1] 09 Sep 14:27:17.175 * The server is now ready to accept connections on port 6379
[1 | signal handler] (1631197679) Received SIGTERM, scheduling shutdown...
[1] 09 Sep 14:27:59.545 # User requested shutdown...
[1] 09 Sep 14:27:59.545 * Saving the final RDB snapshot before exiting.
[1] 09 Sep 14:27:59.547 * DB saved on disk
[1] 09 Sep 14:27:59.547 # Redis is now ready to exit, bye bye...
docker stop命令生效,pid1进程收到了SIGTERM信号,随后退出了。
docker stop命令停止shell命令启动的myredis容器。
[email protected]:~# docker stop myredis_shell
myredis_shell
[email protected]:~# docker logs myredis_shell
[7] 09 Sep 14:23:50.211 # Warning: no config file specified, using the default config. In order to specify a config file use /usr/bin/redis-server /path/to/redis.conf
[7] 09 Sep 14:23:50.211 * Max number of open files set to 10032
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 2.8.4 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in stand alone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 7
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
[7] 09 Sep 14:23:50.211 # Server started, Redis version 2.8.4
[7] 09 Sep 14:23:50.211 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
[7] 09 Sep 14:23:50.211 * The server is now ready to accept connections on port 6379
在myredis容器中,pid1进程没有收到SIGTERM信号。这是因为pid1的进程
/bin/sh
没有对SIGTERM信号的处理逻辑,所以它忽略了SIGTERM信号。当docker等待stop命令执行超过10s以后,docker daemon发送SIGKILL信号强制杀死sh进程。
孤儿进程和僵尸进程
如果子进程退出了,但是父进程没有进行
wait()
系统调用来回收子进程,那么子进程就会成为僵尸进程。
在myredis2中。exec方式,没有创建/bin/sh
首先在
myredis2
容器中启动一个bash进程,并创建子进程
sleep 1000
[email protected]:~# docker restart myredis_exec
myredis_exec
[email protected]:~# docker exec -it myredis_exec bash
[email protected]:/# sleep 1000
在另一个终端窗口,查看当前进程,我们可以发现一个
sleep
进程是
bash
进程的子进程。
[email protected]:~# docker exec myredis_exec ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:30 ? 00:00:00 /usr/bin/redis-server *:6379
root 9 0 0 14:30 pts/0 00:00:00 bash
root 25 9 0 14:30 pts/0 00:00:00 sleep 1000
root 26 0 0 14:31 ? 00:00:00 ps -ef
我们杀死
bash
进程之后查看进程列表,这时候
bash
进程已经被杀死。这时候
sleep
进程(PID为21),虽然已经结束,而且被PID1进程
redis-server
接管,但是其没有被父进程回收,成为僵尸状态。
[email protected]:~# docker exec myredis_exec kill -9 9
[email protected]:~# docker exec myredis_exec ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:30 ? 00:00:00 /usr/bin/redis-server *:6379
root 25 1 0 14:30 ? 00:00:00 [sleep] <defunct>
root 39 0 0 14:31 ? 00:00:00 ps -ef
这是因为PID1进程
redis-server
没有考虑过作为
init
对僵尸子进程的回收的场景。
在myredis中。shell方式,创建了/bin/sh
在用
/bin/sh
作为PID1进程的
myredis
容器中,再启动一个
bash
进程,并创建子进程
sleep 1000
[email protected]:~# docker restart myredis_shell
myredis_shell
[email protected]:~# docker exec -ti myredis_shell bash
[email protected]:/# sleep 1000
查看容器中进程情况
[email protected]:~# docker exec myredis_shell ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:32 ? 00:00:00 /bin/sh -c "/usr/bin/redis-server"
root 7 1 0 14:32 ? 00:00:00 /usr/bin/redis-server *:6379
root 10 0 0 14:32 pts/0 00:00:00 bash
root 25 10 0 14:32 pts/0 00:00:00 sleep 1000
root 26 0 0 14:32 ? 00:00:00 ps -ef
我们杀死
bash
进程之后查看进程列表,发现
bash
和
sleep 1000
进程都已经被杀死和回收
[email protected]:~# docker exec myredis_shell kill -9 10
[email protected]:~# docker exec myredis_shell ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:32 ? 00:00:00 /bin/sh -c "/usr/bin/redis-server"
root 7 1 0 14:32 ? 00:00:00 /usr/bin/redis-server *:6379
root 39 0 0 14:33 ? 00:00:00 ps -ef
这是因为
sh/bash
等应用可以自动清理僵尸进程。
总结:
在创建容器的时候,PID1进程最好要能支持自动清理僵尸/孤儿进程,要能支持SIGTERM信号。
可以在创建PID1进程的时候,添加一个init系统。
例如:
FROM alpine:3.7
...
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--", "COMMAND"]
添加tini系统,这样,tini就是PID1进程。拥有SIGTERM信号支持和自动清理僵尸/孤儿进程的能力。
docker kill
docker kill命令是向容器发送一个SIGKILL信号。