玩轉docker鏡像和鏡像構建

jopen 9年前發布 | 57K 次閱讀 Docker
 

摘要

本次分享從個人的角度,講述對于docker鏡像和鏡像構建的一些實踐經驗。主要內容包括利用docker hub進行在線編譯,下載鏡像,dind的實踐,對于鏡像的一些思考等。

前言

本次分享主要是從個人實踐的角度,講述本人對于docker鏡像的一些玩法和體會。本文中大部分的內容都還處于實驗的階段,未經過大規模生產的實踐。特此說明。思慮不全或者偏頗之處,還請大家指正。

鏡像應該算是docker的核心價值之一。鏡像由多層組成。那么對于一個層來說,就有了兩個角度來看待。

一個角度是把這層當做一個獨立的單位來看,那么這一個層其實主要是包含了文件和配置兩個部分。

另一個角度則是把這一層和它的所有父層結合起來看,那么這個整體則是代表了一個完整的鏡像。

本文所述的docker鏡像,主要是指的從dockerfile構建出來的鏡像。

現在已經有了docker hub/daocloud等多家公有容器服務供應商,為我們提供了非常便捷的鏡像構建服務。我們不再需要在本地運行docker build而是可以借用他們的服務實現方便的鏡像構建。下文中以docker hub為例,介紹一些非常規的用法。各位在實踐中可以使用國內的多家容器服務提供商,如daocloud等。

docker hub之在線編譯

眾所周知,docker鏡像可以用來描述一個App的runtime。比如我們構建一個tomcat的鏡像,鏡像里包含了運行tomcat的環境以及依賴。但是我們再細看,其實docker鏡像不僅僅是一個runtime,而是提供了一個環境,一個軟件棧。從這個角度上來說,鏡像不僅僅可以用來提供app進行運行,還可以提供諸如編譯的環境。

用docker來進行編譯,這個應該來說不是什么新奇玩法。因為docker源碼的編譯就是通過這種方式來獲得的。docker有對應的 Dockerfile 。可以利用這個來完成代碼的編譯。

這里我舉個例子。這里有一個寫的Dockerfile。test.c是一個輸出hello world的c語言源文件。

FROM centos:centos6

RUN yum install -y gcc

ADD test.c /

RUN gcc /test.c

構建這個鏡像,由于最后一步是編譯命令gcc /test.c,所以編譯過程會在docker hub上進行執行。

我們可以通過編寫Dockerfile,使得整個編譯過程都托管在docker hub上。如果我們提交了新的代碼,需要重新編譯,那么只需要重新構建鏡像即可。

鏡像下載

在v1版本中,docker client是串行下載鏡像的各層。對于docker pull的過程進行分析,可以看到docker client總共有這樣幾個步驟:

  • /v1/repositories/{repository}/tags/{tag} 獲取tag的id,
  • /v1/images/{tag_id}/ancestry 獲取tag的各層的id
  • /v1/images/{layer_id}/json 依次獲取各層對應的配置文件json
  • /v1/images/{layer_id}/layer 依次獲取各層對應的鏡像數據layer

docker hub的鏡像數據,并不是在自己的服務器中存儲,而是使用的亞馬遜的s3服務。因此在調用/v1/images/{layer_id}/layer接口,拉取鏡像的layer數據時,會返回302,將請求重定向到亞馬遜的s3服務上進行下載。

為了方便下載,我自己寫了個小程序,使用http協議即可完全模擬docker client的整個過程。自己寫的好處在于你可以依次獲取tag的id,各層的id,以及所有層的配置,進而一次性將所有層對應的鏡像數據存儲在亞馬遜的 s3地址獲取到,然后可以進行并行下載。如果單層下載失敗,只需要重新下載這一層即可。當所有的層在本地下載完畢后。然后打成tar包,再使用 docker client進行load即可。

對于上文中所說的在線編譯,那么我們其實只關心編譯出來的相關文件。如剛剛的舉例,我們其實只需要獲取鏡像的最后一層就可以了。那么使用自己寫的工具,可以僅僅把最后一層下載下來。下載下來的tar包進行解包,就可以直接獲取出編譯結果,即編譯過程生成的相關文件了。docker hub就成為了我們的一個強大的在線編譯器。

注:這里說的鏡像下載過程是針對的registry v1版本。docker hub在不久之后即將全面結束v1的服務。目前國內的幾家容器服務提供商還可以支持v1。該方法同樣有效。v2的協議和代碼我還沒學習,后面研究之后再同大家分享。

鏡像層合并

鏡像層合并這個話題一直是一個有爭議的話題。過長的Dockefile會導致一個冗長的鏡像層數。而因為鏡像層數過多(比如十幾層,幾十層),可能會帶來的性能和穩定性上的擔憂也不無道理,但是似乎docker社區一直不認為這是一個重要的問題。所以基本上對于鏡像層合并的PR最后都被拒了。但是這不影響我們在這里討論他的實現。

我為Dockerfile增加了兩個指令。TAG和COMPRESS。

TAG功能類似于docker build -t 的參數。不過build -t只能給Dockerfile中的最后一層鏡像打上tag。新增加的TAG指令可以在build生成的中間層也用標簽記錄下來。比如

FROM centos:centos6

RUN yum install -y sshd

TAG sshd:latest

ADD test /

CMD /bin/bash

這個TAG功能相當于使用下面的Dockerfile生成了這樣的一個鏡像,并打上了sshd:latest的標簽。

FROM centos:centos6 RUN yum install -y sshd 

COMPRESS功能實現了一個鏡像多層合并的功能。比如下面這個Dockerfile:

FROM centos:centos6

RUN yum install -y sshd

ADD test /

CMD /bin/bash

COMPRESS centos:centos6

我們知道這里假設RUN yum install -y sshd,ADD test /, CMD /bin/bash生成的鏡像層為a,b,c。那么COMPRESS的功能目標就是將新增的a,b,c的文件和配置合并為一個新的層d,并設置層d的父親為鏡像centos:centos6。層d的配置文件可以直接使用層c的配置文件。合并的難點在于如何計算層d的文件。

這里有兩種做法,一種是把層a,b,c中的文件按照合并的規則合并起來。合并的規則包括子層和父層共有的文件則使用子層的,沒有交叉的文件則全部做為新添加的。這種方法效率較低,在需要合并的層數過多的時候,會極為耗時。

另外一種思路則較為簡單,不需要考慮中間總共有多少層。直接比較centos:centos6鏡像和c鏡像(c鏡像是指由c和其所有父層組成的鏡像),將兩者的所有文件做比較,兩者的diff結果即為新層d。

最終,我采用了后者作為COMPRESS的實現。鏡像的合并縮減了層數,但是弊端在于將生成鏡像的Dockerfile信息也消除了(使用Dockerfile生成的鏡像,可以通過docker history進行回溯)。

dind

dind(docker in docker),顧名思義就是在容器里面啟動一個docker daemon。然后使用后者再啟動容器。dind是一種比較高級的玩法,從另一個角度來說也是一種有一定風險的玩法。dind巧妙的利用了docker的嵌套的能力,但是令人頗為擔心的是底層graph driver在嵌套后的性能和穩定性。所以dind我并不推薦作為容器的運行環境來使用(rancherOS其實是使用了這種方式的),但是使用其作為構建鏡像的環境,可以進行實踐。畢竟構建失敗的后果沒有運行時崩潰的后果那么嚴重。

之所以會用到dind,是因為如果用于鏡像構建,那么直接使用多個物理機,未免比較浪費。因為構建并不是隨時都會發生的。而使用dind的方式,只需在需要的時候申請多個容器,然后再在其上進行構建操作。在不需要時候就可以及時釋放容器資源,更加靈活。

