Docker commit 的來龍去脈
作者: 張首富
blog:zhangshoufu.com
相信無需再強調 Docker 鏡像,大家都已經清楚 Docker 除了傳統的 Linux 容器技術之外,還有另辟蹊徑的鏡像技術。鏡像技術的采用,使得 Docker 自底向上打包一個完整的應用,將更多的精力專注於應用本身;而容器技術的延用,則更是在應用的基礎上,囊括了應用對資源的需求,通過容器技術完成資源的隔離與管理。
Docker 鏡像與 Docker 容器相輔相成,共同作為技術基礎支撐着 Docker, 為 Docker 的生態帶來巨大的凝聚力。然而,這兩項技術並非相互孤立,兩者之間的互相轉換使得 Docker 的使用變得尤為方便。說到,Docker 鏡像與 Docker 容器之間的轉化,自然需要從兩個角度來看待:從 Docker 鏡像轉化為 Docker 容器一般是通過 docker run 命令,而從 Docker 容器轉化為 Docker 鏡像,則完全依靠 docker commit 的實現。
docker commit 的作用
是否還記得 docker build 的實現?Dockerfile 唯一的定義了一個鏡像,docker build 對於 Dockerfile 中的每一條命令都會創建一個單獨的鏡像(包含鏡像層內容和鏡像 json 文件),最終產生一個含有標簽(tag)的鏡像,而之前與 Dockerfile 命令對應的鏡像均是這個含有 tag 的鏡像的祖先鏡像。如以下 Dockerfile:
FROM ubuntu:14.04
RUN apt-get update ADD run.sh /
VOLUME /data
CMD ["./run.sh"]
docker build 實現 Dockerfile 到 Docker 鏡像的構建,而對於單條 Dockerfile 中的命令(如命令RUN apt-get update ),則是通過針對 Docker 容器的 commit 操作,實現將其構建為單層鏡像,也就是大家熟悉的 docker commit 操作。簡單的示意圖如下:
docker commit 的原理
深入學習docker commit 的原理前,我不妨先來看看一下 docker help 中關於 commit 命令的闡述:
commit Create a new image from a container's changes
結合上圖與命令docker commit 的描述,我們可以發現有三個關鍵字Image、Container 與Changes 。如何理解這三個關鍵字,我們可以從以下三個步驟入手:
-
Docker Daemon 會通過一個 Docker 鏡像運行一個 Docker 容器,Docker 通過層級文件系統為 Docker 容器提供文件系統視角,最上層的是可讀可寫層(Read-Write Layer)。
-
Docker 容器初始的可讀可寫層內容均為空,Docker 容器對文件系統的內容更新將全部更新於可讀可寫層(Read-Write Layer)。
-
實現 docker commit 操作時,Docker 僅僅是將可讀可寫層(Read-Write Layer)中的更新內容,打包為一個全新的鏡像。
具體的 docker commit 示意圖如下:
觀察上圖,我們可以發現:對於 commit 命令,幾乎所有的操作都圍繞着可讀可寫層(Read-Write Layer),一次 commit 將可讀可寫層打包為一個全新的鏡像,同時也保證鏡像之間的獨立性。當然,由於一個鏡像同時包含鏡像層文件系統內容和鏡像 json 文件,因此對於一個 commit 操作,Docker Daemon 還會為鏡像產生一個全新的 json 文件。
結合上圖,我們也看到 docker commit 命令也會有一些注意事項,最為重要的是:docker commit 命令僅僅打包可讀寫寫層內容 。對於 Docker 容器而言,文件系統視角包含的內容有 Docker 鏡像構成的內容(一個可讀可寫層加上多個只讀層)、數據卷 VOLUME 掛載的目錄內容,還有類似於 hosts、hostsname 和resolv.conf 等掛載文件,當然還有一些如 /proc 和 /sys 等虛擬文件系統的內容。commit 操作只專注於可讀可寫層(Read-Write Layer),因此其他的內容都將不會出現在打包后的鏡像中。舉例說明,類似於 MySQL 的數據容器,由於其自身的數據一般都持久化到數據卷 VOLUME 中,因此 MySQL在運行過程中產生的數據將不會在 commit 操作后被打包進入鏡像。
從 docker commit 看 build 注意事項
命令docker commit 可以實現將一個運行中容器的可讀寫層,打包為一個鏡像。對於 Dockerfile 中的 RUN 命令,Docker的機制同樣如此,主要的步驟如下:
1.以一個容器的形式運行 RUN 關鍵字后緊跟的命令,假設為 command;
2.運行命令 command 時,容器對文件系統的所有更新都將體現在容器的可讀寫層上;
3.Docker 守護進程等待並判斷命令 command 運行后的返回狀態;
4.若 command 返回狀態為0,則表示命令運行完成,Docker 守護進程打包可讀寫層,創建並提交新鏡像;
5.若 command 返回狀態非0,則表示命令未正常運行,構建流程失敗,留下構建失敗現場;
6.若 command 將無休止運行,則此次構建操作也將永遠進行,同樣不是一個成功的構建。
以上步驟完整的展現了 Dockerfile 中 RUN 命令的操作流程,說到注意事項,大家有必要將目光聚焦於兩個關鍵詞「容器」與「返回狀態」 。一旦缺乏對這兩個關鍵詞的認識,很容易在鏡像構建過程中遇到棘手的問題。
首先,從容器的角度談注意事項。談到容器,大家第一印象自然是docker run 運行的容器,很難想到docker build 的世界中也會存在容器的概念。既然如此,前者的容器應該受到資源的限制,大家應該不難理解,本身docker run 命令有諸多的參數來限制容器資源。那么,大家是否應該想到,后者docker build 構建流程中的容器同樣應該受到相應的資源限制呢?一旦docker build 過程中容器運行的命令將占用大量的資源,隔離效果不佳的情況下,很難保證宿主機上其他任務的正常運行。因此,這一點不得不防。在早版本的 Docker 中,docker build 命令不支持任何的資源限制,自然很難達到生產化的要求。隨着 Docker 的發展,新版本中逐漸加入了對部分資源的限制,改善了隔離的現狀,但是依然沒有達到docker run 的隔離程度,另外除了資源之外,docker build 還缺少對容器權限以及能力的管理,如 Linux Capabilities。
再者,我們從返回狀態的角度談注意事項。返回狀態包括三種情況,0、非 0、無返回狀態。返回狀態為 0 的情況,皆大歡喜;返回非 0,那就是命令的運行出現了異常。異常之下,尤其需要注意失敗現場。當前命令的構建失敗意味着整個 docker build 命令的運行失敗,當前命令的構建失敗將導致當前的鏡像層沒有成功構建。然而對於已經成功構建的鏡像層,Docker 守護進程並不會對其做任何的處理。在主機管理員沒有意識的情況下,如果出現大量的構建失敗,很有可能導致主機磁盤的無辜消耗,最終殆盡。無返回狀態,同樣是一件令人頭疼的事。命令較長時間處於運行中,我們也無法確定程序是否進入死循環,帶來的不確定性既會占用資源,同樣也給運維帶來極大的不便。
總結心得:
以前雖然也了解一點commit的過程是不會給mount進去的數據打進去,這就是有時候我們改完文件之后commit進去之后再啟動發現代碼沒有改變.因為我們改的東西掛載出來了
對於docker build的時候以前只知道只推送到docker中心倉庫,不知道到中心倉庫里面執行的是commit動作