Docker 技术原理与实践

Docker 技术原理与实践介绍。

Docker 简介

  Docker 是基于容器技术的开源轻量级虚拟化解决方案,诞生于 2013 年初,基于 Google 公司推出的 Go 语言实现。Docker 是容器引擎,把 Linux 的 Cgroup、Namespace 等容器底层技术进行封装抽象,为用户提供了创建和管理容器的便捷界面(包括命令行和 API)。

27-1

容器与虚拟机

  虚拟机在本质上就是在模拟一台真实的计算机设备,同时遵循同样的程序执行方式。虚拟机能够利用“虚拟机管理程序”运行在物理设备之上。反过来,虚拟机管理程序则可运行在主机设备或者“裸机”之上。

  运行在主机设备上的虚拟机(当然,需要配合虚拟机管理程序)通常被称为一套“客户机”。这套客户机容纳有应用程序及其运行所必需的各类组件(例如系统二进制文件及库)。它同时还包含有完整的虚拟硬件堆栈,其中包括虚拟网络适配器、存储以及CPU——这意味着它也拥有自己的完整访客操作系统。着眼于内部,这套客户机自成体系并拥有专用资源。而从外部来看,这套虚拟机使用的则是由主机设备提供的共享资源。

27-2

  与提供硬件虚拟化机制的虚拟机不同,容器通过对“用户空间”的抽象化处理提供操作系统层级的虚拟化机制。容器与虚拟机间的最大区别在于,各容器系统共享主机系统的内核。

容器的优势与不足
  1. 容器与虚拟机比较:
特性 容器 虚拟机
启动 秒级 分钟级
硬盘使用 一般为 MB 一般为 GB
性能 接近原生 弱于原生
系统支持量 单机支持上千个容器 一般几十个
  1. 容器的优势
  • 更快速的交付和部署:开发人员可以使用镜像快速的构建标准开发环境;开发完成后,测试和运维人员可以使用开发人员提供的 Docker 镜像快速部署应用,可以避免开发和测试运维人员之间的环境差异导致的部署问题。
  • 更高效的资源利用:Docker 容器的运行不需要额外的虚拟化管理程序支持,它是内核级的虚拟化,在占用更少资源的情况实现更高的性能。
  • 更方便的迁移和扩展:Docker 容器几乎可以在任意的平台上运行,包括物理机、虚拟机、公有云、私有云、服务器等。这种兼容使得用户可以在不同的平台之间很方便的完成应用迁移。
  • 更简单的更新管理。使用 Dockerfile,只需要小小的配置修改,就可以替代以往大量的更新工作,并且所有修改都以增量方式进行分发和更新。
  1. 容器的不足
  • 资源隔离方面不如虚拟机,Docker 是利用 Cgroup 实现资源限制的,只能限制资源消耗的最大值,而不能隔绝其他程序占用自己的资源。
  • 安全性问题。Docker 目前并不能分辨具体执行指令的用户,只要一个用户拥有执行 Docker 的权限,那么他就可以对 Docker 的容器进行所有操作,不管该容器是否是由该用户创建。
  • Docker 目前还在版本的快速更新中,细节功能调整比较大。一些核心模块依赖于高版本内核,存在版本兼容问题。
Docker 基本概念
  • 镜像:操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而Docker 镜像(Image) ,就相当于是一个 root 文件系统。

  Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等) 。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

  • 容器:镜像(Image ) 和容器(Container ) 的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

  容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。

  • 仓库:集中的存储、分发镜像的服务(Docker Registry)。通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。

  Docker 仓库分为公有和私有。最常使用的公有仓库是官方的 Docker Hub,这也是默认的仓库,并拥有大量的高质量的官方镜像。用户还可以搭建私有 Docker 仓库。Docker 官方提供了Docker 仓库镜像,可以直接使用做为私有仓库服务。

27-3

