1. 程式人生 > >docker build原始碼分析

docker build原始碼分析

前言:

最近在搞Docker,需要仔細的去了解Docker原始碼,在網上找來找去都是舊版本的,很頭疼,看了眾多的有關部落格和《docker原始碼分析》,總結一下。原始碼基於docker-ce17.9.0(docker-ce17.9.0(目前網上沒有這個版本的),我主要是需要docker build跟docker run的流程,大致差不多,先將build流程碼出。

簡單瞭解 docker build 的作用:

使用者可以通過一個 自定義的 Dockerfile 檔案以及相關內容,從一個基礎映象起步,對於 Dockerfile 中的每一 條命令,都在原先的映象 layer 之上再額外構建一個新的映象 layer ,直至構建出使用者所需 的映象。 

由於 docker build 命令由 Docker 使用者發起,故 docker build 的流程會貫穿 Docker Client Docker Server 以及 Docker Daemon 這三個重要的 Docker 模組。所以咱也是以這三個 Docker 模組為主題,分析 docker build 命令的執行,其中 Docker Daemon 最為重要。

1、Docker Client 作為使用者請求的人口,自然第一個接收並處理 docker build 命令。主要包括:定義並解析flag引數、獲取Dockerfile相關內容。

流程:

   

runBuild()函式只是從Client解析docker build命令和引數,對於我不怎麼需要,先不分析。(點選runBuild可以跳轉到程式碼)

2、docker server 負責根據請求型別以及請求的 URL ,路由轉發 Docker 請求至相應的處理方法。在處理方法中, Docker Server 會建立相應的 Job ,為 Job 置相應的執行引數並觸發該 Job 的執行。在此,將引數資訊配置到JSON資料中。

其中,initRouter 將build命令的路由器初始化,initRouter.NewRouter.initRoutes.NewPostRoute.

NewPostRoute()呼叫

postBuild()函式

3、通過引數資訊配置(newImageBuildOptions) 構建選項資料(buildOption)

      getAuthConfig()請求header名為X-Registry-Config,值為使用者的config資訊(使用者認證資訊)

4、配置好選項資料,將其作為引數,呼叫imgID:= br.backend.Build()

5、return newBuilder(ctx, builderOptions).build(source, dockerfile)

     newBuilder()從可選的dockerfile和options建立一個新的Dockerfile構建器

     build()通過解析Dockerfile並執行檔案中的指令來執行Dockerfile構建器

6、build()函式呼叫dispatchFromDockerfile(),dispatchFromDockerfile()呼叫dispatch()函式。

      dispatch()函式找到每一種Dockerfile命令對應的handler處理函式並執行。

      其中f, ok := evaluateTable[cmd],對run() ,即RUN指令。。。

7、run()函式,是非常重要的,下面詳細分析:

// RUN some command yo    //執行命令並提交image
//
// run a command and commit the image. Args are automatically prepended with
// the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under
// Windows, in the event there is only one argument The difference in processing:
//
// RUN echo hi          # sh -c echo hi       (Linux and LCOW)
// RUN echo hi          # cmd /S /C echo hi   (Windows)
// RUN [ "echo", "hi" ] # echo hi
//
func run(req dispatchRequest) error {
	if !req.state.hasFromImage() {
		return errors.New("Please provide a source image with `from` prior to run")
	}

	if err := req.flags.Parse(); err != nil {
		return err
	}

	//將最底層的Config結構體傳入
	stateRunConfig := req.state.runConfig
	//處理JSON格式的引數,具體解釋轉至handleJSONArgs
	args := handleJSONArgs(req.args, req.attributes)
	if !req.attributes["json"] {
		args = append(getShell(stateRunConfig, req.builder.platform), args...)
	}
	//容器開始要執行的命令
	cmdFromArgs := strslice.StrSlice(args)
	//FilterAllowed返回所有允許的args,不帶過濾的args
	//Env是要在容器中設定的環境變數列表
	buildArgs := req.builder.buildArgs.FilterAllowed(stateRunConfig.Env)

	//以下判斷能否複用本地快取
	saveCmd := cmdFromArgs
	if len(buildArgs) > 0 {
		saveCmd = prependEnvOnCmd(req.builder.buildArgs, buildArgs, cmdFromArgs)
	}

	runConfigForCacheProbe := copyRunConfig(stateRunConfig,
		withCmd(saveCmd),
		withEntrypointOverride(saveCmd, nil))
	hit, err := req.builder.probeCache(req.state, runConfigForCacheProbe)
	if err != nil || hit {
		return err
	}

	runConfig := copyRunConfig(stateRunConfig,
		withCmd(cmdFromArgs),
		withEnv(append(stateRunConfig.Env, buildArgs...)),
		withEntrypointOverride(saveCmd, strslice.StrSlice{""}))

	// set config as already being escaped, this prevents double escaping on windows
	//防止重複轉義
	runConfig.ArgsEscaped = true

	logrus.Debugf("[BUILDER] Command to be executed: %v", runConfig.Cmd)

	//create根據基礎映象ID以及執行容器時所需的runconfig資訊,來建立Container物件
	//進入create,呼叫Create,再呼叫ContainerCreate
	cID, err := req.builder.create(runConfig)
	if err != nil {
		return err
	}
	//Run執行Docker容器,其中包括建立容器檔案系統、建立容器的名稱空間進行資源隔離、為容器配置cgroups引數進行資源控制,還有執行使用者指定的程式。
	if err := req.builder.containerManager.Run(req.builder.clientCtx, cID, req.builder.Stdout, req.builder.Stderr); err != nil {
		if err, ok := err.(*statusCodeError); ok {
			// TODO: change error type, because jsonmessage.JSONError assumes HTTP
			return &jsonmessage.JSONError{
				Message: fmt.Sprintf(
					"The command '%s' returned a non-zero code: %d",
					strings.Join(runConfig.Cmd, " "), err.StatusCode()),
				Code: err.StatusCode(),
			}
		}
		return err
	}
    //對執行後的容器進行commit操作,將執行的結果儲存在一個新的映象中。
    //需注意的是 commitContainer中的Commit()函式是呼叫的/daemon/build.go 下面的
	return req.builder.commitContainer(req.state, cID, runConfigForCacheProbe)
}

——先呼叫了Parse()函式

// Parse reads lines from a Reader, parses the lines into an AST and returns
// the AST and escape token
//Parse從Reader讀取行,將行解析為AST並返回AST和轉義令牌(eacapeToken)
func Parse(rwc io.Reader) (*Result, error) {
	//NewDefaultDirective使用預設的eacapeToken標記返回一個新的Directive
	d := NewDefaultDirective()
	//當前行為0
	currentLine := 0
	//StartLines是Node開始的原始Dockerfile中的行,沒明白-1是什麼意思
	root := &Node{StartLine: -1}
	//從rws讀取,返回一個新的scanner
	scanner := bufio.NewScanner(rwc)
	warnings := []string{}

	var err error
	//Scan將Scanner推進到下一個token,然後可通過Bytes或Text方法使用令牌
	for scanner.Scan() {
		//Bytes通過呼叫Scan生成最新的Token
		bytesRead := scanner.Bytes()
		if currentLine == 0 {
			// First line, strip the byte-order-marker if present
			//TrimPrefix返回沒有包含字首的 物件s
			bytesRead = bytes.TrimPrefix(bytesRead, utf8bom)
		}
		//processLine 是棄用期後刪除stripLeftWhitespace.
		//返回ReplaceAll,ReplaceAll返回src副本,將Regexp的匹配替換為替換文字repl
		//返回possibleParseDirective,possibleParseDirective查詢一個或多個解析器指令'#escapeToken=<char>'和'#platform=<string>'.
		//解析器指令必須在任何構建器指令或其他註釋之前,並且不能重複
		bytesRead, err = processLine(d, bytesRead, true)
		if err != nil {
			return nil, err
		}
		currentLine++

		startLine := currentLine
		//修改延續字元,應該是跟正則表示式的匹配有關係
		line, isEndOfLine := trimContinuationCharacter(string(bytesRead), d)
		if isEndOfLine && line == "" {
			continue
		}

		var hasEmptyContinuationLine bool
		for !isEndOfLine && scanner.Scan() {
			bytesRead, err := processLine(d, scanner.Bytes(), false)
			if err != nil {
				return nil, err
			}
			currentLine++
			//判斷是不是註釋
			if isComment(scanner.Bytes()) {
				// original line was a comment (processLine strips comments)
				continue
			}
			//判斷是不是空的延續行
			if isEmptyContinuationLine(bytesRead) {
				hasEmptyContinuationLine = true
				continue
			}

			continuationLine := string(bytesRead)
			continuationLine, isEndOfLine = trimContinuationCharacter(continuationLine, d)
			line += continuationLine
		}

		if hasEmptyContinuationLine {
			warning := "[WARNING]: Empty continuation line found in:\n    " + line
			warnings = append(warnings, warning)
		}
		//newNodeFromLine將行拆分為多個部分,並根據命令和命令引數排程到一個函式(splitcommand())。根據排程結果建立Node
		//具體解析轉至newNodeFromLine
		child, err := newNodeFromLine(line, d)
		if err != nil {
			return nil, err
		}
		//AddChild新增一個新的子節點,並更新行的資訊
		root.AddChild(child, startLine, currentLine)
	}

	if len(warnings) > 0 {
		warnings = append(warnings, "[WARNING]: Empty continuation lines will become errors in a future release.")
	}
	//Result是解析Dockerfile的結果(呼叫的本檔案下的)
	return &Result{
		AST:         root,
		Warnings:    warnings,
		EscapeToken: d.escapeToken,
		Platform:    d.platformToken,
	}, nil
}

——其中又呼叫了將行拆分的函式newNodeFromLine()

// newNodeFromLine splits the line into parts, and dispatches to a function
// based on the command and command arguments. A Node is created from the
// result of the dispatch.
func newNodeFromLine(line string, directive *Directive) (*Node, error) {
	//呼叫的splitCommand()函式,他接受單行文字並解析cmd和args,這些用於排程到更精確的解析函式
	cmd, flags, args, err := splitCommand(line)
	if err != nil {
		return nil, err
	}

	fn := dispatch[cmd]
	// Ignore invalid Dockerfile instructions
	if fn == nil {
		fn = parseIgnore
	}
	next, attrs, err := fn(args, directive)
	if err != nil {
		return nil, err
	}

	return &Node{
		Value:      cmd,
		Original:   line,
		Flags:      flags,
		Next:       next,
		Attributes: attrs,
	}, nil
}

——其中,又呼叫了splitCommand()函式

// splitCommand takes a single line of text and parses out the cmd and args,
// which are used for dispatching to more exact parsing functions.
//splitCommand接受單行文字並解析cmd和args。這些用於排程到更精確的解析函式
func splitCommand(line string) (string, []string, string, error) {
	var args string
	var flags []string

	// Make sure we get the same results irrespective of leading/trailing spaces
	//無論leading/trailing(前導/字尾)有多少的空格,都得確保得到相同的結果
	//Split將切片拆分為由表示式分隔的子字串,並返回這些表示式匹配之間的子字串切片
	//TrimSpace 返回字串s的一部分,刪除所有的leading/trailing(前導/字尾)空格
	// 2 表示只有兩個子字串
	cmdline := tokenWhitespace.Split(strings.TrimSpace(line), 2)
	//cmdline[0]表示命令型別 如:groupadd
	//cmdline[1]表示命令引數 如:-f -g 842
	cmd := strings.ToLower(cmdline[0])

	if len(cmdline) == 2 {
		var err error
		//extractBuilderFlags() 解析BuilderFlags,並返回該行剩餘部分
		args, flags, err = extractBuilderFlags(cmdline[1])
		if err != nil {
			return "", nil, "", err
		}
	}
	//返回 cmd  選項  引數  
	return cmd, flags, strings.TrimSpace(args), nil
}

——其中解析命令引數的時候呼叫了extractBuilderFlags()函式,這個函式就是解析剩下還有什麼命令、選項、引數。

func extractBuilderFlags(line string) (string, []string, error) {
	// Parses the BuilderFlags and returns the remaining part of the line
	//解析BuilderFlags,並返回該行剩餘部分
	const (
		inSpaces = iota // looking for start of a word  // 每次出現從 0 開始
		inWord											// 1
		inQuote											// 2
	)

	words := []string{}
	phase := inSpaces 
	word := ""
	quote := '\000'
	blankOK := false
	var ch rune

	for pos := 0; pos <= len(line); pos++ {
		if pos != len(line) {
			ch = rune(line[pos])
		}

		if phase == inSpaces { // Looking for start of word
			if pos == len(line) { // end of input
				break
			}
			if unicode.IsSpace(ch) { // skip spaces
				continue
			}

			// Only keep going if the next word starts with --
			if ch != '-' || pos+1 == len(line) || rune(line[pos+1]) != '-' {
				return line[pos:], words, nil
			}

			phase = inWord // found something with "--", fall through
		}
		if (phase == inWord || phase == inQuote) && (pos == len(line)) {
			if word != "--" && (blankOK || len(word) > 0) {
				words = append(words, word)
			}
			break
		}
		if phase == inWord {
			if unicode.IsSpace(ch) {
				phase = inSpaces
				if word == "--" {
					return line[pos:], words, nil
				}
				if blankOK || len(word) > 0 {
					words = append(words, word)
				}
				word = ""
				blankOK = false
				continue
			}
			if ch == '\'' || ch == '"' {
				quote = ch
				blankOK = true
				phase = inQuote
				continue
			}
			if ch == '\\' {
				if pos+1 == len(line) {
					continue // just skip \ at end
				}
				pos++
				ch = rune(line[pos])
			}
			word += string(ch)
			continue
		}
		if phase == inQuote {
			if ch == quote {
				phase = inWord
				continue
			}
			if ch == '\\' {
				if pos+1 == len(line) {
					phase = inWord
					continue // just skip \ at end
				}
				pos++
				ch = rune(line[pos])
			}
			word += string(ch)
		}
	}

	return "", words, nil
}

到此為止,Parse()函式已經分析完畢。回到run()函式上來:

(1)、create()函式執行建立Container物件操作;

(2)、Run()函式執行執行容器操作;

(3)、commitContainer()函式執行提交新映象操作。

注意:commitContainer()呼叫的b.docker.Commit(),一定是/daemon/build.go下面的

 

參考:《Docker原始碼分析.pdf》

             https://guanjunjian.github.io/2017/09/26/study-1-docker-1-client-excuting-flow-for-run/

             https://jimmysong.io/posts/docker-source-code-analysis-code-structure/

             https://blog.csdn.net/warrior_0319/article/details/79931987