1.引言
Docker daemon是docker架构中运行在后台的守护进程,docker daemon架构大致分为四部分:docker server,docker engine,docker job和docker drivers。四者之间的关系大致可以分为,docker daemon通过docker server模块接收 docker client的请求,并在docker engine根据不同的请求创建出不同的docker job并执行这些job。docker engine通过docker driver完成不同的job的执行。docker server又可以细分为http server,router,以及route handlers。docker server根据不同的请求,完成不同的route转发至handler,并通过docker engine api创建不同的docker job。
本章主要介绍docker daemon的创建,初始化流程和docker daemon的销毁流程。
注:代码基于docker-1.9.0-dev
2.Docker daemon的启动流程
docker daemon的启动入口还是在docker项目的main()函数中,通过判断docker启动参数中是否包含daemon cammnd来启动docker client还是docker daemon。因此,docker daemon的启动通过docker daemon [OPTIONS]的形式启动,具体可以见docker daemon -h提示。
2.1 docker参数解析
在/docker/docker/docker.go文件中,定义了docker的main()函数。
main()函数中对docker flag参数解析这里不作详细分析。解析完flag参数进入handleGlobalDaemonFlag()中,判 否带有daemon cammnd,如果是docker daemon则表示启动的是docker daemon守护进程,否则创建一个docker client。
2.2 docker daemon对象创建并初始化/docker/docker/daemon.go handleGlobalDaemonFlag()中判断如果是启动daemon进程,则创建一个docker daemon对象:
func NewDaemonCli() *DaemonCli { daemonFlags = cli.Subcmd("daemon", nil, "Enable daemon mode", true) // TODO(tiborvass): remove InstallFlags? daemonConfig := new(daemon.Config) daemonConfig.LogConfig.Config = make(map[string]string) daemonConfig.ClusterOpts = make(map[string]string) daemonConfig.InstallFlags(daemonFlags, presentInHelp) daemonConfig.InstallFlags(flag.CommandLine, absentFromHelp) registryOptions := new(registry.Options) registryOptions.InstallFlags(daemonFlags, presentInHelp) registryOptions.InstallFlags(flag.CommandLine, absentFromHelp) daemonFlags.Require(flag.Exact, 0) return &DaemonCli{ Config: daemonConfig, registryOptions: registryOptions, }
}
这里最终返回的是一个初始化的daemon cli对象,包含daemon confing和docker registry config,但是config值的设定在CmdDaemon()中。CmdDaemon()中根据docker daemon之后的参数,进行配置初始化。这个函数中解析的参数就是docker daemon -h看到参数,主要包含daemon的日志配置和daemon的pid文件路径,这里不进行详细的分析。
2.3 启动docker serverdocker daemon初始化过程中很重要的一项工作就是要启动docker server的http server接受docker client的请求,也在CmdDaemon()中实现,涉及的最重要的就是Host参数docker.sock文件,用于接受client端的请求。
api, err := apiserver.New(serverConfig) if err != nil { logrus.Fatal(err) } // The serve API routine never exits unless an error occurs // We need to start it as a goroutine and wait on it so // daemon doesn't exit // All servers must be protected with some mechanism (systemd socket, listenbuffer) // which prevents real handling of request until routes will be set. serveAPIWait := make(chan error) go func() { if err := api.ServeAPI(); err != nil { logrus.Errorf("ServeAPI error: %v", err) serveAPIWait <- err return } serveAPIWait <- nil }()
2.3.1 初始化route表这里实际上只是通过goroutine一个匿名函数将docker server启动,并没有开始接受docker client的请求。在开始接受docker client请求之前,初始化docker server的两张route表:local route和network route表。关于这两张route表,前者是docker容器生命周期管理,docker images管理,docker volume等除了docker network相关的操作。后者是docker network相关的管理。
// InitRouters initializes a list of routers for the server. // Sets those routers as Handler for each server. func (s *Server) InitRouters(d *daemon.Daemon) { //创建docker容器生命周期和docker镜像管理的route表 s.addRouter(local.NewRouter(d)) //创建docker netwotk管理的的route表 s.addRouter(network.NewRouter(d)) for _, srv := range s.servers { srv.srv.Handler = s.CreateMux() }
}
2.3.2 设置daemon进程信号处理函数对docker daemon守护进程设置一个信号管理,用于销毁docker daemon进程并清理现场。
//捕捉信号,清理daemon现场 signal.Trap(func() { //关闭docker server api.Close() //通过channel发送消息给docker server <-serveAPIWait //关闭daemon进程 shutdownDaemon(d, 15) if pfile != nil { if err := pfile.Remove(); err != nil { logrus.Error(err) } } })
2.3.3 docker server开始接受请求完成了docker daemon的所有初始化工作之后,docker daemon启动完成。docker server开始接受docker client请求。首先,通过go channel机制向docker server发送消息。当docker server遇到不可恢复错误,销毁docker daemon进程。
// after the daemon is done setting up we can tell the api to start // accepting connections with specified daemon //发送消息通知docker daemon notifySystem() //读取无缓存go channel开始api listen api.AcceptConnections() // Daemon is fully initialized and handling API traffic // Wait for serve API to complete //读取无缓存channel,有不可恢复错误时,销毁docker daemon进程并清理现场 errAPI := <-serveAPIWait shutdownDaemon(d, 15) if errAPI != nil { if pfile != nil { if err := pfile.Remove(); err != nil { logrus.Error(err) } } logrus.Fatalf("Shutting down due to ServeAPI error: %v", errAPI) }
docker daemon启动完成。
3.docker daemon的销毁流程
docker daemon的销毁相对比较简单,daemon进程退出并清理现场。docker daemon守护进程退出有两种原因:
1.外部发送信号,被动销毁;
2.docker server api遇到不可恢复错误,内部主动销毁docker daemon进程
需要注意的是,docker daemon进程退出之前,会将宿主机上所有运行着的(runing and paused)docker container销毁。销毁daemon进程需谨慎。
首先看一下daemon进程退出的两种场景:
3.1 被动销毁被动销毁是值外部同步发送信号将docker daemon进程销毁。这里暂时不展开。
signal.Trap(func() { //关闭docker server api.Close() //通过channel发送消息给docker server <-serveAPIWait //关闭daemon进程 shutdownDaemon(d, 15) if pfile != nil { if err := pfile.Remove(); err != nil { logrus.Error(err) } } })
3.2 主动销毁主动销毁是在docker server api遇到不可恢复错误时,自动将docker daemon进程销毁。docker daemon主进程和docker server协程通过channel进行通信。当docker server遇到错误时,通知到docker daemon,进行自动销毁。
errAPI := <-serveAPIWait shutdownDaemon(d, 15) if errAPI != nil { if pfile != nil { if err := pfile.Remove(); err != nil { logrus.Error(err) } } logrus.Fatalf("Shutting down due to ServeAPI error: %v", errAPI) }
3.3 进程销毁并清理现场
docker dameon进程退出之后,清理现场很简单,就是将docker.pid进程文件删除。docker daemon进程开始退出到完全退出设置了15s的超时时间。如果15s还没有完全退出,则会强制退出。
docker daemon进程销毁的时候将会判断当前宿主机上是否有docker容器。如果有docker容器正在运行进程退出过程将会稍微复杂一些,否则简单一些。下面先看看宿主机上没有docker容器的情况。 当docker宿主机上没有docker容器时,daemon进程退出时,主要完成以下清理:
3.3.1 断开数据库连接
daemon进程将容器实例的信息和容器之间的关联保存在数据库中,称之为graph database。docker daemon进程退出时,将这个数据库连接关闭。
if daemon.containerGraphDB != nil { //关闭和graph database数据库的连接 //graph database数据库存储的是container实例和他们之间的关联 if err := daemon.containerGraphDB.Close(); err != nil { logrus.Errorf("Error during container graph.Close(): %v", err) } }
3.3.2 devicemapper cleanupdocker如果使用的存储驱动是devicemapper,在docker daemon退出的时候,要清理devicemapper设备。
if daemon.driver != nil { //根据不同的联合文件系统的driver进行相应的清理,umount目录,devicemapper设备 if err := daemon.driver.Cleanup(); err != nil { logrus.Errorf("Error during graph storage driver.Cleanup(): %v", err) }
}
3.3.3 umount设备
umount /dev/shm
umount /dev/mqueue
这两个设备的具体作用后面在分析
3.3.4 容器销毁
当docker节点上还有docker container在运行时,daemon进程退出之前,会对这些container做最后的销毁。完成对container最后处理之后,再按照3.3.1-3的步骤,最后docker daemon进程完全退出。
容器有几中状态:runging, paused和exited。
如果当前docker节点上所有的container都已经是exited状态,则只要清理docker network,暂停docker network。
如果容器状态为running,则首先发送一个SIGTERM的信号给容器进程,如果超时(10s)未关闭,则发送KILL信号,强制销毁。
如果容器状态为paused,则处理的时候稍微复杂一点。首先明白,docker在paused一个容器是通过cgroup的freezer子系统实现的。将container的init进程添加到freezer中,将进程从系统调度中拉出。因此,在处理paused状态的容器时:
send SIGTERM signal
upaused container
send SIGTERM signal
send SIGKILL signal(maybe)
也就是说,当宿主机上存在paused状态的容器时,处理流程是最长的。
在docker v1.9.1之后,docker新增了docker network的命令,用来控制docker和docker容器的网络。如果当前宿主机上已经已经运行过docker container,则还需要清理network。
docker daemon shutdown流程图: