Docker概念理解
本文非Docker命令大全,而是對Docker的概念、原理等作說明,適合有一定實操經驗后來加深理解。
轉自:docker從入門到實踐
Docker簡介
本章將帶領你進入 Docker 的世界。
什么是 Docker?
用它會帶來什么樣的好處?
好吧,讓我們帶著問題開始這神奇之旅。
什么是Docker
Docker 最初是 dotCloud
公司創始人 Solomon Hykes 在法國期間發起的一個公司內部項目,它是基于 dotCloud
公司多年云服務技術的一次革新,并于 2013 年 3 月以 Apache 2.0 授權協議開源,主要項目代碼在 GitHub 上進行維護。Docker
項目后來還加入了 Linux 基金會,并成立推動 開放容器聯盟(OCI)。
Docker 自開源后受到廣泛的關注和討論,至今其 GitHub 項目 已經超過 5 萬 7 千個星標和一萬多個 fork
。甚至由于 Docker
項目的火爆,在 2013
年底,dotCloud 公司決定改名為 Docker。Docker
最初是在 Ubuntu 12.04
上開發實現的;Red Hat
則從 RHEL 6.5
開始對 Docker
進行支持;Google
也在其 PaaS
產品中廣泛應用 Docker
。
Docker 使用 Google
公司推出的 Go 語言 進行開發實現,基于 Linux
內核的 cgroup,namespace,以及 OverlayFS 類的 Union FS 等技術,對進程進行封裝隔離,屬于 操作系統層面的虛擬化技術。由于隔離的進程獨立于宿主和其它的隔離的進程,因此也稱其為容器。最初實現是基于 LXC,從 0.7
版本以后開始去除 LXC
,轉而使用自行開發的 libcontainer,從 1.11
版本開始,則進一步演進為使用 runC 和 containerd。
runc
是一個 Linux 命令行工具,用于根據 OCI容器運行時規范 創建和運行容器。
containerd
是一個守護程序,它管理容器生命周期,提供了在一個節點上執行容器和管理鏡像的最小功能集。
Docker 在容器的基礎上,進行了進一步的封裝,從文件系統、網絡互聯到進程隔離等等,極大的簡化了容器的創建和維護。使得 Docker
技術比虛擬機技術更為輕便、快捷。
下面的圖片比較了 Docker 和傳統虛擬化方式的不同之處。傳統虛擬機技術是虛擬出一套硬件后,在其上運行一個完整操作系統,在該系統上再運行所需應用進程;而容器內的應用進程直接運行于宿主的內核,容器內沒有自己的內核,而且也沒有進行硬件虛擬。因此容器要比傳統虛擬機更為輕便。
為什么要用Docker
作為一種新興的虛擬化方式,Docker
跟傳統的虛擬化方式相比具有眾多的優勢。
更高效的利用系統資源
由于容器不需要進行硬件虛擬以及運行完整操作系統等額外開銷,Docker
對系統資源的利用率更高。無論是應用執行速度、內存損耗或者文件存儲速度,都要比傳統虛擬機技術更高效。因此,相比虛擬機技術,一個相同配置的主機,往往可以運行更多數量的應用。
更快速的啟動時間
傳統的虛擬機技術啟動應用服務往往需要數分鐘,而 Docker
容器應用,由于直接運行于宿主內核,無需啟動完整的操作系統,因此可以做到秒級、甚至毫秒級的啟動時間。大大的節約了開發、測試、部署的時間。
一致的運行環境
開發過程中一個常見的問題是環境一致性問題。由于開發環境、測試環境、生產環境不一致,導致有些 bug 并未在開發過程中被發現。而 Docker
的鏡像提供了除內核外完整的運行時環境,確保了應用運行環境一致性,從而不會再出現 「這段代碼在我機器上沒問題啊」 這類問題。
持續交付和部署
對開發和運維(DevOps)人員來說,最希望的就是一次創建或配置,可以在任意地方正常運行。
使用 Docker
可以通過定制應用鏡像來實現持續集成、持續交付、部署。開發人員可以通過 Dockerfile 來進行鏡像構建,并結合 持續集成(Continuous Integration) 系統進行集成測試,而運維人員則可以直接在生產環境中快速部署該鏡像,甚至結合 持續部署(Continuous Delivery/Deployment) 系統進行自動部署。
而且使用 Dockerfile
使鏡像構建透明化,不僅僅開發團隊可以理解應用運行環境,也方便運維團隊理解應用運行所需條件,幫助更好的生產環境中部署該鏡像。
更輕松的遷移
由于 Docker
確保了執行環境的一致性,使得應用的遷移更加容易。Docker
可以在很多平臺上運行,無論是物理機、虛擬機、公有云、私有云,甚至是筆記本,其運行結果是一致的。因此用戶可以很輕易的將在一個平臺上運行的應用,遷移到另一個平臺上,而不用擔心運行環境的變化導致應用無法正常運行的情況。
更輕松的維護和擴展
Docker
使用的分層存儲以及鏡像的技術,使得應用重復部分的復用更為容易,也使得應用的維護更新更加簡單,基于基礎鏡像進一步擴展鏡像也變得非常簡單。此外,Docker
團隊同各個開源項目團隊一起維護了一大批高質量的 官方鏡像,既可以直接在生產環境使用,又可以作為基礎進一步定制,大大的降低了應用服務的鏡像制作成本。
對比傳統虛擬機總結
特性 | 容器 | 虛擬機 |
---|---|---|
啟動 | 秒級 | 分鐘級 |
硬盤使用 | 一般為MB | 一般為GB |
性能 | 接近原生 | 弱于 |
系統支持量 | 單機支持上千個容器 | 一般幾十個 |
基本概念
Docker 包括三個基本概念
- 鏡像(
Image
) - 容器(
Container
) - 倉庫(
Repository
)
理解了這三個概念,就理解了 Docker 的整個生命周期。
鏡像
我們都知道,操作系統分為 內核 和 用戶空間。對于 Linux
而言,內核啟動后,會掛載 root
文件系統為其提供用戶空間支持。而 Docker 鏡像(Image
),就相當于是一個 root
文件系統。比如官方鏡像 ubuntu:18.04
就包含了完整的一套 Ubuntu 18.04 最小系統的 root
文件系統。
Docker 鏡像 是一個特殊的文件系統,除了提供容器運行時所需的程序、庫、資源、配置等文件外,還包含了一些為運行時準備的一些配置參數(如匿名卷、環境變量、用戶等)。鏡像 不包含 任何動態數據,其內容在構建之后也不會被改變。
分層存儲
因為鏡像包含操作系統完整的 root
文件系統,其體積往往是龐大的,因此在 Docker 設計時,就充分利用 Union FS 的技術,將其設計為分層存儲的架構。所以嚴格來說,鏡像并非是像一個 ISO
那樣的打包文件,鏡像只是一個虛擬的概念,其實際體現并非由一個文件組成,而是由一組文件系統組成,或者說,由多層文件系統聯合組成。
鏡像構建時,會一層層構建,前一層是后一層的基礎。每一層構建完就不會再發生改變,后一層上的任何改變只發生在自己這一層。比如,刪除前一層文件的操作,實際不是真的刪除前一層的文件,而是僅在當前層標記為該文件已刪除。在最終容器運行的時候,雖然不會看到這個文件,但是實際上該文件會一直跟隨鏡像。因此,在構建鏡像的時候,需要額外小心,每一層盡量只包含該層需要添加的東西,任何額外的東西應該在該層構建結束前清理掉。
分層存儲的特征還使得鏡像的復用、定制變的更為容易。甚至可以用之前構建好的鏡像作為基礎層,然后進一步添加新的層,以定制自己所需的內容,構建新的鏡像。
關于鏡像構建,將會在后續相關章節中做進一步的講解。
容器
鏡像(Image
)和容器(Container
)的關系,就像是面向對象程序設計中的 類
和 實例
一樣,鏡像是靜態的定義,容器是鏡像運行時的實體。容器可以被創建、啟動、停止、刪除、暫停等。
容器的實質是進程,但與直接在宿主執行的進程不同,容器進程運行于屬于自己的獨立的 命名空間。因此容器可以擁有自己的 root
文件系統、自己的網絡配置、自己的進程空間,甚至自己的用戶 ID 空間。容器內的進程是運行在一個隔離的環境里,使用起來,就好像是在一個獨立于宿主的系統下操作一樣。這種特性使得容器封裝的應用比直接在宿主運行更加安全。也因為這種隔離的特性,很多人初學 Docker 時常常會混淆容器和虛擬機。
前面講過鏡像使用的是分層存儲,容器也是如此。每一個容器運行時,是以鏡像為基礎層,在其上創建一個當前容器的存儲層,我們可以稱這個為容器運行時讀寫而準備的存儲層為 容器存儲層。
容器存儲層的生存周期和容器一樣,容器消亡時,容器存儲層也隨之消亡。因此,任何保存于容器存儲層的信息都會隨容器刪除而丟失。
按照 Docker 最佳實踐的要求,容器不應該向其存儲層內寫入任何數據,容器存儲層要保持無狀態化。所有的文件寫入操作,都應該使用 數據卷(Volume)、或者 綁定宿主目錄,在這些位置的讀寫會跳過容器存儲層,直接對宿主(或網絡存儲)發生讀寫,其性能和穩定性更高。
數據卷的生存周期獨立于容器,容器消亡,數據卷不會消亡。因此,使用數據卷后,容器刪除或者重新運行之后,數據卻不會丟失。
倉庫
鏡像構建完成后,可以很容易的在當前宿主機上運行,但是,如果需要在其它服務器上使用這個鏡像,我們就需要一個集中的存儲、分發鏡像的服務,Docker Registry 就是這樣的服務。
一個 Docker Registry 中可以包含多個 倉庫(Repository
);每個倉庫可以包含多個 標簽(Tag
);每個標簽對應一個鏡像。
通常,一個倉庫會包含同一個軟件不同版本的鏡像,而標簽就常用于對應該軟件的各個版本。我們可以通過 <倉庫名>:<標簽>
的格式來指定具體是這個軟件哪個版本的鏡像。如果不給出標簽,將以 latest
作為默認標簽。
以 Ubuntu 鏡像 為例,ubuntu
是倉庫的名字,其內包含有不同的版本標簽,如,16.04
, 18.04
。我們可以通過 ubuntu:16.04
,或者 ubuntu:18.04
來具體指定所需哪個版本的鏡像。如果忽略了標簽,比如 ubuntu
,那將視為 ubuntu:latest
。
倉庫名經常以 兩段式路徑 形式出現,比如 jwilder/nginx-proxy
,前者往往意味著 Docker Registry 多用戶環境下的用戶名,后者則往往是對應的軟件名。但這并非絕對,取決于所使用的具體 Docker Registry 的軟件或服務。
Docker Registry 公開服務
Docker Registry 公開服務是開放給用戶使用、允許用戶管理鏡像的 Registry 服務。一般這類公開服務允許用戶免費上傳、下載公開的鏡像,并可能提供收費服務供用戶管理私有鏡像。
最常使用的 Registry 公開服務是官方的 Docker Hub,這也是默認的 Registry,并擁有大量的高質量的 官方鏡像。除此以外,還有 Red Hat 的 Quay.io;Google 的 Google Container Registry,Kubernetes 的鏡像使用的就是這個服務;代碼托管平臺 GitHub 推出的 ghcr.io。
由于某些原因,在國內訪問這些服務可能會比較慢。國內的一些云服務商提供了針對 Docker Hub 的鏡像服務(Registry Mirror
),這些鏡像服務被稱為 加速器。常見的有 阿里云加速器、DaoCloud 加速器 等。使用加速器會直接從國內的地址下載 Docker Hub 的鏡像,比直接從 Docker Hub 下載速度會提高很多。在 安裝 Docker 一節中有詳細的配置方法。
國內也有一些云服務商提供類似于 Docker Hub 的公開服務。比如 網易云鏡像服務、DaoCloud 鏡像市場、阿里云鏡像庫 等。
私有 Docker Registry
除了使用公開服務外,用戶還可以在本地搭建私有 Docker Registry。Docker 官方提供了 Docker Registry 鏡像,可以直接使用做為私有 Registry 服務。在 私有倉庫 一節中,會有進一步的搭建私有 Registry 服務的講解。
開源的 Docker Registry 鏡像只提供了 Docker Registry API 的服務端實現,足以支持 docker
命令,不影響使用。但不包含圖形界面,以及鏡像維護、用戶管理、訪問控制等高級功能。
除了官方的 Docker Registry 外,還有第三方軟件實現了 Docker Registry API,甚至提供了用戶界面以及一些高級功能。比如,Harbor 和 Sonatype Nexus。
利用commit理解鏡像構成
注意: docker commit
命令除了學習之外,還有一些特殊的應用場合,比如被入侵后保存現場等。但是,不要使用 docker commit
定制鏡像,定制鏡像應該使用 Dockerfile
來完成。如果你想要定制鏡像請查看下一小節。
鏡像是容器的基礎,每次執行 docker run
的時候都會指定哪個鏡像作為容器運行的基礎。在之前的例子中,我們所使用的都是來自于 Docker Hub 的鏡像。直接使用這些鏡像是可以滿足一定的需求,而當這些鏡像無法直接滿足需求時,我們就需要定制這些鏡像。接下來的幾節就將講解如何定制鏡像。
回顧一下之前我們學到的知識,鏡像是多層存儲,每一層是在前一層的基礎上進行的修改;而容器同樣也是多層存儲,是在以鏡像為基礎層,在其基礎上加一層作為容器運行時的存儲層。
現在讓我們以定制一個 Web 服務器為例子,來講解鏡像是如何構建的。
docker run --name webserver -d -p 80:80 nginx
這條命令會用 nginx
鏡像啟動一個容器,命名為 webserver
,并且映射了 80 端口,這樣我們可以用瀏覽器去訪問這個 nginx
服務器。
如果是在本機運行的 Docker,那么可以直接訪問:http://localhost
,如果是在虛擬機、云服務器上安裝的 Docker,則需要將 localhost
換為虛擬機地址或者實際云服務器地址。
直接用瀏覽器訪問的話,我們會看到默認的 Nginx 歡迎頁面。
現在,假設我們非常不喜歡這個歡迎頁面,我們希望改成歡迎 Docker 的文字,我們可以使用 docker exec
命令進入容器,修改其內容。
$ docker exec -it webserver bash
root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@3729b97e8226:/# exit
exit
我們以交互式終端方式進入 webserver
容器,并執行了 bash
命令,也就是獲得一個可操作的 Shell。
然后,我們用 <h1>Hello, Docker!</h1>
覆蓋了 /usr/share/nginx/html/index.html
的內容。
現在我們再刷新瀏覽器的話,會發現內容被改變了。
我們修改了容器的文件,也就是改動了容器的存儲層。我們可以通過 docker diff
命令看到具體的改動。
$ docker diff webserver
C /root
A /root/.bash_history
C /run
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
現在我們定制好了變化,我們希望能將其保存下來形成鏡像。
要知道,當我們運行一個容器的時候(如果不使用卷的話),我們做的任何文件修改都會被記錄于容器存儲層里。而 Docker 提供了一個 docker commit
命令,可以將容器的存儲層保存下來成為鏡像。換句話說,就是在原有鏡像的基礎上,再疊加上容器的存儲層,并構成新的鏡像。以后我們運行這個新鏡像的時候,就會擁有原有容器最后的文件變化。
docker commit
的語法格式為:
docker commit [選項] <容器ID或容器名> [<倉庫名>[:<標簽>]]
我們可以用下面的命令將容器保存為鏡像:
docker commit \--author "Tao Wang <twang2218@gmail.com>" \--message "修改了默認網頁" \webserver \nginx:v2
sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214
其中 --author
是指定修改的作者,而 --message
則是記錄本次修改的內容。這點和 git
版本控制相似,不過這里這些信息可以省略留空。
我們可以在 docker image ls
中看到這個新定制的鏡像:
docker image ls nginx
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx v2 07e334659748 9 seconds ago 181.5 MB
nginx 1.11 05a60462f8ba 12 days ago 181.5 MB
nginx latest e43d811ce2f4 4 weeks ago 181.5 MB
我們還可以用 docker history
具體查看鏡像內的歷史記錄,如果比較 nginx:latest
的歷史記錄,我們會發現新增了我們剛剛提交的這一層。
docker history nginx:v2
IMAGE CREATED CREATED BY SIZE COMMENT
07e334659748 54 seconds ago nginx -g daemon off; 95 B 修改了默認網頁
e43d811ce2f4 4 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon 0 B
<missing> 4 weeks ago /bin/sh -c #(nop) EXPOSE 443/tcp 80/tcp 0 B
<missing> 4 weeks ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx/ 22 B
<missing> 4 weeks ago /bin/sh -c apt-key adv --keyserver hkp://pgp. 58.46 MB
<missing> 4 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.11.5-1 0 B
<missing> 4 weeks ago /bin/sh -c #(nop) MAINTAINER NGINX Docker Ma 0 B
<missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:23aa4f893e3288698c 123 MB
新的鏡像定制好后,我們可以來運行這個鏡像。
docker run --name web2 -d -p 81:80 nginx:v2
這里我們命名為新的服務為 web2
,并且映射到 81
端口。訪問 http://localhost:81
看到結果,其內容應該和之前修改后的 webserver
一樣。
至此,我們第一次完成了定制鏡像,使用的是 docker commit
命令,手動操作給舊的鏡像添加了新的一層,形成新的鏡像,對鏡像多層存儲應該有了更直觀的感覺。
慎用docker commit
用 docker commit
命令雖然可以比較直觀的幫助理解鏡像分層存儲的概念,但是實際環境中并不會這樣使用。
首先,如果仔細觀察之前的 docker diff webserver
的結果,你會發現除了真正想要修改的 /usr/share/nginx/html/index.html
文件外,由于命令的執行,還有很多文件被改動或添加了。這還僅僅是最簡單的操作,如果是安裝軟件包、編譯構建,那會有大量的無關內容被添加進來,將會導致鏡像極為臃腫。
此外,使用 docker commit
意味著所有對鏡像的操作都是黑箱操作,生成的鏡像也被稱為 黑箱鏡像,換句話說,就是除了制作鏡像的人知道執行過什么命令、怎么生成的鏡像,別人根本無從得知。而且,即使是這個制作鏡像的人,過一段時間后也無法記清具體的操作。這種黑箱鏡像的維護工作是非常痛苦的。
而且,回顧之前提及的鏡像所使用的分層存儲的概念,除當前層外,之前的每一層都是不會發生改變的,換句話說,任何修改的結果僅僅是在當前層進行標記、添加、修改,而不會改動上一層。如果使用 docker commit
制作鏡像,以及后期修改的話,每一次修改都會讓鏡像更加臃腫一次,所刪除的上一層的東西并不會丟失,會一直如影隨形的跟著這個鏡像,即使根本無法訪問到。這會讓鏡像更加臃腫。