docker 源碼分析 一(基於1.8.2版本),docker daemon啓動過程;

最近在研究golang,也學習一下比較火的開源項目docker的源代碼,國內比較出名的docker源碼分析是孫宏亮大牛寫的一系列文章,可是基於的docker版本有點老;索性本身就git 了一下最新的代碼研讀;git

docker是c/s的架構,分爲docker client 和 docker daemon,client端發送命令,daemon端負責完成client發送過來的命令(如獲取和存儲鏡像、管理容器等)。二者之間能夠經過TCP,HTTP和UNIX SOCKET來進行通訊;golang

docker的啓動入口代碼在 docker/docker.godocker

func main() {api

    if reexec.Init() {數組

        return網絡

    }架構

 

    // Set terminal emulation based on platform as required.app

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

 

    logrus.SetOutput(stderr)tcp

 

 

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

 

    flag.Usage = func() {

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

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

 

        flag.CommandLine.SetOutput(os.Stdout)

        flag.PrintDefaults()

 

        help := "\nCommands:\n"

 

        for _, cmd := range dockerCommands {

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

        }

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

        fmt.Fprintf(os.Stdout, "%s\n", 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\n", dockerversion.VERSION, dockerversion.GITCOMMIT)

    } else {

        fmt.Printf("Docker version %s, build %s\n", 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.\n", 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 的命令的發送到執行是如何進行的;

相關文章
相關標籤/搜索