本节书摘来华章计算机《容器技术系列》一书中的第1章 ,第1.3节,孙宏亮 著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1.3 Docker各模块功能与实现分析
下面我们将从Docker的总架构图入手,抽离出架构内的各个模块,并对各个模块进行更为细化的架构分析与功能阐述。
1.3.1 Docker Client
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,并走完以上相同的流程。
1.3.2 Docker Daemon
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所示。
- Docker Server
Docker Server在Docker架构中专门服务于Docker Client,它的功能是接收并调度分发Docker Client发送的请求。Docker Server的架构如图1-3所示。
在Docker Daemon的启动过程中,DockerServer第一个完成。Docker Server通过包gorilla/mux,创建了一个mux.Router路由器,提供请求的路由功能。在Go语言中,gorilla/mux是一个强大的URL路由器以及调度分发器。创建路由器之后,Docker Server为mux.Router中添加有效的路由项,每一个路由项由HTTP请求方法(PUT、POST、GET或DELETE)、URL和Handler三部分组成。
由于Docker Client通过HTTP协议访问Docker Daemon,故DockerServer创建完mux.Router之后,将Server的监听地址以及mux.Router作为参数,创建一个httpSrv=http.Server{},最终执行httpSrv.Serve()开始服务于外部请求。
在服务过程中,Docker Server在listener上接收Docker Client的访问请求。对于每一个Docker Client请求,DockerServer均会创建一个全新的goroutine来服务。在goroutine中,Docker Server首先读取请求内容,然后做请求解析工作,接着匹配相应的路由项,随后调用相应的Handler来处理,最后Handler处理完请求之后给Docker Client回复响应。
需要注意的是:Docker Server在Docker的启动过程中运行,通过一个名为“serveapi”的Job来实现。理论上,Docker Server的运行只是众多Job中的一个,但是为了强调Docker Server的重要性以及它为后续Job服务的重要特性,本书将“serveapi”的Job单独抽离出来分析,理解为Docker Server。
- Engine
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本身。
1.3.3 Docker Registry
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,使容器镜像的获取在内网完成。
1.3.4 Graph
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所示。
本书对Graph以及容器镜像(Docker Image)的分析将以aufs为主,并在第8章进行深入分析。
1.3.5 Driver
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所示。
1.3.6 libcontainer
libcontainer是Docker架构中一个使用Go语言设计实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问内核中与容器相关的系统调用。
正是由于libcontainer的存在,Docker可以直接调用libcontainer,而最终操作容器的namespaces、cgroups、apparmor、网络设备以及防火墙规则等。这一系列操作的完成都不需要依赖LXC或者其他包。libcontainer架构如图1-8所示。
另外,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很有可能会在众多其他以容器为原型的平台出现,同时也很有可能催生出云计算领域全新的项目。
1.3.7 Docker Container
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容器执行指定的任务。