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 编写并由很多包组成。比如,从顶部到底部开始浏览,你会看到 api, builder, builtins, 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.go
的 DockerCli
类型的 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.go
的 CmdRun。这个文件包含了所有的 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 还有更多的代码等待探索,但这将会留给以后的博文。