client 模式

  docker命令对应的源文件是docker/docker.go,

docker [options] command [arg...]

  其中options参数为flag,任何时候执行一个命令docker命令都需要先解析flag,然后按照用户生命的command向指定的子命令执行对应的操作

       如果子命令为daemom,docker都会创建一个运行在宿主机上的daemom进程,即执行daemom模式。其余子命令都会执行client模式。处于client模式命令工作流程包含几个步骤

 1.解析flag信息

      docker命令支持大量的option,或者说flag,列出对于client模式下的docker比较重要的一些flag

      Debug,对应-D和--debug参数,他将向系统中添加DEBUG环境变量且赋值为1,并把日志显示级别调为DEBUG级,这个flag用于启动调试模式

      LogLevel,对应-l和--log-level 参数。默认等级为info,即只输出普通的操作信息。用户可以指定的日志等级现在有panic、fatal、error、warn、info、DEBUG这几种

      hosts,对应-h和--hosts=[]参数,对于client模式,就是指本次操作需要连接的docker daemom位置,而对于daemom模式,则提供所要监听的地址,若host变量或者系统环境变量DOCKER_HOST不为空,说明用户指定了host对象;否则使用默认设置,默认情况下Linux系统设置为unix:///var/run/docker.sock

      protAddrParts,这个参数来自-H参数中://前后的两部分的组合,即与docker daemom建立通信的协议方式与socke地址

 2创建client实例

      client的创建就是在已有配置参数信息的基础上,调用api/client/cli.go#NewDockerCli,需要设置好proto(传输协议)、addr(host的目标地址)和tlsConfig(安全传输层协议的配置),另外还会配置标准输入输出及错误输出

  3执行具体的命令

     Docker client 对象创建成功后,剩下的执行具体命令的过程就交给cli/cli.go来处理

   从命令到映射的方法

    cli主要通过反射机制,从用户输入的命令(如run)得到匹配的执行方法(CmdRun),这就是所谓“约定大于配置”的方法命名规范。

    同时,cli会根据参数列表的长度判断是否用于多级docker命令支持,然后根据找到的执行方法,把剩下的参数传入并执行。若参数传入的方法不正确或者错误,则返回docker的帮助并退出

    每一个类似api/client/commnds.go#CmdRun 的方法都剥离出来作为一个单独的文件存在。docker run 这个命令的执行过程,就需要寻找api/client/run.go这个文件

    执行对应的方法,发起请求

    1.解析传入的参数,并针对参数进行配置处理

    2.获取与Docker daemon通信所需要的认证配置信息

    3.根据命令业务类型,给Docker daemon发送POST、GET等请求

    4.读取来自Docker daemon

 daemom  模式

   一旦进入daemom模式,剩下的初始化工作都由docker的docker/daemon.go#CmdDaemon来完成;docker daemon通过一个server模块(api/server/server.go)接收来自client的请求,然后根据请求的类型,交由具体方法执行,因此daemom首先要启动并初始化这个server,另一方面启动server后。docker 进程需要初始化一个daemon对象(daemon/daemon.go)来负责处理server的请求。

 docker daemon 初始化启动过程

API server的配置和初始化过程

启动过程

(1)整理解析用户指定的各项参数

(2)创建PID文件

(3)加载所需的server辅助配置,包括日志、是否允许远程访问、版本以及TLS认证信等。

(4)根据上述server配置,加上之前解析出来的用户指定的server配置(比如Host),通过goro-utine的方式启动API server。这个server监听的socket位置就是Host的值

(5)创建一个负责处理业务的daemon对象(对应daemon/daemon.go)作为负责处理用户请求的逻辑实体

(6)对APIserver中的路由表进行初始化,即将用户的请求和对应的处理函数相对应起来。

(7)设置一个channel,保证上述goroutine只是在server出错的情况才会退出

(8)设置信号捕获,docker daemon进程收到INT、TERM、QUIT信号时,关闭API server,用shutdowndaemon停止这个daemon

(9)如果上面流程完成后,API server就会与daemon绑定,并接受client的连接。

(10)最后,docker daemon进程向宿主机的init守护进程发送“READY=1”信号,表示docker daemon已经开始工作

  关闭过程

(1)创建并设置一个channel,使用select监听数据。在正确完成关闭daemon工作后将channel关闭,标识该工作的完成;否则在超时15秒后报错

