您的位置:首頁>正文

Docker 鏡像優化與最佳實踐

摘要:雲棲TechDay41期, 阿裡雲高級研發工程師禦阪帶來Docker鏡像優化與最佳實踐。 從Docker鏡像存儲的原理開始, 針對鏡像的存儲、網路傳輸, 介紹如何在構建中對這些關鍵點進行優化。 並介紹Docker最新的多階段構建的功能, 以解決構建依賴的中間產物問題。

以下是精彩內容整理:

鏡像概念

鏡像是什麼?從一個比較具體的角度去看, 鏡像就是一個多層存儲的檔, 相較于普通的ISO系統鏡像來說, 分層存儲會帶來兩個優點, 一個是分層存儲的鏡像比較容易擴展, 比如我們可以基於一個Ubuntu鏡像去構建我們的Nginx鏡像, 這樣我們只需要在Ubuntu鏡像的基礎上面做一些Nginx的安裝配置工作, 一個Nginx鏡像工作就算製作完成了, 我們不需要從頭開始去製作各種鏡像。 另一點我們可以優化鏡像存儲空間, 假如我們有兩個鏡像, Tag1.0鏡像和 Tag2.0鏡像, 我們如果以傳統方式去傳這兩個鏡像, 每個鏡像大概130多兆, 但如果我們以分層的方式去存儲兩個鏡像,

我們通過下面兩個紫色的才能共用, 可以節約大量的空間, 兩個鏡像加起來只需要140多兆的空間就可以存下來。 這樣一是節省了存儲空間, 二是可以減少網路上的開銷, 比如我們已經把下面鏡像下載了, 我們要去下載上面鏡像的時候, 我們只需要去下10M的部分。

如果從抽象的角度去看, Docker鏡像其實是Docker提供的一種標準化的交付手段, 傳統應用在交付的時候其實是交付一個可執行檔, 這個可執行檔不包括它的運行環境, 我們可能會因為32位元系統或64位元系統, 或者開發測試使用1.0軟體, 結果交付時候發現用戶的環境是2.0等各種各樣的問題, 導致我們要去花時間去排查, 如果我們以Docker鏡像的標準化形式去交付,

我們就會避免掉這些問題。

鏡像基本操作與存儲方式

我們的一個鏡像會有一個座標, 一個鏡像座標基本上會由四個部分組成, 前面會有一個鏡像服務功能變數名稱, 每一個服務提供者都會有不同的功能變數名稱, 當我們確定服務提供者給我們的功能變數名稱之後, 我們一般會要到服務提供者那裡去申請自己的命名空間, 倉庫名稱一般是標識鏡像的用途,

比如說Ubuntu鏡像、CentOS鏡像, 標籤一般是用於去區分鏡像版本, 比如我們對Ubuntu鏡像可能會打一些16.04的包, 在我們確定了一個鏡像服務功能變數名稱以及在雲服務商申請命名空間之後, 我們就可以對鏡像做一些操作了。

首先我們需要去登陸, 我們會用第一條命令去登陸, 然後, 當我們在本地準備好一個鏡像想要上傳的時候, 我們先要對這個鏡像進行打標, 把它的座標變成我們現在需要上傳鏡像的座標, 然後再去做一些推送拉取的動作, 最後針對Docker還提供兩個額外命令去做鏡像交付, 如果我們是特殊的環境, 沒有辦法網路連通的時候, 我們可以將這個鏡像打包成一個普通檔進行傳輸。 比如我們和公安合作, 他們沒有辦法通過我們的Registry下載鏡像,

我們可能要把它打成一個普通檔, 然後以U盤的方式去交付。

鏡像存儲細節

Docker鏡像是存在聯合檔案系統的, 每一個鏡像其實是分層存儲的, 比如在第一層我們添加了三個新檔, 然後在這一層基礎上我們又增加了一層, 添加了一個檔, 第三層可能會需要做一些修改, 我們把File3做了一個修改移到上面來, 然後刪掉了File4, 這裡就會引到聯合檔案系統裡面的寫時複製機制,