制作dind的鏡像需要一個centos的鏡像(其他暫未實踐過,fedora/ubuntu也都可以做),和一個wrapdocker的文件。wrapdocker的主要作用是容器啟動后為docker daemon運行時準備所需的環境。

因為容器啟動后,docker還需要一些環境才能啟動daemon。比如在centos下,需要wrapdocker把cgroup等準備好。使用centos的鏡像創建一個容器后,安裝docker等docker需要的組件后,然后把wrapdocker ADD進去。并把wrapdocker添加為ENTRYPOINT或者CMD。然后將容器commit成為鏡像,就獲得了一個dind的鏡像。使用 dind的鏡像時需要使用privileged賦予權限,就可以使用了。

熟悉docker源碼的同學應該知道,dind其實并不陌生。在docker項目里,就有這樣一個dind的 文件 。這個dind文件其實就是一個wrapdocker文件。在docker進行集成測試時,需要使用該文件,協助準備環境以便在容器內部啟動一個daemon來完成集成測試。

如果對于dind有興趣,可以參考 jpetazzo 中的Dockerfile和wrapdocker,構建自己的dind鏡像。

dind中docker的使用跟普通docker一樣。不再贅述。

關于鏡像的思考

docker鏡像由若干層組成。而其中的每一層是由文件和配置組成的。如果把層與層之間的父子關系,看做一種時間上的先后關系,那么 docker鏡像其實與git十分的相像。那么從理論上來說,git的若干功能,比如merge,reset,rebase功能其實我們都可以在 docker的構建過程中予以實現。比如上文中的COMPRESS功能,就類似于git的merge。理論上,docker鏡像其實也可以擁有git般強大的功能。從這點上來說,docker鏡像的靈活性就遠高于kvm之類的鏡像。

在這里,不得不抱怨幾句。docker的維護者們對于dockerfile或者說docker的構建過程并沒有給予非常積極的態度,予以改善。當然這也可能是由于他們的更多的關注點集中在了runc、libnetwork、orchestration上。所以沒有更多的人力來完善docker構建的工具,而是寄希望于社區能自己增加其他的tool來豐富docker的構建過程。

