Docker原理

来源:神州数码企业服务集团

Linux 容器 (LXC)

在典型的虚拟化环境中,一台或多台虚拟机运行在一个物理服务器之上,这些物理服务器又使用着像 Xen、Hyper-V 等管理程序。而容器运行在操作系统内核之上,我们可以将其称为操作系统级虚拟化。
在深入了解底层容器概念之前,我们需要了解两个关键的 Linux 概念:
用户空间(user space):运行用户程序(应用程序、进程)所需的所有代码被称为用户空间。当启动程序操作时,例如创建一个文件,用户空间中的进程会向内核空间发出系统调用。
内核空间(kernel space):这里有着与系统硬件、存储等交互的内核代码,是操作系统的核心。

容器是一个进程

当我们启动一个应用程序,比如说 Nginx Web 服务器,这个时候实际上是在启动一个进程。而进程本身是一个具有有限隔离的自包含指令。
那么如果我们仅使用进程运行和操作所需的文件与配置来隔离进程,又会怎样呢?
而这正是容器的工作原理。
容器基本上是一个具有足够隔离用户空间组件的进程,因此它给人一种独立操作系统的感觉。
父容器进程可能有一个子进程,所以可以说,一个容器也是一组进程。

当我们启动一个 Nginx 服务时,它会启动一个父 Nginx 进程,然后父进程跨越进它的子进程,比如存管理器、缓存加载器和工作器。
因此,当启动一个 Nginx 容器时,我们正在其隔离的环境中启动一个主 Nginx 进程。

每个容器都有其独立的用户空间,我们可以在单个主机上运行多个容器。这是否意味着容器拥有整个操作系统?
当然不是。与拥有自己内核的虚拟机不同,容器只包含与特定发行版相关的所需文件,并使用共享主机内核。
有意思的是,我们可以在共享相同内核空间的单个主机上运行基于不同 Linux 发行版的容器。例如,我们可以在 Ubuntu 服务器上运行 RHEL、CentOS、基于 SUSE 的容器。这是可行的,因为对于所有 Linux 发行版,只有用户空间不同,内核空间相同。

docker 基于 Linux 内核的底层实现

Linux 有两个重要的内核功能,分别是命名空间(namespaces)控制组(control groups)。在主机中,容器与容器之间的隔离正是由这两个内核功能实现的。
为了更好的理解 Linux 容器的基本概念,我们可以想象现实生活中的公寓大楼。虽然公寓楼是一个大的整体,但是在大楼内部,公寓与公寓之间是相互隔离的,每个公寓都有自己的水、电、气。我们用钢筋、水泥这些建筑材料来实现这种隔离。除非被邀请进入他人的公寓,我们是不能非法闯入他人的房屋的。
而一个主机就好比这样的公寓大楼,容器就是楼中的一个个公寓。每个容器有自己的 CPU、内存、IP 地址、挂载点、进程,这就好比每个公寓自己的水电气。而实现容器与容器间隔离的 “钢筋水泥” 正是命名空间和控制组。

Linux 命名空间(NameSpace)

创建容器就是创建一个隔离良好的环境来运行服务(进程)。为了达到这种级别的隔离,容器应该要有自己的文件系统、IP 地址、挂载点、进程 ID 等,这就需要使用 Linux 命名空间来实现这一点。
命名空间为容器设置了边界,让容器拥有了自己的挂载点、用户、IP 地址、以及进程管理等,并且在该命名空间内运行的进程在其他命名空间中没有任何权限。
我们知道 Linux 中的 PID、IPC、网络等资源是全局的,而 NameSpace 机制是一种资源隔离方案,在该机制下这些资源就不再是全局的了,而是属于某个特定的 NameSpace,各个 NameSpace 下的资源互不干扰,这就使得每个 NameSpace 看上去就像一个独立的操作系统一样,但是只有 NameSpace 是不够的。

Linux 控制组(Control groups)

虽然有了 NameSpace 技术可以实现资源隔离,但进程还是可以不受控的访问系统资源,比如 CPU、内存、磁盘、网络等。
当我们启动服务时,我们不会自己指定划分任何内存或者 CPU 限制,而是让内核来为服务划分优先级和分配资源。
但是,我们可以使用 Linux 的内核功能控制组 CGroups,为我们自己的服务设置明确的 CPU 和内存限制。但它不是一种很直接的方法,我们还需要进行一些额外的配置和调整。
由于我们可以在主机内运行多个容器,因此应该有一种机制来限制资源的使用、设备的访问等。而这里正是控制组的用武之地。
Linux 控制组管理着容器使用的资源,我们可以限制容器的 CPU、内存、网络和 IO 资源。

