本文章基于《docker源码分析》(孙宏亮)的做总结,用于学习:
从三个方面叙述:
由上图可知、docker主要包括docker Client、docker daemon、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
Docker Client发送容器请求,请求由Docker Daemon接收后,Docker Client的生命周期结束。若需要继续发送请求,用户必须再次通过可执行文件docker创建Docker Client发送请求
Docker Daemon Docker 架构中一个常驻在后台的系统进程。所谓的"运行 Docker" ,
即代表运行 Docker Daemon 。总之, DockerDaemon 的作用主要有以下两方面:
Docker Daemon 运行时,会在后台启动一个 Server Se er 负责接收 Docker Client 发送的请求;接收请求后, Server 通过路由与分发调度,找到相应的 Handler 来处理请求。
启动 Docker Daemon 所使用的可执行文件同样是 docker ,与 Docker Client 启动所使用的可执行文件 docker 相同。既然 Docker Client Docker Daemon 都可以通过 docker 二进制文件创建,那么如何辨别两者就变得非常重要。实际上,执行 docker 命令时,通过传入的参数可以辨别 Docker Daemon Docker Client ,如 docker -d 代表 Docker Daemon 的启动,docker ps 则代表创建 Docker Client ,并发送 ps 请求。Docker Daemon 的架构大致可以分为三部分: Docker Server Engine Job
在Docker Daemon启动过程中,Docker会启动一个名为serverApi的Job,Docker Server其实也是Docker的一个Job。Docker Server首先通过gorilla/mux ,创建了一个 mux.Router 路由器,提供请求的路由功能。创建路由器之后, Docker Server会为mux.Router 中添加有效的路由项,每一个路由项由 HTTP 请求方法 (PUT、POST、GET、DELETE) URL Handler 三部分组成。然后将Server监听的地址和mux.Router作为参数,创建一个httpSrv=http.Server{}对象。最终执行http.Serve()服务于外部请求。
Docker Server接受Docker Client的每一个请求,Server都会创建一个新的goroutine(go语言的协程概念)。首先读取内容,匹配相对应的路由项,调用Handler处理器处理,处理完成后响应给docker client
Engine Docker 架构中的运行引擎,同时也是 Docker 运行的核心模块。 Engine 存储着大量的容器信息,同时管理着 Docker 大部分 Job 的执行。换言之, Docker 中大部分任务的执行都需要 Engine 协助,并通过 Engine 匹配相应的 Job 完成 Job 的执行。
除了容器管理之外, Engine 还接管 Docker Daemon 的某些特定任务。当 Docker
Daemon 遭遇到自身进程需要退出的情况时, Engine 还负责完成 Docker Daemon 退出前的所有善后工作。
Job 可以认为是 Docker 架构中 Engine 内部最基本的工作执行单元Docker Daemon
以完成的每一项工作都会呈现为一个 Job 。例如,在 Docker 容器内部运行一个进程,这是一个 Job; 创建一个新的容器,这是一个 Job; 在网络上下载一个文档,这是一个 Job; 包括之前在 Docker Server 部分谈及的,创建 Server 服务于 HTTP 协议的 API ,这也是一个 Job等等。
有关 Job 接口的设计,与 UNIX 进程非常相仿。比如说, Job 有一个名称,有运行时参数,有环境变量,有标准输入与标准输出,有标准错误,还有返回状态等。
.Docker Regist叩是一个存储容器镜像( Docker Image) 的仓库。容器镜像( Docker Image) 是容器创建时用来初始化容器 rootfs 的文件系统内容。 Docker Registry 将大量的容器镜像汇集在一起,并为分散的 Docker Daemon 提供镜像服务。
Docker Graph 是docker容器镜像的保管者。。由于 Docker 支持多种不同的镜像存储方式,如 aufs devicemapper Btrfs 等,故 Graph 对镜像的存储也会因以上种类而存在一些差异。对 Docker 而言,同一种类型的镜像被称为一个 repository ,如名称为 ubuntu 的镜像都同属一个 repository ;而同一个 repository 下的镜像则会因 tag 存在差异而不同,如 ubuntu这个 reposit。可下有 tag 12.04 的镜像,也有 tag 14.04 的镜像。
Driver:Docker中的驱动模块可以实现对Docker网络环境,存储方式以及执行方式进行定制。需注意的是Docker 运行的生命周期中,并非用户所有的操作都是针对 Docker 容器的管理,同时包括用户对 Docker 运行信息的获取,还包括 Docker Graph 的存储与记录等。因此,为了将仅与 Docker 容器有关的管理从 Docker Daemon 的所有逻辑中区分开来, Docker 的创造者设计了 Driver 层来抽象不同类别各自的功能范畴。
Docker Driver 的实现可以分为以下三类驱动: graphdriver networkdriver execdriver,volumeDriver
graphdriver:主要是完成容器镜像的管理、包括从远程Docker Register上下载的镜像,也包括本地构建完的镜像。在graphdriver初始化过程中,会在docker daemon注册四个文件系统驱动,aufs 、btrfs、vfs和devmapper。其中aufs,btrfs和devmapper用于容器镜像管理,而vfs是用于数据卷容器volume的管理。Docker 在初始化之时,优先通过获取系统环境变量 “DOCKER VER” 来提取所使用 driver 的指定类型。因此,之后所有的 Graph 操作,都使用该 driver 来执行。(后续更新详细说明)。在1.4.0版本之后,支持Overlay这一类型的graphdriver
networkdriver:用是完成 Docker 容器网络环境的配置。通过网络管理模块network调用libnetwork创建并配置Docker容器创建环境。配置包括Docker Daemon启动时为Docker创建网桥,Docker容器创建前的网络接口资源,Docker容器分配IP、端口并于宿主机做NAT端口映射,设置防火墙策略等。
execdriver:Docker容器执行驱动,负责创建容器运行时的命名空间,容器内部资源使用的统计和限制,容器内部进程的真正运行。通过libcontainer来实现对容器的具体管理,包括利用UTS,IPC,PID,Network、Mount、User等namespace实现容器之间资源隔离,通过cgroups实现资源的限制
volume driver:负责容器数据卷的创建,挂载。通过volume模块调用volumedriver,创建一个数据卷并挂载。
libcontainer 是Docker 架构中一个使用 Go 语言设计实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问内核中与容器相关的系统调用。Docker 可以直接调用 libcontainer ,而最终操作容器的namespaces cgroups apparmor 、网络设备以及防火墙规则等。这一系列操作的完成都不需要依赖 LXC 或者其他包。
libcontainer 提供了一整套标准的接口来满足上层对容器管理的需求。屏蔽了Docker上层对容器的直接管理
libcontainer 基于go语言跨平台开发,承接了Linux内核和Docker Daemon,使Docker在跨平台上有很好的支持
Docker Container (Docker 容器)是 Docker 架构中服务交付的最终体现形式。Docker 通过DockerDaemon 的管理, libcontainer 的执行,最终创建 Docker 容器。
用户对Docker容器的配置主要有一下4个基本方面
(1)通过指定容器镜像,使得Docker容器可以自定义rootfs等文件系统
(2)通过指定物理资源,如内存,CPU等,使Docker受物理资源限制
(3)通过配置容器网络及其安全策略,使得Docker容器拥有独立且安全的的网络环境
(4)通过指定容器运行命令,使得容器执行指定的任务
docker的镜像分发方法有docker pull,docker push、docker save 、docker load。不同的是docker pull/push是通过线上,在docker仓库上下载和上传。而docker save和docker load是线下,通过加载镜像文件。
docker export是用于持久化容器,会丢失容器的历史。而docker save/push是持久化镜像,不会丢失镜像的历史和层。
docker pull的处理流程
(1)Docker Client处理用户发起的docker pull 命令,解析参数,然后发送一个HTTP POST请求方法,URL为"/images/create?"+“XXX”,下载相应的镜像
(2)Docker Se er 接收以上 HTTP 请求,并交给 mux.Router mux.Router 通过URL 以及请求方法类型来确定执行该请求的具体 handler
(3)mux.Router 将请求路由分发至相应的 handler ,具体为 PostImagesCreate
(4)PostImageCreate 这个 handler 之中,创建并初始化一个名为 “pull” Job ,之后触发执行该 Job
(5)根据用户命令行参数解析出其希望拉取的repository信息、repository信息可能是tag格式也可能是digest格式
(6)将repository信息解析为repositoryInfo并验证
(7)根据拉取repository是否为official版本以及用户是否设置Docker Mirror获取endpoint列表,并遍历该列表。向该endpoint 指定的registry发起会话。一般顺序为API版本v2>v1,https>http
(8)名为 “pull” Job 将下载的 Docker 镜像交给 graphdriver 管理。
(9)graphdriver 负责存储 Docker 镜像,一方面将实际镜像存储至本地文件系统中,另一方面为镜像创建对象,由 Docker Daemon 统一管理。
docker run是创建一个全新的Docker容器,并在容器内部运行指定命令。Docker Daemon处理用户发起的命令:
(1)创建Docker容器对象,并为容器准备所需的rootfs
(2)创建容器运行环境,如网络资源,资源限制等,最终运行用户指令
因此Docker run命令中Docker Client给Docker Server发送两次HTTP请求,第二次发送取决于第一次发送返回的状态
图:
步骤:
(1)Docker Client 处理用户发起的 docker run 命令,解析完请求与参数之后,向Docker Server 发送一个 HTTP 请求, HTTP 请求方法为 POST ,请求 URL 为"/containers/create?"+“xxx” ,实际意义为创建一个容器对象,即 Docker Daemon 程序逻辑中的容器对象,并非实际运行的容器。
(2)Docker Server 接收以上 HTTP 请求,并交给 mux.Router mux.Router 通过 URL 以及请求方法来确定执行该请求的具体 handler
(3) mux.Router 将请求路由分发至相应的 handler ,具体PostContainersCreate
(4)PostContainersCreate 这个 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 ,之后触发执行该 Job
(12)名为 “start” Job 执行需要完成一系列与 Docker 容器相关的配置工作,其中之一是Docker 容器网络环境分配网络资源,如 IP 资源等,通过调用 networkdriver 完成。
(13)networkdriver 为指定的 Docker 容器分配网络资源,其中有 IP po此等,另外为容器设置防火墙规则。
(14))返回名为 “start” Job ,执行完一些辅助性操作后, Job 开始执行用户指令,调用execdriver
(15) execdriver 被调用,开始初始化 Docker 容器内部的运行环境,如命名空间、资源控制与隔离,以及用户命令的执行,相应的操作转交至 libcontainer 来完成。
(16) libcontainer 被调用 完成 Docker 容器内部的运行环境初始化,并最终执行用户要求启动的命令。
Docker的镜像构建,Docker提供了两个简单的命令docker commit以及docker build
原则上不能无中生有地创建一个镜像,需要一个基础镜像。docker commit是将容器提交为一个镜像,而docker build是在一个基础镜像上重新构建一个新的镜像
Docker commit步骤:
(1)Docker Daemon接收到对应的HTTP请求后,会根据用户输入的pause参数的设置确定是否暂停该容器的运行
(2)将容器的可读写层导出打包,该读写层代表容器的文件系统与当初启动该容器镜像之间的差异
(3)在存储层中注册可读写的差异包
(4)更新镜像历史信息和rootfs,并据此存储镜像存储中创建一个新的镜像,记录元数据
(5)如果指定了repository信息,则给上述镜像添加tag信息