Docker Client原始碼分析(一)
主要內容:
Docker Client在Docker中的定位,以及Docker Client原始碼的初步分析。
本文選取Docker拆分為DockerCE(社群版)和DockerEE(企業版)之後的Docker-CE的第一個穩定版本v17.06.0-ce。
ofollow,noindex" target="_blank">https://github.com/docker/docker-ceDocker背景:
Docker Client是絕大部分使用者使用Docker的入口,例如一次docker pull請求,需要經過很多層呼叫,如下圖:
Client建立一個docker pull的請求,傳送給Deamon的http server,server接受之後通過Router路由到相應的的Handler,Handler便會建立一個PostImageCreate的Job交給Engine執行(不同的Job功能不同,需要的底層功能也不同), 該Job會先去Docker Registory拉取映象,之後交給Graph Dirver,Graph Dirver再將映象儲存到本地的roootfs中。
上述過程中,我們發現Client不做任何的操作,而是構建請求,傳送給Deamon執行。
那麼我們會想,如果繞過Client,是否也可以向Deamon傳送請求呢?答案是可以的,如下圖:

Client可以訪問Deamon的API,那麼使用者也可以直接訪問Deamon的API,而且為了方便二次開發,Docker同時提供了很多語言的SDK供開發者選擇,例如Docker Client for python。
下面我們正式進入Client。
Docker Client為什麼選擇了golang?
這個問題其實準確的說是“為什麼Docker會選擇golang來開發”,go有很多優點,下面我們細數一下
- python的那種簡潔
- 行末不強制分號
- 支援多值返回
- c的那種高效
- 編譯性語言
- 保留指標(被稱為21世紀的C語言)
- 極少的執行時依賴(部署簡單)
- java的那種安全
- 記憶體管理(垃圾回收)
- 語言層面支援併發
- 關鍵字支援:go select chan
博主認為Docker建立之初,最重要的一點是部署簡單,執行時依賴極少,僅僅依賴glibc,它是linux最底層的API,幾乎其他任何庫都會依賴於glibc。Docker是部署到使用者的機器上的,最重要的是通用性,所以執行時依賴越少越好。此些恰恰原因也是Docker Client使用golang開發的主要原因。
有的讀者會想,我沒學習過golang,會不會影響我學習Docker Client原始碼?其實語言只是一種工具,程式語言大同小異,簡單看看語法,就能夠閱讀個大概,博主寫了一些Java於golang針對相同需求的不同實現的例子,簡單檢視過後,便可以進行接下來的閱讀。 https://www.cnblogs.com/langshiquan/p/9937866.html
Docker Client執行流程
一.initialization階段
- 初始化日誌配置
- 初始化DockerCli例項,填充Stdin,Stdout,Stderr
- 初始化Command根命令並組裝
- 新增Help等通用配置
- 新增所有支援的子命令的配置
具體見下面的程式碼:
Client入口:components/cli/cmd/docker/docker.go
package main // import省略 func main() { // Set terminal emulation based on platform as required. // 獲取Stdin,Stdout,Stderr stdin, stdout, stderr := term.StdStreams() // 1.初始化日誌配置 logrus.SetOutput(stderr) //2.初始化DockerCli例項,填充Stdin,Stdout,Stderr dockerCli := command.NewDockerCli(stdin, stdout, stderr) // 3.初始化Command根命令並組裝 cmd := newDockerCommand(dockerCli) // 執行,判斷err結果 if err := cmd.Execute(); err != nil { if sterr, ok := err.(cli.StatusError); ok { if sterr.Status != "" { fmt.Fprintln(stderr, sterr.Status) } // StatusError should only be used for errors, and all errors should // have a non-zero exit status, so never exit with 0 if sterr.StatusCode == 0 { os.Exit(1) } os.Exit(sterr.StatusCode) } fmt.Fprintln(stderr, err) os.Exit(1) } }
上面提到了一個DockerCli和Command二個物件,接下來對其進行一些說明
DockerCli資料結構
// DockerCli is an instance the docker command line client. // 此結構體是核心的結構體,每個子命令的執行都會用到它 type DockerCli struct { configFile*configfile.ConfigFile// ~/.docker/config.json檔案的配置資訊 in*InStream// Stdin out*OutStream// Stdout errio.Writer// Stderr clientclient.APIClient//用於與deamon通訊(重要) defaultVersionstring//版本資訊 serverServerInfo// ServerInfo資訊 HasExperimental bool// 是否開啟試驗性功能 OSTypestring// 作業系統型別 }
Command資料結構
// Command is just that, a command for your application. type Command struct{ Use string // Use is the one-line usage message. 代表的命令,例如image Short string // Short is the short description shown in the 'help' output.help的簡單資訊 PersistentPreRunE func(cmd *Command, args []string) error // PersistentPreRunE: PersistentPreRun but returns an error.// 在執行前執行 RunE func(cmd *Command, args []string) error // RunE: Run but returns an error.// 真正的執行 commands []*Command // commands is the list of commands supported by this program.// 支援的子命令集合 parent *Command // parent is a parent command for this command.// 父命令 Args PositionalArgs // Expected arguments舉例:docker pull tomcat此次的tomcat對應此處 flags *flag.FlagSet // flags is full set of flags.// 引數的集合 // 省略其他不常用的屬性 }
Command組裝程式碼:
對於docker組裝子Command:components/cli/command/commands/commands.go 對於docker image組裝子Command:components/cli/command/images/cmd.go
二.PreRun階段
Client的生命週期很短,使用者按下回車的時候開始,執行完便結束,Client的宣告週期絕大程度上繫結在命令列框架(Docker Client使用的命令列框架是cobra)之上,所以幾乎所有的命令列工具,都會存在這幾個過程。
PreRun階段無非做一些引數解析、全域性的配置等等,接下來,我們看看Docker Client在此階段都做了哪些事情。
- 解析引數
- Client級別的配置
- 是否開啟debug
- 設定日誌級別
- 配置檔案位置
- 版本號
- 初始化DockerCli上下文(上一步例項化,但是並不完整,一些屬性依賴與使用者的輸入引數)
- 初始化DockerCli.APIClient
- 初始化Server的基本資訊(作業系統型別等)
- 初始化配置檔案位置
- 校驗命令
- 版本是否支援
- 作業系統型別是否支援
- 實驗性功能是否支援
// components/cli/cmd/docker/docker.go func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { // 繫結在根命令上的引數,即docker命令 opts := cliflags.NewClientOptions() var flags *pflag.FlagSet // docker根命令 cmd := &cobra.Command{ Use:"docker [OPTIONS] COMMAND [ARG...]", Short:"A self-sufficient runtime for containers", SilenceUsage:true, SilenceErrors:true, TraverseChildren: true, Args:noArgs, RunE: func(cmd *cobra.Command, args []string) error { if opts.Version { showVersion() return nil } return command.ShowHelp(dockerCli.Err())(cmd, args) }, // 二、PreRun階段的入口 PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // daemon command is special, we redirect directly to another binary if cmd.Name() == "daemon" { return nil } // flags must be the top-level command flags, not cmd.Flags() opts.Common.SetDefaultOptions(flags) dockerPreRun(opts) // 3. 初始化DockerCli上下文,重要函式dockerCli.Initialize,見下面程式碼塊 if err := dockerCli.Initialize(opts); err != nil { return err } // 4. 校驗命令 return isSupported(cmd, dockerCli) }, } cli.SetupRootCommand(cmd) flags = cmd.Flags() // 通過傳遞地址,講解析之後的引數,傳遞到options中 flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit") flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files") opts.Common.InstallFlags(flags) setFlagErrorFunc(dockerCli, cmd, flags, opts) setHelpFunc(dockerCli, cmd, flags, opts) cmd.SetOutput(dockerCli.Out()) // 加入docker deamon命令 cmd.AddCommand(newDaemonCommand()) // 組裝所有子命令入口 commands.AddCommands(cmd, dockerCli) setValidateArgs(dockerCli, cmd, flags, opts) return cmd }
// dockerCli.Initialize函式 func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { // LoadDefaultConfigFile嘗試載入預設配置檔案,如果沒有找到,則返回初始化的ConfigFile結構 cli.configFile = LoadDefaultConfigFile(cli.err) var err error // 此client是向docker deamon傳送請求的APIClient // client.APIClient是一個很大的介面,有很多函式 cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) if tlsconfig.IsErrEncryptedKey(err) { var ( passwd string giveup bool ) passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil) for attempts := 0; tlsconfig.IsErrEncryptedKey(err); attempts++ { // some code and comments borrowed from notary/trustmanager/keystore.go passwd, giveup, err = passRetriever("private", "encrypted TLS private", false, attempts) // Check if the passphrase retriever got an error or if it is telling us to give up if giveup || err != nil { return errors.Wrap(err, "private key is encrypted, but could not get passphrase") } opts.Common.TLSOptions.Passphrase = passwd // NewAPIClientFromFlags creates a new APIClient from command line flags cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) } } if err != nil { return err } cli.defaultVersion = cli.client.ClientVersion() if ping, err := cli.client.Ping(context.Background()); err == nil { cli.server = ServerInfo{ HasExperimental: ping.Experimental, OSType:ping.OSType, } // since the new header was added in 1.25, assume server is 1.24 if header is not present. if ping.APIVersion == "" { ping.APIVersion = "1.24" } // if server version is lower than the current cli, downgrade if versions.LessThan(ping.APIVersion, cli.client.ClientVersion()) { cli.client.UpdateClientVersion(ping.APIVersion) } } return nil }
三.Run階段
真正的具體的命令執行也很簡單,Docker的Client不做任何實質性的功能,所有的請求都是傳送給deamon來處理,所以做的事情很簡單,具體如下
- 引數處理,構建請求
- 向Docker daemon傳送請求
- 處理響應
- 列印結果
- 返回狀態
下文將以docker image list = docker images 命令為例:
// 組裝的時候,會調取這個方法 func newListCommand(dockerCli command.Cli) *cobra.Command { cmd := *NewImagesCommand(dockerCli) cmd.Aliases = []string{"images", "list"} cmd.Use = "ls [OPTIONS] [REPOSITORY[:TAG]]" return &cmd } // NewImagesCommand creates a new `docker images` command func NewImagesCommand(dockerCli command.Cli) *cobra.Command { // images命令需要的所有引數,不同的命令的options不同 options := imagesOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use:"images [OPTIONS] [REPOSITORY[:TAG]]", Short: "List images", Args:cli.RequiresMaxArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { options.matchName = args[0] } // 真正的執行方法,見下 return runImages(dockerCli, options) }, } flags := cmd.Flags() // 將引數解析的結果,放到options flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only show numeric IDs") flags.BoolVarP(&options.all, "all", "a", false, "Show all images (default hides intermediate images)") flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output") flags.BoolVar(&options.showDigests, "digests", false, "Show digests") flags.StringVar(&options.format, "format", "", "Pretty-print images using a Go template") flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") return cmd } // 真正的執行方法 func runImages(dockerCli command.Cli, options imagesOptions) error { // go語言的context包的功能 ctx := context.Background() // 獲取使用者輸入--fiter的內容 filters := options.filter.Value() // 使用者輸入的arg if options.matchName != "" { filters.Add("reference", options.matchName) } listOptions := types.ImageListOptions{ // 使用者輸入的-a引數 All:options.all, Filters: filters, } // 通過此Client訪問deamon,拿到映象列表 images, err := dockerCli.Client().ImageList(ctx, listOptions) if err != nil { return err } // 使用者輸入的--format引數 format := options.format if len(format) == 0 { // 如果無使用者輸入的format,則讀取配置檔案中的配置,且非靜默模式(-q),否則使用靜默模式的format if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !options.quiet { format = dockerCli.ConfigFile().ImagesFormat } else { format = formatter.TableFormatKey } } imageCtx := formatter.ImageContext{ Context: formatter.Context{ // 輸出配置 Output: dockerCli.Out(), // 格式資訊 Format: formatter.NewImageFormat(format, options.quiet, options.showDigests), // 使用者輸入的--no-trunc資訊,意思為全量列印 Trunc:!options.noTrunc, }, // 使用者輸入的--digests 是否顯示摘要資訊 Digest: options.showDigests, } // 具體的格式化列印細節暫略 return formatter.ImageWrite(imageCtx, images) }
Docker Cli架構總結:
此處主要是分析如何組裝如此多的命令,是一個大體的圖示。