(2)调用daemon/daemon.go#Shoutdown方法执行如下工作

   遍历所有运行中的容器,先用SIGTERM软杀死容器进程,如果10秒不能完成,则使用SIGKILL强制杀死

    如果netController被初始化过,调用#libnetwork/controler.go#GC 方法进行垃圾回收

   结束运行中的镜像驱动程序

    在docker1.6版本以前的早期和以前所有版本,server的启动和初始化使用了一种复杂的job机制(API server即被看作一种job),并且依赖于一个专门的docker Engine来管理和运行这些job。1.7版本,这个设计在整个社区的推动下呗重构,上述说的是新的server初始化过程,该server会通过与daemon对象绑定来接受并处理完成具体的请求(类似于一个API接受器绑定了一个业务逻辑处理器)

   daemon对象的创建与初始化

     对象创建过程至少包括功能有:docker容器配置信息、检测系统支持及用户权限、配置工作路径、加载并配置graphdriver、创建docker网络环境、创建并初始化镜像数据库、创建容器管理驱动、检测DNS配置和加载已有Docker容器等。

    docker  容器配置信息

     容器配置信息的主要功能有:提供用户自由配置的docker容器的可选功能,使得docker容器运行更贴近用户期待的运行场景;设置默认的网络最大传输单元:当用户没有对-mut参数进行指定是,将其设置为1500.否则,沿用用户指定参数值  ;检测网桥配置信息:此部分配置为进一步配置docker网络提供铺垫

    检测系统支持及用户权限

     初步处理完docker的配置信息后,docker自身运行的环境进行一系列检测,主要包括3个方面

   * 操作系统类型对docker daemon的支持,目前docker daemon只能运行在Linux上

   * 用户权限的级别,必须是root权限

   * 内核版本与处理器支持,只支持amd64架构的处理,且内核版本必须升至3.10.0及以是上。

   配置daemon工作路径

   配置docker daemon的工作路径,主要是创建Docker  daemon 运行中所在的工作目录,默认为/var/lib/docker.若该目录不存在,则会创建,并赋予0700权限

   配置docker容器所需的文件环境

      这一步docker daemon会在docker工作目录/var/lib/docker 下面初始化一些重要的目录文件,来构建docker容器工作所需的文件系统环境

      这一,创建容器配置文件目录。docker daemon在出创建docker容器之后,需要将容器内的配置文件放到这个目录下统一管理。目录默认位置:/var/lib/docker/containers,它下面会为每个具体容器保存如下几个配置文件,其中xxx为容器ID 

