天天看点

Docker - 这应该就是你想要的Docker架构分析

Docker - 这应该就是你想要的Docker架构分析

原文地址

  • ​​Docker:架构分解​​

Docker和传统虚拟化方式的不同之处

下面的图片比较了Docker和传统虚拟化方式的不同之处,可见容器是在操作系统层面上实现虚拟化,直接复用本地主机的操作系统,而传统方式则是在硬件层面实现。

Docker - 这应该就是你想要的Docker架构分析
Docker - 这应该就是你想要的Docker架构分析

Docker三个基本概念

  • 镜像(​

    ​Image​

    ​):Docker镜像就是一个只读的模板。比如,一个镜像可以包含一个完整的Ubuntu操作系统环境。镜像可以用来创建Docker容器。另外Docker提供了一个很简单的机制来创建镜像或者更新现有镜像,用户甚至可以直接从其他人那里下载一个已经做好的镜像来直接使用。
  • 容器(​

    ​Container​

    ​​):镜像(​

    ​Image​

    ​​)和容器(​

    ​Container​

    ​​)的关系,就像是程序和进程一样,镜像是静态的定义,容器则是动态的定义,是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。每个容器都是相互隔离的、保证安全的平台。可以把容器看做是一个简易版的Linux环境(包括​

    ​root​

    ​用户权限、进程空间、用户空间和网络空间等)和运行在其中的应用程序。 下图从顶层设计层面展示了镜像和容器间的关系。一旦容器从镜像上启动后,二者之间就变成了互相依赖的关系,并且在镜像上启动的容器全部停止之前,镜像是无法被删除的。尝试删除镜像而不停止或销毁使用它的容器,会导致出错。
  • 仓库(​

    ​Repository​

    ​​):仓库是集中存放镜像文件的场所。有时候会把仓库和仓库注册服务器(Registry)混为一谈, 并不严格区分。实际上,仓库注册服务器上往往存放着多个仓库,每个仓库中又包含了多个镜像,每个镜像又有不同的标签(​

    ​tag​

    ​​),如下图所示。

    仓库分为公开仓库(Public)和私有仓库(Private)两种形式。最大的公开仓库是Docker Hub, 存放了数量庞大的镜像供用户下载。国内的公开仓库包括Docker Pool等,可以提供大陆用户更稳定快速的访问。当然,用户也可以在本地网络内创建一个私有仓库。当用户创建了自己的镜像之后,就可以使用​​

    ​push​

    ​​命令将它上传到公有或者私有仓库,这样下次在另外一台机器上使用这个镜像时,只需要从仓库上​

    ​pull​

    ​下来就可以了。

Docker架构分解

Docker对使用者来讲是一个C/S模式的架构。Docker架构图:

Docker - 这应该就是你想要的Docker架构分析

从上图可以知道,用户是通过Docker Client与Docker Daemon建立通信,并发送请求给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 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架构如下图:

Docker - 这应该就是你想要的Docker架构分析

Docker Server

Docker Server在Docker架构中是专门服务于Docker Client的​

​server​

​​。该​

​server​

​的功能是:接受并调度分发Docker Client发送的请求。

在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的启动过程中,是靠一个名为​

​serveapi​

​​的Job的运行来完成的。原则上,Docker Server的运行是众多Job中的一个,但是为了强调Docker Server的重要性以及方便后续展开Job服务的重要特性,将该名为​

​serveapi​

​的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;包括之前在Docker Server部分说过的,创建Server服务于HTTP的API,这也是一个Job,等等。

Job的设计者,把Job设计得与Unix进程相仿。比如说:Job有一个名称,有参数,有环境变量,有标准的输入输出,有错误处理,有返回状态等。

Docker Registry

Docker Registry是一个存储容器镜像的仓库。而容器镜像是在容器被创建时,被加载用来初始化容器的文件架构与目录。

在Docker的运行过程中,Docker Daemon会与Docker Registry通信,并实现搜索镜像、下载镜像、上传镜像三个功能,这三个功能对应的Job名称分别为​

