Docker 面试题

109

Docker 在 Linux 上使用以下几个命名空间

命名空间 (namespaces) 是 Linux 为我们提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法

当我们运行(docker run 或者 docker start)一个 Docker 容器时,Docker 会为该容器设置一系列的 namespaces,这些 namespaces 提供了一层隔离,容器的各个方面都在单独的 namespace 中运行,并且对其的访问仅限于该 namespace。

  • pid namespace:用于进程隔离(PID:进程ID)

  • net namespace:管理网络接口(NET:网络)

  • ipc namespace:管理对 IPC 资源的访问(IPC:进程间通信(信号量、消息队列和共享内存))

  • mnt namespace:管理文件系统挂载点(MNT:挂载)

  • uts namespace:隔离主机名和域名

  • user namespace:隔离用户和用户组(3.8以后的内核才支持)

CGroups

我们通过 Linux 的 namespaces 技术为新创建的进程隔离了文件系统、网络、进程等资源,但是 namespaces 并不能够为我们提供物理资源上的隔离,比如 CPU、内存、IO 或者网络带宽等,所以如果我们运行多个容器的话,则容器之间就会抢占资源互相影响了,所以对容器资源的使用进行限制就非常重要了,而 Control Groups(CGroups)技术就能够隔离宿主机上的物理资源。

  • 在 CGroup 中,所有的任务就是一个系统的一个进程,而 CGroup 就是一组按照某种标准划分的进程,在 CGroup 这种机制中,所有的资源控制都是以 CGroup 作为单位实现的,每一个进程都可以随时加入一个 CGroup 也可以随时退出一个 CGroup。

CGroup 具有以下几个特点:       

  • CGroup 的 API 以一个伪文件系统(/sys/fs/cgroup/)的实现方式,用户的程序可以通过文件系统实现 CGroup 的组件管理

  • CGroup 的组件管理操作单元可以细粒度到线程级别,用户可以创建和销毁 CGroup,从而实现资源载分配和再利用

  • 所有资源管理的功能都以子系统(cpu、cpuset 这些)的方式实现,接口统一子任务创建之初与其父任务处于同一个 CGroup 的控制组

另外 CGroup 具有四大功能:        

  • 资源限制:可以对任务使用的资源总额进行限制

  • 优先级分配:通过分配的 cpu 时间片数量以及磁盘 IO 带宽大小等,实际上相当于控制了任务运行优先级

  • 资源统计:可以统计系统的资源使用量,如 cpu 时长,内存用量等

  • 任务控制:cgroup 可以对任务执行挂起、恢复等操作

UnionFS

Linux 的命名空间和控制组分别解决了不同资源隔离的问题,前者解决了进程、网络以及文件系统的隔离,后者实现了 CPU、内存等资源的隔离,但是在 Docker 中还有另一个非常重要的问题需要解决 - 也就是镜像。

镜像到底是什么,它又是如何组成和组织的呢?而这其中最重要的概念就是镜像层(Layers)(如下图)的概念,而镜像层依赖于一系列的底层技术,比如文件系统(filesystems)、写时复制(copy-on-write)、联合挂载(union mounts)等。

Docker 镜像是由一系列的层组成的,每层代表 Dockerfile 中的一条指令,比如下面的 Dockerfile 文件:

FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py

Dockerfile 是一个文本文件,其内包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。Dockerfile 是我们用来构建 Docker 镜像的一个说明文档.

这里的 Dockerfile 包含4条命令,其中每一行就创建了一层,FROM 语句从 ubuntu:18.04 这个基础镜像创建一个层开始,COPY 命令从 Docker 客户端的当前目录添加一些新的文件,RUN 指令使用 make 命令构建应用,最后一层指定在容器中运行什么命令。

镜像就是由这些层一层一层堆叠起来的,镜像中的这些层都是只读的,当我们运行容器的时候,就可以在这些基础层之上添加新的可写层,也就是我们通常说的容器层,对于运行中的容器所做的所有更改(比如写入新文件、修改现有文件、删除文件)都将写入这个容器层

容器和镜像之间的主要区别就是容器在镜像顶部由一个可写层,在容器中的所有操作都会存储在这个容器层中,删除容器后,容器层也会被删除,但是镜像不会变化。正因为每个容器都有自己的可写容器层,所有更改都存储在自己的容器层中,所以多个容器之间可以共享同一基础镜像的访问,但仍然具有自己的数据状态。

Docker 使用存储驱动程序来管理镜像层和可写容器层的内容,每个存储驱动程序的处理方式不同,但是所有的驱动都使用可堆叠的镜像层和写时复制(Cow)策略,这些驱动程序管理的这些层其实就是 UnionFS(联合文件系统),现在 Docker 主要支持的存储驱动有 aufs、devicemapper、overlay、overlay2、zfs 和 vfs 等等,在新的 Docker 版本中,overlay2 取代了 aufs 成为了推荐的存储驱动。

写时复制是一种共享和复制文件的策略,可以最大程度地提高效率,如果文件或目录位于镜像的较低层中,而另一层(包括可写层)需要对其进行读取访问,则它直接使用现有文件即可。另一层第一次需要修改文件时(在构建镜像或运行容器时),将文件复制到该层并进行修改。这样可以将 I/O 和每个后续层的大小最小化。

Docker 架构

  • Docker 使用 C/S (客户端/服务器)体系的架构,Docker 客户端与 Docker 守护进程(Dockerd)通信,Docker 守护进程负责构建,运行和分发 Docker 容器。Docker 客户端和守护进程可以在同一个系统上运行,也可以将 Docker 客户端连接到远程 Docker 守护进程。Docker 客户端和守护进程使用 REST API 通过 UNIX 套接字或网络接口进行通信。

DockerFile

ADD & COPY 有什么区别

  • COPY 指令和 ADD 指令的唯一区别在于是否支持从远程 URL 获取资源。COPY 指令只能从执行docker build所在的主机上读取资源并复制到镜像中。而 ADD 指令还支持通过 URL 从远程服务器读取资源并复制到镜像中。

  • 一般来说满足同等功能的情况下,推荐使用COPY指令。ADD 指令更擅长读取本地 tar 文件并解压缩。

  • COPY 指令能够将构建命令所在的主机本地的文件或目录,复制到镜像文件系统。COPY 指令同样也支持 exec 和 shell 两种格式

  • ADD 指令不仅能够将构建命令所在的主机本地的文件或目录,而且能够将远程 URL 所对应的文件或目录,作为资源复制到镜像文件系统。所以,可以认为 ADD 是增强版的 COPY,支持将远程 URL 的资源加入到镜像的文件系统。同样也支持 exec 和 shell 两种格式用法

不过需要注意的是对于从远程 URL 获取资源的情况,由于 ADD 指令不支持认证,如果从远程获取资源需要认证,则只能使用RUN wget 或 RUN curl 替代了。

CMD 与 ENTRYPOINT 指令

  • 尽管 ENTRYPOINT 和 CMD 都是在容器里执行一条命令, 但是他们有一些微妙的区别,在绝大多数情况下, 你只要在这2者之间选择一个调用就可以,但是我们还是非常有必要来认真了解下二者的区别。

CMD 指令

  • CMD 指令是容器启动以后,默认的执行命令,需要重点理解下这个默认的含义,意思就是如果我们执行 docker run 没有指定任何的执行命令或者 Dockerfile 里面也没有指定 ENTRYPOINT,那么就会使用 CMD 指定的执行命令执行了。这也说明了 ENTRYPOINT 才是容器启动以后真正要执行的命令。

  • 所以我们经常遇到 CMD 会被覆盖 的情况,为什么会被覆盖呢?主要还是因为 CMD 的定位就是默认,如果不额外指定,那么才会执行 CMD 命令,但是如果我们指定了的话那就不会执行 CMD 命令了,也就是说 CMD 会被覆盖。

  • CMD 总共有三种用法:

CMD ["executable", "param1", "param2"]  # exec 形式
CMD ["param1", "param2"] # 作为 ENTRYPOINT 的默认参数
CMD command param1 param2  # shell 形式

ENTRYPOINT 指令

  • 根据官方定义来说 ENTRYPOINT 才是用于定义容器启动以后的执行程序的,允许将镜像当成命令本身来运行(用 CMD 提供默认选项),从名字也可以理解,是容器的入口。ENTRYPOINT 一共有两种用法:

ENTRYPOINT ["executable", "param1", "param2"] (exec 形式)
ENTRYPOINT command param1 param2 (shell 形式)
  • 对应命令行 exec 模式,也就是带中括号的。和 CMD 的中括号形式是一致的,但是这里貌似是在shell的环境下执行的,与cmd有区别。如果 run 命令后面有执行命令,那么后面的全部都会作为 ENTRYPOINT 的参数。如果 run 后面没有额外的命令,但是定义了 CMD,那么 CMD 的全部内容就会作为 ENTRYPOINT 的参数,这同时是上面我们提到的 CMD 的第二种用法。所以说 ENTRYPOINT 不会被覆盖。当然如果要在 run 里面覆盖,也是有办法的,使用--entrypoint参数即可。

数据共享与持久化

  • 数据卷(Data Volumes)

  • 挂载主机目录 (Bind mounts)

数据卷

数据卷是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:

  • 数据卷可以在容器之间共享和重用

  • 对数据卷的修改会立马生效

  • 对数据卷的更新,不会影响镜像

  • 数据卷默认会一直存在,即使容器被删除

挂载主机目录

  • Docker 同样支持把宿主机上的目录挂载到容器中,同样可以使用 -v 或者 --mount 参数来进行挂载.

区别

不同之处在于 volume 是 docker 自身管理的目录中的子目录,所以不存在权限引发的挂载的问题,并且目录路径是 docker 自身管理的,所以也不需要在不同的服务器上指定不同的路径,你不需要关心路径。它们之间的主要区别有如下几点:

  1. volume 会引起 docker 目录膨胀,因为既要存镜像,又要存 volume,最好不要放在系统盘,将 docker 的安装目录配置到其他更大的挂载盘

  2. 两者有一个不同的行为:当容器外的对应目录是空的,volume 会先将容器内的内容拷贝到容器外目录,而 mount 会将外部的目录覆盖容器内部目录

  3. volume 还有一个不如 bind mount 的地方,不能直接挂载文件,例如挂载 nginx 容器的配置文件:nginx.conf