數(shù)據(jù)主要分為兩類,持久化的與非持久化的。
持久化數(shù)據(jù)是需要保存的數(shù)據(jù)。例如客戶信息、財(cái)務(wù)、預(yù)定、審計(jì)日志以及某些應(yīng)用日志數(shù)據(jù)。非持久化數(shù)據(jù)是不需要保存的那些數(shù)據(jù)。
兩者都很重要,并且 Docker 均有對(duì)應(yīng)的支持方式。
每個(gè) Docker 容器都有自己的非持久化存儲(chǔ)。非持久化存儲(chǔ)自動(dòng)創(chuàng)建,從屬于容器,生命周期與容器相同。這意味著刪除容器也會(huì)刪除全部非持久化數(shù)據(jù)。
如果希望自己的容器數(shù)據(jù)保留下來(持久化),則需要將數(shù)據(jù)存儲(chǔ)在卷上。卷與容器是解耦的,從而可以獨(dú)立地創(chuàng)建并管理卷,并且卷并未與任意容器生命周期綁定。最終效果即用戶可以刪除一個(gè)關(guān)聯(lián)了卷的容器,但是卷并不會(huì)被刪除。
對(duì)于微服務(wù)設(shè)計(jì)模式來說,容器是不錯(cuò)的選擇。通常與微服務(wù)掛鉤的詞有暫時(shí)以及無(wú)狀態(tài)。所以,微服務(wù)就是無(wú)狀態(tài)的、臨時(shí)的工作負(fù)載,同時(shí)容器即微服務(wù)。因此,我們經(jīng)常會(huì)輕易下結(jié)論,認(rèn)為容器就是用于臨時(shí)場(chǎng)景。
但這種說法是錯(cuò)誤的,而且是大錯(cuò)特錯(cuò)!
毫無(wú)疑問,容器擅長(zhǎng)無(wú)狀態(tài)和非持久化事務(wù)。
每個(gè)容器都被自動(dòng)分配了本地存儲(chǔ)。默認(rèn)情況下,這是容器全部文件和文件系統(tǒng)保存的地方。之前我們可能聽過一些非持久存儲(chǔ)相關(guān)的名稱,如本地存儲(chǔ)、GraphDriver 存儲(chǔ)以及 SnapShotter 存儲(chǔ)。
總之,非持久存儲(chǔ)屬于容器的一部分,并且與容器的生命周期一致,容器創(chuàng)建時(shí)會(huì)創(chuàng)建非持久化存儲(chǔ),同時(shí)該存儲(chǔ)也會(huì)隨容器的刪除而刪除。
在 Linux 系統(tǒng)中,該存儲(chǔ)的目錄在 /var/lib/docker// 之下,是容器的一部分。在 Windows 系統(tǒng)中位于 C\ProgramData\Docker\windowsfilter\ 目錄之下。
如果在生產(chǎn)環(huán)境中使用 Linux 運(yùn)行 Docker,需要確認(rèn)當(dāng)前存儲(chǔ)驅(qū)動(dòng)(GraphDriver)與 Linux 版本是否相符。下面列舉了一些指導(dǎo)建議。
? RedHat Enterprise Linux:Docker 17.06 或者更高的版本中使用 Overlay2 驅(qū)動(dòng)。在更早的版本中,使用 Device Mapper 驅(qū)動(dòng)。這適用于 Oracle Linux 以及其他 Red Hat 相關(guān)發(fā)行版。
? Ubuntu:使用 Overlay2 或者 AUFS 驅(qū)動(dòng)。如果正在使用 Linux4.x 或者更高版本的內(nèi)核,建議使用 Overlay2。
? SUSE Linux Enterprise Server:使用 Btrfs 存儲(chǔ)驅(qū)動(dòng)。
? Windows:Windows 只有一種驅(qū)動(dòng),已經(jīng)默認(rèn)設(shè)置。
上述清單只作為建議。隨著時(shí)間發(fā)展,Overlay2 驅(qū)動(dòng)正在逐漸流行,可能在未來會(huì)成為大多數(shù)平臺(tái)上的推薦存儲(chǔ)驅(qū)動(dòng)。如果使用 Docker 企業(yè)版(EE),并且有技術(shù)支持合約,建議通過咨詢獲取最新的兼容矩陣。
默認(rèn)情況下,容器的所有存儲(chǔ)都使用本地存儲(chǔ)。所以默認(rèn)情況下容器全部目錄都是用該存儲(chǔ)。
在容器中持久化數(shù)據(jù)的方式推薦采用卷。
總體來說,用戶創(chuàng)建卷,然后創(chuàng)建容器,接著將卷掛載到容器上。卷會(huì)掛載到容器文件系統(tǒng)的某個(gè)目錄之下,任何寫到該目錄下的內(nèi)容都會(huì)寫到卷中。即使容器被刪除,卷與其上面的數(shù)據(jù)仍然存在。
如下圖所示,Docker 卷掛載到容器的 /code 目錄。任何寫入 /code 目錄的數(shù)據(jù)都會(huì)保存到卷當(dāng)中,并且在容器刪除后依然存在。

