本文首发于我的公众号码农之屋(id: Spider1818),专注于干货分享,包含但不限于Java编程、网络技术、Linux内核及实操、容器技术等。欢迎大家关注,二维码文末可以扫。
导读:Docker对用户来讲是一个C/S模式的架构,而Docker的后端是一个非常松耦合的架构。
一、Docker总架构
Docker对用户来讲是一个C/S模式的架构,而Docker的后端是一个非常松耦合的架构。Docker的总体架构如图1所示,模块各司其职,并有机组合,支撑着Docker的运行。
用户使用Docker Client与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的各个模块进行更为详细的介绍。
1、Docker Client
用户使用Docker Client与Docker Daemon建立通信,通过docker命令行工具可以发起众多管理container的请求。与Docker Daemon建立连接并传输请求的时候,Docker Client可以通过设置命令行flag参数的形式设置安全传输层协议参数,保证传输的安全性。
Docker Client发送容器管理请求后,由Docker Daemon接受并处理请求,当Docker Client接收到返回的请求响应并简单处理后,Docker Client一次完整的生命周期就结束了。当需要继续发送容器管理请求时,用户必须再次通过docker可执行文件创建Docker Client。
2、Docker Daemon
Docker Daemon是一个常驻在后台的系统进程,负责接收并处理DockerClient发送的请求。该进程在后台启动了一个Server,用于接收Docker Client发送的请求,收到请求后通过路由与分发调度,找到相应的Handler处理请求。Docker Daemon由Docker Server、Engine和Job组成,具体的架构示意图请参考图2所示。
1)Docker Server
Docker Server用于接收并调度分发Docker Client发送的请求,具体的架构示意图请参考图3所示。
在Docker的启动过程中,通过包gorilla/mux,创建了一个mux.Router,提供请求的路由功能。该mux.Router中添加了众多的路由项,每一个路由项由HTTP请求方法(PUT、POST、GET或DELETE)、URL、Handler三部分组成。
在Server的服务过程中,Server在listener上接受Docker Client的访问请求,并创建一个全新的goroutine来服务该请求。在goroutine中,首先读取请求内容,然后做解析工作,接着找到相应的路由项,随后调用相应的Handler来处理该请求,最后Handler处理完请求之后回复该请求。
2)Engine
Engine是Docker架构中的运行引擎,同时也Docker运行的核心模块。它扮演Docker container存储仓库的角色,并且通过执行job的方式来操纵管理这些容器。
在Engine数据结构的设计中,有一个handler对象,存储特定job的handler。举个例子,Engine的handler对象中有一项为:{“create”:daemon.ContainerCreate},则说明当名为”create”的job在运行时,执行的是daemon.ContainerCreate的handler。
3)Job
Job可以认为是Engine内部最基本的工作执行单元。Docker做的每一项工作,都可以抽象为一个job。Job的设计者,把Job设计得与Unix进程相仿。比如说:Job有一个名称,有参数,有环境变量,有标准的输入输出,有错误处理,有返回状态等。
3、Docker Registry
Docker Registry是用于存储容器镜像的仓库。在Docker的运行过程中,Docker Daemon会与Docker Registry通信,并实现搜索镜像、下载镜像、上传镜像三个功能。
Docker可以使用公有的Docker Registry,即大家熟知的Docker Hub,如此一来,Docker获取容器镜像文件时,必须通过互联网访问Docker Hub。Docker也允许用户构建本地私有的Docker Registry,这样可以保证容器镜像的获取在内网完成。
4、Graph
Graph在Docker架构中扮演已下载容器镜像的保管者,以及已下载容器镜像之间关系的记录者。一方面,Graph存储着本地具有版本信息的文件系统镜像,另一方面也通过GraphDB记录着所有文件系统镜像彼此之间的关系,具体的架构示意图请参考图4所示。
其中,GraphDB是一个构建在SQLite之上的小型图数据库,实现了节点的命名以及节点之间关联关系的记录。
在Graph的本地目录中,关于每一个的容器镜像,具体存储的信息有:该容器镜像的元数据,容器镜像的大小信息,以及该容器镜像所代表的具体rootfs。
5、Driver
Driver是Docker架构的驱动模块,通过Driver驱动,Docker可以实现对Docker容器执行环境的定制。由于Docker运行的生命周期中,并非用户所有的操作都是针对Docker容器的管理,另外还有关于Docker运行信息的获取,Graph的存储与记录等。因此,为了将Docker容器的管理从Docker Daemon内部业务逻辑中区分开来,设计了Driver层驱动来接管所有这部分请求。
在Docker Driver的实现中,可以分为以下三类驱动:graphdriver、networkdriver和execdriver。
1)graphdriver
graphdriver主要用于完成容器镜像的管理,包括存储与获取。即当用户需要下载指定的容器镜像时,graphdriver将容器镜像存储在本地的指定目录。同时当用户需要使用指定的容器镜像来创建容器的rootfs时,graphdriver从本地镜像存储目录中获取指定的容器镜像。
2)networkdriver
networkdriver主要用于完成Docker容器网络环境的配置,其中包括Docker启动时为Docker环境创建网桥、Docker容器创建时为其创建专属虚拟网卡设备、以及为Docker容器分配IP、端口并与宿主机做端口映射,设置容器防火墙策略等。
3)execdriver
execdriver作为Docker容器的执行驱动,负责创建容器运行命名空间,负责容器资源使用的统计与限制,负责容器内部进程的真正运行等。
6、libcontainer
libcontainer是Docker架构中一个使用Go语言设计实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问内核中与容器相关的API。
正是由于libcontainer的存在,Docker可以直接操纵容器的namespace、cgroups、apparmor、网络设备以及防火墙规则等。而这一系列操作的完成都不需要依赖LXC或者其他包。
三、Docker运行案例分析
上一章节着重于Docker架构中各个部分的介绍。本章的内容,将以串联Docker各模块来简要分析,分析原型为Docker中的docker pull与dockerrun两个命令。
1、docker pull
docker pull命令的作用为:从Docker Registry中下载指定的容器镜像,并存储在本地的Graph中,以备后续创建Docker容器时的使用。docker pull命令执行流程如图5所示。
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之间的关系。
2、docker run
docker run命令的作用是在一个全新的Docker容器内部运行一条指令。Docker在执行这条命令的时候,所做工作可以分为两部分:创建Docker容器所需的rootfs;创建容器的网络等运行环境,并真正运行用户指令。因此,在整个执行流程中,DockerClient给Docker Server发送了两次HTTP请求,第二次请求的发起取决于第一次请求的返回状态。Docker run命令执行流程如图6所示。
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容器内部的运行环境初始化,并最终执行用户要求启动的命令。
我的公众号「码农之屋」(id: Spider1818) ,分享的内容包括但不限于 Linux、网络、云计算虚拟化、容器Docker、OpenStack、Kubernetes、SDN、OVS、DPDK、Go、Python、C/C++编程技术等内容,欢迎大家关注。