标签:
通常我们都习惯了使用Docker run来执行一个Docker容器,那么在我们执行Docker run之后,Docker到底都做了什么工作呢?本文通过追踪Docker run(Docker 1.9版本)的执行流程,借由对volume,network和libcontainer的使用和配置的介绍,对Docker run的原理进行了详细解读。
首先,用户通过Docker client输入docker run
来创建被运行一个容器。Docker client主要的工作是通过解析用户所提供的一系列参数后,分别发送了这样两条请求:
"POST", "/containers/create?"+containerValues
"POST", "/containers/"+createResponse.ID
这样client的工作也就完成了,很显然client做的事情很少,主要是负责给Docker daemon发送请求。不过,通过client所发送的两条请求,我们可以很自然的把docker run
的整个执行过程分成create与start两个阶段。下图对docker run
中各个关键事件的发生时间点分别进行了标注。下面我将分别从这个两个阶段并结合下图,对docker run
的执行流程进行介绍。
这阶段Docker daemon的主要工作是对client提交的POST表单进行分析整理,获得具有可移植性的配置参数结构体config和不可移植的配置结构体hostconfig。然后daemon会调用daemon.newContainer函数来创建一个基本的container对象,并将config和hostconfig中保存的信息填写到container对象中。当然此时的container对象并不是一个具体的物理容器,它其中保存着所有用户指定的参数和Docker生成的一些默认的配置信息。最后,Docker会将container对象进行JSON编码,然后保存到其对应的状态文件中。
上述过程完成后,一个容器的基本配置信息就已经完全具备,用户可以使用docker inspect
来查看这个容器所对应的各种配置信息。
完成了create阶段后,client紧接着会发送start请求来启动一个真正的物理容器。当Docker daemon接收到这个start请求后,会使用在create阶段配置好的container对象中的各种配置参数来完成volume挂点的注册,容器网络的创建和创建并启动物理容器等工作。下面先对容器的网络环境创建过程做一个介绍。
Docker daemon在将hostconfig配置到容器配置信息的过程中,会调用daemon.registerMountPoints函数对client提供的POST表单中的volume相关信息进行注册,并以mountpoint的形式存储在容器的配置信息中,在真正启动物理容器的时候才会进行挂载。
Volume的挂载点可以分为两类,一类为使用其他容器中的挂载点,另一类为用户指定的绑定挂载。下面我们来看一下两种volume挂载点的注册流程。
1.使用其他容器中的挂载点:在对这类挂载点进行注册时,首先会使用容器的id在Docker daemon中查找对应的结构体。然后遍历其中的所有挂载点,并且将其中的挂载点信息全部都注册到当前的容器结构体之中。
2.用户指定的绑定挂载:用户指定的绑定挂载可以有Source Path:Destination Path的格式,也可以是Name:Destination Path的格式。如果用户输入的参数是Source Path:Destination Path的格式那么,daemon会解析其中的Source Path和Destination Path,并使用它们注册对应的挂载点。如果用户输入的参数是Name:Destination Path的格式,那么daemon会查找用户提供的Name是否已经对应了一个使用docker volume
已经创建好了的挂载点信息,如果是的话,则会使用这个挂载点的信息和用户提供的Destination Path进行本容器的挂载点注册。如果这个Name在daemon中没有对应的挂载点的话,daemon则会在其默认文件夹下创建一个目录,作为挂载点中的Source Path,然后使用用户提供的Destination Path和自行创建的Source Path进行本容器的挂载点注册。
在完成了挂载点的注册之后,daemon会将所有的挂载点信息更新到容器的配置信息中,以备后续使用。
Docker daemon使用client提供的POST表单中网络相关的参数,通过调用daemon.initializeNetworking函数来完成容器网络栈的创建和配置。daemon.initializeNetworking函数则通过对Docker的网络依赖库(即libnetwork)的一系列调用,来完成容器的网络栈创建和配置等工作。
要理解Docker容器的网络部分的执行流程,那么首先要清楚libnetwork中的三个核心概念。
沙盒(Sandbox):一个沙盒包含了一个容器网络栈的信息。沙盒可以对容器的接口,路由和DNS设置等进行管理。沙盒的实现可以是Linux Network Namespace, FreeBSD Jail或者类似的机制。一个沙盒可以有多个端点(Endpoint)和多个网络(Network)。
端点(Endpoint):一个端点可以加入一个沙盒和一个网络。端点的实现可以是veth pair, Open vSwitch内部端口或者相似的设备。一个端点只可以属于一个网络并且只属于一个沙盒。
网络(Network):一个网络是一组可以直接互相联通的端点。网络的实现可以是Linux bridge,VLAN等等。一个网络可以包含多个端点。
清楚了以上三个核心概念之后,我们从Docker源码的角度并通过Docker中默认的网络模式(bridge模式)来看一下容器网络栈的创建过程。
在Docker daemon启动之后,会创建一个默认的network,其本质工作就是创建了一个名为docker0的默认网桥。
确定默认网桥之后,daemon会调用container.BuildCreateEndpointOptions来创建此容器中endpoint的配置信息。然后再调用Network.CreateEndpoint使用上面配置好的信息创建对应的endpoint。在bridge模式中,libnetwork创建的设备是veth pair。Libnetwork中调用netlink.LinkAdd(veth)进行了veth pair的创建,把其中的的一个veth设备是加入到docker0网桥中,另一个则是为了sandbox所准备的。
接下来daemon会调用daemon.buildSandboxOptions来创建此容器的sandbox,然后调用Network.NewSandbox来创建属于此容器的新的sandbox。libnetwork在接收到创建sandbox的请求后,会使用系统调用为容器创建一个新的netns,并将这个netns的路径返写入到对应容器的配置信息中,以便后续的使用。
最后,daemon会调用ep.Join(sb)将endpoint加入到容器对应的sandbox中。先将endpoint加入到容器对应的sandbox中,然后对endpoint的ip信息和gateway等信息进行配置,并将所有的信息更新到对应容器的配置信息中。
在完成创建容器的各种准备工作之后,Docker daemon会通过对libcontainer的一系列调用来完成容器的创建和启动工作。Libcontainer是Docker的运行时库,它可以通过调用者提供的配置参数来创建并运行一个容器出来,下面我们来看一Docker是如何使用之前配置的结构体中的各项参数,通过libcontainer创建并运行一个容器的。
所谓的逻辑容器container和逻辑进程process并非时真正运行着的容器和进程,而是libcontainer中所定义的结构体。逻辑容器container中包含了namespace,cgroups,device和mountpoint等各种配置信息。逻辑进程process中则包含了容器中所要运行的指令以其参数和环境变量等。
Docker daemon会调用execdriver.Run来完成和libcontainer的一系列交互工作。首先将会将所有和新建容器相关的参数装入可以被libcontainer使用的结构体config中。然后使用config作为参数来调用libcontainer.New()生成用来产生container的工厂factory。再调用factory.Create(config),就会生成一个将config包含其中的逻辑容器container。接下来调用newProcess(config)来将config中关于容器内所要运行命令的相关信息填充到process结构体中,这个结构体即为逻辑进程process。使用container.Start(process)来启动逻辑容器。
Docker daemon会调用linuxContainer.Start来启动逻辑容器。这个函数的主要工作就是调用newParentProcess()来生成parentprocess实例(结构体)和用于runC与容器内init进程相互通信的管道。
在parentprocess实例中,除了有记录了将来与容器内进程进行通信的管道与各种基本配置等,还有一个极为重要的字段就是其中的cmd。
cmd字段是定义在os/exec包中的一个结构体。os/exec包主要用于创建一个新的进程,并在这个进程中执行指定的命令。开发者可以在工程中导入os/exec包,然后将cmd结构体进行填充,即将所需运行程序的路径和程序名,程序所需参数,环境变量,各种操作系统特有的属性和拓展的文件描述符等。
在Docker中程序将cmd的应用路径字段Path填充为/proc/self/exe(即为应用程序本身,Docker)。参数字段Args填充为init,表示对容器进行初始化。SysProcAttr字段中则填充了各种Docker所需启用的namespace(其中包括前面所讲到的netns路径)等属性。
然后调用parentprocess.cmd.Start()启动物理容器中的init进程。接下来将物理容器中init进程的进程号加入到Cgroup控制组中,对容器内的进程实施资源控制。再把配置参数通过管道传送给init进程。最后通过管道等待init进程根据上述配置完成所有的初始化工作,或者出错退出。
容器中的init进程首先会调用StartInitialization()函数,通过管道从父进程接收各种配置参数。然后对容器进行如下配置:
1.将init进程加入其指定的namespace中,这里会将init进程加入到前面已经创建好的netns中,这样init进程就拥有了自己独立的网络栈,完成了网络创建和配置的最后一步。
2.设置进程的会话ID。
3.使用系统调用,将前面注册好的挂载点全部挂载到物理主机上,这样就完成了volume的创建。
4.对指定目录下的文件系统进行挂载,并切换根目录到新挂载的文件系统下。设置hostname,加载profile信息。
5.最后使用exec系统调用来执行用户所指定的在容器中运行的程序。
这样就完成了一个容器的创建和启动过程。
Docker run执行流详解(以volume,network和libcontainer为线索)
标签:
原文地址:http://blog.csdn.net/gao514916467/article/details/51201932