Docker 结构引擎

  Docker是一个客户端/服务器(C/S)架构的程序,Docker客户端只需向Docker服务器或守护进程(有时也称为Docker引擎)发出请求,服务器或守护进程将完成所有工作并返回结果。

  • Server,就是一个守护进程,它会一直运行在后台;
  • REST API,说明如何与server交互和指示它执行命令;
  • Client,是客户写指令的地方,也俗称shell;
  • Network,俗称网络,容器通过暴露端口与主机端口绑定,达到接受来自主机的信号;
  • Volume,俗称外挂,为了能够持久化数据以及共享容器间的数据,Docker提出了Volume的概念。

27-4

Docker 运行流程
  1. 拉取镜像,若本地已经存在该镜像,则不用到网上去拉取;
  2. 创建新的容器;
  3. 分配union文件系统并且挂着一个可读写的层,任何修改容器的操作都会被记录在这个读写层上,你可以保存这些修改成新的镜像,也可以选择不保存,那么下次运行改镜像的时候所有修改操作都会被消除;
  4. 分配网络\桥接接口,创建一个允许容器与本地主机通信的网络接口;
  5. 设置ip地址,从池中寻找一个可用的ip地址附加到容器上,换句话说,localhost并不能访问到容器;
  6. 运行程序;
  7. 捕获并且提供应用输出,包括输入、输出、报错信息。

27-5

Docker 核心技术

命名空间(Namespaces)

27-6

  命名空间是 Linux 提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法。当服务器上启动了多个服务时,这些服务其实会相互影响的,每一个服务都能看到其他服务的进程,也可以访问宿主机器上的任意文件,我们希望运行在同一台机器上的不同服务能做到完全隔离,就像运行在多台不同的机器上一样。

  Linux 的命名空间机制提供了七种不同的命名空间:

  • PID 命名空间。不同用户的进程通过 PID 命名空间隔离,且不同命名空间中可以有相同 PID 。所有的 LXC 进程在 Docker 中的父进程为 Docker 进程,每个 LXC 进程具有不同的命名空间。同时由于允许嵌套,因此可以很方便的实现嵌套的 Docker 容器。
  • NET 命名空间。 网络隔离是通过 NET 命名空间实现的,每个 NET 命名空间有独立的网络设备,IP 地址,路由表,/proc/net 目录。Docker 默认采用 veth 的方式,将容器中的虚拟网卡同 host 上的一 个Docker 网桥 docker0 连接在一起。
  • IPC 命名空间。容器中进程交互采用 Linux 常见的进程间通信方法(IPC),包括信号量、消息队列和共享内存等。然而与 VM 不同的是,容器的进程间交互实际上还是 host 上具有相同 PID 命名空间中的进程间交互,因此需要在 IPC 资源申请时加入命名空间信息,每个 IPC 资源有一个唯一的 32 位 ID。

  此外,还有:挂载命名空间、UTS 命名空间、用户命名空间。

  Docker 其实就通过 Linux 的 Namespaces 对不同的容器实现了隔离。每个容器有自己单独的命名空间,运行在其中的应用都像是在独立的操作系统中运行一样。

控制组(Control groups)

27-7

  通过 Linux 的命名空间为新创建的进程隔离了文件系统、网络并与宿主机器之间的进程相互隔离,但是命名空间并不能够为我们提供物理资源上的隔离,比如 CPU 或者内存。

  如果其中的某一个容器正在执行 CPU 密集型的任务,那么就会影响其他容器中任务的性能与执行效率,导致多个容器相互影响并且抢占资源。如何对多个容器的资源使用进行限制就成了解决进程虚拟资源隔离之后的主要问题,而 Control Groups(简称 CGroups)就是能够隔离宿主机器上的物理资源,例如 CPU、内存、磁盘 I/O 和网络带宽。

