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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 本节书摘来华章计算机《容器技术系列》一书中的第2章 ,第2.2节,孙宏亮 著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。 2.2 创建Docker Client 对于Docker这样一个Client/Server的架构,客户端的存在意味着Docker相应任务的发起。

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

2.2 创建Docker Client

对于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所示。

image


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

2.2.1 Docker命令的flag参数解析

众所周知,在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函数代码如下:

func main() {
     if reexec.Init() {
          return
     }
     flag.Parse()
     // FIXME: validate daemon flags here
     ...
}

以上源码实现中,首先判断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参数的初始化。代码如下:

var (
        flVersion = flag.Bool([]string{"v", "-version"}, false, "Print version information and quit")
    flDaemon = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode")
    flDebug = flag.Bool([]string{"D", "-debug"}, false, "Enable debug mode")
        flSocketGroup = flag.String([]string{"G", "-group"}, "docker", "Group to assign the unix socket specified by -H when running in daemon modeuse '' (the empty string) to disable setting of a group")
        flEnableCors = flag.Bool([]string{"#api-enable-cors", "-api-enable-cors"}, false, "Enable CORS headers in the remote API")
        flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by tls-verify flags")
        flTlsVerify = flag.Bool([]string{"-tlsverify"}, false, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)")

        // these are initialized in init() below since their default values depend on dockerCertPath which isn't fully initialized until init() runs
    flCa    *string
    flCert  *string
    flKey   *string
    flHosts []string
    )

    func init() {
        flCa = flag.String([]string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Trust only remotes providing a certificate signed by the CA given here")
        flCert = flag.String([]string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file")
        flKey = flag.String([]string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Path to TLS key file")
        opts.HostListVar(&flHosts, []string{"H", "-host"}, "The socket(s) to bind to in daemon mode\nspecified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd.")
}

以上源码展示了Docker如何定义flag参数,以及在init函数中实现部分flag参数的初始化。Docker的main函数执行前,这些变量创建以及初始化工作已经全部完成。这里涉及了Go语言的一个特性,即init函数的执行。Go语言中引入其他包(import package)、变量的定义、init函数以及main函数这四者的执行顺序如图2-2所示。
关于Golang中的init函数,深入分析可以得出以下特性:
init函数用于程序执行前包的初始化工作,比如初始化变量等;
每个包可以有多个init函数;
包的每一个源文件也可以有多个init函数;
同一个包内的init函数的执行顺序没有明确的定义;
不同包的init函数按照包导入的依赖关系决定初始化的顺序;
init函数不能被调用,而是在main函数调用前自动被调用。

image


清楚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。

2.2.2 处理flag信息并收集Docker Client的配置信息

理解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()之后的源码如下:

if *flVersion {
     showVersion()
     return
}

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

if *flDebug {
     os.Setenv("DEBUG", "1")
}

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

if len(flHosts) == 0 {
      defaultHost := os.Getenv("DOCKER_HOST")
      if defaultHost == "" || *flDaemon {
           // If we do not have a host, default to unix socket
           defaultHost = fmt.Sprintf("unix://%s", api.DEFAULTUNIXSOCKET)
      }
      if _, err := api.ValidateHost(defaultHost); err != nil {
           log.Fatal(err)
      }
      flHosts = append(flHosts, defaultHost)
}

以上的源码主要分析内部变量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,则说明用户已经指定地址,同样继续往下执行。

if *flDaemon {
     mainDaemon()
     return
}

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

if len(flHosts) > 1 {
      log.Fatal("Please specify only one -H")
}
protoAddrParts := strings.SplitN(flHosts[0], "://", 2)

由于不执行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等。

var (
     cli       *client.DockerCli
     tlsConfig tls.Config
)
tlsConfig.InsecureSkipVerify = true

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

// If we should verify the server, we need to load a trusted ca
if *flTlsVerify {
     *flTls = true
     certPool := x509.NewCertPool()
     file, err := ioutil.ReadFile(*flCa)
     if err != nil {
          log.Fatalf("Couldn't read ca cert %s: %s", *flCa, err)
     }
     certPool.AppendCertsFromPEM(file)
     tlsConfig.RootCAs = certPool
     tlsConfig.InsecureSkipVerify = false
}

若flTlsVerify这个flag参数为真,则说明Docker Client需Docker Server一起验证连接的安全性。此时,tlsConfig对象需要加载一个受信的ca文件。该ca文件的路径为*flCA参数的值,最终完成tlsConfig对象中RootCAs属性的赋值,并将InsecureSkipVerify属性置为假。

// If tls is enabled, try to load and send client certificates
if *flTls || *flTlsVerify {
     _, errCert := os.Stat(*flCert)
     _, errKey := os.Stat(*flKey)
     if errCert == nil && errKey == nil {
          *flTls = true
          cert, err := tls.LoadX509KeyPair(*flCert, *flKey)
          if err != nil {
               log.Fatalf("Couldn't load X509 key pair: %s. Key encrypted?", err)
          }
          tlsConfig.Certificates = []tls.Certificate{cert}
     }
}

如果flTls和flTlsVerify两个flag参数中有一个为真,则说明需要加载并发送客户端的证书。最终将证书内容交给tlsConfig的Certificates属性。
至此,flag参数已经全部处理完毕,DockerClient也已经收集到所需的配置信息。下一节将主要分析如何创建Docker Client。

2.2.3 如何创建Docker Client

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

if *flTls || *flTlsVerify {
         cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
} else {
         cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, protoAddrParts[0], protoAddrParts[1], nil)
     }

若flag参数flTls为真或者flTlsVerify为真,则说明需要使用TLS协议来保障传输的安全性,故创建Docker Client的时候,将tlsConfig参数传入;否则,同样创建Docker Client,只不过tlsConfig为nil。
关于Client包中的NewDockerCli函数的实现,可以具体参见./docker/api/clie`javascript
nt/cli.go。
func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string, tlsConfig tls.Config) DockerCli {
var (

 isTerminal = false
 terminalFd uintptr
 scheme     = "http"

)

if tlsConfig != nil {

 scheme = "https"

}

if in != nil {

 if file, ok := out.(*os.File); ok {
      terminalFd = file.Fd()
      isTerminal = term.IsTerminal(terminalFd)
 }

}

if err == nil {

 err = out

}
return &DockerCli{

 proto:      proto,
 addr:       addr,
 in:         in,
 out:        out,
 err:        err,
 isTerminal: isTerminal,
 terminalFd: terminalFd,
 tlsConfig:  tlsConfig,
 scheme:     scheme,
 }

}

总体而言,创建DockerCli对象的过程比较简单。较为重要的DockerCli的属性有:proto,DockerClient与Docker Server的传输协议;addr,Docker Client需要访问的host目标地址;tlsConfig,安全传输层协议的配置。若tlsConfig不为空,则说明需要使用安全传输层协议,DockerCli对象的scheme设置为“https”,另外还有关于输入、输出以及错误显示的配置等。最终函数返回DockerCli对象。
相关文章
|
1天前
|
存储 安全 数据安全/隐私保护
【Docker 专栏】Docker 容器化应用的备份与恢复策略
【5月更文挑战第9天】本文探讨了Docker容器化应用的备份与恢复策略,强调了备份在数据保护、业务连续性和合规要求中的关键作用。内容涵盖备份的重要性、内容及方法,推荐了Docker自带工具和第三方工具如Portainer、Velero。制定了备份策略,包括频率、存储位置和保留期限,并详细阐述了恢复流程及注意事项。文章还提及案例分析和未来发展趋势,强调了随着技术发展,备份与恢复策略将持续演进,以应对数字化时代的挑战。
【Docker 专栏】Docker 容器化应用的备份与恢复策略
|
1天前
|
缓存 关系型数据库 数据库
【Docker 专栏】Docker 与容器化数据库的集成与优化
【5月更文挑战第9天】本文探讨了Docker与容器化数据库集成的优势,如快速部署、环境一致性、资源隔离和可扩展性,并列举了常见容器化数据库(如MySQL、PostgreSQL和MongoDB)。讨论了集成方法、注意事项、优化策略,包括资源调整、缓存优化和监控告警。此外,强调了数据备份、恢复测试及性能评估的重要性。未来,随着技术发展,二者的集成将更紧密,为数据管理带来更多可能性。掌握此技术将应对数字化时代的机遇与挑战。
【Docker 专栏】Docker 与容器化数据库的集成与优化
|
1天前
|
监控 Kubernetes Docker
【Docker 专栏】Docker 容器内应用的健康检查与自动恢复
【5月更文挑战第9天】本文探讨了Docker容器中应用的健康检查与自动恢复,强调其对应用稳定性和系统性能的重要性。健康检查包括进程、端口和应用特定检查,而自动恢复则涉及重启容器和重新部署。Docker原生及第三方工具(如Kubernetes)提供了相关功能。配置检查需考虑检查频率、应用特性和监控告警。案例分析展示了实际操作,未来发展趋势将趋向更智能和高效的检查恢复机制。
【Docker 专栏】Docker 容器内应用的健康检查与自动恢复
|
1天前
|
存储 安全 数据库
【Docker 专栏】Docker 容器内应用的状态持久化
【5月更文挑战第9天】本文探讨了Docker容器中应用状态持久化的重要性,包括数据保护、应用可用性和历史记录保存。主要持久化方法有数据卷、绑定挂载和外部存储服务。数据卷是推荐手段,可通过`docker volume create`命令创建并挂载。绑定挂载需注意权限和路径一致性。利用外部存储如数据库和云服务可应对复杂需求。最佳实践包括规划存储策略、定期备份和测试验证。随着技术发展,未来将有更智能的持久化解决方案。
【Docker 专栏】Docker 容器内应用的状态持久化
|
1天前
|
机器学习/深度学习 监控 Kubernetes
【Docker 专栏】Docker 容器内服务的自动扩展与缩容
【5月更文挑战第9天】本文探讨了Docker容器服务的自动扩展与缩容原理及实践,强调其在动态业务环境中的重要性。通过选择监控指标(如CPU使用率)、设定触发条件和制定扩展策略,实现资源的动态调整。方法包括云平台集成和使用Kubernetes等框架。实践中,电商平台和实时数据处理系统受益于此技术。注意点涉及监控数据准确性、扩展速度和资源分配。未来,智能算法将提升扩展缩容的效率和准确性,成为关键技术支持。
【Docker 专栏】Docker 容器内服务的自动扩展与缩容
|
1天前
|
Java 数据库连接 Docker
【Docker 专栏】Docker 容器内环境变量的管理与使用
【5月更文挑战第9天】本文介绍了Docker容器中环境变量的管理与使用,环境变量用于传递配置信息和设置应用运行环境。设置方法包括在Dockerfile中使用`ENV`指令或在启动容器时通过`-e`参数设定。应用可直接访问环境变量或在脚本中使用。环境变量作用包括传递配置、设置运行环境和动态调整应用行为。使用时注意变量名称和值的合法性、保密性和覆盖问题。理解并熟练运用环境变量能提升Docker技术的使用效率和软件部署质量。
【Docker 专栏】Docker 容器内环境变量的管理与使用
|
Docker 容器
Docker技术入门与实战
GitBookhttps://www.gitbook.com/book/yeasy/docker_practice/details pdf 版本 下载 epub 版本 下载
1275 0
|
Docker 容器
《Docker技术入门与实战》——导读
在一台服务器上同时运行一百个虚拟机,肯定会被认为是痴人说梦。而在一台服务器上同时运行一千个Docker容器,这已经成为现实。在计算机技术高速发展的今天,昔日的天方夜谭正在一个个变成现实。
1616 0