天天看点

《容器技术系列》一2.2 创建Docker Client

本节书摘来华章计算机《容器技术系列》一书中的第2章 ,第2.2节,孙宏亮 著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

对于docker这样一个client/server的架构,客户端的存在意味着docker相应任务的发起。用户首先需要创建一个dockerclient,随后将特定的请求类型与参数传递至docker client,最终由docker client转义成docker server能识别的形式,并发送至docker server。

docker client的创建实质上是docker用户通过二进制可执行文件docker,创建与docker server建立联系的客户端。以下分3个小节分别阐述docker client的创建流程。

docker client完整的运行流程如图2-1所示。

《容器技术系列》一2.2 创建Docker Client

通过学习图2-1,我们可以更为清晰地了解docker client创建及执行请求的过程。其中涉及诸多docker源码层次中的专有名词,本章后续会一一解释与分析。

众所周知,在docker的具体实现中,docker server与docker client均由可执行文件docker来完成创建并启动。那么,了解docker可执行文件通过何种方式来区分到底是docker server还是docker client,就显得尤为重要。

首先通过docker命令举例说明其中的区别。docker server的启动,命令为docker -d或docker --daemon=true;而docker client的启动则体现为docker --daemon=false ps、docker pull name等。

其实,对于docker请求中的参数,我们可以将其分为两类:第一类为命令行参数,即docker程序运行时所需提供的参数,如: -d、--daemon=true、--daemon=false等;第二类为docker发送给docker server的实际请求参数,如:ps、pull name等。

对于第一类,我们习惯将其称为flag参数,在go语言的标准库中,专门为该类参数提供了一个flag包,方便进行命令行参数的解析。

清楚docker二进制文件的使用以及基本的命令行flag参数之后,我们可以进入实现docker client创建的源码中,位于./docker/docker/docker.go。这个go文件包含了整个docker的main函数,也就是整个docker(不论docker daemon还是docker client)的运行入口。部分main函数代码如下:

以上源码实现中,首先判断reexec.init()方法的返回值,若为真,则直接退出运行,否则将继续执行。reexec.init()函数的定义位于./docker/reexec/reexec.go,可以发现由于在docker运行之前没有任何initializer注册,故该代码段执行的返回值为假。reexec存在的作用是:协调execdriver与容器创建时dockerinit这两者的关系。第13章在分析dockerinit的启动时,将详细讲解reexec的作用。

判断reexec.init()之后,docker的main函数通过调用flag.parse()函数,解析命令行中的flag参数。如果熟悉go语言中的flag参数,一定知道解析flag参数的值之前,程序必须先定义相应的flag参数。进一步查看docker的源码,我们可以发现docker在./docker/docker/flag.go中定义了多个flag参数,并通过init函数进行部分flag参数的初始化。代码如下:

以上源码展示了docker如何定义flag参数,以及在init函数中实现部分flag参数的初始化。docker的main函数执行前,这些变量创建以及初始化工作已经全部完成。这里涉及了go语言的一个特性,即init函数的执行。go语言中引入其他包(import package)、变量的定义、init函数以及main函数这四者的执行顺序如图2-2所示。

关于golang中的init函数,深入分析可以得出以下特性:

init函数用于程序执行前包的初始化工作,比如初始化变量等;

每个包可以有多个init函数;

包的每一个源文件也可以有多个init函数;

同一个包内的init函数的执行顺序没有明确的定义;

不同包的init函数按照包导入的依赖关系决定初始化的顺序;

init函数不能被调用,而是在main函数调用前自动被调用。

《容器技术系列》一2.2 创建Docker Client

清楚go语言一些基本的特性之后,回到docker中来。docker的main函数执行之前,docker已经定义了诸多flag参数,并对很多flag参数进行初始化。定义并初始化的命令行flag参数有:flversion、fldaemon、fldebug、flsocketgroup、flenablecors、fltls、fltlsverify、flca、flcert、flkey、flhosts等。

以下具体分析fldaemon:

定义:fldaemon = flag.bool([]string{"d", "-daemon"}, false, "enable daemon mode");

fldaemon的类型为bool类型;

fldaemon名称为"d"或者"-daemon",该名称会出现在docker命令中,如docker –d;

fldaemon的默认值为false;

fldaemon的用途信息为"enable daemon mode";

访问fldaemon的值时,使用指针*fldaemon解引用访问。

在解析命令行flag参数时,以下语句为合法的(以fldaemon为例):

-d, --daemon

-d=true, --daemon=true

-d="true", --daemon="true"

-d='true', --daemon='true'

当解析到第一个非定义的flag参数时,命令行flag参数解析工作结束。举例说明,当执行docker命令docker --daemon=false --version=false ps时,flag参数解析主要完成两个工作:

完成命令行flag参数的解析,根据flag的名称-daemon和-version,得知具体的flag参数为fldaemon和flversion,并获得相应的值,均为false。

遇到第一个非定义的flag参数ps时,flag包会将ps及其之后所有的参数存入flag.args(),以便之后执行docker client具体的请求时使用。

