拒絕刪庫跑路!上手 Docker 容器數據管理

本文由圖雀社區成員 mRc 寫做而成,歡迎加入圖雀社區,一塊兒創做精彩的免費技術教程,予力編程行業發展。html

若是您以爲咱們寫得還不錯,記得 點贊 + 關注 + 評論 三連🥰🥰🥰,鼓勵咱們寫出更好的教程💪前端

數據是一切應用和服務的核心,特別是目擊了一次次「刪庫跑路」引起的慘劇以後,咱們更能深刻體會到數據存儲與備份的重要性。Docker 也爲咱們提供了方便且強大的方式去處理容器的數據。在這一篇文章中,咱們將帶你經過理論實戰的方式掌握 Docker 的兩種經常使用的數據管理方式:數據卷(Volume)和綁定掛載(Bind Mount),從而可以遊刃有餘地處理好數據,爲你的應用提供強有力的支撐和保障。git

Docker 數據管理概覽

很久不見,歡迎繼續閱讀「築夢師系列」 Docker 教程,前情回顧:github

  • 《一杯茶的時間,上手 Docker》中,咱們以「工做」和「作夢」來類比「應用開發」和「部署」,並經過一些小實驗讓你理解 Docker 是如何實現從「作夢」到「築夢」的跨越的,而且理解了鏡像容器兩大關鍵概念,併成功地容器化了第一個應用
  • 《夢境亦相通:用 Network 實現容器互聯》中,咱們瞭解了」夢境「是相通的,不一樣的容器能夠經過 Docker 網絡實現相互之間的通訊

而在這一篇教程中,咱們將帶你上手 Docker 數據管理,搭建起」夢境「(容器環境)與」現實「(主機環境)的橋樑。Docker 數據的管理方式主要分爲三種:mongodb

  1. 數據卷(Volume),也是最爲推薦的一種方式
  2. 綁定掛載(Bind Mount),Docker 早期經常使用的數據管理方式
  3. tmpfs 掛載,基於內存的數據管理,本篇教程不會涉及

注意docker

tmpfs 掛載只適用於 Linux 操做系統。數據庫

咱們立刻經過幾個小實驗來體驗一下(已經比較熟悉的同窗能夠直接移步下面的」實戰演練「環節)。編程

數據卷

基本命令

正如在上一篇中最後「記住幾十個 Docker 命令小訣竅」所提到的,數據卷(Volume)也是常見的 Docker 對象類型的一種,所以也支持 create(建立)、inspect (查看詳情)、ls (列出全部數據卷)、prune (刪除無用數據卷)和 rm(刪除)等操做。json

咱們來走一個流程體驗一下。首先建立一個數據卷:後端

docker volume create my-volume
複製代碼

查看當前全部的數據卷:

docker volume ls
複製代碼

輸出了剛剛建立的 my-volume 數據卷:

local               my-volume
複製代碼

查看 my-volume 數據卷的詳細狀況:

docker volume inspect my-volume
複製代碼

能夠看到輸出了 JSON 格式的 my-volume 信息:

[
    {
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
        "Name": "my-volume",
        "Options": {},
        "Scope": "local"
    }
]
複製代碼

提示

好奇的同窗可能會去查看 /var/lib/docker/volumes 目錄下面是否是真的有數據卷,答案是:對於非 Linux 系統而言(Windows 和 Mac 系統),該目錄不存在於你的文件系統中,而是存在於 Docker 虛擬機中。

最後刪除 my-volume 數據卷:

docker volume rm my-volume
複製代碼

單首創建一個數據卷意義不大,畢竟它原本的做用就是爲容器的數據管理服務。請看下圖(來源 Safari Books Online):

能夠看到,數據卷在「主機環境」和「容器環境」之間架起了「一道橋樑」。一般,咱們在容器中將須要存儲的數據寫入數據卷所掛載的路徑(位置),而後就會當即、自動地將這些數據存儲到主機對應的區域。

在建立帶有數據卷的容器時,一般有兩種選擇:1)命名卷(Named Volume);2)匿名卷(Anonymous Volume)。接下來咱們就分別詳細講解。

建立命名卷

首先咱們來演示一下如何建立帶有命名卷的容器,運行如下命令:

docker run -it -v my-vol:/data --name container1 alpine
複製代碼

