Docker 在容器中儲存資料
瞭解 Docker 如何構建和儲存映象以及容器如何使用這些映象對高效使用儲存驅動程式來說很重要。可以使用這些資訊在選擇持久化應用程式資料的最佳方式做出明智的選擇,並避免出現效能問題。
注意:儲存驅動程式允許你將資料儲存在容器的可寫層中。這是持久化資料的效率最低的方式。卷或繫結掛載提供更好的讀寫效能,卷提供比儲存驅動程式或繫結掛載更高的安全性和隔離性。卷或繫結掛載都不適合本主題中描述的大多數概念。
1. 映象和層
Docker 映象由一系列層組成。每個層代表映象的 Dockerfile 中的一條指令。除了最後一個層之外,每個層都是隻讀了。例如下面這個 Dockerfile:
FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py
這個 Dockerfile 包含四條指令,每條指令建立一個層。FROM
語句從 ubuntu:15.04
映象建立一個圖層開始。COPY
命令新增 Docker 客戶端當前目錄中的一些檔案。RUN
命令使用 make
命令構建您的應用程式。最後,最後一層指定在容器內執行的命令。
每個層與之前的層只有一部分差異。這些層堆疊在一起。當你建立一個新的容器時,你在底層上新增一個新的可寫層。這個層通常被稱為“容器層”。對正在執行的容器所做的所有更改(如寫入新檔案,修改現有檔案和刪除檔案)都會寫入此可寫容器層。下圖顯示了一個基於 Ubuntu 15.04 映象的容器。
儲存驅動程式處理有關這些層相互互動方式的詳細資訊。有不同的儲存驅動程式可以使用,在不同情況下它們各有優點和缺點。
2. 容器和層
容器和映象之間的主要區別是頂部的可寫層。所有對容器新增新的或修改現有資料的內容都儲存在該可寫層中。當容器被刪除時,可寫層也被刪除。底層映象保持不變。
由於每個容器都有自己的可寫的容器層,並且所有更改都儲存在此容器層中,因此多個容器可以共享對相同基礎映象的訪問許可權,並且擁有自己的資料狀態。下圖顯示了共享相同 Ubuntu 15.04 映象的多個容器。
注意:如果需要多個映象共享訪問完全相同的資料,請將此資料儲存在 Docker 卷中並將其裝入容器中。
Docker 使用儲存驅動程式來管理映象層和可寫容器層的內容。每個儲存驅動程式都處理實現的方式不同,但所有驅動程式都使用可堆疊的映象層和寫入時複製(CoW)策略。
3. 磁碟上的容器大小
通過 docker ps -s
命令檢視執行中的容器的準確大小。有兩個不同的列和這個大小相關。
size
:磁碟上用於每個容器的可寫層的資料大小。virtual size
:用於容器使用的只讀映象資料的資料量加上容器的可寫層大小。多個容器可能共享部分或全部只讀映象資料。從同一映象開始的兩個容器 100% 共享只讀資料,而如果兩個容器使用的不同映象具有公共層,則共享這些共同層。因此,你不能只總計virtual size
。這會高估可能磁碟總使用量。
磁碟上所有正在執行的容器使用的總磁碟空間是每個容器的 size
和 virtual size
值的組合。如果多個容器從相同的精確映象啟動,則這些容器在磁碟上的總大小將為(容器的 size
之和)加上一個容器的(virtual size
- size
)。
這不會計算容器以下面的方式佔用的磁碟空間:
- 如果使用
json-file
日誌驅動程式時用於日誌檔案的磁碟空間。如果容器生成大量日誌資料並且未配置日誌輪轉,這可能並不重要。(This can be non-trivial if your container generates a large amount of logging data and log rotation is not configured) - 用於容器的卷和繫結掛載。
- 用於容器配置檔案的磁碟空間,通常比較小。
- 如果開啟 swap 時,寫入磁碟的記憶體資料。
- Checkpoint,如果使用了實驗性的 checkpoint/restore 特徵。
4. 寫時複製(CoW)策略
寫入時複製是一種共享和複製檔案以實現最高效率的策略。如果檔案或目錄存在於映象的較低層中,而另一層(包括可寫層)需要對其進行讀取訪問,則它只使用現有檔案。第一次需要修改檔案時(構建映象或執行容器時),該檔案將被複制到該層並進行修改。這最大限度地減少了每個後續層的 I/O
和大小。下面將更深入地解釋這些優點。
4.1 共享有利於縮小映象體積
當使用 docker pull
從倉庫中下載映象時,或者用本地還不存在的映象建立容器時,會獨立下載每個層並存儲在 Docker 的本地儲存區域中(通常是 Linux 主機的 /var/lib/docker/
)。在這個例子中你可以看到這些層被下載:
$ docker pull ubuntu:15.04
15.04: Pulling from library/ubuntu
1ba8ac955b97: Pull complete
f157c4e5ede7: Pull complete
0b7e98f84c4c: Pull complete
a3ed95caeb02: Pull complete
Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e
Status: Downloaded newer image for ubuntu:15.04
每個層儲存在 Docker 主機本地儲存區域中它們自己的目錄中。要檢查檔案系統上的層,列出 /var/lib/docker/<storage-driver>/layers/
中的內容即可。這個例子使用了 aufs
,這是預設的儲存驅動程式:
$ ls /var/lib/docker/aufs/layers
1d6674ff835b10f76e354806e16b950f91a191d3b471236609ab13a930275e24
5dbb0cbe0148cf447b9464a358c1587be586058d9a4c9ce079320265e2bb94e7
bef7199f2ed8e86fa4ada1309cfad3089e0542fec8894690529e4c04a7ca2d73
ebf814eccfe98f2704660ca1d844e4348db3b5ccc637eb905d4818fbfb00a06a
目錄名和層的 ID 無關(從 Docker 1.10 開始)。
現在假設你有兩個不同的 Dockerfile。使用第一個來建立名為 acme/my-base-image:1.0
的映象。
FROM ubuntu:16.10
COPY . /app
第二個基於 acme/my-base-image:1.0
,但增加了額外的層:
FROM acme/my-base-image:1.0
CMD /app/hello.sh
第二個映象包含來自第一個映象的所有的層,增加了一個通過 CMD
指令建立的新層和一個可讀寫的容器層。Docker 已經從第一個映象獲取所有的層,因此不需要再次下載。兩個映象會共享公共的層。
如果用這兩個 Dockerfile 構建映象,可以通過 docker image ls
和 docker history
命令來驗證共享層的加密 ID 是否相同。
1. 建立並進入 cow-test/
目錄
2. 在 cow-test/
目錄中,建立新檔案並填入下面內容:
#!/bin/sh
echo "Hello world"
儲存檔案並新增可執行許可權:
chmod +x hello.sh
3. 複製上面的第一個 Dockerfile 檔案的內容到新建立的 Dockerfile.base 檔案中。
4. 複製上面的第二個 Dockerfile 檔案的內容到新建立的 Dockerfile 檔案中。
5. 在 cow-test/
目錄中,構建第一個映象。別忘了在命令最後有一個 .
。這會設定 PATH 指示 Docker 在哪裡查詢要新增到映象中的檔案。
$ docker build -t acme/my-base-image:1.0 -f Dockerfile.base .
Sending build context to Docker daemon 4.096kB
Step 1/2 : FROM ubuntu:16.10
---> 31005225a745
Step 2/2 : COPY . /app
---> Using cache
---> bd09118bcef6
Successfully built bd09118bcef6
Successfully tagged acme/my-base-image:1.0
6. 構建第二個映象。
$ docker build -t acme/my-final-image:1.0 -f Dockerfile .
Sending build context to Docker daemon 4.096kB
Step 1/2 : FROM acme/my-base-image:1.0
---> bd09118bcef6
Step 2/2 : CMD /app/hello.sh
---> Running in a07b694759ba
---> dbf995fc07ff
Removing intermediate container a07b694759ba
Successfully built dbf995fc07ff
Successfully tagged acme/my-final-image:1.0
7. 檢查映象的大小:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
acme/my-final-image 1.0 dbf995fc07ff 58 seconds ago 103MB
acme/my-base-image 1.0 bd09118bcef6 3 minutes ago 103MB
8. 檢查構成每個映象的層:
$ docker history bd09118bcef6
IMAGE CREATED CREATED BY SIZE COMMENT
bd09118bcef6 4 minutes ago /bin/sh -c #(nop) COPY dir:35a7eb158c1504e... 100B
31005225a745 3 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 3 months ago /bin/sh -c mkdir -p /run/systemd && echo '... 7B
<missing> 3 months ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\... 2.78kB
<missing> 3 months ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B
<missing> 3 months ago /bin/sh -c set -xe && echo '#!/bin/sh' >... 745B
<missing> 3 months ago /bin/sh -c #(nop) ADD file:eef57983bd66e3a... 103MB
$ docker history dbf995fc07ff
IMAGE CREATED CREATED BY SIZE COMMENT
dbf995fc07ff 3 minutes ago /bin/sh -c #(nop) CMD ["/bin/sh" "-c" "/a... 0B
bd09118bcef6 5 minutes ago /bin/sh -c #(nop) COPY dir:35a7eb158c1504e... 100B
31005225a745 3 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 3 months ago /bin/sh -c mkdir -p /run/systemd && echo '... 7B
<missing> 3 months ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\... 2.78kB
<missing> 3 months ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B
<missing> 3 months ago /bin/sh -c set -xe && echo '#!/bin/sh' >... 745B
<missing> 3 months ago /bin/sh -c #(nop) ADD file:eef57983bd66e3a... 103MB
注意,除第二個映象的頂層外,所有層都是相同的。所有其他層都在這兩個映象之間共享,並且只在 /var/lib/docker/
中儲存一次。新層實際上並不佔用任何空間,因為它不會更改任何檔案,而只是執行一個命令。
注意:
docker history
的輸出中的<missing>
行指示那些層在其他系統上構建並且已經不可用了。可以忽略這些。
4.2 複製使容器高效
在啟動容器時,一個很小的可寫容器層會新增到所有層之上。容器中的對檔案系統的任何變化都會儲存到這個可寫容器層。任何容器不支援的檔案都不會複製到這個可寫層。這意味著可寫層會盡可能小。
當容器中的現有檔案被修改時,儲存驅動程式會執行寫時複製操作。實現步驟取決於具體的儲存驅動程式。對於預設的 aufs
、overlay
和 overlay2
驅動程式,寫時複製操作按照下面的順序:
- 在所有映象層搜尋要更新的檔案。該過程從最新層開始,一次處理一層,直到基礎層。找到結果後,它們將被新增到快取中以加速後面的操作。
- 對找到的檔案的第一個副本執行
copy_up
操作,將檔案複製到容器的可寫層。 - 任何修改都只會作用於該檔案的副本,並且該容器不能看到存在於較低層中的檔案的只讀副本。
Btrfs、ZFS 和其他的驅動程式以不同的方式處理寫時複製。可以在後面的介紹中閱讀更多資訊。
寫大量資料的容器比其他容器消耗更多的空間。這是因為大多數寫入操作會在容器的可寫頂層中佔用新的空間。
注意:對於大量寫入的應用程式,不應將資料儲存在容器中。相反,請使用 Docker 卷,這些卷獨立於正在執行的容器,並且設計為對 I/O 有效。另外,卷可以在容器間共享,並且不會增加容器可寫層的大小。
copy_up
操作可能會導致顯著的效能開銷。這個開銷根據使用的儲存驅動程式而不同。大檔案,大量層和深層目錄樹會使影響更加明顯。因為每個 copy_up
操作僅在第一次修改給定檔案時發生,所以效能消耗可以緩解一部分。
為了驗證寫入時複製的工作方式,以下過程根據我們之前構建的 acme/my-final-image:1.0
映象啟動了 5 個容器,並檢查它們佔用的空間。
注意:下面的過程只能在 Linux 版本的 Docker 上工作。
1. 在 Docker 主機的終端中執行 docker run
命令。最後的字串是每個容器的 ID。
$ docker run -dit --name my_container_1 acme/my-final-image:1.0 bash \
&& docker run -dit --name my_container_2 acme/my-final-image:1.0 bash \
&& docker run -dit --name my_container_3 acme/my-final-image:1.0 bash \
&& docker run -dit --name my_container_4 acme/my-final-image:1.0 bash \
&& docker run -dit --name my_container_5 acme/my-final-image:1.0 bash
c36785c423ec7e0422b2af7364a7ba4da6146cbba7981a0951fcc3fa0430c409
dcad7101795e4206e637d9358a818e5c32e13b349e62b00bf05cd5a4343ea513
1e7264576d78a3134fbaf7829bc24b1d96017cf2bc046b7cd8b08b5775c33d0c
38fa94212a419a082e6a6b87a8e2ec4a44dd327d7069b85892a707e3fc818544
1a174fc216cccf18ec7d4fe14e008e30130b11ede0f0f94a87982e310cf2e765
2. 執行 docker ps
命令來驗證 5 個容器都在執行
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1a174fc216cc acme/my-final-image:1.0 "bash" About a minute ago Up About a minute my_container_5
38fa94212a41 acme/my-final-image:1.0 "bash" About a minute ago Up About a minute my_container_4
1e7264576d78 acme/my-final-image:1.0 "bash" About a minute ago Up About a minute my_container_3
dcad7101795e acme/my-final-image:1.0 "bash" About a minute ago Up About a minute my_container_2
c36785c423ec acme/my-final-image:1.0 "bash" About a minute ago Up About a minute my_container_1
3. 列出本地儲存區域的內容
$ sudo ls /var/lib/docker/containers
1a174fc216cccf18ec7d4fe14e008e30130b11ede0f0f94a87982e310cf2e765
1e7264576d78a3134fbaf7829bc24b1d96017cf2bc046b7cd8b08b5775c33d0c
38fa94212a419a082e6a6b87a8e2ec4a44dd327d7069b85892a707e3fc818544
c36785c423ec7e0422b2af7364a7ba4da6146cbba7981a0951fcc3fa0430c409
dcad7101795e4206e637d9358a818e5c32e13b349e62b00bf05cd5a4343ea513
4. 檢查大小
$ sudo du -sh /var/lib/docker/containers/*
32K /var/lib/docker/containers/1a174fc216cccf18ec7d4fe14e008e30130b11ede0f0f94a87982e310cf2e765
32K /var/lib/docker/containers/1e7264576d78a3134fbaf7829bc24b1d96017cf2bc046b7cd8b08b5775c33d0c
32K /var/lib/docker/containers/38fa94212a419a082e6a6b87a8e2ec4a44dd327d7069b85892a707e3fc818544
32K /var/lib/docker/containers/c36785c423ec7e0422b2af7364a7ba4da6146cbba7981a0951fcc3fa0430c409
32K /var/lib/docker/containers/dcad7101795e4206e637d9358a818e5c32e13b349e62b00bf05cd5a4343ea513
每個容器都只佔用了檔案系統上的 32k 儲存空間。
寫時複製不僅節省空間,還縮短了啟動時間。當你啟動一個容器(或者來自同一個映象的多個容器)時,Docker 只需要建立可寫的容器層。
如果 Docker 在每次啟動一個新容器時都必須製作底層映象堆疊的整個副本,則容器啟動時間和使用的磁碟空間將顯著增加。這與虛擬機器的工作方式類似,每個虛擬機器具有一個或多個虛擬磁碟。
5. 資料卷和儲存驅動程式
容器刪除後,任何寫到容器但沒有儲存到資料卷的資料都會和容器一同刪除。
資料卷是 Docker 主機檔案系統上的一個目錄或檔案,直接掛載到容器中。資料卷不被儲存驅動程式控制。對資料卷的讀寫操作繞過儲存驅動程式,並以本地主機速度執行。可以將任意數量的資料卷裝入容器。多個容器也可以共享一個或多個數據卷。
下圖顯示了一個執行兩個容器的獨立 Docker 主機。每個容器都存在於 Docker 主機本地儲存區(/var/lib/docker/...
)內的其自己的地址空間內。在 Docker 主機的 /data
上還有一個共享資料卷。這被直接安裝到兩個容器中。
資料卷位於 Docker 主機本地儲存區域之外,進一步增強了它們與儲存驅動程式控制的獨立性。當容器被刪除時,儲存在資料卷中的任何資料都會保留在 Docker 主機上。