那么如果我们不限制容器的 CPU 和内存资源又会怎样呢?
如果不限制,单个容器可能最终会占用所有的主机资源,无资源可用就会导致其他容器的崩溃。
为了控制容器中进程对资源的访问,Docker 采用 control groups 技术就可以控制容器中进程对系统资源的消耗了,比如你可以限制某个容器使用内存的上限、可以在哪些 CPU 上运行等等。
像 Docker 这样的工具抽象出了所有复杂的后端配置,从而让我们只需要简单地设定参数,就可以指定这些资源限制。
有了这两项技术,容器看起来就真的像是独立的操作系统了。

什么是 Docker?

Docker 是一个流行的用 Go 语言开发的开源项目,由 Dotcloud 公司开发。
Docker 基本上就是一个容器引擎,它使用 Linux 内核功能(如命名空间和控制组)在操作系统之上创建容器。
这就意味着,Docker 可以十分轻易地完成我们在 LXC 部分中所学到的所有有关容器的概念和功能 。只需执行一些 Docker 命令,我们就可以启动和运行一个容器。
在这里我们可能会产生一些困惑, Docker 与 Linux 容器 (LXC) 又有什么不同呢?毕竟两者的概念和实现看起来是那么的相似。

Docker 与容器的区别

Docker 最初是构建在 Linux 容器 (LXC) 之上的,但后来 Docker 用自己的容器运行时 libcontainer(现在是 runc 的一部分)替换了 LXC。
此外,除了作为一种容器技术之外,Docker 还具有定义明确的包装器组件,这使打包应用程序变得十分容易。要知道,在 Docker 出现之前,运行容器并不是一件容易的事。所有的这一切意味着,Docker 通过将所有应用程序系统的需求打包到容器中,完成了将应用程序与基础设施分离的所有工作。
例如,如果有一个 Java jar 文件,我们可以在任何安装了 java 的服务器上运行它。同样的,一旦使用 Docker 将容器与所需的应用程序打包在一起,我们就可以在任何其他安装了 Docker 的主机上运行它。

Docker 优点

Docker 有一个高效的工作流程,可以将应用程序从开发环境转移到测试环境、再转移到生产环境。当我们在实际完成将应用程序打包到 Docker 映像的一个操作时,会对它有更深的了解。
启动 docker 容器非常快速,只需不到一秒钟,而且 Docker 可以在任何具有兼容 Linux 内核的主机上运行。(也支持 Windows)
Docker 使用写时复制联合文件系统( Copy-on-write union file system )来存储图像。每当对容器进行更改时,只会使用写时复制模型将更改写入磁盘。使用写时复制,我们优化了所有容器的共享存储层。
Docker 最大的特点是协作。Docker 镜像可以推送到存储库,也可以下拉到任何其他主机以从该镜像运行容器。
此外,Docker hub 拥有由用户创建的数千个镜像,我们可以根据、需求将这些镜像拉取到我们自己的主机。此外,它主要用于容器编排工具,如 kubernetes。

Docker 的核心架构

当 Docker 最初推出时,它具有单体架构,现在它分为以下三个不同的组成部分:

  1. Docker引擎 (dockerd)
  2. docker-containerd (containerd)
  3. docker-runc (runc)

Docker引擎 (dockerd)

Docker 引擎由 docker daemon、API 接口和 Docker CLI 组成。
Docker daemon (dockerd) 作为 dockerd systemd 服务持续运行,它负责构建 docker 镜像。
dockerd 调用 docker-containerd 接口,来管理图像和运行容器。

docker-containerd (containerd)

containerd 是另一个系统 daemon 程序服务,它负责下载 docker 镜像并将它们作为容器运行。containerd 通过公开 API 来接收来自 dockerd 服务的指令。

docker-runc

runc 是容器运行时,负责创建容器所需的命名空间和 cgroup,然后在这些命名空间内运行容器命令。runc 运行时是按照 OCI 规范实现的。

containerd 和 runc 有什么区别?

containerd 负责管理容器,runc 负责使用来自 containerd 的输入,来运行容器(在容器内创建命名空间、cgroup 和运行命令)

3.6、Docker 是如何工作的?

Docker 组件

Docker 由以下几个组件组成:

  1. Docker 守护进程 (dockerd)
  2. Docker 客户端
  3. Docker 镜像
  4. Docker 注册表
  5. Docker 容器

下面是官方的高级 docker 架构图,展示了常见的 Docker 工作流程。

Docker 工作流程

Docker 守护进程 (dockerd)