[root@mast ~]# ls /var/lib/docker/containers/4d5464672680c97ed061b73e7d8336741b2971c2fb5a81fa5ac2ec8fac096cf9/
4d5464672680c97ed061b73e7d8336741b2971c2fb5a81fa5ac2ec8fac096cf9-json.log  checkpoints  config.v2.json  hostconfig.json  hostname  hosts  mounts  resolv.conf  resolv.conf.hash

       第二,配置graphdriver目录。它用于完成docker容器镜像管理所需的底层存储驱动层,所以在这一步的配置工作就是加载并配置镜像存储驱动graphdriver,创建存在驱动镜像管理层文件系统所需的目录和环境,初始化镜像层元数据存储。创建graphdriver时,首先会从环境变量DOCKER_DRIVER中读用户指定的驱动,若为空,则开始遍历优先级数组选择一个graphdriver,在Linux环境下,优先级从高到低依次为aufs、btrfs、zfs、devicemapper、overlay和vfs。 不同操作系统下,优先级列表的内容和顺序都会不同,而且随着内核的发展以及驱动的完善,会继续发生变化。

       需要注意,目前vfs在docker中时用来管理volume的,并不作为镜像存储使用。另外,由于目前在overlay文件系统上运行的docker容器不兼容SELinux,因此当config中配置信息需要启动SELinux并且driver的类型为overlay时,该过程就会报错

       当识别出对应的driver后,docker执行这个driver对应的初始化方法(位于daemon/graphdriver/aufs/aufs,go),这个初始化的主要工作包括:尝试加载内核aufs模块来确定docker主机支持aufs,发送statfs系统调用获取当前docker主目录(/var/lib/docker)的文件系统信息,确定aufs是否支持该文件系统;创建aufs驱动根目录(默认:/var/lib/docker/aufs)并将该目录配置为私有挂载,在根目录下创建mnt、diff和layers目录作aufs驱动的工作环境,工作完成后,graphdriver的配置工作就完成。

       第三,配置镜像目录。主要工作是在docker主目录下创建一个image目录,来存储所有镜像和镜像层管理数据,默认目录“/var/lib/docker/image”.在image目录下,每一graphdriver都有一个具体的目录用于存储使用该graphdriver存储的镜像相关的元数据

       根据上一步graphdriver的选择情况(以aufs为例)创建image/aufs/layerdb/目录作为镜像层元数据存储目录,并创建MetadataStore用来管理元数据。根据graphdriver与元数据存储结构创建layerStore,用来管理所有的镜像和容器层,将逻辑镜像层的操作映射到物理存储驱动层graphdriver的操作,创建用于registry的镜像上传下载的uploadManager和downloadMannger

       创建image/aufs/imagedb/目录用于存储镜像的元数据,根据layerStore创建imageStore,用来管理镜像的元数据。

       第四,调用volume/local/local.go#New创建volume驱动目录(默认为/var/lib/docker/volumes),docker中volume是宿主机上挂载到docker容器内的特定目录。volume目录下有一个metadata.db 数据库文件用于存储volume相关的元数据,其余以volume ID 命名的文件夹用于存储具体的volume内容。默认的volume驱动是local,用户也可以通过插件的形式使用其他volume驱动来存储

      第五,准备“可信镜像”所需的工作目录。docker工作根目录下创建trust目录。这个存储目录可以根据用户给出的可信URL加载授权文件,用来处理可信镜像的授权和验证过程。

      第六,创建distributionMetadataStore和referenceStore。referenceStore用于存储镜像仓库列表。记录镜像仓库的持久化文件位于docker根目录下的image/[graphdriver]/repositories.json中,主要记录镜像ID与镜像仓库之间的映射。distributionMetadataStore存储与第二版镜像仓库registry有关的元数据,主要用于做镜像层的diff_id与registry中镜像层元数据之间的映射

      第七,将持久化在Docker根目录中的镜像、镜像层以及镜像仓库等的元数据内容恢复到daemon的imageStore、layerStore和reference中

      第八,执行镜像迁移,docker1.10版本以后,镜像管理部分使用了基于内容寻址存储。在第一次启动daemon时,为了将老版本的graph镜像管理迁移到新的镜像管理体系中,这里会根据docker根目录中是否存在graph文件夹,如果存在就会读取graph中的老版本镜像信息,计算校验和并将镜像数据写入到新版本的imageStore和layerStore中,注意的是,迁移镜像中计算校验和是一项非常占CPU的工作,并且在未完成镜像迁移时,docker daemon是不会响应任何请求的,所有如果你本地的老版本镜像和容器比较多时,或者是在对服务器负载和响应比较敏感的线上环境尝试,docker版本升级,那就要注意妥善安排时间,docker提供了迁移工具让用户在老版本daemon运行的时候进行镜像迁移

     这里docker daemon需要在docker根目录(/var/lib/docker)下创建并初始化一系列容器文件系统密切相关的目录和文件。

创建docker   network

     创建docker daemon运行环境的时候,创建网络环境是极为重要的一部分。这不仅关系着容器对外通信,同样也关乎着容器之间的通信。网络部分早已被抽离出来作为一个单独的模块,称为libnetwork,libnetwork通过插件的形式为docker提供网络功能,使得用户可以根据自己需求实现自己的dirver来提供不同的网络功能。截止docker1.10版本,libnetwork实现了host、null、birdge和overlay的驱动。其中,birdge driver 为默认驱动,和之前版本中的docker网络功能是基本等价的,需要注意的是,同之前的docker网络一样,bridge driver并不提供跨主机通信的能力,overlay driver则是用于多主机环境

 初始化execdriver

    execdriver是docker中用来管理容器的驱动,docker会调用execdrivers中NewDriver()函数来创建新的execdriver

    在创建execdriver的时候,需要注意一下5部分信息

    运行时中指定使用的驱动类型,在默认配置文件中默认使用native,即其对应的容器运行时为libcontainer;

    用户定义的execdirver选项,即-exec-opt参数值

    用户定义的-exec-root参数值,docker execdriver运行的root路径,默认为/var/run/docker;

    docker 运行时的root路径,默认为/var/lib//docker

    系统功能的信息,包括容器的内存限制功能,交换分区内存限制功能、数据转发功能以及AppArel安全功能等;AppArel通过host主机是否存在/sys/kernel/security/apparmor来判断是否加入AppArel配置

    最后,如果选择netive作为这个execdriver的驱动实现,上述driver的创建过程就会新建一个libcontainer,这个libcontainer会在后面创建和启动Linux容器时发挥作用

