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

来源:互联网 发布:手机照一寸照的软件 编辑:程序博客网 时间:2024/04/30 10:41

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 还有更多的代码等待探索,但这将会留给以后的博文。

0 0