虽然Kubernetes 在 v1.20 版本之后将不支持 Docker作为容器运行时Container Runtime(Docker的多层封装和调用,导致其在可维护性上略逊一筹),但是现在Docker还是Kubernetes默认的容器运行时(Container Runtime),在使用为 Kubernetes 创建的Container Runtime Interface(CRI)的运行时,Docker 生成的镜像可以继续在你的集群中与所有CRI实现一起工作。后续可以切换至与之兼容的容器运行时containerd、CRI-O等。
为什么要使用Docker而不是其它虚拟化技术?
- 容器不需要进行硬件虚拟以及运行完整操作系统等额外开销,Docker 对系统资源的利用率更高。无论是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。
- Docker 容器应用直接运行于宿主内核,无需启动完整的操作系统,可以做到秒级启动。
- Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性。
- Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。
- Docker 确保了执行环境的一致性,使得应用的迁移更加容易。
- Docker 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也非常简单。
Docker Engine简介
Docker Engine 可以通过Docker Desktop在macOS和Windows10上使用,也可以使用静态二进制在各种Linux上使用。
Docker Engine作为一个client-server应用:
- 一个长期运行的守护进程服务端
dockerd
。Docker守护进程 (dockerd) 监听Docker API的请求和管理Docker对象,比如镜像,容器,网络,数据卷。守护进程并且可以与其他守护进程通信去管理Docker服务。 - Docker API,通过指定接口Docker客户端能与 Docker守护进程进行通信,使守护程序完成了构建、运行和分发Docker容器等工作。.
- 命令行界面(CLI, command line interface) 客户端
docker
。docker
是docker用户与docker交互的一个主要途径,当你使用命令(比如docker run
), 客户端发送这些命令至守护进程dockerd。docker命令使用 Docker API。Docker客户端可以和多个守护进程通信。
Docker Engine版本
Docker Engine分为 CE 和 EE 两大版本。CE 即社区版(免费,支持周期 7 个月),EE 即企业版,强调安全,付费使用,支持周期 24 个月。
Docker CE 分为 stable, test, 和 nightly 三个更新通道。每六个月发布一个 stable 版本:
Stable为您提供一般可用性(GA, general availability)的最新版本。年-月发布是从与主分支的发布分支生成的
test 提供在GA之前准备好进行测试的预发布。
Nightly提供下一个主要版本的最新内容。
Docker组件
docker client
docker client是一个客户端工具,将用户的请求发送给 docker daemon(dockerd)
dockerd
dockerd是docker守护进程(Docker daemon)。是对容器相关操作的api的最上层封装,将请求发送给 containerd 。
/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
containerd
containerd 是一个守护进程,它管理容器生命周期,提供了在一个节点上执行容器和管理镜像的最小功能集。dockerd操作实际调用的还是containerd的api接口(gRPC方式实现),containerd是dockerd和runc之间交流的一个中间组件。
containerd-shim
containerd-shim 是一个是容器的运行时载体,允许runc在创建&运行容器之后退出,每启动一个容器都会启动一个新的containerd-shim进程,他直接通过指定的三个参数:容器id,boundle目录(容器的文件系统和一个 config.json文件)来调用runc的api创建一个容器(比如创建容器:最后拼装的命令如下:runc create )。用containerd-shim作为容器的父进程,而不是直接用containerd作为容器的父进程,是为了防止当containerd挂掉的时候导致容器失败。runC 启动完容器后本身会直接退出,containerd-shim 则会成为容器进程的父进程,负责收集容器进程的状态,上报给 containerd,并在容器中 pid 为 1 的进程退出后接管容器中的子进程进行清理,确保不会出现僵尸进程,这样就不需要containerd来wait子进程。
containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/274f1a80e3946d74354a449954874ae47e4b5dbcb1313966e82d33826ddee3a7 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc -systemd-cgroup
runc
runc是一个命令行工具端,他根据oci(开放容器组织, Open Container Initiative)的标准来创建和运行容器。具体参考:https://www.jianshu.com/p/2f6296190049
创建一个docker容器的步骤
- docker cli发送创建容器命令至Docker daemon;
- Docker daemon 仍然不能帮我们创建容器,转而请求 containerd 创建一个容器;
- containerd 收到请求后,并不会自己直接去操作容器,而是创建一个叫做 containerd-shim 的进程,让 containerd-shim 去操作容器。这是因为容器进程需要一个父进程来做诸如收集状态,维持 stdin 等 fd 打开等工作。而假如这个父进程就是 containerd,那每次 containerd 挂掉或升级,整个宿主机上所有的容器都得退出了。而引入了 containerd-shim 就规避了这个问题(containerd 和 shim 并不是父子进程关系);
- 我们知道创建容器需要做一些设置 namespaces 和 cgroups,挂载 root filesystem 等等操作,而这些事该怎么做已经有了公开的规范了,那就是 OCI(Open Container Initiative,开放容器标准)。它的一个参考实现叫做 runC。于是,containerd-shim 在这一步需要调用 runC 这个命令行工具,来启动容器;
- runC 启动完容器后本身会直接退出,containerd-shim 则会成为容器进程的父进程,负责收集容器进程的状态,上报给 containerd,并在容器中 pid 为 1 的进程退出后接管容器中的子进程进行清理,确保不会出现僵尸进程。
查看相关组件版本
[ ~]# docker version
Client: Docker Engine - Community
Version: 19.03.12
API version: 1.40
Go version: go1.13.10
Git commit: 48a66213fe
Built: Mon Jun 22 15:46:54 2020
OS/Arch: linux/amd64
Experimental: false
Server: Docker Engine - Community
Engine:
Version: 19.03.12
API version: 1.40 (minimum version 1.12)
Go version: go1.13.10
Git commit: 48a66213fe
Built: Mon Jun 22 15:45:28 2020
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.2.13
GitCommit: 7ad184331fa3e55e52b890ea95e65ba581ae3429
runc:
Version: 1.0.0-rc10
GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd
docker-init:
Version: 0.18.0
GitCommit: fec3683</pre>
Docker底层技术
Namespaces
Docker使用namespaces为容器的工作空间做隔离. 当你运行一个容器, Docker 会发该容器创建一系列的namespace。这些namespace提供了一层隔离。一个容器运行在一个单独的命名空间中,并且其访问被限制在当前命名空间内 。
Docker Engine在Linux上使用下列namespace:
- pid namespace: 进程隔离 (PID: Process ID),"chroot"进程树,提供进程隔离能力。PID namespace是划分那些一个进程可以查看并与之交互的PID的方式。当我们创建一个新的PID namespace时,第一个进程的PID会被赋值为1。当该进程退出时,内核会杀死当前namespace内的其他进程。
- net namespace: 管理网络接口 (NET: Networking),提供网络隔离能力。Network Namespace 可以让每个容器拥有自己独立的(虚拟的)网络设备,而且容器内的应用可以绑定到自己的端口,每个 Namespace 内的端口都不会互相冲突。在宿主机上搭建网桥后,就能很方便地实现容器之间的通信,而且不同容器上的应用可以使用相同的端口 。
- The ipc namespace: 管理IPC资源的访问 (IPC: InterProcess Communication),提供进程间通信的隔离能力。
- The mnt namespace: 管理文件系统挂载点 (MNT: Mount),提供磁盘挂载点和文件系统的隔离能力
- The uts namespace: 隔离内核和版本标识符 (UTS: Unix Timesharing System),主要用来完成对容器HOSTNAME和domain的隔离,同时保存内核名称、版本、以及底层体系结构类型等信息。使得用户在容器中查看到的信息是当前容器的系统、版本,不同于主机的,内核通过uts_namespace对当前系统中多个容器的这些信息进行统一管理,每一个容器对应有一个自己的uts_namespace,用来隔离容器的内核名称、版本等信息,不同容器查看到的都是属于自己的信息,相互间不能查看。
Control groups
Docker Engine运行在Linux上也依赖control groups (cgroups). cgroup将应用程序限制为一组特定的资源。 Control groups允许Docker Engine将可用的硬件资源(比如cpu,内存,磁盘和网络IO)共享给容器,并有选择地实施限制和约束。 例如,您可以限制特定容器可用的内存。
Union file systems
Union file systems(或UnionFS)是通过创建layer来操作文件系统,使其非常轻便且快速。 Docker Engine使用UnionFS为容器提供基本组成块(building blocks)。 Docker Engine可以使用多个UnionFS变体,包括aufs,fuse-overlayfs,btrfs,zfs,vfs,devicemapper和overlay2(当前推荐)。
-
overlay2
is the preferred storage driver, for all currently supported Linux distributions, and requires no extra configuration. -
aufs
was the preferred storage driver for Docker 18.06 and older, when running on Ubuntu 14.04 on kernel 3.13 which had no support foroverlay2
. -
fuse-overlayfs
is preferred only for running Rootless Docker on a host that does not provide support for rootlessoverlay2
. On Ubuntu and Debian 10, thefuse-overlayfs
driver does not need to be usedoverlay2
works even in rootless mode. See Rootless mode documentation. -
devicemapper
is supported, but requiresdirect-lvm
for production environments, becauseloopback-lvm
, while zero-configuration, has very poor performance.devicemapper
was the recommended storage driver for CentOS and RHEL, as their kernel version did not supportoverlay2
. However, current versions of CentOS and RHEL now have support foroverlay2
, which is now the recommended driver. - The
btrfs
andzfs
storage drivers are used if they are the backing filesystem (the filesystem of the host on which Docker is installed). These filesystems allow for advanced options, such as creating “snapshots”, but require more maintenance and setup. Each of these relies on the backing filesystem being configured correctly. - The
vfs
storage driver is intended for testing purposes, and for situations where no copy-on-write filesystem can be used. Performance of this storage driver is poor, and is not generally recommended for production use.
storage driver所支持的操作系统
对于Docker Engine - Community,仅测试了一些配置,并且你的操作系统的内核可能不支持每一个storage driver。一般来说, 以下配置适用于最新版本的Linux发行版:
Linux distribution | Recommended storage drivers | Alternative drivers |
---|---|---|
Docker Engine - Community on Ubuntu | overlay2 or aufs (for Ubuntu 14.04 running on kernel 3.13) | overlay¹, devicemapper², zfs, vfs |
Docker Engine - Community on Debian | overlay2 (Debian Stretch), aufs or devicemapper (older versions) | overlay¹, vfs |
Docker Engine - Community on CentOS | overlay2 | overlay¹, devicemapper², zfs, vfs |
Docker Engine - Community on Fedora | overlay2 | overlay¹, devicemapper², zfs, vfs |
¹) The overlay
storage driver is deprecated, 将在未来的版本中移除。 推荐用户将其迁移至overlay2。
²) The devicemapper
storage driver is deprecated, 将在未来的版本中移除。推荐用户将其迁移至overlay2。
storage drivers所支持的文件系统
Storage driver | Supported backing filesystems |
---|---|
overlay2, overlay | xfs with ftype=1, ext4 |
fuse-overlayfs | any filesystem |
aufs | xfs, ext4 |
devicemapper | direct-lvm |
btrfs | btrfs |
zfs | zfs |
vfs | any filesystem |
根据工作负载选型
除其他事项外,每个存储驱动都有其自己的性能特征,这使其或多或少地适合于不同的工作负载。考虑以下概括:
- overlay2, aufs以及overlay都是操作于文件层而不是块(block)层。它使用内存更效率, 但是在写密集型write-heavy工作负载下容器的可写层可能会增长很大。
- 块层Block-level存储驱动,比如devicemapper, btrfs和zfs在写密集型write-heavy workloads的情况下表现的更好 (虽然不如Docker volumes一样好)。
- 对于大量的小型写入,容器包含大量的层或者深层文件系统 , overlay也许性能更优于overlay2,但是会使用更多的inode,这将导致inode耗尽。
- btrfs和zfs 需要大量的内存。
- zfs对于高密集high-density工作负载型(比如 PaaS)是一个好的选择
当前使用的storage driver以及backing filesystem,可以使用docker info
查看
[ ~]# docker info
Server Version: 19.03.12
Storage Driver: overlay2
Backing Filesystem: xfs
Supports d_type: true
Native Overlay Diff: true
Cgroup Driver: systemd
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
devicemapper与overlayFS
devicemapper
devicemapper
驱动使用专用于Docker的块设备,并操作于块级别而非文件级别。可以通过向Docker主机添加物理存储来扩展这些设备,并且它们的性能要好于在操作系统(OS)级别上使用文件系统devicemapper
要求lvm2
和device-mapper-persistent-data
已经安装。且其loop-lvm
模式仅使用于配置direct-lvm
模式之前的测试工作。生产环境使用devicemapper
存储驱动一定要使用direct-lvm
模式。 direct-lvm
模式将使用块设备创建thin pool。这比loop-lvm
模式使用回环设备(loopback devices)更快、能更有效的使用系统资源以及其块设备能按需增长。
当你使用devicemapper
驱动启动Docker时,其与镜像和容器层相关的所有对象都存储在/var/lib/docker/devicemapper/目录中,这些对象由一个或多个块级设备支持,而不是回环设备(loopback devices)(仅测试)或物理磁盘。
基本设备(base device)是最低级别的对象,这就是thin pool本身,你可以使用docker info查看它。 它包含一个文件系统。 此基本设备是每个镜像和容器层的起点。 基本设备是
Device Mapper
实现细节,而不是Docker层。关于基本设备和每个镜像、容器层的元数据使用JSON格式存储在/var/lib/docker/devicemapper/metadata/ 。这些层都是写时复制copy-on-write快照, 这意味着它们是空的,直到它们脱离其父层。
-
每个容器的可写层都挂载在/var/lib/docker/devicemapper/mnt/。每个只读镜像层和每个停止的容器都存在一个空目录。
每个镜像层都是其下一层的快照。 每个镜像的最低层是基本设备的快照,基本设备存在于池中。 当你运行容器时,它是容器所基于的镜像的快照。 以下示例显示了两个正在运行的容器的Docker主机。 第一个是ubuntu容器,第二个是busybox容器。
OverlayFS
如果你要使用OverlayFS
, 那应该使用overlay2
而不是overlay
驱动, 因为overlay2
的inode利用率更高。 为了使用overlay2
, 你的内核版本应该是 4.0或以上, RHEL或CentOS使用内核应该是 3.10.0-514或以上。
下图是OverlayFS是如何将Docker镜像和Docker容器分层。容器层是lowerdir
,容器层是upperdir
,统一视图通过merged
目录显示,该目录实际上是容器的挂载点。 该图显示了Docker构造如何映射到OverlayFS构造。
Container format
Docker Engine将namespaces, control groups和UnionFS组合到一个称为容器格式的包装器中。 默认容器格式为libcontainer。 将来,Docker可以通过与BSD Jails或Solaris Zones等技术集成来支持其他容器格式。