docker 源码分析 一(基于1.8.2版本),docker daemon启动过程;

摘要:
最近,我在学习Golang,还学习了Docker的源代码,Docker是一个流行的开源项目。在中国家喻户晓的Docker的源代码分析是孙宏亮丹尼尔撰写的一系列文章,但基于的Docker版本有点陈旧;我只是获取了最新的代码研究;Docker是一种c/s架构,分为dockerclient和dockerdamon。客户端发送命令,守护进程端负责完成客户端发送的命令。

最近在研究golang,也学习一下比较火的开源项目docker的源代码,国内比较出名的docker源码分析是孙宏亮大牛写的一系列文章,但是基于的docker版本有点老;索性自己就git 了一下最新的代码研读;

docker是c/s的架构,分为docker client 和 docker daemon,client端发送命令,daemon端负责完成client发送过来的命令(如获取和存储镜像、管理容器等)。两者之间可以通过TCP,HTTP和UNIX SOCKET来进行通信;

docker的启动入口代码在 docker/docker.go

func main() {

    if reexec.Init() {

        return

    }

    // Set terminal emulation based on platform as required.

    stdin, stdout, stderr := term.StdStreams()

    logrus.SetOutput(stderr)

    flag.Merge(flag.CommandLine, clientFlags.FlagSet, commonFlags.FlagSet)

    flag.Usage = func() {

        fmt.Fprint(os.Stdout, "Usage: docker [OPTIONS] COMMAND [arg...] "+daemonUsage+"       docker [ --help | -v | --version ] ")

        fmt.Fprint(os.Stdout, "A self-sufficient runtime for containers. Options: ")

        flag.CommandLine.SetOutput(os.Stdout)

        flag.PrintDefaults()

        help := " Commands: "

        for _, cmd := range dockerCommands {

            help += fmt.Sprintf("    %-10.10s%s ", cmd.name, cmd.description)

        }

       help += " Run 'docker COMMAND --help' for more information on a command."

        fmt.Fprintf(os.Stdout, "%s ", help)

    } 

    flag.Parse()

    if *flVersion {

        showVersion()

        return

    }

    clientCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags)

    // TODO: remove once `-d` is retired

    handleGlobalDaemonFlag()

    if *flHelp {

        // if global flag --help is present, regardless of what other options and commands there are,

        // just print the usage.

        flag.Usage()

        return

    }

    c := cli.New(clientCli, daemonCli)

    if err := c.Run(flag.Args()...); err != nil {

        if sterr, ok := err.(cli.StatusError); ok {

            if sterr.Status != "" {

                fmt.Fprintln(os.Stderr, sterr.Status)

                os.Exit(1)

            }

            os.Exit(sterr.StatusCode)

        }

        fmt.Fprintln(os.Stderr, err)

        os.Exit(1)

    }

}

func showVersion() {

    if utils.ExperimentalBuild() {

        fmt.Printf("Docker version %s, build %s, experimental ", dockerversion.VERSION, dockerversion.GITCOMMIT)

    } else {

        fmt.Printf("Docker version %s, build %s ", dockerversion.VERSION, dockerversion.GITCOMMIT)

    }

}

从main函数入口开始,首先是reexec.Init()(在pkg/reexec/reexec.go文件中),看有没有注册的初始化函数,如果有,就直接return了;

stdin, stdout, stderr := term.StdStreams()   返回标准输入、输出、错误流;

logrus 设置log;

之后就进入了比较主要的参数解析的环节

flag.Merge(flag.CommandLine, clientFlags.FlagSet, commonFlags.FlagSet)

使用到了flag包,主要函数定义在是pkg/mflag/flag.go中,里面两个比较重要的类型是:

type FlagSet struct {

    Usage func()
    ShortUsage func()

    name string
    parsed bool
    actual map[string]*Flag
    formal map[string]*Flag
    args []string // arguments after flags
    errorHandling ErrorHandling
    output io.Writer // nil means stderr; use Out() accessor
    nArgRequirements []nArgRequirement
}