​search​

​​,​

​pull​

​​与​

​push​

​。

其中,在Docker架构中,Docker可以使用公有的Docker Registry,即大家熟知的Docker Hub,如此一来,Docker获取容器镜像文件时,必须通过互联网访问Docker Hub;同时Docker也允许用户构建本地私有的Docker Registry,这样可以保证容器镜像的获取在内网完成。

Graph

Graph在Docker架构中扮演已下载容器镜像的保管者,以及已下载容器镜像之间关系的记录者。一方面,Graph存储着本地具有版本信息的文件系统镜像,另一方面也通过GraphDB记录着所有文件系统镜像彼此之间的关系。Graph的架构如下图:

Docker - 这应该就是你想要的Docker架构分析

其中,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​

    ​​来执行。​

    ​graphdriver​

    ​的架构如下图:
  • ​networkdriver​

    ​​的用途是完成Docker容器网络环境的配置,其中包括Docker启动时为Docker环境创建网桥;Docker容器创建时为其创建专属虚拟网卡设备;以及为Docker容器分配IP、端口并与宿主机做端口映射,设置容器防火墙策略等。​

    ​networkdriver​

    ​的架构如下图:
  • ​execdriver​

    ​​作为Docker容器的执行驱动,负责创建容器运行命名空间,负责容器资源使用的统计与限制,负责容器内部进程的真正运行等。在​

    ​execdriver​

    ​​的实现过程中,原先可以使用LXC驱动调用LXC的接口,来操纵容器的配置以及生命周期,而现在​

    ​execdriver​

    ​​默认使用​

    ​native​

    ​​驱动,不依赖于LXC。具体体现在Daemon启动过程中加载的​

    ​ExecDriverflag​

    ​​参数,该参数在配置文件已经被设为​

    ​native​

    ​​。这可以认为是Docker在​

    ​1.2​

    ​​版本上一个很大的改变,或者说是Docker实现跨平台的一个先兆。​

    ​execdriver​

    ​架构如下图:

libcontainer

​libcontainer​

​是Docker架构中一个使用Go语言设计实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问内核中与容器相关的API。

正是由于​

​libcontainer​

​​的存在,Docker可以直接调用​

​libcontainer​

​​,而最终操纵容器的​

​namespace​

​​、​

​cgroups​

​​、​

​apparmor​

​​、网络设备以及防火墙规则等。这一系列操作的完成都不需要依赖LXC或者其他包。​

​libcontainer​

​架构如下图:

Docker - 这应该就是你想要的Docker架构分析

另外,​

​libcontainer​

​​提供了一整套标准的接口来满足上层对容器管理的需求。或者说,​

​libcontainer​

​​屏蔽了Docker上层对容器的直接管理。又由于​

​libcontainer​

​使用Go这种跨平台的语言开发实现,且本身又可以被上层多种不同的编程语言访问,因此很难说,未来的Docker就一定会紧紧地和Linux捆绑在一起。而于此同时,Microsoft在其著名云计算平台Azure中,也添加了对Docker的支持,可见Docker的开放程度与业界的火热度。

暂不谈Docker,由于​

​libcontainer​

​的功能以及其本身与系统的松耦合特性,很有可能会在其他以容器为原型的平台出现,同时也很有可能催生出云计算领域全新的项目。

Docker container

​Docker container​

​(Docker容器)是Docker架构中服务交付的最终体现形式。

Docker按照用户的需求与指令,订制相应的Docker容器。

用户通过指定容器镜像,使得Docker容器可以自定义​

​rootfs​

​等文件系统; 用户通过指定计算资源的配额,使得Docker容器使用指定的计算资源; 用户通过配置网络及其安全策略,使得Docker容器拥有独立且安全的网络环境; 用户通过指定运行的命令,使得Docker容器执行指定的工作。

Docker - 这应该就是你想要的Docker架构分析