當我們要去修改一個檔的時候, 鏡像依賴底層都是唯讀的, 我們不能去直接修改, 比如我們想去修改File3, 我們不能直接去修改這個檔, 我們需要在修改的時候把檔複製到當前這一層, 比如說L3層, 然後再去修改它。

一個鏡像做好之後, 當我們想要知道鏡像裡面有哪一些內容的時候, 我們其實會有一個視圖概念, 我們從聯合檔案系統的角度去看鏡像的時候, 其實我們不會看到L1、L2、L3, 我們會最後看到結果, File1、File2、File3, File4就看不到了, 然後在我們瞭解原理之後, 我們就可以去理解容器運行起來是一個什麼樣的情況。 容器運行起來和上面形成是類似的, 圖中下半部分, 同樣也是L1、L2、L3的三層鏡像, 當容器運行起來的時候, Docker daemon會動態生成一層可寫層作為容器的運行層, 然後當容器裡面需要去修改一些檔,比如File2,也是copy on write機制把檔複製上來,然後做一些修改,新增檔的時候也是一樣,然後容器在運行的時候也會有一個視圖,當我們把容器停掉的時候,視圖一層就沒有了,它會被銷毀,但是容器層讀寫層還會保留,所以我們把容器停掉再啟動的時候,我們依舊會看到我們之前在容器裡面的一些操作。

常見的存儲驅動主要有AUFS、OverlayFS,還有Device Mapper,前兩種驅動都是基於檔,它的原理就是需要修改一個檔的時候把整個檔複製上去做修改, Device Mapper更偏底層一點,它是基於塊設備的,它的好處在於當我想要修改一個檔的時候,我不會將整個檔拷上去,我會將檔修改的一些存儲塊拷上去做一些修改,當我有一些大文件想要修改的時候,Device Mapper會比AUFS、OverlayFS好很多。所以AUFS和OverlayFS就比較適合傳統的WEB應用,它的檔操作不會很多,但是它可能對我們的應用啟動速度會有一些要求,比如我可能經常要發佈,我希望能夠啟動比較快,但是對於檔修改的一些效率我不是很關心,那可以使用基於檔的驅動,當我們是一些計算密集型的應用時候,我們就可以選擇Device Mapper,雖然啟動比較慢,但是它的運行效率相對表現要好一些。

鏡像自動化構建

我們構建一個鏡像的時候,Docker其實提供了一個標準化的構建指令集,當我們去用這些構建指令去寫類似於腳本,這種腳本我們稱之為DockerFile,Docker可以自動解析DockerFile,並將其構建成一個鏡像,所以你就可以簡單的認為這是一個標準化的腳本。DockerFile在做一些什麼?首先第一行FROM指令表示要以哪一個鏡像作為基礎鏡像進行構建,我們用了openJDK的官方鏡像,以JAVA環境作為基礎,我們在鏡像上面準備跑一個JAVA應用,然後接下來兩條LABLE是對鏡像進行打標,標下鏡像版本和構建日期,然後接下來的六個RUN是做了一個maven安裝,maven是JAVA的一個生命週期管理工具,接下來將一些原始程式碼從外面的環境添加到鏡像裡面,然後兩條RUN命令做了打包工作,最後寫了一個啟動命令。

總的來說DockerFile寫的還可以,至少思路是很清晰的,一步一步從基礎鏡像選擇到編譯環境,再把原始程式碼加進去,然後再到最後的構建,啟動命令寫好,可讀性、可維護性都可以,但是還是可以進行優化的。

我們可以減少鏡像的層數, Docker對於Docker鏡像的層數是有一定要求的,除掉最上面在容器運行時候的讀寫層以外,我們一個鏡像最多只能有127層,如果超過可能會出現問題,所以第二行命令LABLE就可以把它合成一層,減少了層數,下面六個RUN命令做了maven的安裝工作,我們也可以把它做成一層,把這些命令串起來,後面的構建我們也可以把它合成一層,這樣我們一下就把鏡像層數從14層減少到7層,減掉了一半。

我們在做鏡像優化的時候,我們希望能夠儘量減少鏡像的層數,但是和它相對應的是我們DockerFile的可讀性,我們需要在這兩者之間做折中,我們在保證可讀性不受很大影響的情況下去儘量減少它,其實六條RUN命令在做一件事,就是做maven環境打結,做編譯環境的準備工作。