Docker 具有客户端 - 服务器架构。Docker Daemon (dockerd) 或服务器负责与容器相关的所有操作。
守护进程通过 CLI 或 REST API 从 Docker 客户端接收命令。Docker 客户端可以与守护进程位于同一主机上,也可以位于任何其他主机上。
在默认情况下,docker 守护进程监听 docker.sock UNIX 套接字。如果有任何远程访问 docker API 的用例,则需要通过主机端口公开它。一个这样的用例我们将 Docker 作为 Jenkins 代理运行
如果我们想在 docker 中运行 docker,可以使用主机中的 docker.sock 。

Docker 引擎和 Docker 守护进程有什么区别?

Docker 引擎由 docker 守护进程、rest 接口和 docker CLI 组成。Docker 守护进程是 systemd dockerd 服务,负责构建 docker 镜像,并将 docker 指令发送到 containerd 运行时。

Docker 镜像

镜像是 Docker 的基本组成部分,我们需要镜像来运行 Docker 容器。镜像包含操作系统库、依赖项和运行应用程序的工具。
为了创建容器,我们可以用应用程序依赖项,来预先构建镜像。

例如,如果要将 Nginx Web 服务器作为一个 Ubuntu 容器来运行,则需要使用 Nginx 二进制文件和运行 Nginx 所需的所有操作系统库,来创建 Docker 镜像。

Docker 有一个用于构建镜像的概念—— Dockerfile 。Dockerfile 基本上就是一个文本文件,每一行包含一个命令(指令)。
以下是一个 Dockerfile 的示例:

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

Docker 镜像是用分层的方式来组织的。Dockerfile 上的每条指令都会在镜像中添加一个图层。
镜像最顶层的可写层是容器。镜像的顶层是可写的,并由正在运行的容器使用。图像中的其他层是只读的。
每个镜像都是从一个基础镜像创建起的。基础镜像可以是父镜像,也可以是从父镜像构建的镜像。

例如,如果我们可以使用 Ubuntu 的基本镜像,并在其中创建另一个包含 Nginx 应用程序的镜像。

那么这个基础镜像(父镜像)又来自哪里呢?有 docker 实用程序可以创建初始父基本镜像。基本上它需要所需的操作系统库并将其创建到基本镜像中。我们不必这样做,因为我们将获得所有 Linux 发行版的官方基础映像。

Docker 注册表

Docker 注册表是 Docker 镜像的存储库。使用 Docker 注册表,我们可以共享镜像。Docker 注册表充当了 Docker 镜像的中央存储库。
注册中心可以是公共的,也可以是私有的。Docker Inc 提供了一项称为 Docker Hub 的托管注册表服务。它允许用户从一个中心位置上传和下载镜像。
Docker hub 的作用类似于 git,我们可以在本地构建镜像,然后镜像提交推送到 Docker Docker hub 上。
默认情况下,当安装 docker 时,除非在 Docker 设置中指定自定义注册表,否则它会从公共的 Docker Hub 中查找镜像。
在企业网络 / 项目中使用 docker 时,请设置自己的 docker 注册表,而不是使用公共的 docker hub。所有云提供商都有自己的容器注册服务。

Docker 容器

Docker 容器是 Docker 的执行环境。容器是从镜像创建的,它属于镜像的可写层。
我们可以将应用程序打包到一个容器中,提交它并使其成为黄金映像(golden image),进而从中构建更多容器。
容器可以启动、停止、提交和终止。如果我们在未提交的情况下终止容器,所有的容器更改都将丢失。
理想情况下,容器被视为不可变的对象,不建议对正在运行的容器进行更改,我们仅出于测试目的对正在运行的容器进行更改。

为什么容器比虚拟机好?

资源利用和成本

我们可以使用虚拟机独立运行应用程序,这里所谓的独立运行是指一台虚拟器运行一个服务。但是虚拟机仍然可能未被充分利用。此外,为应用程序调整虚拟机的大小并不是一件容易的事。
另一方面,容器可以在非常小的 CPU 和内存上运行。此外,您甚至可以在 VM 内运行多个容器以进行应用程序隔离。另外,调整容器大小需要几秒钟。

配置和部署

取决于所涉及的工作流,配置虚拟机并在其上部署应用程序可能需要几分钟到几小时的时间,即使回滚也需要时间。
但是我们却可以在几秒钟内部署一个容器,并在几秒钟内将其回滚。

漂移管理

虚拟机中的漂移管理并不容易。我们需要拥有成熟的自动化和流程,以确保所有环境都相似。遵循不可变的部署模型可避免虚拟机环境中的漂移。
对于容器,一旦镜像生成,那么它在所有环境中都是一样的。要进行任何更改时,我们需要开始在 dev env 中进行更改并重新生成容器映像。

,