docker v1.11 源码重构分析

基于docker v1.12的源代码,对docker engine v1.11中重构后的源码结构进行分析,涵盖dockerd, containerd, containerd-shim, runC。

docker1.11新特性

docker在v1.11版本进行了重大的重构,对docker engine和container进行了解耦,docker engine运行在containerd上,containerd运行在runC上,通过containerd-shim中间层进行了解耦。之前的docker engine分为四个组件:dockerd、containerd、containerd-shim、runC,之间关系如下图所示:

docker v1.11 源码重构分析_第1张图片

containerd是一个守护进程,它可以使用runC的接口管理容器,使用gRPC暴露容器的其他功能。由于容器运行时是孤立的引擎,引擎最终能够启动和升级而无需重新启动容器。在v1.12版本中,在dockerd的启动参数或者配置文件中,开启live-store 参数即可,具体参考 官方live-store升级dockerd文档。

runC是一个轻量级的工具,它是用来运行容器的,runC实际上就是在libcontainer上进行了接口封装,这是一个独立的二进制文件。

关于runC和containerd的更详细的源码分析,请参考我的相应博文。

docker1.12四个组件的介绍

编译

可按照官网的方式直接编译docker:https://docs.docker.com/v1.5/contributing/devenvironment/

注意,containerd和runc的源码并不直接在docker的源码中,而是已经拆分出来。

在docker编译的时候,会在github上拉取containerd和runc的源码并编译生成containerd和runc的二进制文件,包括docker-containerd、docker-containerd-shim、docker-containerd-ctr这三个可执行文件。代码在docker的Dockerfile文件中:

docker v1.11 源码重构分析_第2张图片

同样,会从github上拉取runc的源码并编译生成runc的二进制文件docker-runc。代码在docker的Dockerfile中:

docker v1.11 源码重构分析_第3张图片

docker本身编译后会生成dockerd、docker、docker-proxy这三个二进制文件。其中,dockerd是docker的daemon,docker是client。

运行

启动dockerd后,会自动启动containerd,如下所示:

这里写图片描述

docker1.12源码结构分析

dockerd和docker的主函数入口分别在cmd/dockerd和cmd/docker。首先来分析dockerd的启动流程。

dockerd启动流程

在cmd/dockerd/docker.go的main函数中,进行一些参数的初始化工作后,会调用到cmd/dockerd/daemon.go中的start()函数:

if !stop {
        err = daemonCli.start()
        notifyShutdown(err)
        if err != nil {
            logrus.Fatal(err)
        }
}

在start()函数中,会通过以下源码,根据启动参数中的tcp、unix socket等,分别创建接收client端请求的api server:

api := apiserver.New(serverConfig)
cli.api = api

for i := 0; i < len(cli.Config.Hosts); i++ {
    var err error
    if cli.Config.Hosts[i], err = opts.ParseHost(cli.Config.TLS, cli.Config.Hosts[i]); err != nil {
        return fmt.Errorf("error parsing -H %s : %v", cli.Config.Hosts[i], err)
    }

    protoAddr := cli.Config.Hosts[i]
    protoAddrParts := strings.SplitN(protoAddr, "://", 2)
    if len(protoAddrParts) != 2 {
        return fmt.Errorf("bad format %s, expected PROTO://ADDR", protoAddr)
    }

    proto := protoAddrParts[0]
    addr := protoAddrParts[1]

    // It's a bad idea to bind to TCP without tlsverify.
    if proto == "tcp" && (serverConfig.TLSConfig == nil || serverConfig.TLSConfig.ClientAuth != tls.RequireAndVerifyClientCert) {
        logrus.Warn("[!] DON'T BIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING [!]")
    }
    ls, err := listeners.Init(proto, addr, serverConfig.SocketGroup, serverConfig.TLSConfig)
    if err != nil {
        return err
    }
    ls = wrapListeners(proto, ls)
    // If we're binding to a TCP port, make sure that a container doesn't try to use it.
    if proto == "tcp" {
        if err := allocateDaemonPort(addr); err != nil {
            return err
        }
    }
    logrus.Debugf("Listener created for HTTP on %s (%s)", protoAddrParts[0], protoAddrParts[1])
    api.Accept(protoAddrParts[1], ls...)
}

接下来,会通过以下源码调用到libcontainerd/remote_linux.go当中的New()函数,创建containerdRemote,与containerd建立联系:

containerdRemote, err := libcontainerd.New(cli.getLibcontainerdRoot(), cli.getPlatformRemoteOptions()...)

在这个New()函数当中,会通过以下代码调用到runContainerdDaemon()函数:

 if r.startDaemon {
        if err := r.runContainerdDaemon(); err != nil {
            return nil, err
        }
}

在runContainerdDaemon()函数中,会启动containerd进程:

// Start a new instance
args := []string{
    "-l", fmt.Sprintf("unix://%s", r.rpcAddr),
    "--shim", "docker-containerd-shim",
    "--metrics-interval=0",
    "--start-timeout", "2m",
    "--state-dir", filepath.Join(r.stateDir, containerdStateDir),
}
if r.runtime != "" {
    args = append(args, "--runtime")
    args = append(args, r.runtime)
}
if r.debugLog {
    args = append(args, "--debug")
}
if len(r.runtimeArgs) > 0 {
    for _, v := range r.runtimeArgs {
        args = append(args, "--runtime-args")
        args = append(args, v)
    }
    logrus.Debugf("libcontainerd: runContainerdDaemon: runtimeArgs: %s", args)
}
cmd := exec.Command(containerdBinary, args...)

回到cmd/dockerd/daemon.go中的start()函数中,通过以下代码调用daemon/daemon.go中的NewDaemon()函数,创建daemon:

d, err := daemon.NewDaemon(cli.Config, registryService, containerdRemote)

在NewDaemon()函数中,处理一些基本配置后,通过以下代码调用layer/layer_store.go中的NewStoreFromOptions(options StoreOptions)函数。

d.layerStore, err = layer.NewStoreFromOptions(layer.StoreOptions{
       StorePath:                 config.Root,
       MetadataStorePathTemplate: filepath.Join(config.Root, "image", "%s", "layerdb"),
       GraphDriver:               driverName,
       GraphDriverOptions:        config.GraphOptions,
       UIDMaps:                   uidMaps,
       GIDMaps:                   gidMaps,
   })

在NewStoreFromOptions(options StoreOptions)函数中,会调用graphdriver/driver.go中的New函数,在New函数中,会通过以下代码获取到rootfs driver。rootfs driver或者是dockerd启动参数中指定的,或者是系统推荐的:

func New(root string, name string, options []string, uidMaps, gidMaps []idtools.IDMap) (Driver, error) {
   if name != "" {
       logrus.Debugf("[graphdriver] trying provided driver %q", name) // so the logs show specified driver
       return GetDriver(name, root, options, uidMaps, gidMaps)
   }

   // Guess for prior driver
   driversMap := scanPriorDrivers(root)
   logrus.Debugf("driversMap is v%", driversMap)
   for _, name := range priority {
       if name == "vfs" {
           // don't use vfs even if there is state present.
           continue
       }
       if _, prior := driversMap[name]; prior {
           // of the state found from prior drivers, check in order of our priority
           // which we would prefer
           driver, err := getBuiltinDriver(name, root, options, uidMaps, gidMaps)
           if err != nil {
               // unlike below, we will return error here, because there is prior
               // state, and now it is no longer supported/prereq/compatible, so
               // something changed and needs attention. Otherwise the daemon's
               // images would just "disappear".
               logrus.Errorf("[graphdriver] prior storage driver %q failed: %s", name, err)
               return nil, err
           }

           // abort starting when there are other prior configured drivers
           // to ensure the user explicitly selects the driver to load
           if len(driversMap)-1 > 0 {
               var driversSlice []string
               for name := range driversMap {
                   driversSlice = append(driversSlice, name)
               }

               return nil, fmt.Errorf("%q contains several valid graphdrivers: %s; Please cleanup or explicitly choose storage driver (-s )", root, strings.Join(driversSlice, ", "))
           }

           logrus.Infof("[graphdriver] using prior storage driver %q", name)
           return driver, nil
       }

docker命令行执行分析

docker client端docker的入口函数在cmd/docker/docker.go当中,在main函数中,会对命令行参数进行处理,以docker create为例,最终会调用到api/client/container/create.go中的NewCreateCommand(dockerCli *client.DockerCli)函数,然后继续调用到runCreate(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *createOptions, copts *runconfigopts.ContainerOptions)函数,进而调用到createContainer函数。然后通过以下源码调用到vendor/src/github.com/docker/engine-api/client/container_create.go中的ContainerCreate函数。

//create the container
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name)

在ContainerCreate函数中,会向api server端发起post请求:

serverResp, err := cli.post(ctx, "/containers/create", query, body, nil)
if err != nil {
    if serverResp != nil && serverResp.statusCode == 404 && strings.Contains(err.Error(), "No such image") {
        return response, imageNotFoundError{config.Image}
    }
    return response, err
}

在api/server/router/container/container.go的initRoutes()函数中,定义了以下http router:

……
// POST
router.NewPostRoute("/containers/create", r.postContainersCreate),
router.NewPostRoute("/containers/{name:.*}/kill", r.postContainersKill),
router.NewPostRoute("/containers/{name:.*}/pause", r.postContainersPause),
……

因而,server端会把由api/server/router/container/container_routes.go中的postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string)函数处理该post请求。
在postContainersCreate函数中,会通过以下代码调用到daemon/create.go中的ContainerCreate(params types.ContainerCreateConfig, validateHostname bool)函数,最后由docker daemon来创建container:

// ContainerCreate creates a regular container
func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig, validateHostname bool) (types.ContainerCreateResponse, error) {
    return daemon.containerCreate(params, false, validateHostname)
}

你可能感兴趣的:(docker,runC,containerd)