對於 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前端
若是所謂數據是指運行時動態的數據,那麼這部分數據文件不該該保存於鏡像內。在運行時要保持容器基礎文件不可變的特性,而變化部分使用掛載宿主目錄,或者數據捲來解決。
建議看一下官網 docker volume 的文檔:https://docs.docker.com/engine/tutorials/dockervolumes/node
這裏說到的有兩個層面的無狀態:
容器存儲層的無狀態
這裏提到的存儲層是指用於存儲鏡像、容器各個層的存儲,通常是 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
首先,掛載分爲掛載本地宿主目錄 和 掛載數據卷(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,不過通常不須要、也不該該訪問這個位置。
卷 (Docker Volume) 是受控存儲,是由 Docker 引擎進行管理維護的。所以使用卷,你能夠沒必要處理 uid、SELinux 等各類權限問題,Docker 引擎在創建卷時會自動添加安全規則,以及根據掛載點調整權限。而且能夠統一列表、添加、刪除。另外,除了本地卷外,還支持網絡卷、分佈式卷。
而掛載目錄那就沒人管了,屬於用戶自行維護。你就必須手動處理全部權限問題。特別是在 CentOS 上,不少人碰到 Permission Denied,就是由於沒有使用卷,而是掛載目錄,並且還對 SELinux 安全權限一無所知致使。
在綁定宿主內容的形式中,有一種特殊的形式,就是綁定宿主文件,既:
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 對應的那個文件,也就是舊的內容。
這就出現了以前的那個問題,在宿主內修改綁定文件的內容,結果發現容器內看不到改變,其緣由就在於宿主的那個文件已不是原來的文件了😂。
這類問題解決辦法很簡單,若是文件可能改變,那麼就不要綁定宿主文件,而是綁定一個宿主目錄,這樣只要目錄不跑,裏面文件愛咋改就咋改😁。
若是是同一個宿主,那麼能夠綁定同一個數據卷,固然,程序上要處理好併發問題。
若是是不一樣宿主,則可使用分佈式數據卷驅動,讓分佈在不一樣宿主的容器均可以訪問到的分佈式存儲的位置。如S3之類:
https://docs.docker.com/engine/extend/plugins/#volume-plugins
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 文件可執行權限。
卷(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 文件了,初始化成功了。
不爲何,由於這個說法不對,大部分認爲數據庫必須放到容器外運行的人根本不知道 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
要感謝強大的 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