接下來我們繼續對鏡像進行優化,我們可以做一些什麼工作呢?在安裝maven構建工具的時候我們多加了一行,我們把安裝包和展開目錄刪掉了,我們清理了構建的中間產物,我們要去注意每一個構建指令執行的時候,儘量把垃圾清理掉,我們通過apt-get去裝一些軟體的時候,我們也可以去做這樣的清理工作,就是把這些套裝軟體裝完之後就可以把它刪掉了,這樣可以儘量減少空間,通過增加一行命令,我們可以把鏡像的大小從137M削減到119M。

通過apt-get去裝軟體或者命令基本上是所有編寫DockerFile的人都去寫的,所以官方已經在debian、Ubuntu的倉庫鏡像裡面默認加了Hack,它會去幫助你在install自動去把原始程式碼刪掉。

我們可以利用構建的緩存,Docker構建默認會開啟緩存,緩存生效有三個關鍵點,鏡像父層沒有發生變化,構建指令不變,添加檔校驗和一致。只要一個構建指令滿足這三個條件,這一層鏡像構建就不會再執行,它會直接利用之前構建的結果,根據構建緩存特性我們可以加一行RUN,這裡是以JAVA應用為例,一般一個JAVA應用的pom檔都是描述JAVA的一些依賴,而在我們平常的開發過程中這些依賴包發生變化的頻率比較低,那麼我們就可以把POM加進來,把POM檔依賴全部都準備好,然後再去下原始程式碼,再去做構建工作,只要我們沒有把緩存關掉,我們每次構建的時候就不需要重新下安裝包,這樣可以節省大量時間,也可以節省一些網路流量。

現在阿裡雲的容器鏡像服務其實已經提供了構建功能,我們在統計用戶失敗案例的時候就會發現,網路原因導致的失敗占90%,比如如果用戶通過node開發NPM在安裝一些套裝軟體的時候經常卡在中間。所以我們建議加一個軟體源,我們把阿裡雲maven位址加到裡面去,我們把配置項加到阿裡雲的軟體位址,加阿裡雲的maven源作為套裝軟體的下載目標,時間直接少了40%,這樣對一個鏡像構建的成功率也是有幫助的。

多階段構建

DockerFile最終需要做到的產物其實是JAVA應用,我們對於構建、編譯、打包或者安裝這些事情都不關心,我們要的其實是最後的產物。所以,我們可以採取分步的方式去做鏡像構建,首先我們將之前遇到的所有問題全部都做成基礎鏡像,上面FROM鏡像其實已經改了新的,鏡像裡面已經把軟體源的位址改成了Maven,緩存都已經做好了。我們會去利用緩存,然後添加原始程式碼,我們把前面構建的事情做成了鏡像,讓鏡像去完成構建,然後我們才會去完成把JAVA包拷進去,啟動工作,但是兩個DockerFile其實是兩個鏡像,所以我們需要一段腳本去輔助它,第一行的shell腳本是做第一個構建指令,我們指定以Bulid的DockerFile去啟動構建,然後生成一個APP Bulid鏡像,接下來兩行腳本是把鏡像生成出來,把裡面的構建產物拷出來,然後我們再去做構建,最後把我們需要的JAVA應用給構建出來,這樣我們的DockerFile相比之前就更加清晰了,而且分步很簡單。

Docker在17.05之後官方支持了多階段構建,我們把下面的腳本去掉了,我們不需要一段輔助腳本,我們只需要在後面申明基礎鏡像的地方標記,我們第一階段的構建產物名字叫什麼,我們就可以在第二個構建階段裡面用第一個構建階段的產物。比如我們第一階段把JAVA應用構建好,把Maven包裡面的target下面的JAVA架包拷到新的鏡像裡面,然後在所有優化做完之後效果如圖,我們在第一次構建的時候,優化前102秒,在Docker構建優化後只花55秒就完成了,主要優化在網路上面。當我們修改了JAVA檔重新進行構建,第二次構建花了86秒,因為Maven安裝那一塊被緩存了,我們利用了構建緩存,所以少掉20多秒,優化後只花了8秒,因為所有的原始程式碼前面的一些套裝軟體下載全部被緩存了,我們直接拉新的鏡像,然後依賴沒有變,直接進行構建,所以8秒基本上是完整構建時間。

