<dfn id="hx5t3"><strike id="hx5t3"><em id="hx5t3"></em></strike></dfn>

    <thead id="hx5t3"></thead><nobr id="hx5t3"><font id="hx5t3"><rp id="hx5t3"></rp></font></nobr>

    <listing id="hx5t3"></listing>

    <var id="hx5t3"></var>
    <big id="hx5t3"></big>

      
      

      <output id="hx5t3"><ruby id="hx5t3"></ruby></output>
      <menuitem id="hx5t3"><dfn id="hx5t3"></dfn></menuitem>

      <big id="hx5t3"></big>

        joyqi

        joyqi 查看完整檔案

        杭州編輯華中科技大學  |  電子信息工程 編輯SegmentFault  |  CTO 編輯 joyqi.com 編輯
        編輯

        我的生涯一片無悔,想起那天夕陽下的奔跑,那是我逝去的青春

        個人動態

        joyqi 回答了問題 · 3月25日

        新版使用問題

        Hi,請移步至新版專欄首頁 http://www.tvxinternet.com/blogs

        關注 2 回答 1

        joyqi 回答了問題 · 3月25日

        新版太low了,信息量太少,還是舊版舒服

        Hi,所有的訂閱和文章分區功能(老版主頁的所有功能)即將在新版專欄頁面上線后呈現。

        關注 2 回答 1

        joyqi 關注了用戶 · 3月25日

        doujiang24 @doujiang24

        中年程序員

        關注 119

        joyqi 贊了文章 · 3月25日

        CPU 提供了什么

        為了方便理解,CPU 可以簡單認為是:

        1. 一堆的寄存器,用于暫時存放數據
        2. 可以執行機器指令,完成運算 / 數據讀寫 等操作

        寄存器

        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 中。

        棧操作

        pushpop 這一對用于操作“?!?。
        “?!笔莾却婵臻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

        注意:因為棧在內存空間中是倒過來的,所以是向下生長的。

        查看原文

        贊 2 收藏 0 評論 0

        joyqi 贊了文章 · 3月24日

        為 OpenResty 應用編寫你自己的 Lua 模塊

        https://www.bilibili.com/vide...

        今天我就來演示一下如何在 OpenResty 應用中編寫自己的 Lua 模塊,一步一步來。

        screenshot 1

        讓我們把我們簡單的 OpenResty 應用放到一個新的目錄中,命名為 test-module。

        cd ~/
        mkdir test-module/
        cd test-module/

        screenshot 2

        然后我們像往常一樣創建子目錄結構。

        mkdir logs conf lua

        screenshot 3

        請注意,與上一篇教程中的“Hello World”示例不同,我們在這里創建了一個 lua/ 目錄來存放我們的 lua 模塊文件。

        現在讓我們在 lua 子目錄下創建我們自己的 Lua 模塊文件,命名為 hello.lua。

        vim lua/hello.lua

        我們進行如下編輯操作:

        1. 聲明 Lua 模塊表 _M。
        2. 然后給這個 Lua 模塊添加一個名為 greet 的函數。
        3. 最后,別忘了在最后返回模塊表。
        local _M = {}
        
        function _M.greet(name)
            ngx.say("Greetings from ", name)
        end
        
        return _M

        搞定!一個很簡單的 Lua 模塊就完成了。

        現在是創建 nginx.conf 配置文件的時候了。

        vim conf/nginx.conf

        我們快速完成下列編輯操作:

        1. 我們快速寫出相對固定的配置。
        2. http {} 配置塊中,我們應該告訴 OpenResty 我們的 Lua 模塊在哪里。
        3. 請注意,特殊變量 $prefix 在運行時被 nginx 的 -p 選項值所替代。
        4. 然后我們創建一個 HTTP 服務器,監聽 8080 端口。
        5. 并在根位置配置 content_by_lua"_block。
        6. 這里我們用 require 內置函數加載 Lua 模塊 hello。
        7. 我們用一個參數調用它的 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 .

        screenshot 22

        看起來不錯。

        現在啟動這個 OpenResty 應用程序。

        nginx -p $PWD/

        screenshot 24
        是時候用 curl 命令行工具查詢我們的 HTTP 接口了。

        curl 'http://127.0.0.1:8080/'

        screenshot 25

        酷,我們的 Lua 模塊開始工作了。

        我們也可以在網頁瀏覽器中進行測試。

        screenshot 28

        如果你看到了 500 錯誤頁面,那么你的 Lua 代碼一定有錯誤。

        在這種情況下,你應該檢查 logs 子目錄下的 error.log 文件。

        tail logs/error.log

        screenshot 30

        這里我們沒有任何錯誤消息,正如預期的那樣。

        值得注意的是,我們的 Lua 模塊是在這第一個請求中加載的,后續的請求只是使用內存中緩存的 Lua 模塊。
        為了避免第一次請求的額外開銷,我們可以在服務器啟動時預加載 Lua 模塊。

        要做到這一點,我們需要編輯一下 nginx.conf 文件。

        vim conf/nginx.conf

        http {} 塊里,我們添加一個 init_by_lua_block 指令。在那個上下文中,我們加載我們的 Lua 模塊。

            http {
                init_by_lua_block {
                    require "hello"
                }
                ...

        screenshot 34

        init_by_lua_block 運行在 OpenResty 服務器啟動的時候。

        測試一下配置是否正確。

        nginx -p $PWD/ -t

        screenshot 37

        沒有問題。

        現在我們通過向 nginx 主進程發送 HUP 信號來重新加載服務器。
        Now we reload the server by sending the HUP signal to the nginx master process.

        kill -HUP `cat logs/nginx.pid`

        screenshot 39

        主進程的進程 ID 存儲在這個 nginx.pid 文件中。

        再次發送 HTTP 請求。

        curl 'http://127.0.0.1:8080/'

        screenshot 41

        同樣的行為,只是這次快了一點點。
        如果你喜歡這個教程,請訂閱這個博客網站和我們的 YouTube 頻道B 站頻道。謝謝!

        關于本文和關聯視頻

        本文和相關聯的視頻都是完全由我們的 OpenResty Demo 系統從一個極簡單的劇本文件自動生成的。

        關于作者

        章亦春是開源項目 OpenResty? 的創始人,同時也是 OpenResty Inc. 公司的創始人和 CEO。他貢獻了許多 Nginx 的第三方模塊,相當多 Nginx 和 LuaJIT 核心補丁,并且設計了 OpenResty XRay 等產品。

        關注我們

        如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的博客網站 。也歡迎掃碼關注我們的微信公眾號:

        我們的微信公眾號

        翻譯

        我們提供了英文版原文和中譯版(本文) 。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮采用,非常感謝!

        查看原文

        贊 2 收藏 1 評論 0

        joyqi 回答了問題 · 3月19日

        我發送個文章,你們這是。。。。

        我們已經修復了這個bug,非常抱歉帶來了困擾。

        關注 2 回答 1

        joyqi 回答了問題 · 3月18日

        解決點擊發布沒有反應

        Hi,我們已經修復了這個錯誤,請再次點擊發布按鈕。很抱歉帶來使用上的困擾。

        關注 2 回答 1

        joyqi 贊了文章 · 3月15日

        萬字長文:徹底搞懂容器鏡像構建

        大家好,我是張晉濤。

        我將在這篇文章中深入 Docker 的源碼,與你聊聊鏡像構建的原理。

        Docker 架構

        這里我們先從宏觀上對 Docker 有個大概的認識,它整體上是個 C/S 架構;我們平時使用的 docker 命令就是它的 CLI 客戶端,而它的服務端是 dockerd 在 Linux 系統中,通常我們是使用 systemd 進行管理,所以我們可以使用 systemctl start docker 來啟動服務。(但是請注意,dockerd 是否能運行與 systemd 并無任何關系,你可以像平時執行一個普通的二進制程序一樣,直接通過 dockerd 來啟動服務,注意需要 root 權限)

        實際上也就是

        Docker 架構

        (圖片來源: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 。

        API

        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 進行加密,以免數據泄漏。

        CLI

        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 接下來,我們對兩者進行逐步分解。

        builder v1

        runBuild 函數中,大致經歷了以下階段:

        參數處理

        最開始的部分是一些對參數的處理和校驗。

        • streamcompress 不可同時使用。

        因為如果我們指定了 compress 的話,則 CLI 會使用 gzip 將構建上下文進行壓縮,這樣也就沒法很好的通過 stream 的模式來處理構建的上下文了。

        當然你也可能會想,從技術上來講,壓縮和流式沒有什么必然的沖突,是可實現的。事實的確如此,如果從技術的角度上來講兩者并非完全不能一起存在,無非就是增加解壓縮的動作。但是當開啟 stream 模式,對每個文件都進行壓縮和解壓的操作那將會是很大的資源浪費,同時也增加了其復雜度,所以在 CLI 中便直接進行了限制,不允許同時使用 compressstream

        • 不可同時使用 stdin 讀取 Dockerfilebuild context。

        在進行構建時,如果我們將 Dockerfile 的名字傳遞為 - 時,表示從 stdin 讀取其內容。

        例如,某個目錄下有三個文件 foobarDockerfile,通過管道將 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 的方式能成功的構建鏡像。接下來我們嘗試通過 stdinbuild 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 讀取 Dockerfilebuild 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 用于在需要拉取鏡像時進行身份校驗。

        • 調用 API 進行實際構建任務

        當一切所需的校驗和信息都準備就緒之后,則開始調用 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 寫入到文件之類的。 這部分就不進行展開了。

        小結

        整個過程大致如下圖:

        docker builder 處理流程

        從入口函數 runBuild 開始,經過判斷是否支持 buildkit ,如果不支持 buildkit 則繼續使用 v1 的 builder。接下來讀取各類參數,按照不同的參數執行各類不同的處理邏輯。這里需要注意的就是 Dockerfilebuild context 都可支持從文件或者 stdin 等讀入,具體使用時,需要注意。另外 .dockerignore 文件可過濾掉 build context 中的一些文件,在使用時,可通過此方法進行構建效率的優化,當然也需要注意,在通過 URL 獲取 Dockerfile 的時候,是不存在 build context 的,所以類似 COPY 這樣的命令也就無法使用了。當所有的 build context 和參數都準備就緒后,接下來調用封裝好的客戶端,將這些請求按照本文開始之初介紹的 API 發送給 dockerd ,由其進行真正的構建邏輯。

        最后當構建結束后,CLI 根據參數決定是否要顯示構建進度或者結果。

        buildkit

        接下來我們來看看 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,這里由于我們會傳入 forStreamfalse ,而且當前的 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 獲取 Dockerfilebuild 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 支持了三種不同的輸出模式 localtar 和正常模式(即存儲在 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 的行為基本一致,這里主要有兩個需要注意的點:

        • Allow() 函數
        func (s *Session) Allow(a Attachable) {
            a.Register(s.grpcServer)
        }

        這個 Allow 函數就是允許通過上面提到的 grpc 會話訪問給定的服務。

        • authprovider

        authproviderBuildKit 提供的一組抽象接口集合,通過它們可以訪問到機器上的配置文件,進而拿到認證信息,行為與 builder 基本一致。

        高階特性:mount secretsssh

        我其他的文章講過這兩種高階特性的使用了,本篇中就不再多使用進行過多說明了,只來大體看下該部分的原理和邏輯。

        secretsprovidersshprovider 都是 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 轉發之類的,這里不再深入展開了。

        調用 API 發送構建請求

        這里主要有兩種情況。

        • 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 contextstdin 讀,并且是個 tar 歸檔時,實際會向 dockerd 發起兩次 /build 請求 而一般情況下只會發送一次請求。

        那這里會有什么差別呢?此處先不展開,我們留到下面講 dockerd 服務端的時候再來解釋。

        小結

        這里我們對開啟了 buildkit 支持的 CLI 構建鏡像的過程進行了分析,大致過程如下:

        從入口函數 runBuild 開始,判斷是否支持 buildkit ,如果支持 buildkit 則調用 runBuildBuildKit。與 v1 的 builder 不同的是,開啟了 buildkit 后,會首先創建一個長連接的會話,并一直保持。其次,與 builder 相同,判斷 build context 的來源,格式之類的,校驗參數等。當然,buildkit 支持三種不同的輸出格式 tar, local 或正常的存儲于 Docker 的目錄中。另外是在 buildkit 中新增的高階特性,可以配置 secretsssh 密鑰等功能。最后,再調用 API 與 dockerd 交互完成鏡像的構建。

        服務端:dockerd

        上面分別介紹了 API, CLI 的 v1 builderbuildkit ,接下來我們看看服務端的具體原理和邏輯。

        Client 函數

        還記得上面部分中最后通過 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 builderbuildkitDockerfile 的解析,以及對 build context 的操作了。

        這里涉及到的內容與我下一篇文章《高效構建 Docker 鏡像的最佳實踐》的內部關聯比較大,此處就不再進行展開了。敬請期待下一篇文章。

        總結

        本文首先介紹了 Docker 的 C/S 架構,介紹了構建鏡像所用的 API , API 文檔可以在線查看或者本地構建。之后深入到 Docker CLI 的源碼中,逐步分解 v1 builderbuildkit 在構建鏡像時執行的過程的差異。最后,我們深入到 dockerd 的源碼中,了解到了對不同構建后端的調用。至此,Docker 構建鏡像的原理及主體代碼就介紹完畢。

        但這還并不是結束,我會在后續文章中分享鏡像構建的相關實踐,敬請期待!


        歡迎訂閱我的文章公眾號【MoeLove】

        TheMoeLove

        查看原文

        贊 8 收藏 4 評論 0

        joyqi 回答了問題 · 3月15日

        解決請教大佬們,如何在github上查看一個庫或者框架,可以用于生產環境的第一個版本是多少?

        進入項目的releases列表,比如:https://github.com/mobxjs/mob...

        列表中打上Latest release標簽的就是最近可用的版本

        image

        關注 2 回答 1

        joyqi 回答了問題 · 3月10日

        新開鏈接,只有標題,沒有內容

        這是一個bug,我們正在修復

        關注 3 回答 2

        認證與成就

        • 認證信息 SegmentFault CTO,Typecho 作者
        • SegmentFault 講師
        • 獲得 2004 次點贊
        • 獲得 198 枚徽章 獲得 25 枚金徽章, 獲得 88 枚銀徽章, 獲得 85 枚銅徽章

        擅長技能
        編輯

        開源項目 & 著作
        編輯

        • Typecho

          A PHP Blogging Platform. Simple and Powerful.

        注冊于 2011-04-25
        個人主頁被 15k 人瀏覽

        一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