天天看点

docker镜像构建文件系统镜像构建镜像构建容器生命周期docker logsdocker 启停

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等,如下图所示:

docker镜像构建文件系统镜像构建镜像构建容器生命周期docker logsdocker 启停

一般镜像结构如下:

docker镜像构建文件系统镜像构建镜像构建容器生命周期docker logsdocker 启停

在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。

例如用如下命令构建镜像:

docker镜像构建文件系统镜像构建镜像构建容器生命周期docker logsdocker 启停

构建过程如下:

docker镜像构建文件系统镜像构建镜像构建容器生命周期docker logsdocker 启停

构建出来的镜像层,在容器中都只是可读,不可写,也就是说,在容器运行过程中,对镜像进行修改操作时不会修改镜像的,只会修改镜像层以上的容器层。

当容器启动时,会在镜像层上创建一个新的容器层,该层是可读可写层,容器启动以后的结构如下所示:

docker镜像构建文件系统镜像构建镜像构建容器生命周期docker logsdocker 启停

镜像层的数量可能有很多,所有的镜像层联合在一起组成了镜像的文件系统。如果在不同层中,有一个相同路径的文件,那么在容器中,用户只能访问到在上层的文件,不能访问到在下层的该文件。相当于历史层无法访问到。

容器层操作细节

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)。

如下命令的执行顺序:

docker镜像构建文件系统镜像构建镜像构建容器生命周期docker logsdocker 启停

执行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命令覆盖,始终都会执行。

容器的生命周期取决于启动时运行的命令,只要该命令不结束,容器就不会退出。

docker镜像构建文件系统镜像构建镜像构建容器生命周期docker logsdocker 启停

如上所示的命令中,while死循环,命令不会结束,该容器永不退出。

docker exec

docker exec在容器中打开新的终端,并且可以启动新的进程。

docker exec

进入的容器,新建的终端属于daemon的子进程。

例如:

docker镜像构建文件系统镜像构建镜像构建容器生命周期docker logsdocker 启停

这里,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信号。