所以很多時候,docker build的功能并不盡如人意。比如一直呼聲很高的docker鏡像壓縮功能,幾經討論,終于無果而終。又比如在build過程中,使用--net參數來使得可以控制build過程中容器使用的網絡。該討論從今年的一月份開始討論,至今仍未定論結貼。大家可以去強勢圍觀。地址在這里:( https://github.com/docker/docker/issues/1032 4)

這里特別說一下,在centos6下,dind不能使用網橋(centos7可以支持),所以在centos6下使用dind,進行docker build,需要指定網絡--net=host的方式。

所以很多功能并不能等待docker自己去完善,只好自己動手開發。其實熟悉了docker源碼后,關于docker build這方面的開發難度并不是很大。可以自己去實現。讀一下孫宏亮同學的《docker源碼分析》,會很快上手。

Q&A

Q1: 京東私有云是基于openstack+docker嗎,網絡和存儲的解決方案是什么?

是的。私有云網絡使用的是VLAN。并沒有使用租戶隔離,主要保證效率。存儲使用的是京東自己的存儲。

Q2: 那個鏡像壓縮,有什么好處?

鏡像壓縮或者說合并,主要是減少層數,減少擔憂。其實目前看,好處并不明顯。因為層數過多帶來的更多的是擔憂,但沒有確鑿證據表明會影響穩定。

Q3: 在線編譯應用廣泛嗎?我們一般可能更關注最后的結果。有很多代碼都是,先在本地編譯,成功后,再發布到鏡像中的。

這個玩法應該說并不廣泛。主要是我自己玩的時候,不想自己去拉鏡像的全部層,只關注編譯結果。所以這樣玩

Q4: 對于docker鏡像的存儲 京東是使用什么方式實現的 分布式文件系統京東docker上有使用嗎 能否介紹下

鏡像存儲使用的是官方的registry。v1版本。registry后端是京東自研的JFS存儲。

Q5: 你之前提到了“鏡像的合并縮減了層數,但是弊端在于將生成鏡像的Dockerfile信息也消除了(使用Dockerfile生成的鏡像,可以通過 docker history進行回溯)。”。那如果使用了compress之后,應該如何進行回溯?還是說需要舍棄這部分功能?

是的,確實沒辦法回溯了。所以要舍棄了。不過反過來想,其實如果Dockerfile的ADD和COPY之類的功能,就算能回溯,其實意義也不大。所以我認為保存dockerfile更有意義。

Q6: 為什么不采用將要執行的命令做成腳本,直接add進去執行這種,也能減少層數

這種方法也是可行的。只是Dockerfile更顯式一些。同理,其實只要你做好鏡像,直接export出去,就可以得到所有文件了。再配上配置文件。這樣整個就只有一層了。

Q7: 我平時在,測試的時候并沒-有壓縮過,也不知道,壓縮會帶來什么風險,但是,看你剛才說有可能會帶來一定的風險。 你們遇到過么?

因為我們的鏡像都做過合并層,所以層數并不多。不合并會帶來什么風險,其實更多的是出于性能和穩定性上的擔憂。這種擔憂可能是多余的。但是我們寧愿選擇謹慎一些。

Q8: 鏡像的合并方面 怎么樣能方便的減小鏡像的大小 我做的鏡像有些都在1G以上

減少鏡像大小主要還是靠去除不必要的文件。合并只能減少冗余文件,如果每層的文件都不相同,合并并不會縮小鏡像的大小。

Q9: 網絡這個使用vlan能說詳細一些嗎,是每個容器都有一個和宿主機同網段的真實的物理ip嗎

是的。每個容器都有一個真實的ip。跟宿主機網段不同。是單獨的容器網絡。這個可以參考neutron中的vlan實現。

Q10: 還有,把鏡像壓縮我也覺,但是像你那樣把父鏡像整個合并成新鏡像這點我覺得有點問題,畢竟大家玩容器時都是在基礎鏡像上添加東西,你把常用的鏡像為了壓縮生成一個一次性的鏡像,以后再使用基礎鏡像做其他業務時那不還得重新下載基礎鏡像?

鏡像合并其實主要還是為了獲得一個基礎鏡像。然后大家在基礎鏡像上添加東西。基礎鏡像相對來說,不會輕易改變。

Q11: 在你們的實踐中,大規模部署容器時,每個節點都會從registry節點下載鏡像,給網絡帶來的壓力大嗎?

我們做了一些優化。首先,大部分業務使用的鏡像會提前推送到每個docker節點上。即使節點沒有,registry后端接的是京東的JFS,通過優化,臨時去下載的時候可以直接從JFS去拿鏡像數據。所以網絡壓力并不大。

Q12: 鏡像壓縮或者合并之后,鏡像的層次減少了,但每層鏡像不是變大了嗎,這對于發布不是會占用帶寬降低效率嗎?

這個問題跟上個差不多。合并主要是為基礎鏡像使用的。

Q13: 你們怎么看待openstack和docker的關系?在京東未來會長期兩個并存嗎?現在兩個架構的發展速度和研發力量對比如何?

openstack和docker并不矛盾。私有云采用nova docker的結合更多的是迎合用戶習于使用VM的習慣。magnum也在快速發展中。所以我相信二者都有存在的價值和發展的必要。

Q14: 關于dockfile的優化,你們有沒有什么好的建議或者經驗?

似乎也沒多少新的建議。參考dockerone的相關文章。 Dockerfile之優化經驗淺談大家在寫 dockerfile 時有啥最佳實踐?希望得到大家的建議。

Q15: 比如創建一個rabbitmq鏡像,需要安裝很多依賴包,最后編譯,最后生成的鏡像1.3G,像這種情況,在創建鏡像的時候能否減少鏡像的大小呢?多謝

并沒有什么好的辦法來減少。可能需要一定的人工或者工具去分析不需要的文件,來減少鏡像的大小。

Q16: docker是如何進行自動更新的?自己搭建的鏡像倉庫,如何更新新版本的鏡像?

docker我們固定了一個版本。如果沒出大面積的嚴重問題,幾乎不會更新。目前來看,運行穩定。所以也沒有更新必要。新版本的docker提供的如網絡等,暫時我們還不會大面積跟進使用。自己的鏡像倉庫,如果要更新新版本鏡像,push進去就可以了

Q17: 一個困擾我比較久的問題,如果鏡像間存在依賴關系,基礎鏡像發生改變后其他鏡像你們是跟著更新的呢?

在內部私有云中,一般大家使用的都是一個做好的base鏡像。這里面就有一個問題,一旦這個base鏡像需要打補丁,影響面比較大。首先很多 base的子鏡像會受到影響。另一方面,就是要考慮已經在使用基于base或者base子鏡像的節點。前者我的方案是直接在base鏡像中的layer,把需要打補丁的文件加入進去,重新打包放回。對于后者,目前還沒想到很好的方法解決。

Q18: 在運行容器的時候,1、應用里面的日志或者配置文件,使用本地映射是不是好點,我是考慮到方便查看日志或者修改配置;2、創建的數據庫鏡像,在運行容器的時候把數據文件是不是映射到本地更好些呢?多謝

日志我們的確是使用的本地映射。而且有的業務方狂寫日志不加約束。所以我們給本地映射做了個LVM,掛給容器。做了容量上的限制。配置的話,現在是有一個內部的部署系統會幫他們部署配置。數據庫的話是一個道理,也是映射到本地。也有一部分接入了云硬盤。

Q19: docker中,每層鏡像打標簽那我覺的很奇怪,當pull一個鏡像或生成一個容器時,它如何找到你所命名的鏡像層?

并不是給每層都打標簽,而是你根據你的需要來給某一層打標簽。至于標簽內容,需要自己來進行控制

Q20: 關于compress的實現有些疑問,是不是在實現的過程中,只考慮最后的鏡像和前一層的diff?還是說要逐層做diff?

是只考慮最后的鏡像和你要合并到的父層鏡像做diff。這樣只要做一次diff,就可以獲得中間的所有文件變化了。

Q21: wrapdocker文件的工作原理是什么?

這個工作原理主要是準備一些docker啟動必要的環境。比如在centos下,需要wrapdocker把cgroup等準備好等。你可以參考下wrapdocker里面的代碼。

Q22: 容器運行在物理機上,與openstack 平臺虛擬機是同一套管理系統? 如何與容器的集群系統整合?

是同一套系統,都是用nova。虛擬機KVM和容器主要是鏡像類型不同。在nova調度的時候,會根據鏡像類型調度到kvm或者docker節點進行創建。

Q23: 在一臺物理機上運行docker的數量是否有限定 還是看運行的應用來決定?

沒有特別做限定。主要還是業務方去申請的。業務方習慣用大內存,多cpu的。那這個物理機上創建的容器數就少些。大致這樣。

Q24: 想了解一下,你們對鏡像的tag是怎么管理的?根據什么來打的?對于舊的鏡像你們是丟棄還是像git保存代碼一樣一直保留在倉庫呢?

tag由各個用戶來定。不同的用戶在不同的repository里。鏡像tag自己管理。不過我們更希望他們能夠更加規范一些,比如用git的版本號來打tag。

舊的鏡像如果失去了tag(新的鏡像搶奪了該tag),則舊鏡像會被刪除。不過不是立即,也是定期清理,主要減少存儲量。因為畢竟不需要存儲那么多的版本。

===========================

分享人徐新坤,京東商城云平臺南京研發中心JDOS團隊研發工程師,從2014年初開始從事Docker的研發,主要負責docker在京東落地的相關開發和維護工作。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!