在上文中,笔者通过分析Docker的架构,初步作了Docker的架构图。架构图本身更多的出于笔者的理解,为了便于理解,对于Docker代码本身做了一些抽象,例如Server的运行都是以一个Job的形式存在的,而架构图中并未明显的表明这一点。
本文将从源码的角度分析Docker的启动,主要是作为一个daemon进程的启动。在这之前,需要先清晰Docker内部最主要的几个概念:Daemon,Engine以及Job。
Daemon可以认为是Docker守护进程的载体。从源码的视角来看,Daemon可以认为是Daemon结构体,以及Daemon package中定义的一系列方法的总和。同时Daemon也是Docker内部的一个结构体,从结构体的定义,可以看出Daemon关联了Docker的绝大部分的内容,Daemon结构的定义如下:
type Daemon struct {
repository string
sysInitPath string
containers *contStore
graph *graph.Graph
repositories *graph.TagStore
idIndex *truncindex.TruncIndex
sysInfo *sysinfo.SysInfo
volumes *graph.Graph
eng *engine.Engine
config *Config
containerGraph *graphdb.Database
driver graphdriver.Driver
execDriver execdriver.Driver
}
以下简要介绍结构体内部每个对象:
type Engine struct {
handlers map[string]Handler
catchall Handler
hack Hack // data for temporary hackery (see hack.go)
id string
Stdout io.Writer
Stderr io.Writer
Stdin io.Reader
Logging bool
tasks sync.WaitGroup
l sync.RWMutex // lock for shutdown
shutdown bool
onShutdown []func() // shutdown handlers
} 其中需要特别注意的就是handlers属性,该属性为一个map类型的对象,存储的都是关于某个特定handler的处理方法,之后会详细分析handler。关于Job的定义,源码中注释如此说道:在Docker的engine中,Job是最基本基本工作单位。Docker可以做的所有工作,最终都必须表示成一个Job。例如:在容器内执行一个进程,这是一个Job;创建一个新容器,这是一个Job;从Internet上下载一份文档,这是一个Job;服务于HTTP的API,这也是一个Job,等等。Job的定义源码如下:
type Job struct {
Eng *Engine
Name string
Args []string
env *Env
Stdout *Output
Stderr *Output
Stdin *Input
handler Handler
status Status
end time.Time
}同时,Job的API设计得很像一个unix的进程:比如说,Job有一个名称,有参数,有环境变量,有标准的输入输出,有错误处理,有返回状态,其中返回0表示执行成功,返回其他数字表示错误。
Docker的启动可以认为是通过Docker的可执行文件,启动一个Docker的守护进程,这个守护进程在启动过程中完成了启动所需要的所有工作,并且最终作为一个server可以为docker client发送来的众多请求服务。
以下从源码的角度分析Docker的启动。
首先,Docker的main函数位于./docker/docker.go中。执行流程如下。
在main函数中,首先执行了以下内容:
if reexec.Init() {
return
}
flag.Parse()
// FIXME: validate daemon flags here
if *flVersion {
showVersion()
return
}
if *flDebug {
os.Setenv("DEBUG", "1")
} 首先判断reexec.Init()的返回值,若为真,直接返回;否则进行flag.Parse()方法,该方法主要解析了flag参数,并为之后的flag参数判断做准备。众多的flag参数位于./docker/flag.go中,并且在main函数执行之前就完成var的定义以及init函数的执行。解析玩flag参数之后,随即判断众参数,若*flVersion为真的话,直接通过showVersion()方法显示Docker的版本号,随后返回;若*flVersion不为真,则继续往下判断,若*flDebug为真,对于DEBUG环境变量设置值为1,继续往下执行。
接着的代码执行如下:
if len(flHosts) == 0 {
defaultHost := os.Getenv("DOCKER_HOST")
if defaultHost == "" || *flDaemon {
// If we do not have a host, default to unix socket
defaultHost = fmt.Sprintf("unix://%s", api.DEFAULTUNIXSOCKET)
}
defaultHost, err := api.ValidateHost(defaultHost)
if err != nil {
log.Fatal(err)
}
flHosts = append(flHosts, defaultHost)
}
以上代码的功能主要是查找host地址,作为之后server的监听地址。如果在flag的定义以及初始化之后,flHost的长度依旧为0的话,则说明配置中没有设定Host地址,需要程序自行查找。首先,通过宿主机环境变量中的DOCKER_HOST来给默认host变量defaultHost赋值,如果仍然为空,或者*flDaemon为真的话,通过api定义的DEFAULTDAEMON属性来初始化defaultHost,默认为一个unix socket。经过验证该defaultHost之后,将defaultHost添加至flHost末尾。
以上可以认为是为Docker daemon的运行做了充足的准备工作,以下的代码真正在做Docker Daemon的启动。
if *flDaemon {
mainDaemon()
return
}
也就是说若*flDaemon为真,则直接运行mainDaemon()方法。以下将大篇幅分析介绍mainDaemon()所做的工作。
mainDaemon()的实现位于文件./docker/daemon.go中。
首先,Daemon执行flag.NArg(),当flag参数被处理后,已经没有其他的参数时,继续往下执行。
Daemon创建一个engine,并随时捕获engine的shutdown信号。
加载builtins,代码为builtins.Register(eng),进入./docker/builtins.go,在该方法中主要包含了五个步骤,如下:
func Register(eng *engine.Engine) error {
if err := daemon(eng); err != nil {
return err
}
if err := remote(eng); err != nil {
return err
}
if err := events.New().Install(eng); err != nil {
return err
}
if err := eng.Register("version", dockerVersion); err != nil {
return err
}
return registry.NewService().Install(eng)
}1. daemon(eng) : 所做工作是为engine注册一个handler,具体的handler名称为“init_networkdriver”。具体的功能是初始化Docker环境的docker0网桥,处理方法的实现位于./daemon/networkdriver/bridge/driver.go中的InitDriver.
func daemon(eng *engine.Engine) error {
return eng.Register("init_networkdriver", bridge.InitDriver)
}
2.remote(eng) : 所做的工作是为engine注册两个handler,第一个handler的名称为“serveapi”,具体的功能是使得daemon提供RESTful的API,保证daemon可以与外界建立通信,处理方法的实现为./api/server/server.go中的ServeApi;第二个handler的名称为“acceptconnection”,具体的功能是使得初始化完毕的daemon可以接收请求,处理方法的实现为./api/server/server.go中的AcceptConnections。代码如下:
func remote(eng *engine.Engine) error {
if err := eng.Register("serveapi", apiserver.ServeApi); err != nil {
return err
}
return eng.Register("acceptconnections", apiserver.AcceptConnections)
}// Install installs events public api in docker engine
func (e *Events) Install(eng *engine.Engine) error {
// Here you should describe public interface
jobs := map[string]engine.Handler{
"events": e.Get,
"log": e.Log,
"subscribers_count": e.SubscribersCount,
}
for name, job := range jobs {
if err := eng.Register(name, job); err != nil {
return err
}
}
return nil
}
4.eng.Register("version",dockerVersion): Docker的engine注册一个名称为“ version”的handler,处理方法的实现为当前builtins.go文件中的dockerVersion。
5.registry.NewService().Install(eng):方法实现位于./registry/service.go,首先先获取Service对象,随后通过Install方法来注册两个handler,第一个的名称为“auth”,实现在公有registry中的认证;第二个的名称为“search”,实现在公有registry中查找image的功能。
// Install installs registry capabilities to eng.
func (s *Service) Install(eng *engine.Engine) error {
eng.Register("auth", s.Auth)
eng.Register("search", s.Search)
return nil
}
以上分析大部分builtins.Register(eng)的实现。回到mainDaemon方法中,即进入一个goroutine,如下:
go func() {
d, err := daemon.NewDaemon(daemonCfg, eng)
if err != nil {
log.Fatal(err)
}
if err := d.Install(eng); err != nil {
log.Fatal(err)
}
b := &builder.BuilderJob{eng, d}
b.Install()
// after the daemon is done setting up we can tell the api to start
// accepting connections
if err := eng.Job("acceptconnections").Run(); err != nil {
log.Fatal(err)
}
}()
首先执行的是d, err := daemon.NewDaemon(daemonCfg, eng),作用为创建一个daemon对象,代码实现位于./daemon/daemon.go的NewDaemon方法。在NewDaemon的实现过程中,可以发现具体调用的方法为daemon, err := NewDaemonFromDirectory(config, eng)。在这里,我们可以先来看看该config参数的来历。在加载daemon的goroutine中,NewDaemon的实参为daemonCfg。在./docker/daemon.go中,有daemonCfg = &daemon.Config{},而在该文件中的init()方法中实现了daemonCfg.InstallFlags(),而InstallFlags()的实现位于./docker/daemon/config.go,实现过程中加载了很多需要的配置项,几乎Docker所需要的所有配置信息都在该放啊中实现初始化。
这里涉及到了Golang的一个特性,即init()方法的执行。在golang中init()方法的特性如下:
了解完config的来历,进入NewDaemonFromDirectory的实现。该方法的实现,可以简易的认为提供以下功能。
1.验证或配置config参数
// Apply configuration defaults
if config.Mtu == 0 {
// FIXME: GetDefaultNetwork Mtu doesn't need to be public anymore
config.Mtu = GetDefaultNetworkMtu()
}
// Check for mutually incompatible config options
if config.BridgeIface != "" && config.BridgeIP != "" {
return nil, fmt.Errorf("You specified -b & --bip, mutually exclusive options. Please specify only one.")
}
if !config.EnableIptables && !config.InterContainerCommunication {
return nil, fmt.Errorf("You specified --iptables=false with --icc=false. ICC uses iptables to function. Please set --icc or --iptables to true.")
}
// FIXME: DisableNetworkBidge doesn't need to be public anymore
config.DisableNetwork = config.BridgeIface == DisableNetworkBridge
// Claim the pidfile first, to avoid any and all unexpected race conditions.
// Some of the init doesn't need a pidfile lock - but let's not try to be smart.
if config.Pidfile != "" {
if err := utils.CreatePidFile(config.Pidfile); err != nil {
return nil, err
}
eng.OnShutdown(func() {
// Always release the pidfile last, just in case
utils.RemovePidFile(config.Pidfile)
})
}
2.验证系统支持度以及执行用户的权限
// Check that the system is supported and we have sufficient privileges
// FIXME: return errors instead of calling Fatal
if runtime.GOOS != "linux" {
log.Fatalf("The Docker daemon is only supported on linux")
}
if os.Geteuid() != 0 {
log.Fatalf("The Docker daemon needs to be run as root")
}
if err := checkKernelAndArch(); err != nil {
log.Fatalf(err.Error())
}
3.配置或创建Docker所需要的工作路径
// set up the TempDir to use a canonical path
tmp, err := utils.TempDir(config.Root)
if err != nil {
log.Fatalf("Unable to get the TempDir under %s: %s", config.Root, err)
}
realTmp, err := utils.ReadSymlinkedDirectory(tmp)
if err != nil {
log.Fatalf("Unable to get the full path to the TempDir (%s): %s", tmp, err)
}
os.Setenv("TMPDIR", realTmp)
if !config.EnableSelinuxSupport {
selinuxSetDisabled()
}
// get the canonical path to the Docker root directory
var realRoot string
if _, err := os.Stat(config.Root); err != nil && os.IsNotExist(err) {
realRoot = config.Root
} else {
realRoot, err = utils.ReadSymlinkedDirectory(config.Root)
if err != nil {
log.Fatalf("Unable to get the full path to root (%s): %s", config.Root, err)
}
}
config.Root = realRoot
// Create the root directory if it doesn't exists
if err := os.MkdirAll(config.Root, 0700); err != nil && !os.IsExist(err) {
return nil, err
}
4.设置以及加载多种driver
// Set the default driver
graphdriver.DefaultDriver = config.GraphDriver
// Load storage driver
driver, err := graphdriver.New(config.Root, config.GraphOptions)
if err != nil {
return nil, err
}
log.Debugf("Using graph driver %s", driver)
// As Docker on btrfs and SELinux are incompatible at present, error on both being enabled
if config.EnableSelinuxSupport && driver.String() == "btrfs" {
return nil, fmt.Errorf("SELinux is not supported with the BTRFS graph driver!")
}
5.创建Docker Image所需要的graph,graphdb,volumns等
log.Debugf("Creating images graph")
g, err := graph.NewGraph(path.Join(config.Root, "graph"), driver)
if err != nil {
return nil, err
}
// We don't want to use a complex driver like aufs or devmapper
// for volumes, just a plain filesystem
volumesDriver, err := graphdriver.GetDriver("vfs", config.Root, config.GraphOptions)
if err != nil {
return nil, err
}
log.Debugf("Creating volumes graph")
volumes, err := graph.NewGraph(path.Join(config.Root, "volumes"), volumesDriver)
if err != nil {
return nil, err
}
log.Debugf("Creating repository list")
repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g)
if err != nil {
return nil, fmt.Errorf("Couldn't create Tag store: %s", err)
……
graphdbPath := path.Join(config.Root, "linkgraph.db")
graph, err := graphdb.NewSqliteConn(graphdbPath)
if err != nil {
return nil, err
}
6.关于dockerinit的一系列操作
localCopy := path.Join(config.Root, "init", fmt.Sprintf("dockerinit-%s", dockerversion.VERSION))
sysInitPath := utils.DockerInitPath(localCopy)
if sysInitPath == ""
return nil, fmt.Errorf("Could not locate dockerinit: This usually means docker was built incorrectly. See http://docs.docker.com/contributing/devenvironment for official build instructions.")
}
7.验证DNS,判断docker container是否可以使用host的resolv.conf文件,若不能的话,使用默认的外界DNS server:“8.8.8.8”和“8.8.4.4”;
if err := daemon.checkLocaldns(); err != nil {
return nil, err
}8.重新加载之前的docker container。例如,当Docker进程重启后,会restore之前运行着的docker container。
if err := daemon.restore(); err != nil {
return nil, err
}
9.最终返回daemon对象
return daemon, nil
以上的9个步骤执行完NewDaemonFromDirectory之后,在goroutine之间执行d.Install(eng),该方法的实现位于./daemon/daemon.go中的Install方法,功能是为engine注册众多的handler,handler的actions位于./daemon/下的众多go文件中。例如有以下{"create": daemon.ContainerCreate}handler,则当job的名称为create时,运行时的action为daemon.ContainerCreate, 位于./daemon/create.go。
随后执行代码为:
b := &builder.BuilderJob{eng, d}
b.Install()
这部分内容的功能为注册build的handler,位于./builder/job.go文件中,job的名称为“build”,处理方法为CmdBuild具体实现如下:
func (b *BuilderJob) Install() {
b.Engine.Register("build", b.CmdBuild)
}
goroutine的最后一个步骤就是开始执行接收请求,即执行名称为“acceptconnections”的job,处理方法为./api/server/server.go中的AcceptConnections。
以上的部分,即表示goroutine的运行流程,即加载daemon的运行流程。
在goroutine运行的同时,mainDaemon同时还在执行名称为“serveapi“的job,代码如下:
// Serve api
job := eng.Job("serveapi", flHosts...)
job.SetenvBool("Logging", true)
job.SetenvBool("EnableCors", *flEnableCors)
job.Setenv("Version", dockerversion.VERSION)
job.Setenv("SocketGroup", *flSocketGroup)
job.SetenvBool("Tls", *flTls)
job.SetenvBool("TlsVerify", *flTlsVerify)
job.Setenv("TlsCa", *flCa)
job.Setenv("TlsCert", *flCert)
job.Setenv("TlsKey", *flKey)
job.SetenvBool("BufferRequests", true)
if err := job.Run(); err != nil {
log.Fatal(err)
}
在创建job的同时,使用到了参数flHost,也就是在mainDaemon之前的获取的flHost。由于在./builtins/builtins.go中注册了名称为“serveapi”的handler,所以只要运行相应的处理方法即可,为./api/server/server.go中的ServeApi方法。
由于Docker中所有关于container以及image等工作都必须暴露为一个job,因此Docker启动的完毕标志,可以认为是Docker完成server的启动,并最终为通过api来访问的请求进行服务。通过server来代理请求,并最终分发到相应的job上来执行。
在Docker整个启动过程中,笔者认为最为重要,最为核心的部分为NewDaemonFromDirectory的实现,该部分配置了众多Daemon结构内部的属性,而这些属性在之后,都会涉及到很多实际操作container以及graph的工作,换言之,daemon保留了其他模块的访问接口。
因此,在Docker内部,运行靠engine,执行靠job,访问driver等靠daemon。
转载请注明出处。
本文更多出于我本人的理解,肯定在一些地方存在不足和错误。希望本文能够对接触Docker的人有些帮助,如果你对这方面感兴趣,并有更好的想法和建议,也请联系我。
原文地址:http://blog.csdn.net/shlazww/article/details/39188933