Docker 鏡像優化與最佳實踐

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

如下是精彩內容整理:shell

鏡像概念


鏡像是什麼?從一個比較具體的角度去看,鏡像就是一個多層存儲的文件,相較於普通的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的包,在咱們肯定了一個鏡像服務域名以及在雲服務商申請命名空間以後,咱們就能夠對鏡像作一些操做了。app

首先咱們須要去登錄,咱們會用第一條命令去登錄,而後,當咱們在本地準備好一個鏡像想要上傳的時候,咱們先要對這個鏡像進行打標,把它的座標變成咱們如今須要上傳鏡像的座標,而後再去作一些推送拉取的動做,最後針對Docker還提供兩個額外命令去作鏡像交付,若是咱們是特殊的環境,沒有辦法網絡連通的時候,咱們能夠將這個鏡像打包成一個普通文件進行傳輸。好比咱們和公安合做,他們沒有辦法經過咱們的Registry下載鏡像,咱們可能要把它打成一個普通文件,而後以U盤的方式去交付。maven

鏡像存儲細節工具


Docker鏡像是存在聯合文件系統的,每個鏡像實際上是分層存儲的,好比在第一層咱們添加了三個新文件,而後在這一層基礎上咱們又增長了一層,添加了一個文件,第三層可能會須要作一些修改,咱們把File3作了一個修改移到上面來,而後刪掉了File4,這裏就會引到聯合文件系統裏面的寫時複製機制,當咱們要去修改一個文件的時候,鏡像依賴底層都是隻讀的,咱們不能去直接修改,好比咱們想去修改File3,咱們不能直接去修改這個文件,咱們須要在修改的時候把文件複製到當前這一層,好比說L3層,而後再去修改它。測試

一個鏡像作好以後,當咱們想要知道鏡像裏面有哪一些內容的時候,咱們其實會有一個視圖概念,咱們從聯合文件系統的角度去看鏡像的時候,其實咱們不會看到L一、L二、L3,咱們會最後看到結果,File一、File二、File3,File4就看不到了,而後在咱們瞭解原理以後,咱們就能夠去理解容器運行起來是一個什麼樣的狀況。容器運行起來和上面造成是相似的,圖中下半部分,一樣也是L一、L二、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的優化就已經作完了。

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

1. 減小鏡像的層數,儘可能把一些功能上面統一的命令合到一塊兒來作;

2. 注意清理鏡像構建的中間產物,好比一些安裝包在裝完以後就把它刪掉;

3. 注意優化網絡請求,咱們去用一些鏡像源,去用一些網絡比較好的開源站點,這樣能夠節約時間、減小失敗率;

4. 儘可能去用構建緩存,咱們儘可能把一些不變的東西或者變的比較少的東西放在前面,由於不變的東西都是能夠被緩存的;

5. 多階段進行鏡像構建,將咱們鏡像製做的目的作一個明確,把咱們的構建和真正的一些產物作分離,構建就用構建的鏡像去作,最終產物就打最終產物的鏡像。

容器鏡像服務

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