type Flag struct {
    Names []string // name as it appears on command line
    Usage string // help message
    Value Value // value as set
    DefValue string // default value (as text); for usage message
}

Flag是用来处理命令行中类似于如下写法的命令行参数的;

-flag
-flag=x
-flag="x"
-flag='x'
-flag x

一个横线和两个横线的效果是相同的,flag包类似于golang中的flag包,只不过是自己实现了一个。Flag中的Names是一个字符串数组,表示flag的名字,比如["v", "-verbose"]

 FlagSet一组Flag的集合,Usage函数是解析出错的时候要执行的回调函数,args[]是指解析完flag之后还剩下的参数,一般是docker的cmd命令,例如pull,run等等

actual和formal分别是一个map对象,key是string,value是Flag,两者区别:actual的key存放的是实际解析时候遇到实际flag的名字,formal的则是将 flag Names属性中的值都作为key加入进来;

比如: 实际运行的命令是 "docker -verbose",那么actual中存放的是 [verbose] = flag ,但是formal中存放的是有两个纪录,第一个是[v] = flag ,另一个则是 [verbose] = flag; 后面还会提高;

接下来是flag的Merge操作

顾名思义,Merge操作的作用其实就是将几个FlagSet合并成一个;代码在pkg/mflag/flag.go中;

func Merge(dest *FlagSet, flagsets ...*FlagSet) error {

    for _, fset := range flagsets {

        for k, f := range fset.formal {

            if _, ok := dest.formal[k]; ok {

                var err error

                if fset.name == "" {

                    err = fmt.Errorf("flag redefined: %s", k)

                } else {

                    err = fmt.Errorf("%s flag redefined: %s", fset.name, k)

                }

                fmt.Fprintln(fset.Out(), err.Error())

                // Happens only if flags are declared with identical names

                switch dest.errorHandling {

                case ContinueOnError:

                    return err

                case ExitOnError:

                    os.Exit(2)

                case PanicOnError:

                    panic(err)

                }

            }

            newF := *f

            newF.Value = mergeVal{f.Value, k, fset}

            dest.formal[k] = &newF

        }

    }

    return nil

}

代码将最终的结果dest返回来;

 clientFlag 和 commonFlag 的定义分别位于docker/client.go, docker/common.go

docker/client.go 主要定义了 客户端config文件的所在路径

client := clientFlags.FlagSet

client.StringVar(&clientFlags.ConfigDir, []string{"-config"}, cliconfig.ConfigDir(), "Location of client config files"), 

这个写法的含义是将命令行的 -config参数绑定到clientFlags.ConfigDir这个变量之上,如果命令行包含了-config的参数,则可以通过clientFlags.ConfigDir来获取;

而docker/common.go主要定义了log-level、debug模式、TLS相关key,证书的设置,还有host,daemon启动后,客户端要去链接daemon的哪一个地址;

接来下是给flag的Usage函数赋值,当flag解析参数出问题的时候,将docker命令打印出来;

然后是flag.Parse() 开始解析参数;flag.Parse()函数主要是调用的是pkg/flag/flag.go里面的parseOne()函数,来看一下parseOne函数;

parseOne函数主要来解析命令行 os.Args[1:],主要做了这样几件事情:

(a)遇到第一个不是'-'开头的就停止解析;

(b)遇到第一个以'--'开头的也停止解析;

(c)将解析好的参数放到上文提到的fs.actual结构中;

(d)如果遇到的参数名字是过时(deprecated)的(过时参数在fs.formal中一般用'#' 开头表示),则用不过时的名字来替换掉;

接下来继续回到docker.go 

if *flVersion {

        showVersion()

        return

    }

 如果 flVersion是true的话,打印version信息,然后直接return了;

再下来,

clientCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags), NewDockerCli表示的是docker客户端,在api/client/cli.go中定义;

在api/client包下面,还有很多go文件。例如 ps.go,pull.go 等等,这些就是我们使用docker客户端的时候发送的命令的实现。这些命令的代码遵循一个规则,命名都是用Cmd开始的。

例如,CmdPull,CmdPs等等;后面会谈到这些方法怎样被调用的;

接着是  handleGlobalDaemonFlag()  函数的定义在 docker/daemon.go 中;

var (

    flDaemon              = flag.Bool([]string{"#d", "#-daemon"}, false, "Enable daemon mode (deprecated; use docker daemon)")

    daemonCli cli.Handler = NewDaemonCli()

)

// TODO: remove once `-d` is retired

func handleGlobalDaemonFlag() {

    // This block makes sure that if the deprecated daemon flag `--daemon` is absent,

    // then all daemon-specific flags are absent as well.

    if !*flDaemon && daemonFlags != nil {

        flag.CommandLine.Visit(func(fl *flag.Flag) {

            for _, name := range fl.Names {

                name := strings.TrimPrefix(name, "#")

                if daemonFlags.Lookup(name) != nil {

                    // daemon flag was NOT specified, but daemon-specific flags were

                    // so let's error out

                    fmt.Fprintf(os.Stderr, "docker: the daemon flag '-%s' must follow the 'docker daemon' command. ", name)

                    os.Exit(1)

                }

            }

        })

    }

    if *flDaemon {

        if *flHelp {

            // We do not show the help output here, instead, we tell the user about the new daemon command,

            // because the help output is so long they would not see the warning anyway.

            fmt.Fprintln(os.Stderr, "Please use 'docker daemon --help' instead.")

            os.Exit(0)

        }

        daemonCli.(*DaemonCli).CmdDaemon(flag.Args()...)

        os.Exit(0)

    }

这个函数的定义是如果--daemon参数被设置成false(或者说没有出现),那么与daemon有关的其他参数如果出现,则打印错误信息,并且退出;

与daemon相关的其他参数定义在 daemonFlags ,实现文件是docker/daemon.go中,相关的具体的参数在 daemon/config_unix.go中,相关的参数主要有,

dns,graph,pidfile等参数;

当flDaemon参数为true的时候,说明docker以daemon形式启动,则调用daemonCli的CmdDaemon启动docker daemon,docker daemon的启动主要伴随着 httpserver的启动(接收docker client发送过来的需求)和 docker 守护进程的创建(包括docker网络设置初始化,存储初始化等等)。看下CmdDaemon的细节,在docker/daemon.go中;

上面说了 CmdDaemon的启动主要包括两个部分,第一个部分是httpserver的启动,这个server就是用来接收docker client端发过来的命令请求的,另一个部分做一些docker daemon启动时的一些准备工作;看代码;截取部分代码片段:

     api := apiserver.New(serverConfig)

    // 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

    serveAPIWait := make(chan error)

    go func() {

        if err := api.ServeAPI(commonFlags.Hosts); err != nil {

            logrus.Errorf("ServeAPI error: %v", err)

            serveAPIWait <- err

            return

        }

        serveAPIWait <- nil

    }()

    if err := migrateKey(); err != nil {

        logrus.Fatal(err)

    }

    cli.TrustKeyPath = commonFlags.TrustKey

    registryService := registry.NewService(cli.registryOptions)

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

    if err != nil {

        if pfile != nil {

            if err := pfile.Remove(); err != nil {

                logrus.Error(err)

            }

        }

        logrus.Fatalf("Error starting daemon: %v", err)

    }

    logrus.Info("Daemon has completed initialization")

    logrus.WithFields(logrus.Fields{

        "version":     dockerversion.VERSION,

        "commit":      dockerversion.GITCOMMIT,

        "execdriver":  d.ExecutionDriver().Name(),

        "graphdriver": d.GraphDriver().String(),

    }).Info("Docker daemon")

    signal.Trap(func() {

        api.Close()

        <-serveAPIWait

        shutdownDaemon(d, 15)

        if pfile != nil {

            if err := pfile.Remove(); err != nil {

                logrus.Error(err)

            }

        }

    })

    // after the daemon is done setting up we can tell the api to start

    // accepting connections with specified daemon

    api.AcceptConnections(d)

    // Daemon is fully initialized and handling API traffic

    // Wait for serve API to complete

    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)

    }

    return nil

}