我們再來看一下存儲空間上面的優化,第一次構建我們在優化前把鏡像打出來有137M,但是在我們整個優化之後,只有81M了,這裡的基礎鏡像由JDK改成JRE,為什麼?因為之前我們把所有流程都放在一個鏡像裡面時,我們是需要去做構建的,構建時需要去RUN Maven,這種情況下沒有JDK環境是RUN不起來的,但是如果我們分階段,把構建交給Maven鏡像來做,把真正運行交給新的鏡像來做,就沒必要用JDK了,我們直接用JRE,優化之後鏡像少了將近50%。當我們修改原始程式碼重新進行構建的時候,由於鏡像成共用的原因,第二次構建在優化前其實多加了兩層到三層,一共有9M,但是優化後的第二次構建只增加1.93KB,這樣我們針對DockerFile的優化就已經做完了。

鏡像優化有哪些重要的點呢?具體如下:

減少鏡像的層數,儘量把一些功能上面統一的命令合到一起來做;注意清理鏡像構建的中間產物,比如一些安裝包在裝完之後就把它刪掉;注意優化網路請求,我們去用一些鏡像源,去用一些網路比較好的開源網站,這樣可以節約時間、減少失敗率;儘量去用構建緩存,我們儘量把一些不變的東西或者變的比較少的東西放在前面,因為不變的東西都是可以被緩存的;多階段進行鏡像構建,將我們鏡像製作的目的做一個明確,把我們的構建和真正的一些產物做分離,構建就用構建的鏡像去做,最終產物就打最終產物的鏡像。容器鏡像服務

最後介紹一下阿裡雲容器鏡像服務。這個服務已經公測一年了,現在我們的服務公測是全部免費的,現在在全球的12個Region都已經部署了我們的服務,每個Region其實都有內網服務和VPC網路服務,如果ECS也在同樣的Region,那麼它的服務是非常快的。然後團隊管理和組織帳號功能也已經上線了,鏡像購建和鏡像消息通知其實都是一些DevOps能力,針對一些鏡像優化我們提供了一些鏡像層資訊流覽功能,我們後續也會提供分析,推出鏡像安全掃描、鏡像同步。

然後當容器裡面需要去修改一些檔,比如File2,也是copy on write機制把檔複製上來,然後做一些修改,新增檔的時候也是一樣,然後容器在運行的時候也會有一個視圖,當我們把容器停掉的時候,視圖一層就沒有了,它會被銷毀,但是容器層讀寫層還會保留,所以我們把容器停掉再啟動的時候,我們依舊會看到我們之前在容器裡面的一些操作。

常見的存儲驅動主要有AUFS、OverlayFS,還有Device Mapper,前兩種驅動都是基於檔,它的原理就是需要修改一個檔的時候把整個檔複製上去做修改, Device Mapper更偏底層一點,它是基於塊設備的,它的好處在於當我想要修改一個檔的時候,我不會將整個檔拷上去,我會將檔修改的一些存儲塊拷上去做一些修改,當我有一些大文件想要修改的時候,Device Mapper會比AUFS、OverlayFS好很多。所以AUFS和OverlayFS就比較適合傳統的WEB應用,它的檔操作不會很多,但是它可能對我們的應用啟動速度會有一些要求,比如我可能經常要發佈,我希望能夠啟動比較快,但是對於檔修改的一些效率我不是很關心,那可以使用基於檔的驅動,當我們是一些計算密集型的應用時候,我們就可以選擇Device Mapper,雖然啟動比較慢,但是它的運行效率相對表現要好一些。

鏡像自動化構建

