第4章 存儲問題

4.1 容器磁盤能夠限制配額麼?

對於 devicemapper, btrfs, zfs 來講,能夠經過 --storage-opt size=100G 這種形式限制 rootfs 的大小。php

docker create -it --storage-opt size=120G fedora /bin/bash

參考官網文檔:https://docs.docker.com/engine/reference/commandline/run/#/set-storage-driver-options-per-container前端

4.2 容器內的數據該保存在鏡像裏仍是物理機裏?

若是所謂數據是指運行時動態的數據,那麼這部分數據文件不該該保存於鏡像內。在運行時要保持容器基礎文件不可變的特性,而變化部分使用掛載宿主目錄,或者數據捲來解決。
建議看一下官網 docker volume 的文檔:https://docs.docker.com/engine/tutorials/dockervolumes/node

4.3 看到總說要保持容器無狀態,那什麼是無狀態?

這裏說到的有兩個層面的無狀態:
容器存儲層的無狀態
這裏提到的存儲層是指用於存儲鏡像、容器各個層的存儲,通常是 Union FS,如 AUFS,或者是使用塊設備的一些機制(如 snapshot )進行模擬,如 devicemapper。
Union FS 這類存儲系統,至關因而在現有存儲上,再加一層或多層存儲,這類存儲的讀寫性能並很差。而且對於 CentOS 這類只能使用 devicemapper 的系統而言,存儲層的讀寫還常常出 bug。所以,在 Docker 使用過程當中,要避免存儲層的讀寫。頻繁讀寫的部分,應該使用卷。須要持久化的部分,可使用命名捲進行持久化。因爲命名卷的生存週期和容器不一樣,容器消亡重建,卷不會跟隨消亡。因此容器能夠隨便刪了從新run,而其掛載的卷則會保持以前的數據。
服務層面的無狀態
使用卷持久化容器狀態,雖然從存儲層的角度看,是無狀態的,可是從服務層面看,這個服務是有狀態的。
從服務層面上說,也存在無狀態服務。就是說服務自己不須要寫入任何文件。好比前端 nginx,它不須要寫入任何文件(日誌走Docker日誌驅動),中間的 php, node.js 等服務,可能也不須要本地存儲,它們所需的數據都在 redis, mysql, mongodb 中了。這類服務,因爲不須要卷,也不發生本地寫操做,刪除、重啓、不保存自身狀態,並不影響服務運行,它們都是無狀態服務。這類服務因爲不須要狀態遷移,不須要分佈式存儲,所以它們的集羣調度更方便。
以前沒有 docker volume 的時候,有些人說 Docker 只能夠支持無狀態服務,緣由就是隻看到了存儲層需求無狀態,而沒有 docker volume 的持久化解決方案。
如今這個說法已經不成立,服務能夠有狀態,狀態持久化用 docker volume。
當服務能夠有狀態後,若是使用默認的 local 卷驅動,而且使用本地存儲進行狀態持久化的狀況,單機服務、容器的再調度運行沒有問題。可是顧名思義,使用本地存儲的卷,只能夠爲當前主機提供持久化的存儲,而沒法跨主機。
但這只是使用默認的 local 驅動,而且使用 本地存儲 而已。使用分佈式/共享存儲就能夠解決跨主機的問題。docker volume 天然支持不少分佈式存儲的驅動,好比 flocker、glusterfs、ceph、ipfs 等等。經常使用的插件列表能夠參考官方文檔:https://docs.docker.com/engine/extend/legacy_plugins/#/volume-pluginspython

4.4 數據容器、數據卷、命名卷、匿名卷、掛載目錄這些都有什麼區別?

首先,掛載分爲掛載本地宿主目錄 和 掛載數據卷(Volume)。而數據卷又分爲匿名數據卷和命名數據卷。
綁定宿主目錄的概念很容易理解,就是將宿主目錄綁定到容器中的某個目錄位置。這樣容器能夠直接訪問宿主目錄的文件。其形式是mysql

docker run -d -v /var/www:/app nginx

這裏注意到 -v 的參數中,前半部分是絕對路徑。在 docker run 中必須是絕對路徑,而在 docker-compose 中,能夠是相對路徑,由於 docker-compose 會幫你補全路徑。nginx

另外一種形式是使用 Docker Volume,也就是數據卷。這是不少看古董書的人不瞭解的概念,不要跟數據容器(Data Container)弄混。數據卷是 Docker 引擎維護的存儲方式,使用 docker volume create 命令建立,能夠利用卷驅動支持多種存儲方案。其默認的驅動爲 local,也就是本地卷驅動。本地驅動支持命名卷和匿名卷。web

