基于docker v1.12的源代码,对docker engine v1.11中重构后的源码结构进行分析,涵盖dockerd, containerd, containerd-shim, runC。
docker在v1.11版本进行了重大的重构,对docker engine和container进行了解耦,docker engine运行在containerd上,containerd运行在runC上,通过containerd-shim中间层进行了解耦。之前的docker engine分为四个组件:dockerd、containerd、containerd-shim、runC,之间关系如下图所示:
containerd是一个守护进程,它可以使用runC的接口管理容器,使用gRPC暴露容器的其他功能。由于容器运行时是孤立的引擎,引擎最终能够启动和升级而无需重新启动容器。在v1.12版本中,在dockerd的启动参数或者配置文件中,开启live-store
参数即可,具体参考 官方live-store升级dockerd文档。
runC是一个轻量级的工具,它是用来运行容器的,runC实际上就是在libcontainer上进行了接口封装,这是一个独立的二进制文件。
关于runC和containerd的更详细的源码分析,请参考我的相应博文。
可按照官网的方式直接编译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文件中:
同样,会从github上拉取runc的源码并编译生成runc的二进制文件docker-runc。代码在docker的Dockerfile中:
docker本身编译后会生成dockerd、docker、docker-proxy这三个二进制文件。其中,dockerd是docker的daemon,docker是client。
启动dockerd后,会自动启动containerd,如下所示:
dockerd和docker的主函数入口分别在cmd/dockerd和cmd/docker。首先来分析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 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)
}