原文
Docker可以帮助用户在容器内部快速自动化部署应用,并利用Linux内核特性命名空间(namespaces)及控制组(cgroups)等为容器提供隔离的运行环境。Docker借助操作系统层的虚拟化实现资源的隔离,因此Docker容器在运行时与虚拟机(VM)的运行有很大的区别,Docker容器与宿主机共享同一个操作系统,不会有额外的操作系统开销。这样的优势很明显,因而大大提高了资源利用率,并且提升了I/O等方面的性能。
Docker的总架构如图1-1所示。架构中主要的模块有:DockerClient、DockerDaemon、Docker Registry、Graph、Driver、libcontainer以及Docker Container。
对用户而言,Docker Client是与Docker Daemon建立通信的最佳途径。用户通过Docker Client发起容器的管理请求,请求最终发往Docker Daemon。
Docker Daemon作为Docker架构中的主体部分,首先具备服务端的功能,有能力接收Docker Client发起的请求;其次具备Docker Client请求的处理能力。Docker Daemon内部所有的任务均由Engine来完成,且每一项工作都以一个Job的形式存在。
Docker Daemon需要完成的任务很多,因此Job的种类也很多。若用户需要下载容器镜像,Docker Daemon则会创建一个名为“pull”的Job,运行时从Docker Registry中下载镜像,并通过镜像管理驱动graphdriver将下载的镜像存储在graph中;若用户需要为Docker容器创建网络环境,Docker Daemon则会创建一个名“allocate_interface”的Job,通过网络驱动networkdriver分配网络接口的资源……
libcontainer是一套独立的容器管理解决方案,这套解决方案涉及了大量Linux内核方面的特性,如:namespaces、cgroups以及capabilities等。libcontainer很好地抽象了Linux的内核特性,并提供完整、明确的接口给Docker Daemon。
当用户执行运行容器这个命令之后,一个Docker容器就处于运行状态,该容器拥有隔离的运行环境、独立的网络栈资源以及受限的资源等。
Docker Client是Docker架构中用户与Docker Daemon建立通信的客户端。在一台安装有Docker的机器上,用户可以使用可执行文件docker作为Docker Client,发起众多Docker容器的管理请求。
Docker Client可以通过以下三种方式和Docker Daemon建立通信,分别为:
tcp://host:port、
unix://path_to_socket
fd://socketfd。
为简单起见,本书主要使用第一种方式作为讲述两者通信的原型。通信方式确定后,DockerClient与Docker Daemon建立连接并传输请求时,可以通过命令行flag参数的形式,设置安全传输层协议(TLS)的有关参数,保证传输的安全性。
Docker Client发送容器管理请求后,请求由Docker Daemon接收并处理,当Docker Client接收到返回的请求响应并做简单处理后,Docker Client一次完整的生命周期就此结束。若需要继续发送容器管理请求,用户必须再次通过可执行文件docker创建Docker Client,并走完以上相同的流程。
Docker Daemon是Docker架构中一个常驻在后台的系统进程。所谓的“运行Docker”,即代表运行Docker Daemon。总之,DockerDaemon的作用主要有以下两方面:
·接收并处理Docker Client发送的请求。
·管理所有的Docker容器。
Docker Daemon运行时,会在后台启动一个Server,Server负责接收Docker Client发送的请求;接收请求后,Server通过路由与分发调度,找到相应的Handler来处理请求。
启动Docker Daemon所使用的可执行文件同样是docker,与Docker Client启动所使用的可执行文件docker相同。既然Docker Client与Docker Daemon都可以通过docker二进制文件创建,那么如何辨别两者就变得非常重要。实际上,执行docker命令时,通过传入的参数可以辨别Docker Daemon与Docker Client,如docker–d代表Docker Daemon的启动,dockerps则代表创建Docker Client,并发送ps请求。
Docker Daemon的架构大致可以分为三部分:Docker Server、Engine和Job。Daemon的架构如图1-2所示。
Engine是Docker架构中的运行引擎,同时也是Docker运行的核心模块。Engine存储着大量的容器信息,同时管理着Docker大部分Job的执行。换言之,Docker中大部分任务的执行都需要Engine协助,并通过Engine匹配相应的Job完成Job的执行。
在Docker源码中,有关Engine的数据结构定义中含有一个名为handlers的对象。该handlers对象存储的是关于众多特定Job各自的处理方式handler。举例说明,Engine的handlers对象中有一项为:{“create”:daemon.ContainerCreate,},则说明当执行名为“create”的Job时,执行的是daemon.ContainerCreate这个handler。
除了容器管理之外,Engine还接管Docker Daemon的某些特定任务。当Docker Daemon遭遇到自身进程需要退出的情况时,Engine还负责完成DockerDaemon退出前的所有善后工作。
Job
Job可以认为是Docker架构中Engine内部最基本的工作执行单元。DockerDaemon可以完成的每一项工作都会呈现为一个Job。例如,在Docker容器内部运行一个进程,这是一个Job;创建一个新的容器,这是一个Job;在网络上下载一个文档,这是一个Job;包括之前在Docker Server部分谈及的,创建Server服务于HTTP协议的API,这也是一个Job,等等。
有关Job接口的设计,与UNIX进程非常相仿。比如说,Job有一个名称,有运行时参数,有环境变量,有标准输入与标准输出,有标准错误,还有返回状态等。
对于Job而言,定义完毕之后,运行才能完成Job自身真正的使命。Job的运行函数Run()则用以执行Job本身。
Docker Registry是一个存储容器镜像(Docker Image)的仓库。容器镜像(Docker Image)是容器创建时用来初始化容器rootfs的文件系统内容。Docker Registry将大量的容器镜像汇集在一起,并为分散的Docker Daemon提供镜像服务。
Docker的运行过程中,有三种情况可能与Docker Registry通信,分别为搜索镜像、下载镜像、上传镜像。这三种情况所对应的Job名称分别为search、pull和push。
不同场景下,Docker Daemon可以使用不同的Docker Registry。公有Registry与私有Regsitry就是两种场景模式不同的Docker Registry。其中,大家熟知的Docker Hub,就是全球范围内最大的公有Registry。Docker可以通过互联网访问Docker Hub,并下载容器镜像;同时Docker也允许用户构建本地私有Registry,使容器镜像的获取在内网完成。
Graph在Docker架构中扮演的角色是容器镜像的保管者。不论是Docker下载的镜像,还是Docker构建的镜像,均由Graph统一化管理。由于Docker支持多种不同的镜像存储方式,如aufs、devicemapper、Btrfs等,故Graph对镜像的存储也会因以上种类而存在一些差异。对Docker而言,同一种类型的镜像被称为一个repository,如名称为ubuntu的镜像都同属一个repository;而同一个repository下的镜像则会因tag存在差异而不同,如ubuntu这个repository下有tag为12.04的镜像,也有tag为14.04的镜像。Docker中Graph的架构如图1-4所示。
Docker的graphdriver负责镜像本地的管理和存储以及对运行中的容器生成镜像等工作。(与registry不同)
Driver是Docker架构中的驱动模块。通过Driver驱动,Docker可以实现对Docker容器运行环境的定制,定制的维度主要有网络环境、存储方式以及容器执行方式。需要注意的是,Docker运行的生命周期中,并非用户所有的操作都是针对Docker容器的管理,同时包括用户对Docker运行信息的获取,还包括Docker对Graph的存储与记录等。因此,为了将仅与Docker容器有关的管理从Docker Daemon的所有逻辑中区分开来,Docker的创造者设计了Driver层来抽象不同类别各自的功能范畴。
Docker Driver的实现可以分为以下三类驱动:graphdriver、networkdriver和execdriver。
graphdriver主要用于完成容器镜像的管理,包括从远程Docker Registry上下载镜像并进行存储,也包括本地构建完镜像后的存储。当用户下载指定的容器镜像时,graphdriver将容器镜像分层存储在本地的指定目录下;同时当用户需要使用指定的容器镜像来创建容器时,graphdriver从本地镜像存储目录中获取指定的容器镜像,并按特定规则为容器准备rootfs;另外,当用户需要通过指定Dockerfile构建全新镜像时,graphdriver会负责新镜像的存储管理。
在graphdriver的初始化过程之前,有4种文件系统或类文件系统的驱动Driver在DockerDaemon内部注册,它们分别是aufs、btrfs、vfs和devmapper。其中,aufs、btrfs以及devmapper用于容器镜像的管理,vfs用于容器volume的管理。Docker在初始化之时,优先通过获取系统环境变量“DOCKER_DRIVER”来提取所使用driver的指定类型。因此,之后所有的Graph操作,都使用该driver来执行。Docker镜像是Docker技术中非常关键的。2014年12月,在Linux 3.18-rc2版本中OverlayFS被合并至Linux内核主线,在Docker 1.4.0版本发布时,Docker官方宣布支持overlay这一类graphdriver,即用户在启动Docker Daemon时可以选择制定graphdriver的类型为overlay。graphdriver的架构如图1-5所示。
networkdriver的作用是完成Docker容器网络环境的配置,其中包括Docker Daemon启动时为Docker环境创建网桥;Docker容器创建前为其分配相应的网络接口资源;以及为Docker容器分配IP、端口并与宿主机做NAT端口映射,设置容器防火墙策略等。networkdriver的架构如图1-6所示。
execdriver作为Docker容器的执行驱动,负责创建容器运行时的命名空间,负责容器资源使用的统计与限制,负责容器内部进程的真正运行等。在Docker 0.9.0版本之前,execdriver只能通过LXC驱动来实现容器的启动管理。实际上,当时Docker通过LXC驱动调用Linux下的LXC工具管理容器的创建,并控制管理容器的生命周期。从Docker 0.9.0开始,在继续支持LXC的情况下,Docker的execdriver默认使用native驱动,native驱动完全独立于LXC,属于Docker项目下第一个全新的子项目,用于容器的创建与管理。Docker默认使用native驱动的具体体现为:Docker Daemon启动过程中加载的ExecDriverflag参数在配置文件中已经被设为native。native这个execdriver的存在,使得Docker对Linux容器的创建与管理有了自己的解决方案。execdriver架构如图1-7所示。
libcontainer是Docker架构中一个使用Go语言设计实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问内核中与容器相关的系统调用。
正是由于libcontainer的存在,Docker可以直接调用libcontainer,而最终操作容器的namespaces、cgroups、apparmor、网络设备以及防火墙规则等。这一系列操作的完成都不需要依赖LXC或者其他包。libcontainer架构如图1-8所示。
图1-8 libcontainer示意图
另外,libcontainer提供了一整套标准的接口来满足上层对容器管理的需求。或者说,libcontainer屏蔽了Docker上层对容器的直接管理。又由于libcontainer使用Go这种跨平台的语言开发实现,且本身又可以被上层多种不同的编程语言访问,因此,很难说未来的Docker一定会与Linux平台紧紧捆绑在一起。Docker Daemon的逻辑完全有可能位于其他非Linux操作系统的平台上,仅仅通过libcontainer的远程调用来实现对Docker容器的管理。另一方面,libcontainer与Docker Daemon的松耦合设计,似乎让用户感受到了除Linux Container之外其他的容器技术接入Docker Daemon的可能性。libcontainer承接Linux内核与Docker Daemon的同时,也让Docker的生态在跨平台方面充满生机。与此同时,Microsoft在其著名云计算平台Azure中,也添加了对Docker的支持,可见Docker的开放程度与业界的火热度。
暂不谈Docker,由于本身完善的功能以及与应用系统的松耦合特性,libcontainer很有可能会在众多其他以容器为原型的平台出现,同时也很有可能催生出云计算领域全新的项目。
Docker Container(Docker容器)是Docker架构中服务交付的最终体现形式。Docker通过DockerDaemon的管理,libcontainer的执行,最终创建Docker容器。Docker容器作为一个交付单位,功能类似于传统意义上的虚拟机(Virtual Machine),具备资源受限、环境与外界隔离的特点。然而,实现手段却与KVM、Xen等传统虚拟化技术大相径庭。
Docker容器的从无到有,涉及Docker利用到的很多技术。总而言之,用户可以根据自己的需求,通过Docker Client向Docker Daemon发送容器的创建与启动请求,请求中将携带容器的配置信息,从而达到定制相应Docker容器的目的。用户对Docker容器的配置有以下4个基本方面:
·通过指定容器镜像,使得Docker容器可以自定义rootfs等文件系统。
·通过指定物理资源的配额,如CPU、内存等,使得Docker容器使用受限的物理资源。
·通过配置容器网络及其安全策略,使得Docker容器拥有独立且安全的网络环境。
·通过指定容器的运行命令,使得Docker容器执行指定的任务。
Docker容器示意图如图1-9所示。