docker镜像构建

文件系统

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:shellmyredis: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

root@ubuntu-desktop:~# docker run -d --name myredis_shell myredis:shell
128fdc38cdccc222234ee2c2bc3a4c84355bc97a33e66ddf53c580b86cfe298e
root@ubuntu-desktop:~# 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,并没有其他子进程存在。

root@ubuntu-desktop:~# docker run -d --name myredis_exec myredis:exec
ffd77fef180740ef199281747e372c33d6e2df06d9edbf11a03c0d30caf61fa3
root@ubuntu-desktop:~# 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容器。

root@ubuntu-desktop:~# docker stop myredis_exec
myredis_exec
root@ubuntu-desktop:~# 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容器。

root@ubuntu-desktop:~# docker stop myredis_shell 
myredis_shell
root@ubuntu-desktop:~# 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

root@ubuntu-desktop:~# docker restart myredis_exec 
myredis_exec
root@ubuntu-desktop:~# docker exec -it myredis_exec bash
root@ffd77fef1807:/# sleep 1000

在另一个终端窗口,查看当前进程,我们可以发现一个sleep进程是bash进程的子进程。

root@ubuntu-desktop:~# 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接管,但是其没有被父进程回收,成为僵尸状态。

root@ubuntu-desktop:~# docker exec myredis_exec kill -9 9
root@ubuntu-desktop:~# 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

root@ubuntu-desktop:~# docker restart myredis_shell
myredis_shell
root@ubuntu-desktop:~# docker exec -ti myredis_shell bash
root@128fdc38cdcc:/# sleep 1000

查看容器中进程情况

root@ubuntu-desktop:~# 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进程之后查看进程列表,发现bashsleep 1000进程都已经被杀死和回收

root@ubuntu-desktop:~# docker exec myredis_shell kill -9 10
root@ubuntu-desktop:~# 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信号。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值