Hi,請移步至新版專欄首頁 http://www.tvxinternet.com/blogs
沒有足夠的數據
joyqi 回答了問題 · 3月25日
Hi,請移步至新版專欄首頁 http://www.tvxinternet.com/blogs
Hi,請移步至新版專欄首頁 [鏈接]
關注 2 回答 1
joyqi 回答了問題 · 3月25日
Hi,所有的訂閱和文章分區功能(老版主頁的所有功能)即將在新版專欄頁面上線后呈現。
Hi,所有的訂閱和文章分區功能(老版主頁的所有功能)即將在新版專欄頁面上線后呈現。
關注 2 回答 1
joyqi 贊了文章 · 3月25日
為了方便理解,CPU 可以簡單認為是:
CPU 有很多的寄存器,這里我們只介紹 指令寄存器 和 通用寄存器。
64 位下,指令寄存器叫 rip
(32 位下叫 eip
)。
指令寄存器用于存放下一條指令的地址,CPU 的工作模式,就是從 rip
指向的內存地址取一條指令,然后執行這條指令,同時 rip
指向下一條指令,如此循環,就是 CPU 的基本工作。
也就意味著,通常模式下 CPU 是按照順序執行指令的。但是,CPU 也有一些特殊的指令,用于直接修改 rip
的地址。比如,jmp 0xff00
指令,就是把 rip
改為 0xff00
,讓 CPU 接下來執行內存中 0xff00
這個位置的指令。
以 x86_64 來說,有 16 個“通用”寄存器?!巴ㄓ谩币馕吨梢苑湃我獾臄祿?,這 16 個寄存器并沒有什么區別,但是實際上還是存在一些約定俗稱的用法:
先看看這 8 個:
(這是原來 32 位架構下就有的,只是 32 位下是 e 開頭的)
rax: "累加器"(accumulator), 很多加法乘法指令的缺省寄存器,函數返回值一般也放在這里
rbx: "基地址"(base)寄存器, 在內存尋址時存放基地址
rcx: 計數器(counter), 是重復(REP)前綴指令和 LOOP 指令的內定計數器
rdx: 用來放整數除法產生的余數,或者讀寫I/O端口時,用來存放端口號
rsp: 棧頂指針,指向棧的頂部
rbp: 棧底指針,指向棧的底部,通常用`rbp+偏移量`的形式來定位函數存放在棧中的局部變量
rsi: 字符串操作時,用于存放數據源的地址
rdi: 字符串操作時,用于存放目的地址的,和 rsi 經常搭配一起使用,執行字符串的復制等操作
另外還有 8 個,是 64 位架構下新增的:
r8, r9, r10, r11, r12, r13, r14, r15
在 CPU 的世界里,只有 0 1 這種二進制的表示,所以指令也是用 0 1 二進制表示的。
然而,二進制對人類并不友好,所以有了匯編這種助記符。
比如這段加法:
add rax,rdx
比如這個匯編指令,表示:rax = rax + rdx
,這就完成了一個加法的運算。
通常我們用 rax
寄存器來做加法運算,但是其他寄存器一樣也可以完成加法運算的,比如:
add rbx,0x1
這個表示 rbx = rbx + 0x1
。
這里的加法運算,都是在寄存器上完成的,也就是直接修改的寄存器的值。
比如這段無條件跳轉指令
jmp 0x269e001c
CPU 默認是按照順序執行指令的,跳轉指令則是,讓 CPU 不再順序執行后續的指令,轉而執行 0x269e001c
這個內存地址中的指令。
具體來說,將指令寄存器中的值改為 0x269e001c
即可,即:rip = 0x269e001c
。
比如這一對 mov
指令:
mov rbp, [rcx]
mov [rcx], rbp
這里假設 rcx
的值,是一個內存地址,比如:0xff00
。
第一行 mov
指令,是將內存地址 0xff00
中的值,讀取到 rbp
寄存器。
第二行 mov
指令,則是反過來,將 rbp
寄存器的值,寫入到內存 0xff00
中。
push
和 pop
這一對用于操作“?!?。
“?!笔莾却婵臻g中的一段地址,我們約定是以棧的形式來使用它,并且用 rsp
寄存器指向棧頂。
棧操作本質也是內存讀寫操作,只是以棧的方式來使用。
比如這一對:
push rbp
pop rbp
第一行是將 rbp
寄存器中的值壓入棧,等效于:
sub rsp, 8 // rsp = rsp - 8; 棧頂向下生長 8 byte
mov [rsp], rbp // rbp 的值寫入新的棧頂
第二行則是反過來,棧頂彈出一個值,寫入到 rbp
寄存器中,等效于:
mov rbp, [rsp] // 棧頂的值寫入 rbp
add rsp, 8 // rsp = rsp + 8; 棧頂向上縮小 8 byte
注意:因為棧在內存空間中是倒過來的,所以是向下生長的。
查看原文為了方便理解,CPU 可以簡單認為是:一堆的寄存器,用于暫時存放數據可以執行機器指令,完成運算 / 數據讀寫 等操作寄存器CPU 有很多的寄存器,這里我們只介紹 指令寄存器 和 通用寄存器。指令寄存器64 位下,指令寄存器叫 rip (32 位下叫 eip)。指令寄存器用于存...
贊 2 收藏 0 評論 0
joyqi 贊了文章 · 3月24日
https://www.bilibili.com/vide...
今天我就來演示一下如何在 OpenResty 應用中編寫自己的 Lua 模塊,一步一步來。
讓我們把我們簡單的 OpenResty 應用放到一個新的目錄中,命名為 test-module
。
cd ~/
mkdir test-module/
cd test-module/
然后我們像往常一樣創建子目錄結構。
mkdir logs conf lua
請注意,與上一篇教程中的“Hello World”示例不同,我們在這里創建了一個 lua/
目錄來存放我們的 lua 模塊文件。
現在讓我們在 lua
子目錄下創建我們自己的 Lua 模塊文件,命名為 hello.lua
。
vim lua/hello.lua
我們進行如下編輯操作:
_M
。greet
的函數。local _M = {}
function _M.greet(name)
ngx.say("Greetings from ", name)
end
return _M
搞定!一個很簡單的 Lua 模塊就完成了。
現在是創建 nginx.conf
配置文件的時候了。
vim conf/nginx.conf
我們快速完成下列編輯操作:
http {}
配置塊中,我們應該告訴 OpenResty 我們的 Lua 模塊在哪里。$prefix
在運行時被 nginx 的 -p
選項值所替代。content_by_lua"_block
。require
內置函數加載 Lua 模塊 hello
。greet
函數。worker_processes 1;
events {
worker_connections 1024;
}
http {
lua_package_path "$prefix/lua/?.lua;;";
server {
listen 8080 reuseport;
location / {
default_type text/plain;
content_by_lua_block {
local hello = require "hello"
hello.greet("a Lua module")
}
}
}
}
我們現在就來檢查整個目錄樹。
tree .
看起來不錯。
現在啟動這個 OpenResty 應用程序。
nginx -p $PWD/
是時候用 curl
命令行工具查詢我們的 HTTP 接口了。
curl 'http://127.0.0.1:8080/'
酷,我們的 Lua 模塊開始工作了。
我們也可以在網頁瀏覽器中進行測試。
如果你看到了 500 錯誤頁面,那么你的 Lua 代碼一定有錯誤。
在這種情況下,你應該檢查 logs
子目錄下的 error.log
文件。
tail logs/error.log
這里我們沒有任何錯誤消息,正如預期的那樣。
值得注意的是,我們的 Lua 模塊是在這第一個請求中加載的,后續的請求只是使用內存中緩存的 Lua 模塊。
為了避免第一次請求的額外開銷,我們可以在服務器啟動時預加載 Lua 模塊。
要做到這一點,我們需要編輯一下 nginx.conf
文件。
vim conf/nginx.conf
在 http {}
塊里,我們添加一個 init_by_lua_block
指令。在那個上下文中,我們加載我們的 Lua 模塊。
http {
init_by_lua_block {
require "hello"
}
...
init_by_lua_block
運行在 OpenResty 服務器啟動的時候。
測試一下配置是否正確。
nginx -p $PWD/ -t
沒有問題。
現在我們通過向 nginx 主進程發送 HUP
信號來重新加載服務器。
Now we reload the server by sending the HUP
signal to the nginx master process.
kill -HUP `cat logs/nginx.pid`
主進程的進程 ID 存儲在這個 nginx.pid
文件中。
再次發送 HTTP 請求。
curl 'http://127.0.0.1:8080/'
同樣的行為,只是這次快了一點點。
如果你喜歡這個教程,請訂閱這個博客網站和我們的 YouTube 頻道 或 B 站頻道。謝謝!
本文和相關聯的視頻都是完全由我們的 OpenResty Demo 系統從一個極簡單的劇本文件自動生成的。
章亦春是開源項目 OpenResty? 的創始人,同時也是 OpenResty Inc. 公司的創始人和 CEO。他貢獻了許多 Nginx 的第三方模塊,相當多 Nginx 和 LuaJIT 核心補丁,并且設計了 OpenResty XRay 等產品。
如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的博客網站 。也歡迎掃碼關注我們的微信公眾號:
我們提供了英文版原文和中譯版(本文) 。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮采用,非常感謝!
查看原文[鏈接]今天我就來演示一下如何在 OpenResty 應用中編寫自己的 Lua 模塊,一步一步來。讓我們把我們簡單的 OpenResty 應用放到一個新的目錄中,命名為 test-module。 {代碼...} 然后我們像往常一樣創建子目錄結構。 {代碼...} 請注意,與上一篇教程中的“Hello World”...
贊 2 收藏 1 評論 0
joyqi 回答了問題 · 3月18日
Hi,我們已經修復了這個錯誤,請再次點擊發布按鈕。很抱歉帶來使用上的困擾。
Hi,我們已經修復了這個錯誤,請再次點擊發布按鈕。很抱歉帶來使用上的困擾。
關注 2 回答 1
joyqi 贊了文章 · 3月15日
大家好,我是張晉濤。
我將在這篇文章中深入 Docker 的源碼,與你聊聊鏡像構建的原理。
這里我們先從宏觀上對 Docker
有個大概的認識,它整體上是個 C/S 架構;我們平時使用的 docker
命令就是它的 CLI 客戶端,而它的服務端是 dockerd
在 Linux 系統中,通常我們是使用 systemd
進行管理,所以我們可以使用 systemctl start docker
來啟動服務。(但是請注意,dockerd
是否能運行與 systemd
并無任何關系,你可以像平時執行一個普通的二進制程序一樣,直接通過 dockerd
來啟動服務,注意需要 root 權限)
實際上也就是
(圖片來源:docker overview)
docker
CLI 與 dockerd
的交互是通過 REST API 來完成的,當我們執行 docker version
的時候過濾 API 可以看到如下輸出:
? ~ docker version |grep API
API version: 1.41
API version: 1.41 (minimum version 1.12)
上面一行是 docker
CLI 的 API 版本,下面則代表了 dockerd
的 API 版本,它的后面還有個括號,是因為 Docker 具備了很良好的兼容性,這里表示它最小可兼容的 API 版本是 1.12 。
對于我們進行 C/S 架構的項目開發而言,一般都是 API 先行, 所以我們先來看下 API 的部分。
當然,本文的主體是構建系統相關的,所以我們就直接來看構建相關的 API 即可。
接下來會說 CLI,代碼以 v20.10.5 為準。最后說服務端 Dockerd 。
Docker 維護團隊在每個版本正式發布之后,都會將 API 文檔發布出來,可以通過 Docker Engine API 在線瀏覽,也可以自行構建 API 文檔。
首先 clone Docker 的源代碼倉庫, 進入項目倉庫內執行 make swagger-docs
即可在啟動一個容器同時將端口暴露至本地的 9000
端口, 你可以直接通過 http://127.0.0.1:9000 訪問本地的 API 文檔。
(MoeLove) ? git clone https://github.com/docker/docker.git docker
(MoeLove) ? cd docker
(MoeLove) ? docker git:(master) git checkout -b v20.10.5 v20.10.5
(MoeLove) ? docker git:(v20.10.5) make swagger-docs
API docs preview will be running at http://localhost:9000
打開 http://127.0.0.1:9000/#operation/ImageBuild 這個地址就可以看到 1.41 版本的構建鏡像所需的 API 了。我們對此 API 進行下分析。
接口地址是 /v1.41/build
方法是 POST
,我們可以使用一個較新版本的 curl
工具來驗證下此接口(需要使用 --unix-socket
連接 Docker 監聽的 UNIX Domain Socket )。dockerd
默認情況下監聽在 /var/run/docker.sock
,當然你也可以給 dockerd
傳遞 --host
參數用于監聽 HTTP 端口或者其他路徑的 unix socket .
/ # curl -X POST --unix-socket /var/run/docker.sock localhost/v1.41/build
{"message":"Cannot locate specified Dockerfile: Dockerfile"}
從上面的輸出我們可以看到,我們確實訪問到了該接口,同時該接口的響應是提示需要 Dockerfile
.
A tar archive compressed with one of the following algorithms: identity (no compression), gzip, bzip2, xz.
string <binary>
請求體是一個 tar
歸檔文件,可選擇無壓縮、gzip
、bzip2
、xz
壓縮等形式。關于這幾種壓縮格式就不再展開介紹了,但值得注意的是 如果使用了壓縮,則傳輸體積會變小,即網絡消耗會相應減少。但壓縮/解壓縮需要耗費 CPU 等計算資源 這在我們對大規模鏡像構建做優化時是個值得權衡的點。
因為要發送的是個 tar
歸檔文件,Content-type
默認是 application/x-tar
。另一個會發送的頭是 X-Registry-Config
,這是一個由 Base64 編碼后的 Docker Registry 的配置信息,內容與 $HOME/.docker/config.json
中的 auths
內的信息一致。
這些配置信息,在你執行 docker login
后會自動寫入到 $HOME/.docker/config.json
文件內的。這些信息被傳輸到 dockerd
在構建過程中作為拉取鏡像的認證信息使用。
最后就是請求參數了,參數有很多,通過 docker build --help
基本都可以看到對應含義的,這里不再一一展開了,后面會有一些關鍵參數的介紹。
上面我們介紹了 Docker
構建鏡像相關的 API,我們可以直接訪問Docker Engine 的 API 文檔?;蛘咄ㄟ^源碼倉庫,自己來構建一個本地的 API 文檔服務,使用瀏覽器進行訪問。
通過 API 我們也知道了該接口所需的請求體是一個 tar
歸檔文件(可選擇壓縮算法進行壓縮),同時它的請求頭中會攜帶用戶在鏡像倉庫中的認證信息。這提醒我們, 如果在使用遠程 Dockerd
構建時,請注意安全,盡量使用 tls
進行加密,以免數據泄漏。
API 已經介紹完了,我們來看下 docker
CLI,我以前的文章中介紹過現在 Docker 中有兩個構建系統,一個是 v1 版本的 builder
另一個是 v2 版本的即 BuildKit
我們來分別深入源碼來看看在構建鏡像時,他們各自的行為吧。
CLI 的代碼倉庫在 https://github.com/docker/cli 本文的代碼以 v20.10.5
為準。
通過以下步驟使用此版本的代碼:
(MoeLove) ? git clone https://github.com/docker/cli.git
(MoeLove) ? cd cli
(MoeLove) ? cli git:(master) git checkout -b v20.10.5 v20.10.5
docker
是我們所使用的客戶端工具,用于與 dockerd
進行交互。關于構建相關的部分, 我們所熟知的便是 docker build
或者是 docker image build
,在 19.03 中新增的是 docker builder build
,但其實他們都是同一個只是做了個 alias 罷了:
// cmd/docker/docker.go#L237
if v, ok := aliasMap["builder"]; ok {
aliases = append(aliases,
[2][]string{{"build"}, {v, "build"}},
[2][]string{{"image", "build"}, {v, "build"}},
)
}
真正的入口函數其實在 cli/command/image/build.go
;區分如何調用的邏輯如下:
func runBuild(dockerCli command.Cli, options buildOptions) error {
buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo())
if err != nil {
return err
}
if buildkitEnabled {
return runBuildBuildKit(dockerCli, options)
}
// 省略掉了對于 builder 的實際邏輯
}
這里就是判斷下是否支持 buildkit
// cli/command/cli.go#L176
func BuildKitEnabled(si ServerInfo) (bool, error) {
buildkitEnabled := si.BuildkitVersion == types.BuilderBuildKit
if buildkitEnv := os.Getenv("DOCKER_BUILDKIT"); buildkitEnv != "" {
var err error
buildkitEnabled, err = strconv.ParseBool(buildkitEnv)
if err != nil {
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
}
}
return buildkitEnabled, nil
}
當然,從這里可以得到兩個信息:
dockerd
的配置可開啟 buildkit
。在 /etc/docker/daemon.json
中添加如下內容,并重啟 dockerd
即可:{
"features": {
"buildkit": true
}
}
docker
CLI 上也可開啟 buildkit
的支持,并且 CLI 的配置可覆蓋服務端配置。通過 export DOCKER_BUILDKIT=1
即可開啟 buildkit
的支持,設置為 0 則關閉(0/false/f/F 之類的也都是相同的結果)從上面的介紹也看到了,對于原本默認的 builder 而言, 入口邏輯在 runBuild
中, 而對于使用 buildkit 的則是 runBuildBuildKit
接下來,我們對兩者進行逐步分解。
在 runBuild
函數中,大致經歷了以下階段:
最開始的部分是一些對參數的處理和校驗。
stream
和 compress
不可同時使用。因為如果我們指定了 compress
的話,則 CLI 會使用 gzip
將構建上下文進行壓縮,這樣也就沒法很好的通過 stream
的模式來處理構建的上下文了。
當然你也可能會想,從技術上來講,壓縮和流式沒有什么必然的沖突,是可實現的。事實的確如此,如果從技術的角度上來講兩者并非完全不能一起存在,無非就是增加解壓縮的動作。但是當開啟 stream
模式,對每個文件都進行壓縮和解壓的操作那將會是很大的資源浪費,同時也增加了其復雜度,所以在 CLI 中便直接進行了限制,不允許同時使用 compress
和 stream
stdin
讀取 Dockerfile
和 build context
。在進行構建時,如果我們將 Dockerfile
的名字傳遞為 -
時,表示從 stdin
讀取其內容。
例如,某個目錄下有三個文件 foo
bar
和 Dockerfile
,通過管道將 Dockerfile
的內容通過 stdin
傳遞給 docker build
(MoeLove) ? x ls
bar Dockerfile foo
(MoeLove) ? x cat Dockerfile | DOCKER_BUILDKIT=0 docker build -f - .
Sending build context to Docker daemon 15.41kB
Step 1/3 : FROM scratch
--->
Step 2/3 : COPY foo foo
---> a2af45d66bb5
Step 3/3 : COPY bar bar
---> cc803c675dd2
Successfully built cc803c675dd2
可以看到通過 stdin
傳遞 Dockerfile
的方式能成功的構建鏡像。接下來我們嘗試通過 stdin
將 build context
傳遞進去。
(MoeLove) ? x tar -cvf x.tar foo bar Dockerfile
foo
bar
Dockerfile
(MoeLove) ? x cat x.tar| DOCKER_BUILDKIT=0 docker build -f Dockerfile -
Sending build context to Docker daemon 10.24kB
Step 1/3 : FROM scratch
--->
Step 2/3 : COPY foo foo
---> 09319712e220
Step 3/3 : COPY bar bar
---> ce88644a7395
Successfully built ce88644a7395
可以看到通過 stdin
傳遞 build context
的方式也可以成功構建鏡像。
但如果 Dockerfile
的名稱與構建的上下文都指定為 -
即 docker build -f - -
時,會發生什么呢?
(MoeLove) ? x DOCKER_BUILDKIT=0 docker build -f - -
invalid argument: can't use stdin for both build context and dockerfile
就會報錯了。所以, 不能同時使用 stdin
讀取 Dockerfile
和 build context
。
build context
支持四種行為。switch {
case options.contextFromStdin():
// 省略
case isLocalDir(specifiedContext):
// 省略
case urlutil.IsGitURL(specifiedContext):
// 省略
case urlutil.IsURL(specifiedContext):
// 省略
default:
return errors.Errorf("unable to prepare context: path %q not found", specifiedContext)
}
從 stdin
傳入,上文已經演示過了,傳遞給 stdin
的是 tar
歸檔文件。當然也可以是指定一個具體的 PATH
,我們通常使用的 docker build .
便是這種用法;
或者可以指定一個 git
倉庫的地址,CLI 會調用 git
命令將倉庫 clone
至一個臨時目錄,進行使用;
最后一種是,給定一個 URL
地址,該地址可以是 一個具體的 Dockerfile 文件地址 或者是 一個 tar
歸檔文件的下載地址 。
這幾種基本就是字面上的區別,至于 CLI 的行為差異,主要是最后一種,當 URL
地址是一個具體的 Dockerfile
文件地址,在這種情況下 build context
相當于只有 Dockerfile
自身,所以并不能使用 COPY
之類的指定,至于 ADD
也只能使用可訪問的外部地址。
.dockerignore
忽略不需要的文件我在之前的文章中有分享過相關的內容。這里我們看看它的實現邏輯。
// cli/command/image/build/dockerignore.go#L13
func ReadDockerignore(contextDir string) ([]string, error) {
var excludes []string
f, err := os.Open(filepath.Join(contextDir, ".dockerignore"))
switch {
case os.IsNotExist(err):
return excludes, nil
case err != nil:
return nil, err
}
defer f.Close()
return dockerignore.ReadAll(f)
}
.dockerignore
是一個固定的文件名,并且需要放在 build context
的根目錄下。類似前面提到的,使用一個 Dockerfile
文件的 URL 地址作為 build context
傳入的方式,便無法使用 .dockerignore
。.dockerignore
文件可以不存在,但在讀取的時候如果遇到錯誤,便會拋出錯誤。.dockerignore
將會過濾掉不希望加入到鏡像內,或者過濾掉與鏡像無關的內容。最后 CLI 會將 build context
中的內容經過 .dockerignore
過濾后,打包成為真正的 build context
即真正的構建上下文。這也是為什么有時候你發現自己明明在 Dockerfile
里面寫了 COPY xx xx
但是最后沒有發現該文件的情況。 很可能就是被 .dockerignore
給忽略掉了。 這樣有利于優化 CLI 與 dockerd
之間的傳輸壓力之類的。
docker
CLI 還會去讀取 ~/.docker/config.json
中的內容。這與前面 API 部分所描述的內容基本是一致的。將認證信息通過 X-Registry-Config
頭傳遞給 dockerd
用于在需要拉取鏡像時進行身份校驗。
當一切所需的校驗和信息都準備就緒之后,則開始調用 dockerCli.Client
封裝的 API 接口,將請求發送至 dockerd
,進行實際的構建任務。
response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
if err != nil {
if options.quiet {
fmt.Fprintf(dockerCli.Err(), "%s", progBuff)
}
cancel()
return err
}
defer response.Body.Close()
到這里其實一次構建的過程中 CLI 所處理的流程就基本結束了,之后便是按照傳遞的參數進行進度的輸出或是將鏡像 ID 寫入到文件之類的。 這部分就不進行展開了。
整個過程大致如下圖:
從入口函數 runBuild
開始,經過判斷是否支持 buildkit
,如果不支持 buildkit
則繼續使用 v1 的 builder
。接下來讀取各類參數,按照不同的參數執行各類不同的處理邏輯。這里需要注意的就是 Dockerfile
及 build context
都可支持從文件或者 stdin
等讀入,具體使用時,需要注意。另外 .dockerignore
文件可過濾掉 build context
中的一些文件,在使用時,可通過此方法進行構建效率的優化,當然也需要注意,在通過 URL 獲取 Dockerfile
的時候,是不存在 build context
的,所以類似 COPY
這樣的命令也就無法使用了。當所有的 build context
和參數都準備就緒后,接下來調用封裝好的客戶端,將這些請求按照本文開始之初介紹的 API 發送給 dockerd
,由其進行真正的構建邏輯。
最后當構建結束后,CLI 根據參數決定是否要顯示構建進度或者結果。
接下來我們來看看 buildkit
如何來執行構建,方法入口與 builder
一致,但是在 buildkitEnabled
處,由于開啟了 buildkit
支持,所以跳轉到了 runBuildBuildKit
。
func runBuild(dockerCli command.Cli, options buildOptions) error {
buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo())
if err != nil {
return err
}
if buildkitEnabled {
return runBuildBuildKit(dockerCli, options)
}
// 省略掉了對于 builder 的實際邏輯
}
但是與 builder
不同的是,這里先執行了一次 trySession
函數。
// cli/command/image/build_buildkit.go#L50
s, err := trySession(dockerCli, options.context, false)
if err != nil {
return err
}
if s == nil {
return errors.Errorf("buildkit not supported by daemon")
}
這個函數是用來做什么的呢?我們來找到該函數所在的文件 cli/command/image/build_session.go
// cli/command/image/build_session.go#L29
func trySession(dockerCli command.Cli, contextDir string, forStream bool) (*session.Session, error) {
if !isSessionSupported(dockerCli, forStream) {
return nil, nil
}
sharedKey := getBuildSharedKey(contextDir)
s, err := session.NewSession(context.Background(), filepath.Base(contextDir), sharedKey)
if err != nil {
return nil, errors.Wrap(err, "failed to create session")
}
return s, nil
}
當然還包括它其中最主要的 isSessionSupported
函數:
// cli/command/image/build_session.go#L22
func isSessionSupported(dockerCli command.Cli, forStream bool) bool {
if !forStream && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.39") {
return true
}
return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
}
isSessionSupported
很明顯是用于判斷是否支持 Session
,這里由于我們會傳入 forStream
為 false
,而且當前的 API 版本是 1.41 比 1.39 大,所以此函數會返回 true
。其實在 builder
中也執行過相同的邏輯,只不過是在傳遞了 --stream
參數后,使用 Session
獲取一個長連接以達到 stream
的處理能力。
這也就是為什么會有下面 dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
這個判斷存在的原因了。
當確認支持 Session
時,則會調用 session.NewSession
創建一個新的會話。
// github.com/moby/buildkit/session/session.go#L47
func NewSession(ctx context.Context, name, sharedKey string) (*Session, error) {
id := identity.NewID()
var unary []grpc.UnaryServerInterceptor
var stream []grpc.StreamServerInterceptor
serverOpts := []grpc.ServerOption{}
if span := opentracing.SpanFromContext(ctx); span != nil {
tracer := span.Tracer()
unary = append(unary, otgrpc.OpenTracingServerInterceptor(tracer, traceFilter()))
stream = append(stream, otgrpc.OpenTracingStreamServerInterceptor(span.Tracer(), traceFilter()))
}
unary = append(unary, grpcerrors.UnaryServerInterceptor)
stream = append(stream, grpcerrors.StreamServerInterceptor)
if len(unary) == 1 {
serverOpts = append(serverOpts, grpc.UnaryInterceptor(unary[0]))
} else if len(unary) > 1 {
serverOpts = append(serverOpts, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(unary...)))
}
if len(stream) == 1 {
serverOpts = append(serverOpts, grpc.StreamInterceptor(stream[0]))
} else if len(stream) > 1 {
serverOpts = append(serverOpts, grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(stream...)))
}
s := &Session{
id: id,
name: name,
sharedKey: sharedKey,
grpcServer: grpc.NewServer(serverOpts...),
}
grpc_health_v1.RegisterHealthServer(s.grpcServer, health.NewServer())
return s, nil
}
它創建了一個長連接會話,接下來的操作也都會基于這個會話來做。接下來的操作與 builder
大體一致,先判斷 context
是以哪種形式提供的;當然它也與 builder
一樣,是不允許同時從 stdin
獲取 Dockerfile
和 build context
。
switch {
case options.contextFromStdin():
// 省略處理邏輯
case isLocalDir(options.context):
// 省略處理邏輯
case urlutil.IsGitURL(options.context):
// 省略處理邏輯
case urlutil.IsURL(options.context):
// 省略處理邏輯
default:
return errors.Errorf("unable to prepare context: path %q not found", options.context)
}
這里的處理邏輯與 v1 builder
保持一致的原因,主要在于用戶體驗上,當前的 CLI 的功能已經基本穩定,用戶也已經習慣,所以即使是增加了 BuildKit
也并沒有對主體的操作邏輯造成多大改變。
BuildKit
支持了三種不同的輸出模式 local
tar
和正常模式(即存儲在 dockerd
中), 格式為 -o type=local,dest=path
如果需要將構建的鏡像進行分發,或是需要進行鏡像內文件瀏覽的話,使用這個方式也是很方便的。
outputs, err := parseOutputs(options.outputs)
if err != nil {
return errors.Wrapf(err, "failed to parse outputs")
}
for _, out := range outputs {
switch out.Type {
case "local":
// 省略
case "tar":
// 省略
}
}
其實它支持的模式還有第 4 種, 名為 cacheonly
但它并不會像前面提到的三種模式一樣,有個很直觀的輸出,而且用的人可能會很少,所以就沒有單獨寫了。
dockerAuthProvider := authprovider.NewDockerAuthProvider(os.Stderr)
s.Allow(dockerAuthProvider)
這里的行為與上面提到的 builder
的行為基本一致,這里主要有兩個需要注意的點:
func (s *Session) Allow(a Attachable) {
a.Register(s.grpcServer)
}
這個 Allow
函數就是允許通過上面提到的 grpc 會話訪問給定的服務。
authprovider
authprovider
是 BuildKit
提供的一組抽象接口集合,通過它們可以訪問到機器上的配置文件,進而拿到認證信息,行為與 builder
基本一致。
secrets
和 ssh
我其他的文章講過這兩種高階特性的使用了,本篇中就不再多使用進行過多說明了,只來大體看下該部分的原理和邏輯。
secretsprovider
和 sshprovider
都是 buildkit
在提供的,利用這兩種特性可以在 Docker 鏡像進行構建時更加安全,且更加靈活。
func parseSecretSpecs(sl []string) (session.Attachable, error) {
fs := make([]secretsprovider.Source, 0, len(sl))
for _, v := range sl {
s, err := parseSecret(v)
if err != nil {
return nil, err
}
fs = append(fs, *s)
}
store, err := secretsprovider.NewStore(fs)
if err != nil {
return nil, err
}
return secretsprovider.NewSecretProvider(store), nil
}
關于 secrets
方面,最終的 parseSecret
會完成格式相關的校驗之類的;
func parseSSHSpecs(sl []string) (session.Attachable, error) {
configs := make([]sshprovider.AgentConfig, 0, len(sl))
for _, v := range sl {
c := parseSSH(v)
configs = append(configs, *c)
}
return sshprovider.NewSSHAgentProvider(configs)
}
而關于 ssh
方面,則與上方的 secrets
基本一致,通過 sshprovider
允許進行 ssh 轉發之類的,這里不再深入展開了。
這里主要有兩種情況。
build context
是從 stdin
讀,并且是一個 tar
文件時buildID := stringid.GenerateRandomID()
if body != nil {
eg.Go(func() error {
buildOptions := types.ImageBuildOptions{
Version: types.BuilderBuildKit,
BuildID: uploadRequestRemote + ":" + buildID,
}
response, err := dockerCli.Client().ImageBuild(context.Background(), body, buildOptions)
if err != nil {
return err
}
defer response.Body.Close()
return nil
})
}
它會執行上述這部分邏輯,但同時也要注意,這是使用的是 Golang 的 goroutine
,到這里也并不是結束,這部分代碼之后的代碼也同樣會被執行。這就說到了另一種情況了(通常情況)。
doBuild
完成邏輯eg.Go(func() error {
defer func() {
s.Close()
}()
buildOptions := imageBuildOptions(dockerCli, options)
buildOptions.Version = types.BuilderBuildKit
buildOptions.Dockerfile = dockerfileName
buildOptions.RemoteContext = remote
buildOptions.SessionID = s.ID()
buildOptions.BuildID = buildID
buildOptions.Outputs = outputs
return doBuild(ctx, eg, dockerCli, stdoutUsed, options, buildOptions)
})
那 doBuild
會做些什么呢?它同樣也調用了 API 向 dockerd
發起了構建請求。
func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, stdoutUsed bool, options buildOptions, buildOptions types.ImageBuildOptions, at session.Attachable) (finalErr error) {
response, err := dockerCli.Client().ImageBuild(context.Background(), nil, buildOptions)
if err != nil {
return err
}
defer response.Body.Close()
// 省略
}
從以上的介紹我們可以先做個小的總結。 當 build context
從 stdin
讀,并且是個 tar
歸檔時,實際會向 dockerd
發起兩次 /build
請求 而一般情況下只會發送一次請求。
那這里會有什么差別呢?此處先不展開,我們留到下面講 dockerd
服務端的時候再來解釋。
這里我們對開啟了 buildkit
支持的 CLI 構建鏡像的過程進行了分析,大致過程如下:
從入口函數 runBuild
開始,判斷是否支持 buildkit
,如果支持 buildkit
則調用 runBuildBuildKit
。與 v1 的 builder
不同的是,開啟了 buildkit
后,會首先創建一個長連接的會話,并一直保持。其次,與 builder
相同,判斷 build context
的來源,格式之類的,校驗參數等。當然,buildkit
支持三種不同的輸出格式 tar
, local
或正常的存儲于 Docker 的目錄中。另外是在 buildkit
中新增的高階特性,可以配置 secrets
和 ssh
密鑰等功能。最后,再調用 API 與 dockerd
交互完成鏡像的構建。
上面分別介紹了 API, CLI 的 v1 builder
和 buildkit
,接下來我們看看服務端的具體原理和邏輯。
還記得上面部分中最后通過 API 與服務端交互的 ImageBuild
函數嗎?在開始 dockerd
的介紹前,我們來看下這個客戶端接口的具體內容。
// github.com/docker/docker/client/image_build.go#L20
func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
query, err := cli.imageBuildOptionsToQuery(options)
if err != nil {
return types.ImageBuildResponse{}, err
}
headers := http.Header(make(map[string][]string))
buf, err := json.Marshal(options.AuthConfigs)
if err != nil {
return types.ImageBuildResponse{}, err
}
headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf))
headers.Set("Content-Type", "application/x-tar")
serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers)
if err != nil {
return types.ImageBuildResponse{}, err
}
osType := getDockerOS(serverResp.header.Get("Server"))
return types.ImageBuildResponse{
Body: serverResp.body,
OSType: osType,
}, nil
}
沒有什么太特別的地方,行為與 API 一致。 通過這里我們確認它確實訪問的 /build
接口,所以,我們來看看 dockerd
的 /build
接口,看看它在構建鏡像的時候做了什么。
dockerd
由于本文集中討論的是構建系統相關的部分,所以也就不再過多贅述與構建無關的內容了,我們直接來看,當 CLI 通過 /build
接口發送請求后,會發生什么。
先來看該 API 的入口:
// api/server/router/build/build.go#L32
func (r *buildRouter) initRoutes() {
r.routes = []router.Route{
router.NewPostRoute("/build", r.postBuild),
router.NewPostRoute("/build/prune", r.postPrune),
router.NewPostRoute("/build/cancel", r.postCancel),
}
}
dockerd
提供了一套類 RESTful 的后端接口服務,處理邏輯的入口便是上面的 postBuild
函數。
該函數的內容較多,我們來分解下它的主要步驟。
buildOptions, err := newImageBuildOptions(ctx, r)
if err != nil {
return errf(err)
}
newImageBuildOptions
函數就是構造構建參數的,將通過 API 提交過來的參數轉換為構建動作實際需要的參數形式。
buildOptions.AuthConfigs = getAuthConfigs(r.Header)
getAuthConfigs
函數用于從請求頭拿到認證信息
imgID, err := br.backend.Build(ctx, backend.BuildConfig{
Source: body,
Options: buildOptions,
ProgressWriter: buildProgressWriter(out, wantAux, createProgressReader),
})
if err != nil {
return errf(err)
}
這里就需要注意了: 真正的構建過程要開始了。使用 backend 的 Build
函數來完成真正的構建過程
// api/server/backend/build/backend.go#L53
func (b *Backend) Build(ctx context.Context, config backend.BuildConfig) (string, error) {
options := config.Options
useBuildKit := options.Version == types.BuilderBuildKit
tagger, err := NewTagger(b.imageComponent, config.ProgressWriter.StdoutFormatter, options.Tags)
if err != nil {
return "", err
}
var build *builder.Result
if useBuildKit {
build, err = b.buildkit.Build(ctx, config)
if err != nil {
return "", err
}
} else {
build, err = b.builder.Build(ctx, config)
if err != nil {
return "", err
}
}
if build == nil {
return "", nil
}
var imageID = build.ImageID
if options.Squash {
if imageID, err = squashBuild(build, b.imageComponent); err != nil {
return "", err
}
if config.ProgressWriter.AuxFormatter != nil {
if err = config.ProgressWriter.AuxFormatter.Emit("moby.image.id", types.BuildResult{ID: imageID}); err != nil {
return "", err
}
}
}
if !useBuildKit {
stdout := config.ProgressWriter.StdoutFormatter
fmt.Fprintf(stdout, "Successfully built %s\n", stringid.TruncateID(imageID))
}
if imageID != "" {
err = tagger.TagImages(image.ID(imageID))
}
return imageID, err
}
這個函數看著比較長,但主要功能就以下三點:
NewTagger
是用于給鏡像打標簽,也就是我們的 -t
參數相關的內容,這里不做展開。buildkit
來調用不同的構建后端。useBuildKit := options.Version == types.BuilderBuildKit
var build *builder.Result
if useBuildKit {
build, err = b.buildkit.Build(ctx, config)
if err != nil {
return "", err
}
} else {
build, err = b.builder.Build(ctx, config)
if err != nil {
return "", err
}
}
到這個函數之后,就分別是 v1 builder
與 buildkit
對 Dockerfile
的解析,以及對 build context
的操作了。
這里涉及到的內容與我下一篇文章《高效構建 Docker 鏡像的最佳實踐》的內部關聯比較大,此處就不再進行展開了。敬請期待下一篇文章。
本文首先介紹了 Docker 的 C/S 架構,介紹了構建鏡像所用的 API , API 文檔可以在線查看或者本地構建。之后深入到 Docker CLI 的源碼中,逐步分解 v1 builder
與 buildkit
在構建鏡像時執行的過程的差異。最后,我們深入到 dockerd
的源碼中,了解到了對不同構建后端的調用。至此,Docker 構建鏡像的原理及主體代碼就介紹完畢。
但這還并不是結束,我會在后續文章中分享鏡像構建的相關實踐,敬請期待!
歡迎訂閱我的文章公眾號【MoeLove】
這里我們先從宏觀上對 Docker 有個大概的認識,它整體上是個 C/S 架構;我們平時使用的 docker 命令就是它的 CLI 客戶端,而它的服務端是 dockerd 在 Linux 系統中,通常我們是使用 systemd 進行管理,所以我們可以使用 systemctl start docker 來啟動服務。(但是請...
贊 8 收藏 4 評論 0
joyqi 回答了問題 · 3月15日
進入項目的releases列表,比如:[鏈接]列表中打上Latest release標簽的就是最近可用的版本
關注 2 回答 1
查看全部 個人動態 →
A PHP Blogging Platform. Simple and Powerful.
注冊于 2011-04-25
個人主頁被 15k 人瀏覽
推薦關注
我要該,理由是: