容器是鏡像的運(yùn)行時(shí)實(shí)例。正如從虛擬機(jī)模板上啟動(dòng) VM 一樣,用戶(hù)也同樣可以從單個(gè)鏡像上啟動(dòng)一個(gè)或多個(gè)容器。
虛擬機(jī)和容器最大的區(qū)別是容器更快并且更輕量級(jí)——與虛擬機(jī)運(yùn)行在完整的操作系統(tǒng)之上相比,容器會(huì)共享其所在主機(jī)的操作系統(tǒng)/內(nèi)核。
下圖為使用單個(gè) Docker 鏡像啟動(dòng)多個(gè)容器的示意圖。
啟動(dòng)容器的簡(jiǎn)便方式是使用docker container run命令。
該命令可以攜帶很多參數(shù),在其基礎(chǔ)的格式docker container run <image> <app>中,指定了啟動(dòng)所需的鏡像以及要運(yùn)行的應(yīng)用。
docker container run -it ubuntu /bin/bash則會(huì)啟動(dòng)某個(gè) Ubuntu Linux 容器,并運(yùn)行 Bash Shell 作為其應(yīng)用。
如果想啟動(dòng) PowerShell 并運(yùn)行一個(gè)應(yīng)用,則可以使用命令docker container run -it microsoft- /powershell:nanoserver pwsh.exe。
-it 參數(shù)可以將當(dāng)前終端連接到容器的 Shell 終端之上。
容器隨著其中運(yùn)行應(yīng)用的退出而終止。其中 Linux 容器會(huì)在 Bash Shell 退出后終止,而 Windows 容器會(huì)在 PowerShell 進(jìn)程終止后退出。
一個(gè)簡(jiǎn)單的驗(yàn)證方法就是啟動(dòng)新的容器,并運(yùn)行 sleep 命令休眠 10s。容器會(huì)啟動(dòng),然后運(yùn)行休眠命令,在 10s 后退出。
如果在 Linux 主機(jī)(或者在 Linux 容器模式下的 Windows 主機(jī)上)運(yùn)行docker container run alpine:latest sleep 10命令,Shell 會(huì)連接到容器 Shell 10s 的時(shí)間,然后退出。
可以使用 docker container stop 命令手動(dòng)停止容器運(yùn)行,并且使用 docker container start 再次啟動(dòng)該容器。
如果再也不需要該容器,則使用 docker container rm 命令來(lái)刪除容器。
容器和虛擬機(jī)
容器和虛擬機(jī)都依賴(lài)于宿主機(jī)才能運(yùn)行。宿主機(jī)可以是筆記本,是數(shù)據(jù)中心的物理服務(wù)器,也可以是公有云的某個(gè)實(shí)例。
在下面的示例中,假設(shè)宿主機(jī)是一臺(tái)需要運(yùn)行 4 個(gè)業(yè)務(wù)應(yīng)用的物理服務(wù)器。
在虛擬機(jī)模型中,首先要開(kāi)啟物理機(jī)并啟動(dòng) Hypervisor 引導(dǎo)程序。一旦 Hypervisor 啟動(dòng),就會(huì)占有機(jī)器上的全部物理資源,如 CPU、RAM、存儲(chǔ)和 NIC。
Hypervisor 接下來(lái)就會(huì)將這些物理資源劃分為虛擬資源,并且看起來(lái)與真實(shí)物理資源完全一致。
然后 Hypervisor 會(huì)將這些資源打包進(jìn)一個(gè)叫作虛擬機(jī)(VM)的軟件結(jié)構(gòu)當(dāng)中。這樣用戶(hù)就可以使用這些虛擬機(jī),并在其中安裝操作系統(tǒng)和應(yīng)用。
前面提到需要在物理機(jī)上運(yùn)行 4 個(gè)應(yīng)用,所以在 Hypervisor 之上需要?jiǎng)?chuàng)建 4 個(gè)虛擬機(jī)并安裝 4 個(gè)操作系統(tǒng),然后安裝 4 個(gè)應(yīng)用。當(dāng)操作完成后,結(jié)構(gòu)如下圖所示。
而容器模型則略有不同。
服務(wù)器啟動(dòng)之后,所選擇的操作系統(tǒng)會(huì)啟動(dòng)。在 Docker 世界中可以選擇 Linux,或者內(nèi)核支持內(nèi)核中的容器原語(yǔ)的新版本 Windows。
與虛擬機(jī)模型相同,OS 也占用了全部硬件資源。在 OS 層之上,需要安裝容器引擎(如 Docker)。
容器引擎可以獲取系統(tǒng)資源,比如進(jìn)程樹(shù)、文件系統(tǒng)以及網(wǎng)絡(luò)棧,接著將資源分割為安全的互相隔離的資源結(jié)構(gòu),稱(chēng)之為容器。
每個(gè)容器看起來(lái)就像一個(gè)真實(shí)的操作系統(tǒng),在其內(nèi)部可以運(yùn)行應(yīng)用。按照前面的假設(shè),需要在物理機(jī)上運(yùn)行 4 個(gè)應(yīng)用。
因此,需要?jiǎng)澐殖?4 個(gè)容器并在每個(gè)容器中運(yùn)行一個(gè)應(yīng)用,如下圖所示。
從更高層面上來(lái)講,Hypervisor 是硬件虛擬化(Hardware Virtualization)——Hypervisor 將硬件物理資源劃分為虛擬資源。
容器是操作系統(tǒng)虛擬化(OS Virtualization)——容器將系統(tǒng)資源劃分為虛擬資源。
虛擬機(jī)的額外開(kāi)銷(xiāo)
基于前文所述內(nèi)容,接下來(lái)會(huì)著重探討 Hypervisor 模型的一個(gè)主要問(wèn)題。
首先我們的目標(biāo)是在一臺(tái)物理機(jī)上運(yùn)行 4 個(gè)業(yè)務(wù)相關(guān)應(yīng)用。每種模型示例中都安裝了一個(gè)操作系統(tǒng)或者 Hypervisor(一種針對(duì)虛擬機(jī)高度優(yōu)化后的操作系統(tǒng))。
虛擬機(jī)模型將底層硬件資源劃分到虛擬機(jī)當(dāng)中。每個(gè)虛擬機(jī)都是包含了虛擬 CPU、虛擬 RAM、虛擬磁盤(pán)等資源的一種軟件結(jié)構(gòu)。
因此,每個(gè)虛擬機(jī)都需要有自己的操作系統(tǒng)來(lái)聲明、初始化并管理這些虛擬資源。
但是,操作系統(tǒng)本身是有其額外開(kāi)銷(xiāo)的。例如,每個(gè)操作系統(tǒng)都消耗一點(diǎn) CPU、一點(diǎn) RAM、一點(diǎn)存儲(chǔ)空間等。
每個(gè)操作系統(tǒng)都需要獨(dú)立的許可證,并且都需要打補(bǔ)丁升級(jí),每個(gè)操作系統(tǒng)也都面臨被攻擊的風(fēng)險(xiǎn)。
通常將這種現(xiàn)象稱(chēng)作 OS Tax 或者 VM Tax,每個(gè)操作系統(tǒng)都占用一定的資源。
容器模型具有在宿主機(jī)操作系統(tǒng)中運(yùn)行的單個(gè)內(nèi)核。在一臺(tái)主機(jī)上運(yùn)行數(shù)十個(gè)甚至數(shù)百個(gè)容器都是可能的——容器共享一個(gè)操作系統(tǒng)/內(nèi)核。
這意味著只有一個(gè)操作系統(tǒng)消耗 CPU、RAM 和存儲(chǔ)資源,只有一個(gè)操作系統(tǒng)需要授權(quán),只有一個(gè)操作系統(tǒng)需要升級(jí)和打補(bǔ)丁。同時(shí),只有一個(gè)操作系統(tǒng)面臨被攻擊的風(fēng)險(xiǎn)。簡(jiǎn)言之,就是只有一份 OS 損耗。
在上述單臺(tái)機(jī)器上只需要運(yùn)行 4 個(gè)業(yè)務(wù)應(yīng)用的場(chǎng)景中,也許問(wèn)題尚不明顯。但當(dāng)需要運(yùn)行成百上千應(yīng)用的時(shí)候,就會(huì)引起質(zhì)的變化。
另一個(gè)值得考慮的事情是啟動(dòng)時(shí)間。因?yàn)槿萜鞑⒉皇峭暾牟僮飨到y(tǒng),所以其啟動(dòng)要遠(yuǎn)比虛擬機(jī)快。
切記,在容器內(nèi)部并不需要內(nèi)核,也就沒(méi)有定位、解壓以及初始化的過(guò)程——更不用提在內(nèi)核啟動(dòng)過(guò)程中對(duì)硬件的遍歷和初始化了。
這些在容器啟動(dòng)的過(guò)程中統(tǒng)統(tǒng)都不需要!唯一需要的是位于下層操作系統(tǒng)的共享內(nèi)核是啟動(dòng)了的!最終結(jié)果就是,容器可以在 1s 內(nèi)啟動(dòng)。唯一對(duì)容器啟動(dòng)時(shí)間有影響的就是容器內(nèi)應(yīng)用啟動(dòng)所花費(fèi)的時(shí)間。
這就是容器模型要比虛擬機(jī)模型簡(jiǎn)潔并且高效的原因了。使用容器可以在更少的資源上運(yùn)行更多的應(yīng)用,啟動(dòng)更快,并且支付更少的授權(quán)和管理費(fèi)用,同時(shí)面對(duì)未知攻擊的風(fēng)險(xiǎn)也更小。
除了上述的理論基礎(chǔ)之外,接下來(lái)請(qǐng)跟隨本書(shū)一起使用容器完成一些實(shí)戰(zhàn)。
檢查 Docker daemon
通常登錄 Docker 主機(jī)后的第一件事情是檢查 Docker 是否正在運(yùn)行。
$ docker version
Client:
Version: API 17.05.0-ce
version: Go 1.29
version: Git go1.7.5
commit: 89658be
Built: Thu May 4 22:10:54 2017
OS/Arch: linux/amd64
Server:
Version: 17.05.0-ce
API version: 1.29 (minimum version 1.12)
Go version: go1.7.5
Git commit: 89658be
Built: Thu May 4 22:10:54 2017
OS/Arch: linux/amd64
Experimental: false當(dāng)命令輸出中包含 Client 和 Server 的內(nèi)容時(shí),可以繼續(xù)下面的操作。如果在 Server 部分中包含了錯(cuò)誤碼,這表示 Docker daemon 很可能沒(méi)有運(yùn)行,或者當(dāng)前用戶(hù)沒(méi)有權(quán)限訪(fǎng)問(wèn)。
如果在 Linux 中遇到無(wú)權(quán)限訪(fǎng)問(wèn)的問(wèn)題,需要確認(rèn)當(dāng)前用戶(hù)是否屬于本地 Docker UNIX 組。如果不是,可以通過(guò)usermod -aG docker <user>來(lái)添加,然后退出并重新登錄 Shell,改動(dòng)即可生效。
如果當(dāng)前用戶(hù)已經(jīng)屬于本地 docker 用戶(hù)組,那么問(wèn)題可能是 Docker daemon 沒(méi)有運(yùn)行導(dǎo)致。
根據(jù) Docker 主機(jī)的操作系統(tǒng)在下面的內(nèi)容中選擇一條合適的命令,來(lái)檢查 Docker daemon 的狀態(tài)。
//使用 Systemd 在 Linux 系統(tǒng)中執(zhí)行該命令
$ service docker status
docker start/running, process 29393
//使用Systemd在Linux系統(tǒng)中執(zhí)行該命令
$ systemctl is-active docker
active
//在Windows Server 2016的PowerShell窗口中運(yùn)行該命令
> Get-Service docker
Status Name DisplayName
------ ---- -----------
Running Docker docker
啟動(dòng)一個(gè)簡(jiǎn)單容器
啟動(dòng)容器的一個(gè)簡(jiǎn)單的方式是通過(guò) docker container run 命令。
下面的命令啟動(dòng)了一個(gè)簡(jiǎn)單的容器,其中運(yùn)行了容器化版本的 Ubuntu Linux。
Windows 示例。
docker container run -it microsoft/powershell:nanoserver pwsh.exe
命令的基礎(chǔ)格式為:
docker container run <options> <im- age>:<tag> <app>
示例中使用 docker container run 來(lái)啟動(dòng)容器,這也是啟動(dòng)新容器的標(biāo)準(zhǔn)命令。
命令中使用了 -it 參數(shù)使容器具備交互性并與終端進(jìn)行連接。接下來(lái),命令中指定了具體鏡像 ubuntu:latest 或者 microsoft/powershell:nanoserver。
最終,在命令中指定了運(yùn)行在容器中的程序,Linux 示例中是 Bash Shell,Windows 示例中為 PowerShell。
當(dāng)敲擊回車(chē)鍵之后,Docker 客戶(hù)端選擇合適的 API 來(lái)調(diào)用 Docker daemon。
Docker daemon 接收到命令并搜索 Docker 本地緩存,觀察是否有命令所請(qǐng)求的鏡像。
在上面引用的示例中,本地緩存并未包含該鏡像,所以 Docker 接下來(lái)查詢(xún)?cè)?Docker Hub 中是否存在對(duì)應(yīng)鏡像。找到該鏡像后,Docker 將鏡像拉取到本地,存儲(chǔ)在本地緩存當(dāng)中。
在標(biāo)準(zhǔn)的、開(kāi)箱即用的 Linux 安裝版中,Docker daemon 通過(guò)位于 /var/run/docker.sock 的本地 IPC/Unix socket 來(lái)實(shí)現(xiàn) Docker 遠(yuǎn)程 API;在 Windows 中,Docker daemon 通過(guò)監(jiān)聽(tīng)名為 npipe:////./pipe/docker_engine 的管道來(lái)實(shí)現(xiàn)。
通過(guò)配置,也可以借助網(wǎng)絡(luò)來(lái)實(shí)現(xiàn) Docker Client 和 daemon 之間的通信。
Docker 默認(rèn)非 TLS 網(wǎng)絡(luò)端口為 2375,TLS 默認(rèn)端口為 2376。
一旦鏡像拉取到本地,daemon 就創(chuàng)建容器并在其中運(yùn)行指定的應(yīng)用。
如果仔細(xì)觀察,就會(huì)發(fā)現(xiàn) Shell 提示符發(fā)生了變化,說(shuō)明目前已經(jīng)位于容器內(nèi)部了。
在上面的示例中,Shell 提示符已經(jīng)變?yōu)?root@3027eb644874:/#。@ 之后的一長(zhǎng)串?dāng)?shù)字就是容器唯一 ID 的前 12 個(gè)字符。
若嘗試在容器內(nèi)執(zhí)行一些基礎(chǔ)命令,可能會(huì)發(fā)現(xiàn)某些指令無(wú)法正常工作。這是因?yàn)榇蟛糠秩萜麋R像都是經(jīng)過(guò)高度優(yōu)化的。這意味著某些命令或者包可能沒(méi)有安裝。
下面的示例展示了兩個(gè)命令,一條執(zhí)行成功,一條執(zhí)行失敗。
root@3027eb644874:/# ls -l
total 64
drwxr-xr-x 2 root root 4096 Aug 19 00:50 bin
drwxr-xr-x 2 root root 4096 Apr 12 20:14 boot
drwxr-xr-x 5 root root 380 Sep 13 00:47 dev
drwxr-xr-x 45 root root 4096 Sep 13 00:47 etc
drwxr-xr-x 2 root root 4096 Apr 12 20:14 home
drwxr-xr-x 8 root root 4096 Sep 13 2015 lib
drwxr-xr-x 2 root root 4096 Aug 19 00:50 lib64
drwxr-xr-x 2 root root 4096 Aug 19 00:50 media
drwxr-xr-x 2 root root 4096 Aug 19 00:50 mnt
drwxr-xr-x 2 root root 4096 Aug 19 00:50 opt
dr-xr-xr-x 129 root root 0 Sep 13 00:47 proc
drwx------ 2 root root 4096 Aug 19 00:50 root
drwxr-xr-x 6 root root 4096 Aug 26 18:50 run
drwxr-xr-x 2 root root 4096 Aug 26 18:50 sbin
drwxr-xr-x 2 root root 4096 Aug 19 00:50 srv
dr-xr-xr-x 13 root root 0 Sep 13 00:47 sys
drwxrwxrwt 2 root root 4096 Aug 19 00:50 tmp
drwxr-xr-x 11 root root 4096 Aug 26 18:50 usr
drwxr-xr-x 13 root root 4096 Aug 26 18:50 var
root@3027eb644874:/# ping www.docker.com
bash: ping: command not found
從上面的輸出中可以看出,ping 工具包并不是官方 Ubuntu 鏡像的一部分。
容器進(jìn)程
在前面啟動(dòng) Ubuntu 容器之時(shí),讓容器運(yùn)行 Bash Shell(/bin/bash)。這使得 Bash Shell 成為容器中運(yùn)行的且唯一運(yùn)行的進(jìn)程??梢酝ㄟ^(guò)ps -elf命令在容器內(nèi)部查看。
root@3027eb644874:/# ps -elf
F S UID 4 PID PPID NI ADDR SZ WCHAN STIME TTY TIME CMD
S root 0 1 0 0 - 4558 wait 00:47 ? 00:00:00 /bin/bash
R root 11 1 0 - 8604 - 00:52 ? 00:00:00 ps -elf
上面的輸出中看起來(lái)好像有兩個(gè)正在運(yùn)行的進(jìn)程,其實(shí)并非如此。
列表中 PID 為 1 的進(jìn)程,是容器被告知要運(yùn)行的 Bash Shell;第二個(gè)進(jìn)程是 ps -elf 命令產(chǎn)生的,這是個(gè)臨時(shí)進(jìn)程,并且在輸出后就已經(jīng)退出了。也就是說(shuō),這個(gè)容器當(dāng)前只運(yùn)行了一個(gè)進(jìn)程 /bin/bash。
Windows 容器有所不同,通常會(huì)運(yùn)行相當(dāng)多的進(jìn)程。
這意味著如果通過(guò)輸入 exit 退出 Bash Shell,那么容器也會(huì)退出(終止)。
原因是容器如果不運(yùn)行任何進(jìn)程則無(wú)法存在,殺死 Bash Shell 即殺死了容器唯一運(yùn)行的進(jìn)程,導(dǎo)致這個(gè)容器也被殺死。
這對(duì)于 Windows 容器來(lái)說(shuō)也是一樣的,殺死容器中的主進(jìn)程,則容器也會(huì)被殺死。
按下 Ctrl-PQ 組合鍵則會(huì)退出容器但并不終止容器運(yùn)行。這樣做會(huì)切回到 Docker 主機(jī)的 Shell,并保持容器在后臺(tái)運(yùn)行。
可以使用 docker container ls 命令來(lái)觀察當(dāng)前系統(tǒng)正在運(yùn)行的容器列表。
$ docker container ls
CNTNR ID IMAGE COMMAND CREATED STATUS NAMES
302...74 ubuntu:latest /bin/bash 6 mins Up 6mins sick_montalcini
當(dāng)前容器仍然在運(yùn)行,并且可以通過(guò) docker container exec 命令將終端重新連接到 Docker,理解這一點(diǎn)很重要。
$ docker container exec -it 3027eb644874 bash
root@3027eb644874:/#
用于重連 Windows Nano Server PowerShell 容器的命令是 docker container exec -it <container-name-or-ID> pwsh.exe。
Shell 提示符切換到了容器。這時(shí)再次運(yùn)行 ps 命令,會(huì)看到兩個(gè) Bash 或者 PowerShell 進(jìn)程,這是因?yàn)?docker container exec 命令創(chuàng)建了新的 Bash 或者 PowerShell 進(jìn)程并且連接到容器。
這意味著在當(dāng)前 Shell 輸入 exit 并不會(huì)導(dǎo)致容器終止,因?yàn)樵?Bash 或者 PowerShell 進(jìn)程還在運(yùn)行當(dāng)中。
輸入 exit 退出容器,并通過(guò)命令 docker container ps 來(lái)確認(rèn)容器依然在運(yùn)行中。果然容器還在運(yùn)行。
如果在自己的 Docker 主機(jī)上運(yùn)行示例,則需要使用下面兩個(gè)命令來(lái)停止并刪除容器(需要將 ID 替換為自己容器的 ID)。
$ docker container stop 3027eb64487
3027eb64487
$ docker container rm 3027eb64487
3027eb64487
容器生命周期
人們認(rèn)為容器不擅長(zhǎng)持久化工作或者持久化數(shù)據(jù),很大程度上是因?yàn)槿萜髟诜浅志没I(lǐng)域上表現(xiàn)得太出色。
但是在一個(gè)領(lǐng)域做得很好并不意味著不擅長(zhǎng)其他的領(lǐng)域。很多虛擬機(jī)管理員會(huì)記得微軟或者 Oracle 告訴他們不能在虛擬機(jī)中運(yùn)行他們的應(yīng)用,至少他們不會(huì)支持這么做。
下面來(lái)介紹一下容器的生命周期,從創(chuàng)建、運(yùn)行、休眠,直至銷(xiāo)毀的整個(gè)過(guò)程。
前面介紹了如何使用 docker container run 命令來(lái)啟動(dòng)容器。接下來(lái)會(huì)重新啟動(dòng)一個(gè)新的容器,這樣就可以觀察期完整的生命周期。
下面的示例中會(huì)采用 Linux Docker 主機(jī)來(lái)運(yùn)行 Ubuntu 容器。但同時(shí),示例內(nèi)容在前面例子中使用過(guò)的 Windows PowerShell 容器中也是生效的。
$ docker container run --name percy -it ubuntu:latest /bin/bash
root@9cb2d2fd1d65:/#
這就是新建的容器,名稱(chēng)為“percy”,意指持久化(persistent)。
接下來(lái)把該容器投入使用,將一部分?jǐn)?shù)據(jù)寫(xiě)入其中。
在新容器內(nèi)部 Shell 中,執(zhí)行下面的步驟來(lái)將部分?jǐn)?shù)據(jù)寫(xiě)入到 tmp 目錄下的某個(gè)文件中,并確認(rèn)數(shù)據(jù)是否寫(xiě)入成功。
root@9cb2d2fd1d65:/# cd tmp
root@9cb2d2fd1d65:/tmp# ls -l
total 0
root@9cb2d2fd1d65:/tmp# echo "DevOps FTW" > newfile
root@9cb2d2fd1d65:/tmp# ls -l
total 4
-rw-r--r-- 1 root root 14 May 23 11:22 newfile
root@9cb2d2fd1d65:/tmp# cat newfile
DevOps FTW
按 Ctrl-PQ 組合鍵退出當(dāng)前容器。
現(xiàn)在使用 docker container stop 命令來(lái)停止容器運(yùn)行,切換到暫停(vacation)狀態(tài)。
$ docker container stop percy
percy
可以在 docker container stop命令中指定容器的名稱(chēng)或者 ID。具體格式為:
docker container stop <container-id or container-name>
現(xiàn)在運(yùn)行 docker container ls 命令列出全部處于運(yùn)行中狀態(tài)的容器。
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
新建的容器沒(méi)有在上面的列表中出現(xiàn),原因是讀者通過(guò) docker container stop 命令使該容器停止運(yùn)行。加上 -a 參數(shù)再次運(yùn)行前面的命令,就會(huì)顯示出全部的容器,包括處于停止?fàn)顟B(tài)的。
$ docker container ls -a
CNTNR ID IMAGE COMMAND CREATED STATUS NAMES
9cb...65 ubuntu:latest /bin/bash 4 mins Exited (0) percy
現(xiàn)在可以看到該容器顯示當(dāng)前狀態(tài)為 Exited(0)。停止容器就像停止虛擬機(jī)一樣。盡管已經(jīng)停止運(yùn)行,容器的全部配置和內(nèi)容仍然保存在 Docker 主機(jī)的文件系統(tǒng)之中,并且隨時(shí)可以重新啟動(dòng)。
使用 docker container start 命令可以將容器重新啟動(dòng)。
$ docker container start percy
percy
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES
9cb2d2fd1d65 ubuntu:latest "/bin/bash" 4 mins Up 3 secs percy
現(xiàn)在停止的容器已經(jīng)重新啟動(dòng)了,此時(shí)可以確認(rèn)之前創(chuàng)建的文件是否還存在。使用 docker container exec 命令連接到重啟后的容器。
$ docker container exec -it percy bash
root@9cb2d2fd1d65:/#
Shell 提示符發(fā)生變化,提示正在容器內(nèi)部空間進(jìn)行操作。
確認(rèn)之前創(chuàng)建的文件依然存在,并且文件中仍包含之前寫(xiě)入的數(shù)據(jù)。
root@9cb2d2fd1d65:/# cd tmp
root@9cb2d2fd1d65:/# ls -l
-rw-r--r-- 1 root root 14 Sep 13 04:22 newfile
root@9cb2d2fd1d65:/#
root@9cb2d2fd1d65:/# cat newfile
DevOps FTW
像是魔術(shù)一般,之前創(chuàng)建的文件依然存在,并且文件中包含的數(shù)據(jù)正是離開(kāi)的方式!這證明停止容器運(yùn)行并不會(huì)損毀容器或者其中的數(shù)據(jù)。
盡管上面的示例闡明了容器的持久化特性,還是需要指出卷(volume)才是在容器中存儲(chǔ)持久化數(shù)據(jù)的首選方式。
現(xiàn)在停止該容器并從系統(tǒng)中刪除它。
通過(guò)在 docker container rm 命令后面添加 -f 參數(shù)來(lái)一次性刪除運(yùn)行中的容器是可行的。
但是,刪除容器的最佳方式還是分兩步,先停止容器然后刪除。這樣可以給容器中運(yùn)行的應(yīng)用/進(jìn)程一個(gè)停止運(yùn)行并清理殘留數(shù)據(jù)的機(jī)會(huì)。
在下一個(gè)示例中會(huì)停止 percy 容器,刪除它并確認(rèn)操作成功。如果讀者終端仍連接到 percy 容器,則需要按下 Ctrl-PQ 組合鍵先返回 Docker 主機(jī)終端。
$ docker container stop percy
percy
$ docker container rm percy
percy
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
現(xiàn)在容器已經(jīng)刪除了——在系統(tǒng)中消失。如果這是一個(gè)有用的容器,那么之后可以作為無(wú)服務(wù)的工具使用;如果沒(méi)有用處,則充其量也就是一個(gè)蹩腳的終端。
總結(jié)一下容器的生命周期。可以根據(jù)需要多次停止、啟動(dòng)、暫停以及重啟容器,并且這些操作執(zhí)行得很快。
但是容器及其數(shù)據(jù)是安全的。直至明確刪除容器前,容器都不會(huì)丟棄其中的數(shù)據(jù)。就算容器被刪除了,如果將容器數(shù)據(jù)存儲(chǔ)在卷中,數(shù)據(jù)也會(huì)被保存下來(lái)。
優(yōu)雅地停止容器
Linux 世界中,大部分容器都會(huì)運(yùn)行單一進(jìn)程;在Windows 中可能運(yùn)行若干個(gè),但是下面的原則對(duì)于兩者都適用。
前面的示例中容器正在運(yùn)行 /bin/bash 應(yīng)用。當(dāng)使用 docker container rm <container> -f來(lái)銷(xiāo)毀運(yùn)行中的容器時(shí),不會(huì)發(fā)出任何告警。
這個(gè)過(guò)程相當(dāng)暴力——有點(diǎn)像悄悄接近容器后在腦后突施冷槍。毫無(wú)征兆地被銷(xiāo)毀,會(huì)令容器和應(yīng)用猝不及防,來(lái)不及“處理后事”。
但是,docker container stop 命令就有禮貌多了(就像用槍指著容器的腦袋然后說(shuō)“你有 10s 時(shí)間說(shuō)出你的遺言”)。
該命令給容器內(nèi)進(jìn)程發(fā)送將要停止的警告信息,給進(jìn)程機(jī)會(huì)來(lái)有序處理停止前要做的事情。一旦 docker stop 命令返回后,就可以使用 docker container rm 命令刪除容器了。
這背后的原理可以通過(guò) Linux/POSIX 信號(hào)來(lái)解釋。docker container stop 命令向容器內(nèi)的 PID 1 進(jìn)程發(fā)送了 SIGTERM 這樣的信號(hào)。
就像前文提到的一樣,會(huì)為進(jìn)程預(yù)留一個(gè)清理并優(yōu)雅停止的機(jī)會(huì)。如果 10s 內(nèi)進(jìn)程沒(méi)有終止,那么就會(huì)收到 SIGKILL 信號(hào)。這是致命一擊。但是,進(jìn)程起碼有 10s 的時(shí)間來(lái)“解決”自己。
docker container rm <container> -f 命令不會(huì)先友好地發(fā)送 SIGTERM,這條命令會(huì)直接發(fā)出 SIGKILL。就像剛剛所打的比方一樣,該命令悄悄接近并對(duì)容器發(fā)起致命一擊。
利用重啟策略進(jìn)行容器的自我修復(fù)
通常建議在運(yùn)行容器時(shí)配置好重啟策略。這是容器的一種自我修復(fù)能力,可以在指定事件或者錯(cuò)誤后重啟來(lái)完成自我修復(fù)。
重啟策略應(yīng)用于每個(gè)容器,可以作為參數(shù)被強(qiáng)制傳入 docker-container run 命令中,或者在 Compose 文件中聲明(在使用 Docker Compose 以及 Docker Stacks 的情況下)。
容器支持的重啟策略包括 always、unless-stopped 和 on-failed。
always 策略是一種簡(jiǎn)單的方式。除非容器被明確停止,比如通過(guò) docker container stop 命令,否則該策略會(huì)一直嘗試重啟處于停止?fàn)顟B(tài)的容器。
一種簡(jiǎn)單的證明方式是啟動(dòng)一個(gè)新的交互式容器,并在命令后面指定 --restart always 策略,同時(shí)在命令中指定運(yùn)行 Shell 進(jìn)程。
當(dāng)容器啟動(dòng)的時(shí)候,會(huì)登錄到該 Shell。退出 Shell 時(shí)會(huì)殺死容器中 PID 為 1 的進(jìn)程,并且殺死這個(gè)容器。
但是因?yàn)橹付?--restart always 策略,所以容器會(huì)自動(dòng)重啟。如果運(yùn)行 docker container ls 命令,就會(huì)看到容器的啟動(dòng)時(shí)間小于創(chuàng)建時(shí)間。下面請(qǐng)看示例。
$ docker container run --name neversaydie -it --restart always alpine sh
//等待幾秒后輸入exit
/# exit
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS
0901afb84439 alpine "sh" 35 seconds ago Up 1 second
注意,容器于 35s 前被創(chuàng)建,但卻在 1s 前才啟動(dòng)。這是因?yàn)樵谌萜髦休斎胪顺雒畹臅r(shí)候,容器被殺死,然后 Docker 又重新啟動(dòng)了該容器。
--restart always 策略有一個(gè)很有意思的特性,當(dāng) daemon 重啟的時(shí)候,停止的容器也會(huì)被重啟。
例如,新創(chuàng)建一個(gè)容器并指定 --restart always 策略,然后通過(guò) docker container stop命令停止該容器。
現(xiàn)在容器處于 Stopped (Exited) 狀態(tài)。但是,如果重啟 Docker daemon,當(dāng) daemon 啟動(dòng)完成時(shí),該容器也會(huì)重新啟動(dòng)。
always 和 unless-stopped 的最大區(qū)別,就是那些指定了 --restart unless-stopped 并處于 Stopped (Exited) 狀態(tài)的容器,不會(huì)在 Docker daemon 重啟的時(shí)候被重啟。
下面創(chuàng)建兩個(gè)新容器,其中“always”容器指定 --restart always 策略,另一個(gè)“unless- stopped”容器指定了 --restart unless-stopped 策略。
兩個(gè)容器均通過(guò) docker container stop 命令停止,接著重啟 Docker。結(jié)果“always”容器會(huì)重啟,但是“unless-stopped”容器不會(huì)。
1) 創(chuàng)建兩個(gè)新容器
$ docker container run -d --name always \
--restart always \
alpine sleep 1d
$ docker container run -d --name unless-stopped \
--restart unless-stopped \
alpine sleep 1d
$ docker container ls
CONTAINER ID IMAGE COMMAND STATUS NAMES
3142bd91ecc4 alpine "sleep 1d" Up 2 secs unless-stopped
4f1b431ac729 alpine "sleep 1d" Up 17 secs always
現(xiàn)在有兩個(gè)運(yùn)行的容器了。一個(gè)叫作“always”,另一個(gè)叫作“unless-stopped”。
2) 停止兩個(gè)容器
$ docker container stop always unless-stopped
$ docker container ls -a
CONTAINER ID IMAGE STATUS NAMES
3142bd91ecc4 alpine Exited (137) 3 seconds ago unless-stopped
4f1b431ac729 alpine Exited (137) 3 seconds ago always
3) 重啟 Docker
重啟 Docker 的過(guò)程在不同的操作系統(tǒng)上可能不同。下面介紹一下如何在 Linux 上使用 systemd 重啟 Docker,在 Windows Server 2016 上可以使用 restart-service 重啟。
$ systemlctl restart docker
4) 一旦 Docker 重啟成功,檢查兩個(gè)容器的狀態(tài)
$ docker container ls -a
CONTAINER CREATED STATUS NAMES
314..cc4 2 minutes ago Exited (137) 2 minutes ago unless-stopped
4f1..729 2 minutes ago Up 9 seconds always
注意到“always”容器(啟動(dòng)時(shí)指定了 --restart always 策略)已經(jīng)重啟了,但是“unless-stopped”容器(啟動(dòng)時(shí)指定了 --restart unless-stopped 策略)并沒(méi)有重啟。
on-failure 策略會(huì)在退出容器并且返回值不是 0 的時(shí)候,重啟容器。
就算容器處于 stopped 狀態(tài),在 Docker daemon 重啟的時(shí)候,容器也會(huì)被重啟。
如果讀者使用 Docker Compose 或者 Docker Stack,可以在 service 對(duì)象中配置重啟策略,示例如下。
version: "3.5"
services:
myservice:
<Snip>
restart_policy:
condition: always | unless-stopped | on-failure
Web 服務(wù)器示例
到目前為止,已經(jīng)介紹了如何啟動(dòng)一個(gè)簡(jiǎn)單的容器,并與其進(jìn)行交互。同時(shí)也知道了如何停止、重啟以及刪除一個(gè)容器。現(xiàn)在來(lái)看一個(gè) Linux Web 服務(wù)器示例。
在該示例中,會(huì)使用到 Pluralsight 視頻教程網(wǎng)站中的一個(gè)鏡像。這個(gè)鏡像會(huì)在 8080 端口啟動(dòng)一個(gè)相當(dāng)簡(jiǎn)單的 Web 服務(wù)。
使用 docker container stop 以及 docker container rm 命令清理當(dāng)前系統(tǒng)中的全部容器,然后運(yùn)行下面的 docker container run 命令。
$ docker container run -d --name webserver -p 80:8080 \
nigelpoulton/pluralsight-docker-ci
Unable to find image 'nigelpoulton/pluralsight-docker-ci:latest' locally
latest: Pulling from nigelpoulton/pluralsight-docker-ci
a3ed95caeb02: Pull complete
3b231ed5aa2f: Pull complete
7e4f9cd54d46: Pull complete
929432235e51: Pull complete
6899ef41c594: Pull complete
0b38fccd0dab: Pull complete
Digest: sha256:7a6b0125fe7893e70dc63b2...9b12a28e2c38bd8d3d
Status: Downloaded newer image for nigelpoulton/plur...docker-ci:latest
6efa1838cd51b92a4817e0e7483d103bf72a7ba7ffb5855080128d85043fef21
注意,當(dāng)前 Shell 提示符并未發(fā)生變化。這是因?yàn)槭褂昧?-d 參數(shù)啟動(dòng)容器,并在后臺(tái)運(yùn)行。這種后臺(tái)啟動(dòng)的方式不會(huì)將當(dāng)前終端連接到容器當(dāng)中。
該示例在 docker container run命令中拋出了一些額外的參數(shù),一起來(lái)快速了解一下。
已經(jīng)知道 docker container run 會(huì)啟動(dòng)一個(gè)新容器,但是這次使用 -d 參數(shù)替換了 -it。-d 表示后臺(tái)模式,告知容器在后臺(tái)運(yùn)行。
然后為容器命名,并且指定了 -p 80:8080。-p 參數(shù)將 Docker 主機(jī)的端口映射到容器內(nèi)。
本例中,將 Docker 主機(jī)的 80 端口映射到了容器內(nèi)的 8080 端口。這意味著當(dāng)有流量訪(fǎng)問(wèn)主機(jī)的 80 端口的時(shí)候,流量會(huì)直接映射到容器內(nèi)的 8080 端口。
之所以如此是因?yàn)楫?dāng)前使用的鏡像,其 Web 服務(wù)監(jiān)聽(tīng)了 8080 端口。這意味著容器啟動(dòng)時(shí)會(huì)運(yùn)行一個(gè) Web 服務(wù),監(jiān)聽(tīng) 8080 端口。
最終,命令中還指定 Docker 所使用的鏡像:nigelpoulton/pluralsight-docker-ci。這個(gè)鏡像不一定保持更新,并且可能存在缺陷。
使用 docker container ls 命令可以查看當(dāng)前運(yùn)行的容器以及端口的映射情況。端口信息按照 host-port:container-port 的格式顯示,明確這一點(diǎn)很重要。
$ docker container ls
CONTAINER ID COMMAND STATUS PORTS NAMES
6efa1838cd51 /bin/sh -c... Up 2 mins 0.0.0.0:80->8080/tcp webserver
為了提高可讀性,上面輸出中的部分列并未展示。
現(xiàn)在容器已經(jīng)運(yùn)行,端口也映射成功,可以通過(guò)瀏覽器來(lái)訪(fǎng)問(wèn)該容器,需要在瀏覽器中指定 Docker 主機(jī)的 IP 地址或 DNS 名稱(chēng),端口號(hào)是 80。下圖展示了由容器服務(wù)提供的網(wǎng)頁(yè)。
docker container stop、docker container pause、docker container start 和 docker container rm命令同樣適用于容器。
同時(shí),持久性的規(guī)則也適用于容器——停止或暫停容器并不會(huì)導(dǎo)致容器銷(xiāo)毀,或者內(nèi)部存儲(chǔ)的數(shù)據(jù)丟失。
查看容器詳情
在前面的示例當(dāng)中,讀者可能發(fā)現(xiàn)當(dāng)運(yùn)行docker container run 命令的時(shí)候,并沒(méi)有指定容器中的具體應(yīng)用。但是容器卻啟動(dòng)了一個(gè)簡(jiǎn)單的 Web 服務(wù)。這是如何發(fā)生的?
當(dāng)構(gòu)建 Docker 鏡像的時(shí)候,可以通過(guò)嵌入指令來(lái)列出希望容器運(yùn)行時(shí)啟動(dòng)的默認(rèn)應(yīng)用。如果運(yùn)行 docker image inspect 命令來(lái)查看運(yùn)行容器時(shí)使用的鏡像,就能看到容器啟動(dòng)時(shí)將要運(yùn)行的應(yīng)用列表了。
$ docker image inspect nigelpoulton/pluralsight-docker-ci
[
{
"Id": "sha256:07e574331ce3768f30305519...49214bf3020ee69bba1",
"RepoTags": [
"nigelpoulton/pluralsight-docker-ci:latest"
<Snip>
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) CMD [\"/bin/sh\" \"-c\" \"cd /src \u0026\u0026 node \
.app.js\"]"
],
<Snip>
為了方便閱讀,僅截取輸出內(nèi)容中我們感興趣的部分。
Cmd 一項(xiàng)中展示了容器將會(huì)執(zhí)行的命令或應(yīng)用,除非在啟動(dòng)的時(shí)候讀者指定另外的應(yīng)用。如果去掉示例腳本中的轉(zhuǎn)義字符,可以得到這樣的命令:/bin/sh -c "cd /src && node ./app.js。這是基于該鏡像的容器會(huì)默認(rèn)運(yùn)行的應(yīng)用。
在構(gòu)建鏡像時(shí)指定默認(rèn)命令是一種很普遍的做法,因?yàn)檫@樣可以簡(jiǎn)化容器的啟動(dòng)。
這也為鏡像指定了默認(rèn)的行為,并且從側(cè)面闡述了鏡像的用途——可以通過(guò) Inspect 鏡像的方式來(lái)了解所要運(yùn)行的應(yīng)用。
快速清理
接下來(lái)了解一種簡(jiǎn)單且快速的清理 Docker 主機(jī)上全部運(yùn)行容器的方法。
這種處理方式會(huì)強(qiáng)制刪除所有的容器,并且不會(huì)給容器完成清理的機(jī)會(huì)。
這種操作一定不能在生產(chǎn)環(huán)境系統(tǒng)或者運(yùn)行著重要容器的系統(tǒng)上執(zhí)行。
在 Docker 主機(jī)的 Shell 中運(yùn)行下面的命令,可以刪除全部容器。
$()
在本例中,因?yàn)橹挥幸粋€(gè)運(yùn)行中的容器,所以只有一個(gè)容器被刪除(6efa1838cd51)。
但是該命令的工作方式,就跟前面章節(jié)中用于刪除某臺(tái) Docker 主機(jī)上全部容器的命令 rm $(docker image ls -q) 一樣,docker container rm 命令會(huì)刪除容器。
如果將 $(docker container ls -aq) 作為參數(shù)傳遞給 docker container rm 命令,等價(jià)于將系統(tǒng)中每個(gè)容器的 ID 傳給該命令。
-f 標(biāo)識(shí)表示強(qiáng)制執(zhí)行,所以即使是處于運(yùn)行狀態(tài)的容器也會(huì)被刪除。接下來(lái),無(wú)論是運(yùn)行中還是停止的容器,都會(huì)被刪除并從系統(tǒng)中移除。
上面的命令在 Windows Docker 主機(jī)的 PowerShell 終端內(nèi)同樣生效。