一、背景
    本文关于Docker架构的分析都是基于Docker的源码与Docker相应版本的运行结果,其中Docker为1.2版本。
    二、Docker架构分析
    在理解Docker源代码的基础上,分析Docker架构。分析过程主要按照以下三个步骤进行:

    • Docker的总体架构图展示
    • Docker架构图内部各模块功能与实现分析
    • Docker运行流程阐述

    三、Docker总体架构图
    Docker对使用者来讲是一个C/S模式的架构,而Docker的后端是一个非常松耦合的架构,模块各司其职,并有机组合,支撑Docker的运行。
    下图是Docker总体架构图:image.png
    图3.1 Docker总架构图,不难看出,用户是使用Docker Client与Docker Daemon建立通信,并发送请求给后者。
    而Docker Daemon作为Docker架构中的主体部分,首先提供Server的功能使其可以接受Docker Client的请求;
    而后Engine执行Docker内部的一系列工作,每一项工作都是以一个Job的形式的存在。Job的运行过程中,当需要容器镜像时,则从Docker Registry中下载镜像,并通过镜像管理驱动graphdriver将下载镜像以Graph的形式存储;当需要Docker创建网络环境时,通过网络管理驱动networkdriver创建并配置Docker容器网络环境;当需要限制Docker容器运行资源或者执行用户指令等操作时,则通过execdriver来完成。
    而libcontainer是一项独立的容器管理包,networkdriver以及execdriver都是通过libcontainer来实现具体对容器进行的操作。当执行完运行容器的命令后,一个实际的Docker容器就处于运行状态,该容器拥有独立的文件系统,独立并安全的运行环境等。
    四、Docker架构内各模块的功能与实现分析
    Docker主要的模块有:Docker Client、Docker Daemon、Docker Registry、Graph、Driver、libcontainer以及Docker container。

    • Docker Client

    Docker Client是Docker架构中用户用来和Docker Daemon建立通信的客户端。用户使用的可执行文件为docker,通过docker命令行工具可以发起众多管理container的请求。
    Docker Client可以通过以下三种方式和Docker Daemon建立通信:tcp://host:port,unix://path_to_socket和fd://socketfd。为了简单起见,本文一律使用第一种方式作为讲述两者通信的原型。与此同时,与Docker Daemon建立连接并传输请求的时候,Docker Client可以通过设置命令行flag参数的形式设置安全传输层协议(TLS)的有关参数,保证传输的安全性。
    Docker Client发送容器管理请求后,由Docker Daemon接受并处理请求,当Docker Client接受到返回的请求响应并简单处理后,Docker Client一次完整的生命周期就结束了。当需要继续发送容器管理请求时,用户必须再次通过docker可执行文件创建Docker Client。

    • Docker Daemon

    Docker Daemon是Docker架构中一个常驻后台的系统进程,功能是:接受并处理Docker Client发送的请求。该守护进程在后台启动了一个Server,Server负责接受Docker Client发送的请求;接受请求后,Server通过路由与分发调度,找到相应的Handler来执行请求。
    Docker Daemon启动所使用的执行文件也为docker,与Docker Client启动所使用的可执行文件docker相同。在docker命令执行时,通过传入的参数来判别Docker Daemon与Docker Client。
    Docker Daemon的架构大致可以分为三个部分:Docker Server、Engine和Job。Daemon架构如图4.1所示。
    image.png

    • Docker Server

    Docker Server在Docker架构中是专门服务于Docker Client的Server。该server的功能是:接受并调度分发Docker Client发送的请求。Docker Server的架构图如图4.2所示。
    image.png
    在Docker的启动过程中,通过gorilla/mux,创建一个mux.Router,提供请求的路由功能。在Golang中,gorilla/mux是一个强大的URL路由器以及调度分发器。该mux.Router中添加了众多的路由项,每一个路由项由HTTP请求方法(PUT、POST、GET或DELETE)、URL、Handler三部分组成。
    若Docker Client通过HTTP的形式访问Docker Daemon,创建完mux.Router之后,Docker将Server的监听地址以及mux.Router作为参数,创建一个httpSrv=http.Server{},最终执行httpSrv.Serve()为请求服务。
    在Server的服务过程中,Server在listener上接受Docker Client的访问请求,并创建一个全新的goroutine来服务该请求。在goroutine中,首先读取请求内容,然后做解析工作,接着找到相应的路由项,随后调用相应的Handler来处理该请求,最后Handler处理完请求之后恢复该请求。
    需要注意的是:Docker Server的运行在Docker的启动过程中,是靠一个名为“serverapi”的Job的运行来完成的。原则上,Docker Server的运行是众多Job中的一个,但是为了强调Docker Server的重要性以及后续Job服务的重要特性,将该“serverapi”的Job单独抽离出来分析,理解为Docker Server。

    • Engine

    Engine是Docker架构中的运行引擎,同时也是Docker运行的核心模块。它扮演Docker container存储仓库的角色,并且通过job的方式来操作管理这些容器。
    在Engine数据结构的设计与实现过程中,有⼀个handler对象。该handler对象存储的都是关于众多特定job的handler处理访问。举例说明,Engine的handler对象中有⼀项为:{“create”: daemon.ContainerCreate,},则说明当名为”create”的job在运⾏时,执⾏的是daemon.ContainerCreate的handler。

    • Job

    ⼀个Job可以认为是Docker架构中Engine内部最基本的⼯作执⾏单元。Docker可以做的每⼀项⼯作,都可以抽象为⼀个job。例如:在容器内部运⾏⼀个进程,这是⼀个job;创建⼀个新的容器,这是⼀个job,从Internet上下载⼀个⽂档,这是⼀个job;包括之前在DockerServer部分说过的,创建Server服务于HTTP的API,这也是⼀个job,等等。
    Job的设计者,把Job设计得与Unix进程相仿。⽐如说:Job有⼀个名称,有参数,有环境变量,有标准的输⼊输出,有错误处理,有返回状态等。

    • Docker Registry

    Docker Registry是⼀个存储容器镜像的仓库。⽽容器镜像是在容器被创建时,被加载⽤来初始化容器的⽂件架构与⽬录。在Docker的运⾏过程中,Docker Daemon会与Docker Registry通信,并实现搜索镜像、下载镜像、上传镜像三个功能,这三个功能对
    应的job名称分别为“serach”、“pull”与 “push”。
    其中,在Docker架构中,Docker可以使⽤公有的Docker Registry,即⼤家熟知的Docker Hub,如此⼀来,Docker获取容器镜像⽂件时,必须通过互联⽹访问;同时Docker也允许⽤户构建本地私有的Docker Registry,这样可以保证容器镜像的获取在内⽹完成。

    • Graph

    Graph在Docker架构中扮演已下载容器镜像的保管者,以及已下载容器镜像之间关系的记录者。⼀⽅⾯,Graph存储着本地具有版本信息的⽂件系统镜像,另⼀⽅⾯也通过GraphDB记录着所有⽂件系统镜像彼此之间的关系。Graph的架构如图4.3。
    image.png
    其中,GraphDB是⼀个构建在SQLite之上的⼩型图数据库,实现了节点的命名以及节点之间关联关系的记录。它仅仅实现了⼤多数图数据库所拥有的⼀个⼩的⼦集,但是提供了简单的接⼝表⽰节点之间的关系。
    同时在Graph的本地⽬录中,关于每⼀个的容器镜像,具体存储的信息有:该容器镜像的元数据,容器镜像的⼤⼩信息,以及该容器镜像所代表的具体rootfs。

    • Driver

    Driver是Docker架构中的驱动模块。通过Driver驱动,Docker可以实现对Docker容器执⾏环境的定制。由于Docker运⾏的⽣命周期中,并⾮⽤户所有的操作都是针对Docker容器的管理,另外还有关于Docker运⾏信息的获取,Graph的存储与记录等。因此,为了将Docker容器的管理从Docker Daemon内部业务逻辑中区分开来,设计了Driver层驱动来接管所有这部分请求。
    在Docker Driver的实现中,可以分为以下三类驱动:graphdriver、networkdriver和execdriver。
    graphdriver主要⽤于完成容器镜像的管理,包括存储与获取。即当⽤户需要下载指定的容器镜像时,graphdriver将容器镜像存储在本地的指定⽬录;同时当⽤户需要使⽤指定的容器镜像来创建容器的rootfs时,graphdriver从本地镜像存储⽬录中获取指定的容器镜像。
    在graphdriver的初始化过程之前,有4种⽂件系统或类⽂件系统在其内部注册,它们分别是aufs、btrfs、vfs和devmapper。⽽Docker在初始化之时,通过获取系统环境变量”DOCKER_DRIVER”来提取所使⽤driver的指定类型。⽽之后所有的graph操作,都使⽤该driver来执⾏。
    graphdrvier的架构如图4.4所示:
    image.png
    networkdriver的⽤途是完成Docker容器⽹络环境的配置,其中包括Docker启动时为Docker环境创建⽹桥;Docker容器创建时为其创建专属虚拟⽹卡设备;以及为Docker容器分配IP、端⼝并与宿主机做端⼝映射,设置容器防⽕墙策略等。networkdriver的架构如图4.5:image.png
    execdriver作为Docker容器的执⾏驱动,负责创建容器运⾏命名空间,负责容器资源使⽤的统计与限制,负责容器内部进程的真正运⾏等。在execdriver的实现过程中,原先可以使⽤LXC驱动调⽤LXC的接⼝,来操纵容器的配置以及⽣命周期,⽽现在execdriver默认使⽤native驱动,不依赖于LXC。具体体现在Daemon启动过程中加载的ExecDriverflag参数,该参数在配置⽂件已经被设为”native”。这可以认为是Docker在1.2版本上⼀个很⼤的改变,或者说Docker实现跨平台的⼀个先兆。execdriver架构如图4.6:
    image.png

    • libcontainer

    libcontainer是Docker架构中⼀个使⽤Go语⾔设计实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问内核中与容器相关的API。
    正是由于libcontainer的存在,Docker可以直接调⽤libcontainer,⽽最终操纵容器的namespace、cgroups、apparmor、⽹络设备以及防⽕墙规则等。这⼀系列操作的完成都不需要依赖LXC或者其他包。libcontainer架构如图4.7:
    image.png
    另外,libcontainer提供了⼀整套标准的接⼝来满⾜上层对容器管理的需求。或者说,libcontainer屏蔽了Docker上层对容器的直接管理。⼜由于libcontainer使⽤Go这种跨平台的语⾔开发实现,且本⾝⼜可以被上层多种不同的编程语⾔访问,因此很难说,未来的Docker就⼀定会紧紧地和Linux捆绑在⼀起。⽽于此同时,Microsoft在其著名云计算平台Azure中,也添加了对Docker的⽀持,可见Docker的开放程度与业界的⽕热度。暂不谈Docker,由于libcontainer的功能以及其本⾝与系统的松耦合特性,很有可能会在其他以容器为原型的平台出现,同时也很有可能催⽣出云计算领域全新的项⽬。

    • Docker container

    Docker container(Docker容器)是Docker架构中服务交付的最终体现形式。
    Docker按照⽤户的需求与指令,订制相应的Docker容器:

    1. ⽤户通过指定容器镜像,使得Docker容器可以⾃定义rootfs等⽂件系统;
    2. ⽤户通过指定计算资源的配额,使得Docker容器使⽤指定的计算资源;
    3. ⽤户通过配置⽹络及其安全策略,使得Docker容器拥有独⽴且安全的⽹络环境;
    4. ⽤户通过指定运⾏的命令,使得Docker容器执⾏指定的⼯作。

    Docker容器⽰意图如图4.8:
    image.png
    五、Docker运行案例分析
    上⼀章节着重于Docker架构中各个部分的介绍。本章的内容,将以串联Docker各模块来简要分析,分析原型为Docker中的docker pull与docker run两个命令。

    • docker pull

    docker pull命令的作⽤为:从Docker Registry中下载指定的容器镜像,并存储在本地的Graph中,以备后续创建Docker容器时的使⽤。
    docker pull命令执⾏流程如图5.1。
    image.png
    如图,图中标记的红⾊箭头表⽰docker pull命令在发起后,Docker所做的⼀系列运⾏。以下逐⼀分析这些步骤。

    1. Docker Client接受docker pull命令,解析完请求以及收集完请求参数之后,发送⼀个HTTP请求给Docker Server,HTTP请求⽅法为POST,请求URL为”/images/create? “+”xxx”;
    2. Docker Server接受以上HTTP请求,并交给mux.Router,mux.Router通过URL以及请求⽅法来确定执⾏该请求的具体handler;
    3. mux.Router将请求路由分发⾄相应的handler,具体为PostImagesCreate;
    4. 在PostImageCreate这个handler之中,⼀个名为”pull”的job被创建,并开始执⾏;
    5. 名为”pull”的job在执⾏过程中,执⾏pullRepository操作,即从Docker Registry中下载相应的⼀个或者多个image;
    6. 名为”pull”的job将下载的image交给graphdriver;
    7. graphdriver负责将image进⾏存储,⼀⽅创建graph对象,另⼀⽅⾯在GraphDB中记录image之间的关系。
    • docker run

    docker run命令的作⽤是在⼀个全新的Docker容器内部运⾏⼀条指令。Docker在执⾏这条命令的时候,所做⼯作可以分为两部分:第⼀,创建Docker容器所需的rootfs;第⼆,创建容器的⽹络等运⾏环境,并真正运⾏⽤户指令。因此,在整个执⾏流程中,Docker Client给Docker Server发送了两次HTTP请求,第⼆次请求的发起取决于第⼀次请求的返回状态。Docker run命令执⾏流程如图5.2。
    image.png
    如图,图中标记的红⾊箭头表⽰docker run命令在发起后,Docker所做的⼀系列运⾏。以下逐⼀分析这些步骤。

    1. Docker Client接受docker run命令,解析完请求以及收集完请求参数之后,发送⼀个HTTP请求给Docker Server,HTTP请求⽅法为POST,请求URL为”/containers/create? “+”xxx”;
    2. Docker Server接受以上HTTP请求,并交给mux.Router,mux.Router通过URL以及请求⽅法来确定执⾏该请求的具体handler;
    3. mux.Router将请求路由分发⾄相应的handler,具体为PostContainersCreate;
    4. 在PostImageCreate这个handler之中,⼀个名为”create”的job被创建,并开始让该job运⾏;
    5. 名为”create”的job在运⾏过程中,执⾏Container.Create操作,该操作需要获取容器镜像来为Docker容器创建rootfs,即调⽤graphdriver;
    6. graphdriver从Graph中获取创建Docker容器rootfs所需要的所有的镜像;
    7. graphdriver将rootfs所有镜像,加载安装⾄Docker容器指定的⽂件⽬录下;
    8. 若以上操作全部正常执⾏,没有返回错误或异常,则Docker Client收到Docker Server返回状态之后,发起第⼆次HTTP请求。请求⽅法为”POST”,请求URL为”/containers/”+container_ID+”/start”;
    9. Docker Server接受以上HTTP请求,并交给mux.Router,mux.Router通过URL以及请求⽅法来确定执⾏该请求的具体handler;
    10. mux.Router将请求路由分发⾄相应的handler,具体为PostContainersStart;
    11. 在PostContainersStart这个handler之中,名为”start”的job被创建,并开始执⾏;
    12. 名为”start”的job执⾏完初步的配置⼯作后,开始配置与创建⽹络环境,调⽤networkdriver;
    13. networkdriver需要为指定的Docker容器创建⽹络接⼝设备,并为其分配IP,port,以及设置防⽕墙规则,相应的操作转交⾄libcontainer中的netlink包来完成;
    14. netlink完成Docker容器的⽹络环境配置与创建;
    15. 返回⾄名为”start”的job,执⾏完⼀些辅助性操作后,job开始执⾏⽤户指令,调⽤execdriver;
    16. execdriver被调⽤,初始化Docker容器内部的运⾏环境,如命名空间,资源控制与隔离,以及⽤户命令的执⾏,相应的操作转交⾄libcontainer来完成;
    17. libcontainer被调⽤,完成Docker容器内部的运⾏环境初始化,并最终执⾏⽤户要求启动的命令。

    六、总结
    本⽂从Docker 1.2的源码⼊⼿,分析抽象出Docker的架构图,并对该架构图中的各个模块进⾏功能与实现的分析,最后通过两个docker命令展⽰了Docker内部的运⾏。
    通过对Docker架构的学习,可以全⾯深化对Docker设计、功能与价值的理解。同时在借助Docker实现⽤户定制的分布式系统时,也能更好地找到已有平台与Docker较为理想的契合点。另外,熟悉Docker现有架构以及设计思想,也能对云计算PaaS领域带来更多的启发,催⽣出更多实践与创新。