顧名思義,命名卷就是有名字的卷,使用 docker volume create --name xxx 形式建立並命名的卷;而匿名卷就是沒名字的卷,通常是 docker run -v /data 這種不指定卷名的時候所產生,或者 Dockerfile 裏面的定義直接使用的。
有名字的卷,在用過一次後,之後掛載容器的時候還可使用,由於有名字能夠指定。因此通常須要保存的數據使用命名卷保存。redis

而匿名卷則是隨着容器創建而創建,隨着容器消亡而淹沒於卷列表中(對於 docker run 匿名卷不會被自動刪除)。對於二代 Swarm 服務而言,匿名卷會隨着服務刪除而自動刪除。 所以匿名卷只存放可有可無的臨時數據,隨着容器消亡,這些數據將失去存在的意義。sql

此外,還有一個叫數據容器 (Data Container) 的概念,也就是使用 --volumes-from 的東西。這早就不用了,若是看了書還在說這種方式,那說明書已通過時了。按照今天的理解,這類數據容器,無非就是掛了個匿名卷的容器罷了。mongodb

在 Dockerfile 中定義的掛載,是指 匿名數據卷。Dockerfile 中指定 VOLUME 的目的,只是爲了將某個路徑肯定爲卷。

咱們知道,按照最佳實踐的要求,不該該在容器存儲層內進行數據寫入操做,全部寫入應該使用卷。若是定製鏡像的時候,就能夠肯定某些目錄會發生頻繁大量的讀寫操做,那麼爲了不在運行時因爲用戶疏忽而忘記指定卷,致使容器發生存儲層寫入的問題,就能夠在 Dockerfile 中使用 VOLUME 來指定某些目錄爲匿名卷。這樣即便用戶忘記了指定卷,也不會產生不良的後果。

這個設置能夠在運行時覆蓋。經過 docker run 的 -v 參數或者 docker-compose.yml 的 volumes 指定。使用命名卷的好處是能夠複用,其它容器能夠經過這個命名數據卷的名字來指定掛載,共享其內容(不過要注意併發訪問的競爭問題)。

好比,Dockerfile 中說 VOLUME /data,那麼若是直接 docker run,其 /data 就會被掛載爲匿名卷,向 /data 寫入的操做不會寫入到容器存儲層,而是寫入到了匿名卷中。

可是若是運行時 docker run -v mydata:/data,這就覆蓋了 /data 的掛載設置,要求將 /data 掛載到名爲 mydata 的命名卷中。

因此說 Dockerfile 中的 VOLUME 其實是一層保險,確保鏡像運行能夠更好的遵循最佳實踐,不向容器存儲層內進行寫入操做。

數據卷默承認能會保存於 /var/lib/docker/volumes,不過通常不須要、也不該該訪問這個位置。

4.5 卷和掛載目錄有什麼區別?

卷 (Docker Volume) 是受控存儲,是由 Docker 引擎進行管理維護的。所以使用卷,你能夠沒必要處理 uid、SELinux 等各類權限問題,Docker 引擎在創建卷時會自動添加安全規則,以及根據掛載點調整權限。而且能夠統一列表、添加、刪除。另外,除了本地卷外,還支持網絡卷、分佈式卷。

而掛載目錄那就沒人管了,屬於用戶自行維護。你就必須手動處理全部權限問題。特別是在 CentOS 上,不少人碰到 Permission Denied,就是由於沒有使用卷,而是掛載目錄,並且還對 SELinux 安全權限一無所知致使。

4.6 爲何綁定了宿主的文件到容器,宿主修改了文件,容器內看到的仍是舊的內容啊?

在綁定宿主內容的形式中,有一種特殊的形式,就是綁定宿主文件,既:

docker run -d -v $PWD/myapp.ini:/app/app.ini myapp

在 myapp.ini 文件不發生改變的狀況下,這樣的綁定是和綁定宿主目錄性質同樣,一樣是將宿主文件綁定到容器內部,容器內能夠看到這個文件。可是,一旦文件發生改變,狀況則有不一樣。

簡單的文件修改,好比 echo "name = jessie" >> myapp.ini,這類修改依舊仍是原來的文件,宿主(或容器)對文件進行的改動,另外一方是能夠看到的。

而複雜的文件操做,好比使用 vim,或者其它編輯器編輯文件,則頗有可能會致使一方的修改,另外一方看不到。

其緣由是這類編輯器在保存文件的時候,常常會採用一種避免寫入過程當中發生故障而致使文件丟失的策略,既先把內容寫到一個新的文件中去,寫好了後,再刪除舊的文件,而後把新文件更名爲舊的文件名,從而完成保存的操做。從這個操做流程能夠看出,雖然修改後的文件的名字和過去同樣,但對於文件系統而言是一個新的文件了。換句話說,雖然是同名文件,可是舊的文件的 inode 和修改後的文件的 inode 不一樣。