首先实例化一个api server,apiserver的定义在 api/server/server.go 文件中,

// Config provides the configuration for the API server

type Config struct {

    Logging     bool

    EnableCors  bool

    CorsHeaders string

    Version     string

    SocketGroup string

    TLSConfig   *tls.Config

}

// Server contains instance details for the server

type Server struct {

    daemon  *daemon.Daemon

    cfg     *Config

    router  *mux.Router

    start   chan struct{}

    servers []serverCloser

}

 // New returns a new instance of the server based on the specified configuration.

func New(cfg *Config) *Server {

    srv := &Server{

        cfg:   cfg,

        start: make(chan struct{}),

    }

    r := createRouter(srv)

    srv.router = r

    return srv

}

New函数返回了一个Server的实例,我理解一个Server可以应对不同的协议(http,tcp等等),所以Server有一个servers的数组,其中的每一个元素对应服务一种协议的请求;

Server中还有一个阻塞的 通道 start,这个start的作用在于:http server先启动,会往start里面添加一个元素,由于是阻塞的通道,所以server会一直阻塞在那里,还不能对外服务。这样是为了等待 docker daemon的其他初始化工作的完成(网络初始化等),待docker daemon的初始化工作完成,会从通道中获取元素,这样http server正式开始对外提供服务;后面还会讲到;

Server中还有一个Router,通过createRouter函数创建,就是来提供Url映射的(使用的是gorilla.mux),将一个url映射到处理这个url的服务上; 可以看一下createRouter的代码,核心元素是m,是一个map[string]map[string]HTTPAPIFunc,这样一个二维结构,记录url与方法之间的关系;

接下来创建一个ServerApiWait阻塞通道, 采用一个go routine 来启动api的ServerAPI,ServerAPI的代码在api/server/server.go中,如下所示:

func (s *Server) ServeAPI(protoAddrs []string) error {

    var chErrors = make(chan error, len(protoAddrs))

    for _, protoAddr := range protoAddrs {

        protoAddrParts := strings.SplitN(protoAddr, "://", 2)

        if len(protoAddrParts) != 2 {

            return fmt.Errorf("bad format, expected PROTO://ADDR")

        }

        srv, err := s.newServer(protoAddrParts[0], protoAddrParts[1])

        if err != nil {

            return err

        }

        s.servers = append(s.servers, srv...)

        for _, s := range srv {

            logrus.Infof("Listening for HTTP on %s (%s)", protoAddrParts[0], protoAddrParts[1])

            go func(s serverCloser) {

                if err := s.Serve(); err != nil && strings.Contains(err.Error(), "use of closed network connection") {

                    err = nil

                }

                chErrors <- err

            }(s)

        }

    }

    for i := 0; i < len(protoAddrs); i++ {

        err := <-chErrors

        if err != nil {

            return err

        }

    }

    return nil

}

根据地址信息,每一个地址开启一个新的goroutine 启动一个server来对外提供服务,如果启动过程中有错误,则将错误信息放入chErrors通道中。最后便利chErrors的通道,如果有错误,则将错误作为返回值返回;

回到ServerAPIWait,如果在api.ServeAPI(commonFlags.Hosts)启动的过程中有错误发生的话,那么则go routine直接返回,如果没有错误则处于阻塞状态,准备对外提供服务;