daemon对象诞生   

   docker daemon进程在经过以上诸多设置以及创建对象之后,最终创建出了daemon对象实例

ID 根据传入的证书生成的容器ID,若没有传入则自动使用ECDSA算法生成
repository 部署所有docker容器的路径
containers 用于存储具体的docker容器信息的对象
execCommands docker容器所执行的命令
referenceStore 存储docker镜像仓库名和镜像ID的映射
distributionMetadataStore v2版registry相关的元数据存储
trustkey 可信任证书
IDInfo 用于通过简短有效的字符串前缀定位唯一的镜像
sysInfo docker所在宿主机的系统信息
configStore docker所需配置信息
execDriver docker 容器执行驱动,默认native类型
statsCollector 收集容器网络以及cgroups的信息
dafaultLogConfig

提供日志的默认配置信息

registryService 镜像存储服务相关信息
EvenetsServer 事件服务相关信息
volume

volume所使用的驱动,默认为local

root docker运行的工作根目录
uidMaps uid的对应图
gidMaps gid的对应图
seccompEnabled 是否使用seccompute
nameIndex 记录建和其名字的对应关系
linkIndex 容器的link目录,记录容器的link关系

 

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

恢复已有的docker容器

      当docker daemon启动时,会去查看在daemon.repository也就是在/var/lib/docker/containers中的内容。若有已经存在的docker容器,则将相应信息收集并进行维护,同时重启restart policy 为always的容器

      docker daemon的启动看起来非常复杂,这是docker在演进的过程中不断增加功能点造成的,但不管今后docker的功能点增加多少,docker daemon进程的启动都将遵循3步

(1)首先创建一个API server,它工作在用户通过-H指定socket

(2)然后docker使用NewDaemon方法创建一个daemon对象来保存信息和处理业务逻辑

(3)最后将上述API server和daemon对象绑定起来,接受并处理client的请求

只不过,NewDaemon方法的长度会不断增加而已

从client到daemon

   发起请求

   (1)docker  run命令开始运行,用户端的docker进入client模式

   (2)经过初始化,新建出了一个client

   (3)上述client通过反射机制找到了CmdRun方法

    CmdRun在解析过程用户提供的容器参数等一系列操作后,最终发出了这样两个请求:

 “POST”,“/containers/create?”+containerValues   //创建容器

 “POST” ,“/containers/”+createResponse.ID+"/start"  //启动容器

   至此,client  任务结束

   创建容器 

   在这一步docker daemon并不需要创建一个真正的Linux容器,它只需要理解用户通过client提交的POST表单,然后使用这些参数在daemon中新建一个container对象出来即可,这个container实体就是container/container_unix.go,其中的commonContainer字段定义在平台为主。

   启动容器

    这个时候daemon这边的重点来了。API server接受到start请求后告诉docker daemon进行container启动容器操作,这个过程daemon/start.go

    此时,由于container所需的各项参数,如NetworkSetings、ImageID等,都已经在容器过程中赋好了值,docker daemon会在start.go 中直接执行daemon.ContainerStart,就能够宿主机上创建对应的容器了;创建容器过程是docker daemon,containerMonitor将daemon设置为自己的supervisor。所以经过一系列调用后。daemon.ContainerStart 实际上执行的操作是

    即告诉daemon进程,请使用container相关的信息作参数,执行对应的execdriver的Run方法

   最后一步

   “万事俱备,只欠东风”。在docker daemon已经完成所有的准备工作,最后下达了执行Run操作的命令后,跟系统打交道的任务都交给ExecDriver.Run来完成;execdriver是docker的重要组成部分,它封装了对namespace、cgroups等所有对OS资源操作的方法,而在docker中。execdriver的默认实现(native)就是libcontainer了,到这一步。docker daemon只需要提供三大参数,接下来等着返回结果

    * commandv:该容器需要的所有配置信息集合

    * pipes:用于将容器stdin、stdout、stderr重定向到daemon

    * startCallback():回调方法