如需深入学习flag的实现,可以参见docker源码./docker/pkg/mflag/flag.go。

理解go语言解析flag参数的相关知识,可以很大程度上帮助理解docker的main函数的执行流程。通过总结,首先列出源码中处理的flag信息以及收集docker client的配置信息,然后再一一进行分析:

处理的flag参数有:flversion、fldebug、fldaemon、fltlsverify以及fltls。

为docker client收集的配置信息有:protoaddrparts(通过flhosts参数获得,作用是提供docker client与docker server的通信协议以及通信地址)、tlsconfig(通过一系列flag参数获得,如fltls、fltlsverify,作用是提供安全传输层协议的保障)。

清楚flag参数以及docker client的配置信息之后,我们进入main函数的源码,具体分析如下。

在flag.parse()之后的源码如下:

以上代码很好理解,解析flag参数后,若docker发现flag参数flversion为真,则说明docker用户希望查看docker的版本信息。此时,docker调用showversion()显示版本信息,并从main函数退出;否则的话,继续往下执行。

若fldebug参数为真的话,docker通过os包中的setenv函数创建一个名为debug的环境变量,并将其值设为"1";继续往下执行。

以上的源码主要分析内部变量flhosts。flhosts的作用是为docker client提供所要连接的host对象,也就是为docker server提供所要监听的对象。

在分析过程中,首先判断flhosts变量是否长度为0。若是的话,则说明用户并没有显性传入地址,此时docker的策略为选用默认值。docker通过os包获取名为docker_host环境变量的值,将其赋值于defaulthost。若defaulthost为空或者fldaemon为真,说明目前还没有一个定义的host对象,则将其默认设置为unix socket,值为api.defaultunixsocket,该常量位于./docker/api/common.go,值为"/var/run/docker.sock",故defaulthost为"unix:///var/run/docker.sock"。验证该defaulthost的合法性之后,将defaulthost的值追加至flhost的末尾,继续往下执行。当然若flhost的长度不为0,则说明用户已经指定地址,同样继续往下执行。

若fldaemon参数为真,则说明用户的需求是启动docker daemon。docker随即执行maindaemon函数,实现docker daemon的启动;若maindaemon函数执行完毕,则退出main函数。一般maindaemon函数不会主动终结,docker daemon将作为一个常驻进程运行在宿主机上。本章着重介绍docker client的启动,故假设fldaemon参数为假,不执行以上代码块。继续往下执行。

由于不执行docker daemon的启动流程,故属于docker client的执行逻辑。首先,判断flhosts的长度是否大于1。若flhosts的长度大于1,则说明需要新创建的docker client访问不止1个docker daemon地址,显然逻辑上行不通,故抛出错误日志,提醒用户只能指定一个docker daemon地址。接着,docker将flhosts这个string数组中的第一个元素进行分割,通过"://"来分割,分割出的两个部分放入变量protoaddrparts数组中。protoaddrparts的作用是:解析出docker client与docker server建立通信的协议与地址,为docker client创建过程中不可或缺的配置信息之一。一般情况下,flhosts[0]的值可以是tcp://0.0.0.0:2375或者unix:///var/run/docker.sock等。

由于之前已经假设过fldaemon为假,可以认定main函数的运行是为了docker client的创建与执行。docker在这里创建了两个变量:一个为类型是*client.dockercli的对象cli,另一个为类型是tls.config的对象tlsconfig。定义完变量之后,docker将tlsconfig的insecureskipverify属性置为真。tlsconfig对象的创建是为了保障cli在传输数据的时候遵循安全传输层协议(tls)。安全传输层协议(tls)用于确保两个通信应用程序之间的保密性与数据完整性。tlsconfig是docker client创建过程中可选的配置信息。

若fltlsverify这个flag参数为真,则说明docker client需docker server一起验证连接的安全性。此时,tlsconfig对象需要加载一个受信的ca文件。该ca文件的路径为*flca参数的值,最终完成tlsconfig对象中rootcas属性的赋值,并将insecureskipverify属性置为假。

如果fltls和fltlsverify两个flag参数中有一个为真,则说明需要加载并发送客户端的证书。最终将证书内容交给tlsconfig的certificates属性。

至此,flag参数已经全部处理完毕,dockerclient也已经收集到所需的配置信息。下一节将主要分析如何创建docker client。

docker client的创建其实就是在已有配置参数信息的情况下,通过client包中的newdockercli方法创建一个docker clinet实例cli。具体源码实现如下:

若flag参数fltls为真或者fltlsverify为真,则说明需要使用tls协议来保障传输的安全性,故创建docker client的时候,将tlsconfig参数传入;否则,同样创建docker client,只不过tlsconfig为nil。

关于client包中的newdockercli函数的实现,可以具体参见./docker/api/clie<code>`</code>javascript

nt/cli.go。

func newdockercli(in io.readcloser, out, err io.writer, proto, addr string, tlsconfig tls.config) dockercli {

var (

)

if tlsconfig != nil {

}

if in != nil {

if err == nil {

return &amp;dockercli{

继续阅读