我們構建一個鏡像的時候,Docker其實提供了一個標準化的構建指令集,當我們去用這些構建指令去寫類似於腳本,這種腳本我們稱之為DockerFile,Docker可以自動解析DockerFile,並將其構建成一個鏡像,所以你就可以簡單的認為這是一個標準化的腳本。DockerFile在做一些什麼?首先第一行FROM指令表示要以哪一個鏡像作為基礎鏡像進行構建,我們用了openJDK的官方鏡像,以JAVA環境作為基礎,我們在鏡像上面準備跑一個JAVA應用,然後接下來兩條LABLE是對鏡像進行打標,標下鏡像版本和構建日期,然後接下來的六個RUN是做了一個maven安裝,maven是JAVA的一個生命週期管理工具,接下來將一些原始程式碼從外面的環境添加到鏡像裡面,然後兩條RUN命令做了打包工作,最後寫了一個啟動命令。

總的來說DockerFile寫的還可以,至少思路是很清晰的,一步一步從基礎鏡像選擇到編譯環境,再把原始程式碼加進去,然後再到最後的構建,啟動命令寫好,可讀性、可維護性都可以,但是還是可以進行優化的。

我們可以減少鏡像的層數, Docker對於Docker鏡像的層數是有一定要求的,除掉最上面在容器運行時候的讀寫層以外,我們一個鏡像最多只能有127層,如果超過可能會出現問題,所以第二行命令LABLE就可以把它合成一層,減少了層數,下面六個RUN命令做了maven的安裝工作,我們也可以把它做成一層,把這些命令串起來,後面的構建我們也可以把它合成一層,這樣我們一下就把鏡像層數從14層減少到7層,減掉了一半。

我們在做鏡像優化的時候,我們希望能夠儘量減少鏡像的層數,但是和它相對應的是我們DockerFile的可讀性,我們需要在這兩者之間做折中,我們在保證可讀性不受很大影響的情況下去儘量減少它,其實六條RUN命令在做一件事,就是做maven環境打結,做編譯環境的準備工作。

接下來我們繼續對鏡像進行優化,我們可以做一些什麼工作呢?在安裝maven構建工具的時候我們多加了一行,我們把安裝包和展開目錄刪掉了,我們清理了構建的中間產物,我們要去注意每一個構建指令執行的時候,儘量把垃圾清理掉,我們通過apt-get去裝一些軟體的時候,我們也可以去做這樣的清理工作,就是把這些套裝軟體裝完之後就可以把它刪掉了,這樣可以儘量減少空間,通過增加一行命令,我們可以把鏡像的大小從137M削減到119M。

通過apt-get去裝軟體或者命令基本上是所有編寫DockerFile的人都去寫的,所以官方已經在debian、Ubuntu的倉庫鏡像裡面默認加了Hack,它會去幫助你在install自動去把原始程式碼刪掉。

我們可以利用構建的緩存,Docker構建默認會開啟緩存,緩存生效有三個關鍵點,鏡像父層沒有發生變化,構建指令不變,添加檔校驗和一致。只要一個構建指令滿足這三個條件,這一層鏡像構建就不會再執行,它會直接利用之前構建的結果,根據構建緩存特性我們可以加一行RUN,這裡是以JAVA應用為例,一般一個JAVA應用的pom檔都是描述JAVA的一些依賴,而在我們平常的開發過程中這些依賴包發生變化的頻率比較低,那麼我們就可以把POM加進來,把POM檔依賴全部都準備好,然後再去下原始程式碼,再去做構建工作,只要我們沒有把緩存關掉,我們每次構建的時候就不需要重新下安裝包,這樣可以節省大量時間,也可以節省一些網路流量。

現在阿裡雲的容器鏡像服務其實已經提供了構建功能,我們在統計用戶失敗案例的時候就會發現,網路原因導致的失敗占90%,比如如果用戶通過node開發NPM在安裝一些套裝軟體的時候經常卡在中間。所以我們建議加一個軟體源,我們把阿裡雲maven位址加到裡面去,我們把配置項加到阿裡雲的軟體位址,加阿裡雲的maven源作為套裝軟體的下載目標,時間直接少了40%,這樣對一個鏡像構建的成功率也是有幫助的。

多階段構建