能夠看到,咱們經過 -v (或者 --volume )參數指定了數據卷的配置爲 my-vol:/data ,其中(你應該猜到了)my-vol 就是數據卷的名稱,/data 就是容器中數據卷的路徑。

在進入容器中後,咱們向 /data 目錄中添加一個文件後退出:

/ # touch /data/file.txt
/ # exit
複製代碼

注意

/ # 是 alpine 鏡像默認的命令提示符,後面的 touch /data/file.txt 纔是真正要執行的命令哦。

爲了驗證 /data 中的數據是否真的保存下來,咱們刪除 container1 容器,而後再建立一個新的容器 container2 ,查看其中的 /data 目錄內容:

docker rm container1
docker run -it -v my-vol:/data --name container2 alpine
/ # ls /data
file.txt
/ # exit
複製代碼

能夠看到剛剛在 container1 中建立的 file.txt 文件!事實上,這種在容器之間共享數據卷的模式很是常見,Docker 提供了一個方便的參數 --volumes-from 來輕鬆實現數據卷共享:

docker run -it --volumes-from container2 --name container3 alpine
/ # ls /data
file.txt
複製代碼

一樣,container3 中也能訪問到數據卷中的內容。

建立匿名卷

建立匿名卷的方式就很簡單了,以前咱們經過 my-vol:/data 做爲 -v 的參數,而建立匿名卷只需省略數據卷名稱(my-vol 便可):

docker run -v /data --name container4 alpine
複製代碼

咱們經過 inspect 命令來查看一下 container4 的狀況:

docker inspect container4
複製代碼

咱們能夠在其中的 Mounts 字段中看到以下數據:

"Mounts": [
    {
        "Type": "volume",
        "Name": "dfee1d707956e427cc1818a6ee6060699514102e145cde314d4d938ceb12dfd3",
        "Source": "/var/lib/docker/volumes/dfee1d707956e427cc1818a6ee6060699514102e145cde314d4d938ceb12dfd3/_data",
        "Destination": "/data",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
]
複製代碼

咱們來分析一下重要的字段:

  • Name 即數據卷的名稱,因爲是匿名卷,因此 Name 字段就是一串長長的隨機數,命名卷則爲指定的名稱
  • Source 爲數據卷在主機文件系統中的存儲路徑(以前說了,Windows 和 Mac 在 Docker 虛擬機中)
  • Destination 爲數據卷在容器中的掛載點
  • RW 指可讀寫(Read-Write),若是爲 false ,則爲只讀數據卷

在 Dockerfile 中使用數據卷

在 Dockerfile 中使用數據卷很是簡單,只需經過 VOLUME 關鍵詞指定數據卷就能夠了:

VOLUME /data 
# 或者經過 JSON 數組的方式指定多個數據卷
VOLUME ["/data1", "/data2", "/data3"] 複製代碼

有兩點須要注意:

  • 只能建立匿名卷
  • 當經過 docker run -v 指定數據卷時,Dockerfile 中的配置會被覆蓋

綁定掛載

綁定掛載(Bind Mount)是出現最先的 Docker 數據管理和存儲解決方案,它的大體思路和數據卷是一致的,只不過是直接創建本機文件系統容器文件系統之間的映射關係,很是適合簡單、靈活地在本機和容器之間傳遞數據。

咱們能夠試着把本身機器的桌面(或者其餘路徑)掛載到容器中:

docker run -it --rm -v ~/Desktop:/desktop alpine
複製代碼

咱們仍是經過 -v 參數來進行配置,~/Desktop 是本機文件系統路徑,/desktop 則是容器中的路徑,~/Desktop:/desktop 則是將本機路徑和容器路徑進行綁定,彷彿架起了一道橋樑。這裏的 --rm 選項是指在容器中止以後自動刪除(關於容器生命週期的更多細節,請參考第一篇文章)。

進入到容器以後,能夠試試看 /desktop 下面有沒有本身桌面上的東西,而後再在容器中建立一個文件,看看桌面上有沒有收到這個文件:

/# ls /desktop
# 我本身桌面上的不少東西 :D
/# touch /desktop/from-container.txt
複製代碼

你應該能看到本身的桌面上多了容器中建立的 from-container.txt 文件!

小結

咱們貼出官方文檔這張示意圖:

能夠看到:

  • 數據卷(Volume)是 Docker 在本地文件系統中專門維護了一個區域用於存儲容器數據
  • 綁定掛載(Bind Mount)則是創建容器文件系統和本地文件系統的映射
  • tmpfs 則是直接在內存中管理容器數據

在指定數據卷或綁定掛載時,-v 參數的格式爲 <first_field>:<second_field>:<rw_options> (注意經過冒號分隔),包括三個字段,分別是:

  • 數據卷名稱或者本機路徑,可省略(省略的話就是匿名卷)
  • 數據卷在容器內的掛載點(路徑),必填
  • 讀寫選項,默認是可讀寫,若是指定 ro (Read-only),則爲只讀

提示

Docker 在 17.06 版本以後引入了 --mount 參數,功能與 -v / --volume 參數幾乎一致,經過鍵值對的方式指定數據卷的配置,更爲冗長但也更清晰。這篇文章將詳細講解更爲常見和廣泛的 -v 參數,--mount 參數的更多使用可參考文檔

實戰演練

準備工做和目標

好的,終於到了實戰演練環節——繼續部署咱們以前一直在作的全棧待辦事項項目(React 前端 + Express 後端 + MongoDB 數據庫)。若是你沒有閱讀以前的教程,想直接從這一步開始作起,請運行如下命令:

git clone -b volume-start https://github.com/tuture-dev/docker-dream.git
cd docker-dream
複製代碼

在以前項目的基礎上,咱們打算

  • 存儲和備份 Express 服務器輸出的日誌數據,而不是存儲在」朝生暮死「的容器中
  • MongoDB 鏡像已經作了數據卷配置,因此咱們只需實踐一波怎麼備份和恢復數據

爲 Express 服務器掛載數據卷

OK,咱們在 server/Dockerfile 中添加 VOLUME 配置,而且指定 LOG_PATH (日誌輸出路徑環境變量,可參考 server/index.js 的源碼)爲 /var/log/server/access.log,代碼以下:

# ...

# 指定工做目錄爲 /usr/src/app,接下來的命令所有在這個目錄下操做
WORKDIR /usr/src/app 
VOLUME /var/log/server 
# ...

# 設置環境變量(服務器的主機 IP 和端口)
ENV MONGO_URI=mongodb://dream-db:27017/todos
ENV HOST=0.0.0.0
ENV PORT=4000
ENV LOG_PATH=/var/log/server/access.log

# ...
複製代碼

而後 build 服務器鏡像:

docker build -t dream-server server/
複製代碼

稍等片刻後,咱們把整個項目開起來:

# 建立網絡,便於容器互聯
docker network create dream-net

# 啓動 MongoDB 容器(dream-db)
docker run --name dream-db --network dream-net -d mongo

# 啓動 Express API 容器(dream-api)
docker run -p 4000:4000 --name dream-api --network dream-net -d dream-server

# 構建提供 React 前端頁面的 Nginx 服務器
docker build -t dream-client client

# 啓動 Nginx 服務器容器(client)
docker run -p 8080:80 --name client -d dream-client
複製代碼

經過 docker ps 確保三個容器都已經開啓:

訪問 localhost:8080,進入到待辦事項頁面,建立幾個事項:

日誌數據的備份

以前咱們把日誌數據存儲到了匿名卷中,因爲直接獲取數據卷中的數據是比較麻煩的,推薦的作法是經過建立一個新的臨時容器,經過共享數據卷的方式來備份數據。聽着有點暈?請看下圖:

按照如下步驟進行:

第一步,實現 dream-api 容器和數據卷之間的數據共享(已實現)。

第二步,建立臨時容器,獲取 dream-api 的數據卷。運行如下命令:

docker run -it --rm --volumes-from dream-api -v $(pwd):/backup alpine
複製代碼

上面這句命令同時用到了上面講解的數據卷和綁定掛載:

  • --volumes-from dream-api 用於容器之間共享數據卷,這裏咱們獲取 dream-api 的數據卷
  • -v $(pwd):/backup 用於創建當前本機文件路徑(pwd 命令獲取)和臨時容器內 /backup 路徑的綁定掛載

第三步,進入臨時容器以後,咱們把日誌數據壓縮成 tar 包放到 /backup 目錄下,而後退出:

/ # tar cvf /backup/backup.tar /var/log/server/
tar: removing leading '/' from member names
var/log/server/
var/log/server/access.log
/ # exit
複製代碼

退出以後,是否是在當前目錄看到了日誌的備份 backup.tar ?事實上,咱們能夠經過一條命令搞定:

docker run -it --rm --volumes-from dream-api -v $(pwd):/backup alpine tar cvf /backup/backup.tar /var/log/server
複製代碼

若是你以爲上面這條命令難以理解的話,答應我,必定要去仔細看看上一篇文章中的」回憶與昇華「-」理解命令:夢境的主旋律「這一部分!

數據庫備份與恢復

接下里就是這篇文章的重頭戲,各位打起十二分的精神!咱們的應用會不會遭遇刪庫跑路的危機全看你有沒有學會這一節的操做技巧了!

提示

咱們這裏使用 MongoDB 自帶的備份與恢復命令(mongodumpmongorestore ),其餘數據庫(例如 MySQL)也有相似的命令,均可以借鑑本文的方式。

備份思路一:臨時容器+容器互聯

按照以前共享數據卷的思路,咱們也嘗試經過一個臨時 Mongo 容器來備份數據。示意圖以下:

首先,咱們的臨時容器得鏈接上 dream-db 容器,並配置好綁定掛載,命令以下:

docker run -it --rm -v $(pwd):/backup --network dream-net mongo sh
複製代碼

和以前備份日誌數據相比,咱們要把這個臨時容器鏈接到 dream-net 網絡中,它才能訪問到 dream-db 的數據進行備份(不熟悉 Docker 網絡的同窗可複習前一篇文章)。

第二步,進入到這個臨時容器後,運行 mongodump 命令:

/ # mongodump -v --host dream-db:27017 --archive --gzip > /backup/mongo-backup.gz
複製代碼

此時,因爲綁定掛載,輸出到 /backup 的文件將保存到當前目錄(pwd)中。退出後,就能夠在當前目錄下看到 mongo-backup.gz 文件了。

備份思路二:提早作好綁定掛載

前一篇教程的」回憶與昇華「部分,咱們輕描淡寫地講解了經過 docker exec 執行 mongodump 命令來作備份,可是當時輸出的備份文件仍是停留在容器中,只要容器被刪除,備份文件也就消失了。因而一個很天然的想法就出現了:咱們能不能在建立數據庫容器的時候就作好綁定掛載,而後經過 mongodump 把數據備份到掛載區域?

事實上,以前在建立數據庫容器的時候,運行如下命令:

docker run --name dream-db --network dream-net -v $(pwd):/backup -d mongo
複製代碼

而後再經過 docker exec 執行 mongodump 命令:

docker exec dream-db sh -c 'mongodump -v --archive --gzip > /backup/mongo-backup.gz'
複製代碼

就能夠輕鬆實現。這裏咱們用 sh -c 來執行一整條 Shell 命令(字符串形式),這樣避免了重定向符 > 引起的歧義(不理解的話能夠把 sh -c 'xxx' 替換成 xxx)。能夠看到,mongodump 的命令簡單了許多,咱們不再須要指定 --host 參數,由於數據庫就在本容器內。

可是有個問題:若是已經建立了數據庫,而且沒有提早作綁定掛載,這種方法就行不通了!

注意,這不是演習!

有了數據庫備份文件,咱們就能夠肆無忌憚地來作一波」演習「了。經過如下命令,直接端了目前的數據庫和 API 服務器:

docker rm -f --volumes dream-db
docker rm -f dream-api
複製代碼

沒錯,經過 --volumes 開關,咱們不只把 dream-db 容器刪了,還順帶把掛載的數據卷所有刪除!演習就是要足夠逼真才行。這時候再訪問 localhost:8080 ,以前的待辦數據所有丟失!

開始災後重建,讓咱們再次建立新的 dream-db 容器:

docker run --name dream-db --network dream-net -v $(pwd):/backup -d mongo
複製代碼

注意到,咱們經過綁定掛載的方式把當前目錄映射到容器的 /backup 目錄,這意味着能夠在這個新的容器中經過 /backup/mongo-backup.gz 來恢復數據,運行如下命令:

docker exec dream-db sh -c 'mongorestore --archive --gzip < /backup/mongo-backup.gz'
複製代碼

咱們應該會看到輸出了一些日誌,提示咱們數據恢復成功。最後從新開啓 API 服務器:

docker run -p 4000:4000 --name dream-api --network dream-net -d dream-server
複製代碼

回頭訪問咱們的待辦應用,數據是否是都回來了!?

回憶與昇華

另外一種共享數據的方式:docker cp

以前,咱們經過共享數據卷或者綁定掛載的方式來把容器的數據傳送到容器以外。事實上,在容器和本機之間還能夠經過另外一種方式傳遞和共享數據:docker cp 命令。沒錯,若是你用過 cp 命令拷貝文件,它的用法必定不會陌生。例如,咱們將 dream-api 容器內的日誌文件拷貝到當前目錄下:

docker cp dream-api:/var/log/server/access.log .
複製代碼

看!access.log 就有了!固然,咱們還能夠」反向操做「一波,把本地的文件拷貝到容器裏面去:

docker cp /path/to/some/file dream-api:/dest/path
複製代碼

能夠看到,docker cp 用起來很是方便,很適合一次性的操做。缺陷也很明顯:

  1. 徹底手動的數據管理
  2. 須要知道數據在容器中的具體路徑,這對於反覆迭代的應用來講很麻煩
  3. 實現多個容器之間的數據共享比較繁瑣

另外一種備份恢復的方式:docker import/export

在備份和恢復數據庫時,有一個更加簡單粗暴的思路:爲何咱們不能直接備份整個容器呢?事實上,Docker 確實爲咱們提供了兩個命令來搞定整個容器的打包和裝載:exportimport

例如,經過如下命令將整個容器的文件系統導出爲 tar 包:

docker export my-container > my-container.tar
複製代碼

注意

export 命令不會導出容器相關數據卷的內容。

而後能夠經過 import 命令建立擁有徹底相同內容的鏡像

docker import my-container.tar
複製代碼

import 命令會輸出一個 SHA256 字符串,就是鏡像的 UUID。接着能夠用 docker run 命令啓動這個鏡像(能夠指定 SHA256 串,也能夠先經過 docker tag 打個標籤)。

若是你剛剛嘗試了 exportimport 命令,必定會發現一個至關嚴重的問題:容器打包以後的 tar 包有好幾百兆。很顯然,簡單粗暴地打包容器也包括了不少根本無用的數據(例如操做系統中的其餘文件),對硬盤的壓力陡然增長。

追本溯源:探尋鏡像和容器的本質(UFS)

在學習和實踐了數據卷的知識後,咱們還接觸了一下 docker cpdocker export/import 命令。至此,咱們不由追問,鏡像和容器的本質究竟是什麼,其中的數據是怎樣存儲的?

或者咱們提一個更具體的問題:爲何鏡像中的數據(例如操做系統中的各類文件)每次建立容器時都會存在,而在建立容器後寫入的數據會在容器刪除後卻丟失?

這背後的一切就是 Docker 賴以生存的 Union File System(UFS)機制。咱們經過一張圖(來源:The Docker Ecosystem)來大體感覺一下:

咱們來一點點分析上面這張 UFS 示意圖的要點:

  • 整個 UFS 都是由一層層的內容組成的,從底層的操做系統內核(Kernel),到上層的軟件(例如 Apache 服務器)
  • UFS 中的每一層可分爲只讀層(read-only,也就是圖中的不透明盒子)和可寫層(writable,也就是圖中的透明盒子
  • 鏡像(例如圖中的 add Apache 和 Busybox)由一系列只讀層構成
  • 當咱們根據鏡像建立容器時,就是在該鏡像全部只讀層之上加一層可寫層,在容器中進行的任何數據的修改都會記錄在這個可寫層中,而不會影響到底下的只讀層
  • 當容器銷燬後,在可寫層中修改的全部內容將丟失

而咱們這一篇文章所講解的數據管理技巧(數據卷、綁定掛載),則是徹底繞開了 UFS,讓重要的業務數據獨立存儲,而且可備份、可恢復,而不是陷入在容器的可寫層中讓整個容器變得臃腫不堪。

再回過頭看上面的問題,是否是有思路了?

想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。

本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+Github倉庫加星❤️哦

相關文章
相關標籤/搜索