$ ls -i
268541 hello.txt
$ vi hello.txt
$ ls -i
268716 hello.txt

如上面的例子能夠看到,通過 vim 編輯文件後,inode 從 268541 變爲了 268716,這就是剛纔說的,名字仍是那個名字,文件已不是原來的文件了。
而 Docker 的 綁定宿主文件,實際上在文件系統眼裏,針對的是 inode,而不是文件名。所以容器內所看到的,依舊是以前舊的 inode 對應的那個文件,也就是舊的內容。
這就出現了以前的那個問題,在宿主內修改綁定文件的內容,結果發現容器內看不到改變,其緣由就在於宿主的那個文件已不是原來的文件了😂。
這類問題解決辦法很簡單,若是文件可能改變,那麼就不要綁定宿主文件,而是綁定一個宿主目錄,這樣只要目錄不跑,裏面文件愛咋改就咋改😁。

4.7 多個 Docker 容器之間共享數據怎麼辦?NFS ?

若是是同一個宿主,那麼能夠綁定同一個數據卷,固然,程序上要處理好併發問題。
若是是不一樣宿主,則可使用分佈式數據卷驅動,讓分佈在不一樣宿主的容器均可以訪問到的分佈式存儲的位置。如S3之類:
https://docs.docker.com/engine/extend/plugins/#volume-plugins

4.8 既然一個容器一個應用,那麼我想在該容器中用計劃任務 cron 怎麼辦?

cron 實際上是另外一個服務了,因此應該另起一個容器來進行,如需訪問該應用的數據文件,那麼能夠共享該應用的數據卷便可。而 cron 的容器中,cron 之前臺運行便可。
好比,咱們但願有個 python 腳本能夠定時執行。那麼能夠這樣構建這個容器。
首先基於 python 的鏡像定製:

FROM python:3.5.2
ENV TZ=Asia/Shanghai
RUN apt-get update \
&& apt-get install -y cron \
&& apt-get autoremove -y
COPY ./cronpy /etc/cron.d/cronpy
CMD ["cron", "-f"]

其中所說起的 cronpy 就是咱們須要計劃執行的 cron 腳本。

* * * * * root /app/task.py >> /var/log/task.log 2>&1

在這個計劃中,咱們但願定時執行 /app/task.py 文件,日誌記錄在 /var/log/task.log 中。這個 task.py 是一個很是簡單的文件,其內容只是輸出個時間而已。

#!/usr/local/bin/python
from datetime import datetime
print("Cron job has run at {0} with environment variable ".format(str(datetime.now())))
這 task.py 能夠在構建鏡像時放進去,也能夠掛載宿主目錄。在這裏,我以掛載宿主目錄舉例。
# 構建鏡像
docker build -t cronjob:latest .
# 運行鏡像
docker run \
--name cronjob \
-d \
-v $(pwd)/task.py:/app/task.py \
-v $(pwd)/log/:/var/log/ \
cronjob:latest

須要注意的是,應該在構建主機上賦予 task.py 文件可執行權限。

4.9 如何初始化卷?

卷(Volume),是用於動態數據持久化的。所以其內存儲的都是動態數據,運行時會變化。若是這裏面須要初始化裏面的數據,須要在運行時進行。或者在鏡像里加入初始化的腳本,好比 mysql 鏡像中的初始化目錄中的腳本;或者本身單獨製做純粹用於初始化卷用的鏡像,單獨一次性運行以將初始化數據灌入卷中。
舉個例子來講,假設你須要個卷 mydata,而後裏面須要有個 hello.txt 文件是必須存在的,不然容器運行就要出大事兒了……(這需求很傻我知道……😅好吧,假設如此)。
固然,咱們得先有這個卷。

docker volume create --name mydata

那怎麼把這個超重要的 hello.txt 文件放入卷中呢?有幾種辦法。

正常掛載該 mydata 卷,而後 docker cp 進去
這是個很傻的辦法,不過若是容器運行並不依賴於 hello.txt 的話,這樣作是能夠的。

$ docker run -d --name web -v mydata:/data nginx
$ docker cp ./hello.txt web:/data/

這樣是先讓容器啓動,啓動後,再把所需數據導入卷裏面去。之後容器就可使用 /data/hello.txt 文件了。
可是,若是容器是嚴重依賴於這個 hello.txt 文件的話,這樣作就會出問題。容器會由於 hello.txt 文件不存在,而報錯退出,致使根本沒有 docker cp 的機會。
這種狀況,咱們能夠變通一下。

$ docker run --rm \
-v $PWD:/source \
-v mydata:/data \
busybox \
cp /source/hello.txt /data/
$ docker run -d --name web -v mydata:/data nginx

這裏咱們先啓動了一個 busybox 容器,分別掛載要複製的源以及目標的 mydata 卷,而後用 cp 命令將 hello.txt 複製到 mydata 中去。數據導入結束後,咱們再正式掛載 mydata 捲到正式的容器上並啓動。這個時候嚴重依賴 /data/hello.txt 的這個容器就能夠順利運行了。
專門製做初始化鏡像 #
手動的去執行 docker cp,或者 docker run ... cp ... 並非很正規。能夠寫個腳本讓一切都標準化,可是,除了流程外,還須要確保當前環境中的初始化數據的版本必須是所指望的,不然初始化了錯誤的數據,也會讓運行時狀態達不到預期的效果。
所以,另外一種辦法是專門製做一個初始化卷的鏡像,這樣的作法也比較方便在 CI/CD 流程中對初始化數據的過程進行測試確認。

FROM busybox
COPY hello.txt /source/
VOLUME /data
CMD ["cp", "/source/hello.txt", "/data/"]

這樣的鏡像只有一個生存目的,就是掛載 mydata 卷,而且把數據導入進去。假設構建好的鏡像名爲 volume-prepare,只須要執行下面的命令就能夠完成導入:

$ docker run --rm -v mydata:/data volume-prepare

在鏡像的 Dockerfile 製做中,加入初始化部分
在以前的問答中咱們已經瞭解到,官方鏡像 mysql 中可使用 Dockerfile 來添加初始化腳本,而且會在運行時判斷是否爲第一次運行,若是確實須要初始化,則執行定製的初始化腳本。
咱們也可使用這種方法將 hello.txt 在初始化的時候加入到 mydata 卷中去。
首先咱們須要寫一個進入點的腳本,用以確保在容器執行的時候都會運行,而這個腳本將判斷是否須要數據初始化,而且進行初始化操做。

#!/bin/bash
# entrypoint.sh
if [ ! -f "/data/hello.txt" ]; then
cp /source/hello.txt /data/
fi
exec "$@"

名爲 entrypoint.sh 的這個腳本很簡單,判斷一下 /data/hello.txt 是否存在,若是不存在就須要初始化。初始化行爲也很簡單,將實現準備好的 /source/hello.txt 複製到 /data/ 目錄中去,以完成初始化。程序的最後,將執行送入的命令。
咱們能夠這樣寫 Dockerfile:

FROM nginx
COPY hello.txt /source/
COPY entrypoint.sh /
VOLUME /data
ENTRYPOINT ["/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

當咱們構建鏡像、啓動容器後,就會發現 /data 目錄下已經存在了 hello.txt 文件了,初始化成功了。

4.10 爲何說數據庫不適合放在 Docker 容器裏運行?

不爲何,由於這個說法不對,大部分認爲數據庫必須放到容器外運行的人根本不知道 Docker Volume 爲什麼物。
在早年 Docker 沒有 Docker Volume 的時候,其數據持久化是一個問題,可是這已經不少年過去了。如今有 Docker Volume 解決持久化問題,從本地目錄綁定、受控存儲空間、塊設備、網絡存儲到分佈式存儲,Docker Volume 都支持,不存在數據讀寫類的服務不適於運行於容器內的說法。
Docker 不是虛擬機,使用數據卷是直接向宿主寫入文件,不存在性能損耗。並且卷的生存週期獨立於容器,容器消亡卷不消亡,從新運行容器能夠掛載指定命名卷,數據依然存在,也不存在沒法持久化的問題。
建議去閱讀一下官方文檔:
https://docs.docker.com/engine/tutorials/dockervolumes/
https://docs.docker.com/engine/reference/commandline/volume_create/
https://docs.docker.com/engine/extend/legacy_plugins/#/volume-plugins

4.11 如何列出容器和所使用的卷的關係?

要感謝強大的 Go Template,可使用下面的命令來顯示:

docker inspect --format '{{.Name}} => {{with .Mounts}}{{range .}}
    {{.Name}},{{end}}{{end}}' $(docker ps -aq)

注意這裏的換行和空格是有意如此的,這樣就能夠再返回結果控制縮進格式。其結果將是以下形式:

$ docker inspect --format '{{.Name}} => {{with .Mounts}}{{range .}}
    {{.Name}}{{end}}{{end}}' $(docker ps -aq)
/device_api_1 =>
/device_dashboard-debug_1 =>
/device_redis_1 =>
    device_redis-data
/device_mongo_1 =>
    device_mongo-data
    61453e46c3409f42e938324d7feffc6aeb6b7ce16d2080566e3b128c910c9570
/prometheus_prometheus_1 =>
    fc0185ed3fc637295de810efaff7333e8ff2f6050d7f9368a22e19fb2c1e3c3f
相關文章
相關標籤/搜索