上圖中,/code 目錄是一個(gè) Docker 卷。容器其他目錄均使用臨時(shí)的本地存儲(chǔ)。卷與目錄 /code 之間采用帶箭頭的虛線連接,這是為了表明卷與容器是非耦合的關(guān)系。
Docker 中卷屬于一等公民。拋開其他原因,這意味著卷在 API 中擁有一席之地,并且有獨(dú)立的 docker volume 子命令。
使用下面的命令創(chuàng)建名為 myvol 的新卷。
$ docker volume create myvol
默認(rèn)情況下,Docker 創(chuàng)建新卷時(shí)采用內(nèi)置的 local 驅(qū)動(dòng)。恰如其名,本地卷只能被所在節(jié)點(diǎn)的容器使用。使用 -d 參數(shù)可以指定不同的驅(qū)動(dòng)。
第三方驅(qū)動(dòng)可以通過插件方式接入。這些驅(qū)動(dòng)提供了高級(jí)存儲(chǔ)特性,并為 Docker 集成了外部存儲(chǔ)系統(tǒng)。下圖展示的就是外部存儲(chǔ)系統(tǒng)被用作卷存儲(chǔ)。驅(qū)動(dòng)集成了外部存儲(chǔ)系統(tǒng)到 Docker 環(huán)境當(dāng)中,同時(shí)能使用其高級(jí)特性。

截止到目前為止,已經(jīng)存在 25 種卷插件,涵蓋了塊存儲(chǔ)、文件存儲(chǔ)、對(duì)象存儲(chǔ)等。
? 塊存儲(chǔ):相對(duì)性能更高,適用于對(duì)小塊數(shù)據(jù)的隨機(jī)訪問負(fù)載。目前支持 Docker 卷插件的塊存儲(chǔ)例子包括 HPE 3PAR、Amazon EBS 以及 OpenStack 塊存儲(chǔ)服務(wù)(Cinder)。
? 文件存儲(chǔ):包括 NFS 和 SMB 協(xié)議的系統(tǒng),同樣在高性能場(chǎng)景下表現(xiàn)優(yōu)異。支持 Docker 卷插件的文件存儲(chǔ)系統(tǒng)包括 NetApp FAS、Azure 文件存儲(chǔ)以及 Amazon EFS。
? 對(duì)象存儲(chǔ):適用于較大且長(zhǎng)期存儲(chǔ)的、很少變更的二進(jìn)制數(shù)據(jù)存儲(chǔ)。通常對(duì)象存儲(chǔ)是根據(jù)內(nèi)容尋址,并且性能較低。支持 Docker 卷驅(qū)動(dòng)的例子包括 Amazon S3、Ceph 以及 Minio。
現(xiàn)在卷已經(jīng)創(chuàng)建成功,可以通過 docker volume ls 命令進(jìn)行查看,還可以使用 docker volume inspect 命令查看詳情。
$ docker volume ls
DRIVER VOLUME NAME
local myvol
$ docker volume inspect myvol
[
{
"CreatedAt": "2018-01-12T12:12:10Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/myvol/_data",
"Name": "myvol",
"Options": {},
"Scope": "local"
}
]
inspect 命令輸出中有幾點(diǎn)很有意思。Driver 和 Scope 都是 local。這意味著卷使用默認(rèn) local 驅(qū)動(dòng)創(chuàng)建,只能用于當(dāng)前 Docker 主機(jī)上的容器。Mountpoint 屬性說明卷位于 Docker 主機(jī)上的位置。
在本例中卷位于 Docker 主機(jī)的 /var/lib/docker/volumes/myvol/_data 目錄。在 Windows Docker 主機(jī)上對(duì)應(yīng)內(nèi)容為 Mountpoint": "C:\\ProgramData\\Docker\\ volumes\\myvol\\_data。
使用 local 驅(qū)動(dòng)創(chuàng)建的卷在 Docker 主機(jī)上均有其專屬目錄,在 Linux 中位于 /var/lib/docker/volumes 目錄下,在 Windows 中位于 C:\ProgramData\Docker\volumes 目錄下。
這意味著可以在 Docker 主機(jī)文件系統(tǒng)中查看卷,甚至在 Docker 主機(jī)中對(duì)其進(jìn)行讀取數(shù)據(jù)或者寫入數(shù)據(jù)操作。
可以在 Docker 服務(wù)以及容器中使用 myvol 卷。例如,可以在 docker container run 命令后增加參數(shù) --flag 將卷掛載到新建容器中。稍后通過幾個(gè)例子進(jìn)行說明。
有兩種方式刪除 Docker 卷。
docker volume prune。
docker volume rm。
docker volume prune 會(huì)刪除未裝入到某個(gè)容器或者服務(wù)的所有卷,所以謹(jǐn)慎使用!docker volume rm 允許刪除指定卷。兩種刪除命令都不能刪除正在被容器或者服務(wù)使用的卷。
因?yàn)闆]有使用 myvol 卷,所以請(qǐng)通過 prune 命令進(jìn)行刪除。
$ docker volume prune
WARNING! This will remove all volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Volumes:
myvol
Total reclaimed space: 0B
現(xiàn)在已經(jīng)完成創(chuàng)建、查看以及刪除卷的操作了。上述操作均未涉及容器,這也驗(yàn)證了卷是獨(dú)立的這一特性。
到目前,讀者已經(jīng)了解了卷的創(chuàng)建、列表、查看以及刪除命令。此外,還可以通過在 Dockerfile 中使用 VOLUME 指令的方式部署卷。具體的格式為 VOLUME
但是,在 Dockerfile 中無(wú)法指定主機(jī)目錄。這是因?yàn)橹鳈C(jī)目錄通常情況下是相對(duì)主機(jī)的一個(gè)目錄,意味著這個(gè)目錄在不同主機(jī)間會(huì)變化,并且可能導(dǎo)致構(gòu)建失敗。如果通過 Dockerfile 指定,那么每次部署時(shí)都需要指定主機(jī)目錄。
通過前面的學(xué)習(xí)我們已經(jīng)了解了卷相關(guān)的基本命令,接下來看一下如何在容器和服務(wù)中使用卷。
接下來的內(nèi)容是基于某個(gè)沒有卷的系統(tǒng),演示內(nèi)容適用于 Linux 和 Windows。
使用下面的命令創(chuàng)建一個(gè)新的獨(dú)立容器,并掛載一個(gè)名為 bizvol 的卷。
Windows 示例如下。
所有的 Windows 示例都在 PowerShell 中執(zhí)行,請(qǐng)注意反引號(hào)(`)用于將命令拆至多行。
> docker container run -dit --name voltainer `
--mount source=bizvol,target=c:\vol `
microsoft/powershell:nanoserver
即使系統(tǒng)中沒有叫作 bizvol 的卷,命令也應(yīng)該能夠成功運(yùn)行。這里引出了很有意思的一點(diǎn)。如果指定了已經(jīng)存在的卷,Docker 會(huì)使用該卷。如果指定的卷不存在,Docker 會(huì)創(chuàng)建一個(gè)卷。
在當(dāng)前示例中,bizvol 這個(gè)卷并不存在,所以 Docker 新建一個(gè)卷并掛載到新容器內(nèi)部。這意味著讀者可以通過 docker volume ls 命令看到該卷。
$ docker volume ls
DRIVER VOLUME NAME
local bizvol
盡管容器和卷各自擁有獨(dú)立的生命周期,Docker 也不允許刪除正在被容器使用的卷。
$ docker volume rm bizvol
Error response from daemon: unable to remove volume: volume is in use -
[b44 d3f82...dd2029ca]
目前卷是空的。執(zhí)行 exec 連接到容器并向卷中寫入一部分?jǐn)?shù)據(jù)。示例引用的是 Linux,如果讀者使用 Windows 示例,則需要將 docker container exec 命令結(jié)尾的 sh 替換為 pwsh.exe。其他命令在 Linux 和 Windows 上面均可以生效。
$ docker container exec -it voltainer sh
/# echo "I promise to write a review of the book on Amazon" > /vol/file1
/# ls -l /vol
total 4
-rw-r--r-- 1 root root 50 Jan 12 13:49 file1
/# cat /vol/file1
I promise to write a review of the book on Amazon
輸入 exit 命令返回到 Docker 主機(jī) Shell 中,然后使用下面命令刪除容器。
$ docker container rm voltainer -f
voltainer
即使容器被刪除,卷依舊存在。
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS
$ docker volume ls
DRIVER VOLUME NAME
local bizvol
由于卷仍然存在,因此可以進(jìn)入到其在主機(jī)的掛載點(diǎn)并查看前面寫入的數(shù)據(jù)是否還在。
在 Docker 主機(jī)的終端上執(zhí)行下面的命令。第一條命令會(huì)證明文件依然存在,第二條命令展示了文件的內(nèi)容。
如果是 Windows 示例,一定要使用 C:\ProgramData\Docker\volumes\bizvol\_data 目錄。
$ ls -l /var/lib/docker/volumes/bizvol/_data/
total 4
-rw-r--r-- 1 root root 50 Jan 12 14:25 file1
$ cat /var/lib/docker/volumes/bizvol/_data/file1
I promise to write a review of the book on Amazon
根據(jù)運(yùn)行結(jié)果可以看出,卷和數(shù)據(jù)都還在。
甚至將 bizvol 掛載到一個(gè)新的服務(wù)或者容器都是可以的。下面的命令會(huì)創(chuàng)建一個(gè)名為 hellcat 的新 Docker 服務(wù),并且將 bizvol 掛載到該服務(wù)副本的 /vol 目錄。
$ docker service create \
--name hellcat \
--mount source=bizvol,target=/vol \
alpine sleep 1d
overall progress: 1 out of 1 tasks
1/1: running [====================================>]
verify: Service converged
上述命令沒有指定 --replicas 參數(shù),所以服務(wù)只會(huì)部署一個(gè)副本。找到 Swarm 集群中運(yùn)行了該服務(wù)的節(jié)點(diǎn)。
$ docker service ps hellcat
ID NAME NODE DESIRED STATE CURRENT STATE
l3nh... hellcat.1 node1 Running Running 19 seconds ago
在本例中,副本運(yùn)行在 node1 節(jié)點(diǎn)上。登錄到 node1 節(jié)點(diǎn),然后獲取服務(wù)副本容器 ID。
node1$ docker container ls
CTR ID IMAGE COMMAND STATUS NAMES
df6..a7b alpine:latest "sleep 1d" Up 25 secs hellcat.1.l3nh...
注意,容器的名稱包括了 service-name、replica-number 以及 replica-ID,采用句號(hào)分隔。
登錄到該容器并檢查數(shù)據(jù)是否在 /vol 中。在 exec 例子中會(huì)使用服務(wù)副本的容器 ID。如果使用 Windows 示例,記得將 sh 替換為 pwsh.exe。
node1$ docker container exec -it df6 sh
/# cat /vol/file1
I promise to write a review of the book on Amazon
這樣卷中保存了原始數(shù)據(jù),并且在新容器中也可以使用。
Docker 能夠集成外部存儲(chǔ)系統(tǒng),使得集群間節(jié)點(diǎn)共享外部存儲(chǔ)數(shù)據(jù)變得簡(jiǎn)單。例如,獨(dú)立存儲(chǔ) LUN 或者 NFS 共享可以應(yīng)用到多個(gè) Docker 主機(jī),因此無(wú)論容器或者服務(wù)副本運(yùn)行在哪個(gè)節(jié)點(diǎn)上,都可以共享該存儲(chǔ)。下圖展示了位于共享存儲(chǔ)的卷被兩個(gè) Docker 節(jié)點(diǎn)共享的場(chǎng)景。接下來這些 Docker 節(jié)點(diǎn)可以將共享卷應(yīng)用到容器之上。

構(gòu)建這樣的環(huán)境需要外部存儲(chǔ)系統(tǒng)的相關(guān)知識(shí),并了解應(yīng)用如何從共享存儲(chǔ)讀取或者寫入數(shù)據(jù)。這種配置主要關(guān)注數(shù)據(jù)損壞(Data Corruption)。
基于下圖,設(shè)想下面的場(chǎng)景:Node 1 上的容器 A 在共享卷中更新了部分?jǐn)?shù)據(jù)。但是為了快速返回,數(shù)據(jù)實(shí)際寫入了本地緩存而不是卷中。此時(shí),容器 A 認(rèn)為數(shù)據(jù)已經(jīng)更新。但是,在 Node 1 的容器 A 將緩存數(shù)據(jù)刷新并提交到卷前,Node 2 的容器 B 更新了相同部分的數(shù)據(jù),但是值不同,并且更新方式為直接寫入卷中。
此時(shí),兩個(gè)容器均認(rèn)為自己已經(jīng)將數(shù)據(jù)寫入卷中,但實(shí)際上只有容器 B 寫入了。容器 A 會(huì)在稍后將自己的緩存數(shù)據(jù)寫入緩存,覆蓋了 Node 2 的容器 B 所做的一些變更。但是 Node 2 上的容器 B 對(duì)此一無(wú)所知。數(shù)據(jù)損壞就是這樣發(fā)生的。
為了避免這種情況,需要在應(yīng)用程序中進(jìn)行控制。