Docker 源码走读 - 在运行 Docker run 时发生了什么?

Docker 源码走读 – 在运行 Docker run 时发生了什么?

标签(空格分隔): Docker

原文作者是 Frank Scholten,原文地址是 Docker code walkthrough – What happens during a Docker run?

在这篇博文中我将回答一下问题:运行一个 Docker run 期间 Docker 内部发生了什么?

开始

首先从 Docker Github repo clone 并检查代码。

$ git clone https://github.com/docker/docker

范读

用你喜欢的编辑器打开工程并且范读下 main tree。Docker 是使用 golang 编写并由很多包组成。比如,从顶部到底部开始浏览,你会看到 apibuilderbuiltins, contrib 等等。一些目录包含更多的子目录。

Docker executable 内部

第一件事,就是寻找 func main,当我们运行 Docker executable 时被执行的 Golang 函数。实际上,在代码树中有超过 30 个 main 函数。这些都是工具类的函数,我不会立即就深入其中。让我们继续我们主要寻找的:存在于 docker/docker.go 中的一个 main 函数。让我们更仔细的看。

解析 flags

看以下的代码片段,显示了 main func 的前十几行。当 Docker 启动,它通过 reexec 运行任何的初始化(initializers),如果有,这时它为 Docker executable 解析通过 mflag 包传递的参数。这个包在 pkg/mgflag 下面,别名为 flag。如果需要的话,在这一点它可以打印出版本信息或是开启 debug 模式并记录 debug 日志。

func main() {
  if reexec.Init() {
    return
  }

  flag.Parse()
  // FIXME: validate daemon flags here

  if *flVersion {
    showVersion()
    return
  }

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

  initLogging(*flDebug)

  ... SNIP

}

Dispatch Docker command & error handling

在选择解析 Docker 捕获的主机设置后,如果有必要,对服务器执行 TLS verification 校验。这个发生在 40 和 107 行之间。看以下的代码片段。flags 解析早于传递给来自于 api/client/cli.goDockerCli 类型的 Cmd 方法。
如果一个错误发生,它会被记录,然后程序退出。

func main() {

  ... SNIP

  if err := cli.Cmd(flag.Args()...); err != nil {
    if sterr, ok := err.(*utils.StatusError); ok {
      if sterr.Status != "" {
        log.Println("%s", sterr.Status)
      }
      os.Exit(sterr.StatusCode)
    }
    log.Fatal(err)
  }
}

cli 包

让我们深入 cli 包看看 Docker 命令被怎样处理的。为了能够运行子命令,我们关注 3 件事:

  • DockerCli struct
  • Cmd 方法
  • GetMethod 方法

DockerCli

DockerCli struct 包含每个 Docker 命令必需的数据结构,比如使用的协议, in-, output- 和 error writers 以及 TLS 指定的数据结构。

type DockerCli struct {
    proto      string
    addr       string
    configFile *registry.ConfigFile
    in         io.ReadCloser
    out        io.Writer
    err        io.Writer
    key        libtrust.PrivateKey
    tlsConfig  *tls.Config
    scheme     string
    // inFd holds file descriptor of the client's STDIN, if it's a valid file
    inFd uintptr
    // outFd holds file descriptor of the client's STDOUT, if it's a valid file
    outFd uintptr
    // isTerminalIn describes if client's STDIN is a TTY
    isTerminalIn bool
    // isTerminalOut describes if client's STDOUT is a TTY
    isTerminalOut bool
    transport     *http.Transport
}

Cmd 方法

Cmd 函数的职责是通过使用 getMethod 函数 把命令参数转换成一个函数。它将来已经支持多个命令,也许是 docker groups create,尽管目前为止我知道还没有这样的命令实现。

func (cli *DockerCli) Cmd(args ...string) error {
    if len(args) > 1 {
        method, exists := cli.getMethod(args[:2]...)
        if exists {
            return method(args[2:]...)
        }
    }
    if len(args) > 0 {
        method, exists := cli.getMethod(args[0])
        if !exists {
            fmt.Println("Error: Command not found:", args[0])
            return cli.CmdHelp(args[1:]...)
        }
        return method(args[1:]...)
    }
    return cli.CmdHelp(args...)
}

getMethod

注意 getMethod 是小写字母。这意味着它无法导出包外,因此它仅仅是在 cli 内可用的。因此这个方法怎样找出正确的函数?看下面的代码片段。它首先建立一个以 Cmd 开始的由大写字母组合的 string。万一 Docker 运行 methodName 变量将被 CmdRun。使用来自于 Golang reflect 包的 MethodByName 函数,它检索到一个函数指针并返回它。

func (cli *DockerCli) getMethod(args ...string) (func(...string) error, bool) {
    camelArgs := make([]string, len(args))
    for i, s := range args {
        if len(s) == 0 {
            return nil, false
        }
        camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
    }
    methodName := "Cmd" + strings.Join(camelArgs, "")
    fmt.Println(methodName)
    method := reflect.ValueOf(cli).MethodByName(methodName)
    if !method.IsValid() {
        return nil, false
    }
    return method.Interface().(func(...string) error), true
}

CmdRun

最后我们到达一个负责容器运行的函数:在 api/client/commands.goCmdRun。这个文件包含了所有的 Docker 命令。它自己运行的参数被解析,比如 image,command 和其他参数。因为我们已经通读,我不会展示那些代码,我会以展示更有趣的事代替:运行命令启动一个新的容器。

创建容器

以下代码片段展示了容器是怎样被创建的。容器的配置被合并到 run config 和 host config。

实际创建一个容器是调用一个 HTTP POST 给 Docker 服务器。

func (cli *DockerCli) CmdRun(args ...string) error {

  SNIP ...

  runResult, err := cli.createContainer(config, hostConfig, hostConfig.ContainerIDFile, *flName)
    if err != nil {
      return err
    }
  SNIP ...

}

func (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runconfig.HostConfig, cidfile, name string) (engine.Env, error) {
    containerValues := url.Values{}
    if name != "" {
        containerValues.Set("name", name)
    }

    mergedConfig := runconfig.MergeConfigs(config, hostConfig)

    var containerIDFile *cidFile
    if cidfile != "" {
        var err error
        if containerIDFile, err = newCIDFile(cidfile); err != nil {
            return nil, err
        }
        defer containerIDFile.Close()
    }

    //create the container
    stream, statusCode, err := cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, false)
    //if image not found try to pull it
    if statusCode == 404 {
        fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", config.Image)

        // we don't want to write to stdout anything apart from container.ID
        if err = cli.pullImageCustomOut(config.Image, cli.err); err != nil {
            return nil, err
        }
        // Retry
        if stream, _, err = cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, false); err != nil {
            return nil, err
        }
    } else if err != nil {
        return nil, err
    }

    var result engine.Env
    if err := result.Decode(stream); err != nil {
        return nil, err
    }

    for _, warning := range result.GetList("Warnings") {
        fmt.Fprintf(cli.err, "WARNING: %s\n", warning)
    }

    if containerIDFile != nil {
        if err = containerIDFile.Write(result.Get("Id")); err != nil {
            return nil, err
        }
    }

    return result, nil
}

这些覆盖了在 Docker 客户端里面发生了什么。当然在 Docker 服务器和 libcontainer 还有更多的代码等待探索,但这将会留给以后的博文。

    原文作者:yexiaobai
    原文地址: https://segmentfault.com/a/1190000002360919
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