27-8

  Cgroups 是Linux 内核的一个特性,主要用来对共享资源进行隔离、限制、审计等。只有能控制分配到容器的资源,才能避免当多个容器同时运行时的对系统资源的竞争。

  Linux 的 Cgroups 能够为一组进程分配资源,即 CPU、内存、网络带宽等资源,通过对资源的分配,CGroup 能够提供以下的几种功能:

  • 限制资源使用,比如内存使用上限以及文件系统的缓存限制。
  • 优先级控制,通过优先级让一些组优先得到更多到CPU等资源。
  • 一些审计或一些统计,主要目的是为了计费。
  • 挂起进程,恢复执行进程。

27-9

  每一个 Cgroup 都是一组被相同的标准和参数限制的进程,不同的 Cgroup 之间是有层级关系的,它们之间可以从父类继承一些用于限制资源使用的标准和参数。

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

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

联合文件系统(Union File Systems)

  当镜像被 docker run 命令创建时就会在镜像的最上层添加一个可写的层,也就是容器层,所有对于运行时容器的修改其实都是对这个容器读写层的修改。

27-10

  容器和镜像的区别就在于,所有的镜像都是只读的,而每一个容器其实等于镜像加上一个可读写的层,也就是同一个镜像可以对应多个容器。

27-11

  AUFS(Advanced UnionFS)是 UnionFS 的升级版,它能够将不同文件夹中的层联合到同一个文件夹中,这些文件夹在 AUFS 中称作分支,整个联合的过程被称为联合挂载(Union Mount):

27-12

  所有镜像层和容器层的内容都存储在 /var/lib/docker/aufs/diff/ 目录中。而 /var/lib/docker/aufs/layers/ 中存储着镜像层的元数据,每一个文件都保存着镜像层的元数据,最后的 /var/lib/docker/aufs/mnt/ 包含镜像或者容器层的挂载点,最终会被 Docker 通过联合的方式进行组装。

27-13

  每一个镜像层都是建立在另一个镜像层之上的,同时所有的镜像层都是只读的,只有每个容器最顶层的容器层才可以被用户直接读写,所有的容器都建立在一些底层服务(Kernel)上。这种容器的组装方式提供了非常大的灵活性,只读的镜像层通过共享也能够减少磁盘的占用。

Linux 网络虚拟化

  每一个使用 docker run 启动的容器其实都具有单独的网络命名空间,Docker 提供了四种不同的网络模式:

27-14

  如果 Docker 的容器通过 Linux 的命名空间完成了与宿主机进程的网络隔离,但是却有没有办法通过宿主机的网络与整个互联网相连,就会产生很多限制,所以 Docker 虽然可以通过命名空间创建一个隔离的网络环境,但是 Docker 中的服务仍然需要与外界相连才能发挥作用。

  Docker 通过 Linux 的命名空间实现了网络的隔离,又通过 iptables 进行数据包转发,让 Docker 容器能够优雅地为宿主机器或者其他容器提供服务。

  网桥模式下,除了分配隔离的网络命名空间之外,Docker 还会为所有的容器设置 IP 地址。当 Docker 服务器在主机上启动之后会创建新的虚拟网桥 docker0,随后在该主机上启动的全部服务在默认情况下都与该网桥相连。

27-15

  网桥是在内核中虚拟出来的,可以将主机上真实的物理网卡,或虚拟的网卡桥接上来。桥接上来的网卡就相当于网桥上的端口。 端口收到的数据包都提交给这个虚拟的网桥,让其进行转发。

  在默认情况下,每一个容器在创建时都会创建一对虚拟网卡,两个虚拟网卡组成了数据的通道,其中一个会放在创建的容器中,会加入到名为 docker0 网桥中。docker0 会为每一个容器分配一个新的 IP 地址并将 docker0 的 IP 地址设置为默认的网关。网桥 docker0 通过 iptables 中的配置与宿主机器上的网卡相连,所有符合条件的请求都会通过 iptables 转发到 docker0 并由网桥分发给对应的机器。

  参考资料:

[1] Docker 核心技术与实现原理

[2] Docker 原理篇

[3] Docker — 从入门到实践

[4] Docker入门学习笔记

坚持原创技术分享,您的支持将鼓励我继续创作!