接着就是启动docker daemon进程了,daemon.NewDaemon(cli.Config, registryService),这个部分的具体细节等到下一篇博客在仔细分析;

signal.Trap() 用来接收 用户发出的命令,例如 control + c之类的: 首先是解除serveAPIWait的阻塞,然后是shutdownDaemon 以及  remove pidfile;

当启动完毕NewDaemon之后, 通过 api.AcceptConnections(d) 来通知ServeAPI启动的http server: docker daemon的启动和初始化工作已经做好,http server们可以对外来提供服务接收请求了;

那么它是怎样通知的呢? 我们可以看一下api.AcceptConnection(d)的代码:

func (s *Server) AcceptConnections(d *daemon.Daemon) {
// Tell the init daemon we are accepting requests
    s.daemon = d
    s.registerSubRouter()
    go systemdDaemon.SdNotify("READY=1")
    // close the lock so the listeners start accepting connections
    select {
        case <-s.start:
        default:
            close(s.start)
    }
}

之前我们在讲Server是的时候,提高 Server在启动的时候,会在监听(listener)工作开始之前往start阻塞通道里面写值,这样就会阻塞住,在这里将阻塞释放,随后http server就可以开始监听了;

最后一段代码是:

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)
}
return nil

这段代码的作用在于:如果http server的在Serve的过程中,如果有error发生,那么这个error会被放入serveAPIWait的通道中,如果发现错误,则要关闭daemon程序;

整体的daemon的启动过程大致讲完了。下面会具体的跟踪一条命令,看一下究竟从docker client到docker daemon 的命令的发送到执行是如何进行的;

免责声明:文章转载自《docker 源码分析 一(基于1.8.2版本),docker daemon启动过程;》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇解决elasticsearch报错报错“java.lang.IllegalArgumentException: Rejecting mapping update to [这里是索引名称保密] as the final mapping would have more than 1 type: [_doc, log]"”java 邮件发送下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Docker 安装、卸载、启动、停止

1.1 查看当前系统的内核版本 查看当前系统的内核版本是否高于 3.10 英文文档:https://docs.docker.com/ 中文文档:https://docs.docker-cn.com/ (最近无法访问) 1.2 安装 Docker 服务 使用镜像仓库进行安装,采用 yum 命令在线安装(即电脑需要联网) root 用户运行以下命令: 1....

容器化部署Cassandra高可用集群

前提: 三台装有docker的虚拟机,这里用VM1,VM2,VM3表达(当然生产环境要用三个独立物理机,否则无高可用可言),装docker可参见Ubuntu离线安装docker。 开始部署: 部署图 如上图所示,三台VM的IP分别为: 192.168.0.101 192.168.0.102 192.168.0.103 客户端将使用这三个IP来连接集群,每...

Redis分布式锁

加锁 所以需要保证设置锁及其过期时间两个操作的原子性,spring data的 RedisTemplate 当中并没有这样的方法。但是在jedis当中是有这种原子操作的方法的,需要通过 RedisTemplate 的 execute 方法获取到jedis里操作命令的对象,代码如下: String result = redisTemplate.execut...

二、获取微信用户openId

/// <summary> /// 登录首页 /// </summary> /// <returns></returns> public ActionResult Index() { if (Session["isTrue"] == null) { string weixinA...

项目部署(一、docker安装与使用)

linux系统 国内使用daoclound一键安装 curl -sSL https://get.daocloud.io/docker | sh 查看docker版本 docker version systemctl start docker 启动dockersystemctl stop docker 停止systemctl restart docker 重...

使用 AFNetworking 进行 XML 和 JSON 数据请求

(1)XML 数据请求 使用 AFNetworking 中的 AFHTTPRequestOperation 和 AFXMLParserResponseSerializer,另外结合第三方框架 XMLDictionary 进行数据转换 使用 XMLDictionary 的好处:有效避免自行实现 NSXMLParserDelegate 委托代理协议方法来进行繁...