DockerFile最終需要做到的產物其實是JAVA應用,我們對於構建、編譯、打包或者安裝這些事情都不關心,我們要的其實是最後的產物。所以,我們可以採取分步的方式去做鏡像構建,首先我們將之前遇到的所有問題全部都做成基礎鏡像,上面FROM鏡像其實已經改了新的,鏡像裡面已經把軟體源的位址改成了Maven,緩存都已經做好了。我們會去利用緩存,然後添加原始程式碼,我們把前面構建的事情做成了鏡像,讓鏡像去完成構建,然後我們才會去完成把JAVA包拷進去,啟動工作,但是兩個DockerFile其實是兩個鏡像,所以我們需要一段腳本去輔助它,第一行的shell腳本是做第一個構建指令,我們指定以Bulid的DockerFile去啟動構建,然後生成一個APP Bulid鏡像,接下來兩行腳本是把鏡像生成出來,把裡面的構建產物拷出來,然後我們再去做構建,最後把我們需要的JAVA應用給構建出來,這樣我們的DockerFile相比之前就更加清晰了,而且分步很簡單。

Docker在17.05之後官方支持了多階段構建,我們把下面的腳本去掉了,我們不需要一段輔助腳本,我們只需要在後面申明基礎鏡像的地方標記,我們第一階段的構建產物名字叫什麼,我們就可以在第二個構建階段裡面用第一個構建階段的產物。比如我們第一階段把JAVA應用構建好,把Maven包裡面的target下面的JAVA架包拷到新的鏡像裡面,然後在所有優化做完之後效果如圖,我們在第一次構建的時候,優化前102秒,在Docker構建優化後只花55秒就完成了,主要優化在網路上面。當我們修改了JAVA檔重新進行構建,第二次構建花了86秒,因為Maven安裝那一塊被緩存了,我們利用了構建緩存,所以少掉20多秒,優化後只花了8秒,因為所有的原始程式碼前面的一些套裝軟體下載全部被緩存了,我們直接拉新的鏡像,然後依賴沒有變,直接進行構建,所以8秒基本上是完整構建時間。

我們再來看一下存儲空間上面的優化,第一次構建我們在優化前把鏡像打出來有137M,但是在我們整個優化之後,只有81M了,這裡的基礎鏡像由JDK改成JRE,為什麼?因為之前我們把所有流程都放在一個鏡像裡面時,我們是需要去做構建的,構建時需要去RUN Maven,這種情況下沒有JDK環境是RUN不起來的,但是如果我們分階段,把構建交給Maven鏡像來做,把真正運行交給新的鏡像來做,就沒必要用JDK了,我們直接用JRE,優化之後鏡像少了將近50%。當我們修改原始程式碼重新進行構建的時候,由於鏡像成共用的原因,第二次構建在優化前其實多加了兩層到三層,一共有9M,但是優化後的第二次構建只增加1.93KB,這樣我們針對DockerFile的優化就已經做完了。

鏡像優化有哪些重要的點呢?具體如下:

減少鏡像的層數,儘量把一些功能上面統一的命令合到一起來做;注意清理鏡像構建的中間產物,比如一些安裝包在裝完之後就把它刪掉;注意優化網路請求,我們去用一些鏡像源,去用一些網路比較好的開源網站,這樣可以節約時間、減少失敗率;儘量去用構建緩存,我們儘量把一些不變的東西或者變的比較少的東西放在前面,因為不變的東西都是可以被緩存的;多階段進行鏡像構建,將我們鏡像製作的目的做一個明確,把我們的構建和真正的一些產物做分離,構建就用構建的鏡像去做,最終產物就打最終產物的鏡像。容器鏡像服務

最後介紹一下阿裡雲容器鏡像服務。這個服務已經公測一年了,現在我們的服務公測是全部免費的,現在在全球的12個Region都已經部署了我們的服務,每個Region其實都有內網服務和VPC網路服務,如果ECS也在同樣的Region,那麼它的服務是非常快的。然後團隊管理和組織帳號功能也已經上線了,鏡像購建和鏡像消息通知其實都是一些DevOps能力,針對一些鏡像優化我們提供了一些鏡像層資訊流覽功能,我們後續也會提供分析,推出鏡像安全掃描、鏡像同步。

同類文章
喜欢就按个赞吧!!!
点击关闭提示