docker存儲卷
背景:一個程序,對於容器來說,啟動時依賴於可能不止一層的鏡像,聯合掛載啟動而成,使用overlay2文件系統,引導最上層的可寫層,對於讀寫層來說,所有在容器中可執行的操作,包括對數據和內容的修改,都是保存在最上層之上的,對於下層內容的操作,假設要刪除一個文件,需要使用寫時復制。
- docker鏡像由多個只讀層疊加面成,啟動容器時,docker會加載只讀鏡像層並在鏡像棧頂部加一個讀寫層
- 如果運行中的容器修改了現有的一個已經存在的文件,那該文件將會從讀寫層下面的只讀層復制到讀寫層,該文件版本仍然存在,只是已經被讀寫層中該文件的副本所隱藏,此即“寫時復制(COW)”機制
描述:如果一個文件在最底層是可見的,如果在layer1上標記為刪除,最高的層是用戶看到的Layer2的層,在layer0上的文件,在layer2上可以刪除,但是只是標記刪除,用戶是不可見的,總之在到達最頂層之前,把它標記來刪除,對於最上層的用戶是不可見的,當標記一刪除,只有用戶在最上層建一個同名一樣的文件,才是可見的。
對於這類的操作,修改刪除等,一般效率非常低,如果對一於I/O要求比較高的應用,如redis在實現持久化存儲時,是在底層存儲時的性能要求比較高。
假設底層運行一個存儲庫mysql,mysql本來對於I/O的要求就比較高,如果mysql又是運行在容器中自己的文件系統之上時,也就是容器在停止時,就意味着刪除,其實現數據存取時效率比較低,要避免這個限制要使用存儲卷來實現。
存儲卷:可以想象來在各全局的名稱空間中,也就是理解為在宿主機中找一個本地的文件系統,可能存在某一個目錄中,直接與容器上的文件系統中的某一目錄建立綁定關系。
類似於掛載一樣,宿主機的/data/web目錄與容器中的/container/data/web目錄綁定關系,然后容器中的進程向這個目錄中寫數據時,是直接寫在宿主機的目錄上的,繞過容器文件系統與宿主機的文件系統建立關聯關系,使得可以在宿主機和容器內共享數據庫內容,讓容器直接訪問宿主機中的內容,也可以宿主機向容器供集內容,兩者是同步的。
mount名稱空間本來是隔離的,可以讓兩個本來是隔離的文件系統,在某個子路徑上建立一定程度的綁定關系,從而使得在兩個容器之間的文件系統的某個子路徑上不再是隔離的,實現一定程度上共享的效果。
在宿主機上能夠被共享的目錄(可以是文件)就被稱為volume。
優點是容器中進程所生成的數據,都保存在存儲卷上,從而脫離容器文件系統自身后,當容器被關閉甚至被刪除時,都不用擔心數據被丟失,實現數據可以脫離容器生命周期而持久,當再次重建容器時,如果可以讓它使用到或者關聯到同一個存儲卷上時,再創建容器,雖然不是之前的容器,但是數據還是那個數據,特別類似於進程的運行邏輯,進程本身不保存任何的數據,數據都在進程之外的文件系統上,或者是專業的存儲服務之上,所以進程每次停止,只是保存程序文件,對於容器也是一樣,
容器就是一個有生命周期的動態對象來使用,容器關閉就是容器刪除的時候,但是它底層的鏡像文件還是存在的,可以基於鏡像再重新啟動容器。
但是容器有一個問題,一般與進程的啟動不太一樣,就是容器啟動時選項比較多,如果下次再啟動時,很容器會忘記它啟動時的選項,所以最好有一個文件來保存容器的啟動,這就是容器編排工具的作用。一般情況下,是使用命令來啟動操作docker,但是可以通過文件來讀,也就讀文件來啟動,讀所需要的存儲卷等,但是它也只是操作一個容器,這也是需要專業的容器編排工具的原因。
另一個優勢就是容器就可以不置於啟動在那台主機之上了,如幾台主機后面掛載一個NFS,在各自主機上創建容器,而容器上通過關聯到宿主機的某個目錄上,而這個目錄也是NFS所掛載的目錄中,這樣容器如果停止或者是刪除都可以不限制於只能在原先的宿主機上啟動才可以,可以實現全集群范圍內調試容器的使用,當再分配存儲、計算資源時,就不會再局限於單機之上,可以在集群范圍內建立起來,基本各種docker的編排工具都能實現此功能,但是后面嚴重依賴於共享存儲的使用。
考慮到容器應用是需要持久存儲數據的,可能是有狀態的,如果考慮使用NFS做反向代理是沒必要存儲數據的,應用可以分為有狀態和無狀態,有狀態是當前這次連接請求處理一定此前的處理是有關聯的,無狀態是前后處理是沒有關聯關系的,大多數有狀態應用都是數據持久存儲的,如mysql,redis有狀態應用,在持久存儲,如nginx作為反向代理是無狀態應用,tomcat可以是有狀態的,但是它有可能不需要持久存儲數據,因為它的session都是保存在內存中就可以的,會導致節點宕機而丟失
session,如果有必要應該讓它持久,這也算是有狀態的。
應用狀態:是否有狀態或無狀態,是否需要持久存儲,可以定立一個正軸坐標系,第一象限中是那些有狀態需要存儲的,像mysql,redis等服務,有些有有狀態但是無需進行存儲的,像tomcat把會話保存在內存中時,無狀態也無需要存儲的數據,如各種反向代理服務器nginx,lvs請求連接都是當作一個獨立的連接來調度,本地也不需要保存數據,第四象限是無狀態,但是需要存儲數據是比較少見。
運維起來比較難的是有狀態且需要持久的,需要大量的運維經驗和大量的操作步驟才能操作起來的,如做一個Mysql主從需要運維知識、經驗整合進去才能實現所謂的部署,擴展或縮容,出現問題后修復,必須要了解集群的規模有多大,有多少個主節點,有多少個從節點,主節點上有多少個庫,這些都要一清二楚,才能修復故障,這些就強依賴於運維經驗,無狀態的如nginx一安裝就可以了,並不復雜,對於無狀態的應用可以迅速的實現復制,在運維上實現自動化是很容易的,對於有狀態的現狀比較難脫離運維人員來管理,即使是k8s在使用上也暫時沒有成熟的工具來實現。
總之:對於有狀態的應用的數據,不使用存儲卷,只能放在容器本地,效率比較低,而導致一個很嚴重問題就是無法遷移使用,而且隨着容器生命周期的停止,還不能把它刪除,只能等待下次再啟動狀態才可以,如果刪除了數據就可能沒了,因為它的可寫層是隨着容器的生命周期而存在的,所以只要持久存儲數據,存儲卷就是必需的。
docker存儲卷難度:對於docker存儲卷運行起來並不太麻煩,如果不自己借助額外的體系來維護,它本身並沒有這么強大,因為docker存儲卷是使用其所在的宿主機上的本地文件系統目錄,也就是宿主機有一塊磁盤,這塊磁盤並沒有共享給其他的docker主要,然后容器所使用的目錄,只是關聯到宿主機磁盤上的某個目錄而已,也就是容器在這宿主機上停止或刪除,是可以重新再創建的,但是不能調度到其他的主機上,這也是docker本身沒有解決的問題,所以docker存儲卷默認就是docker所在主機的本地,但是自己搭建一個共享的NFS來存儲docker存儲的數據,也可以實現,但是這個過程強依賴於運維人員的能力,
使用存儲卷的原因
- 關閉並重啟容器,其數據不受影響,但是刪除docker容器,則其更改將會全部丟失
- 存在的問題
存儲於聯合文件系統中,不易於宿主機訪問
容器間數據共享不便
刪除容器其數據會丟失
- 解決方案:卷
卷是容器上一個或多個"目錄“,此類目錄可繞過聯合文件系統,與宿主機上的某目錄綁定(關聯)
存儲卷原理
- volume於容器初始化之時會創建,由base image提供的卷中的數據會於此期間完成復制
- volume的初意是獨立於容器的生命周期實現數據持久化,因此刪除容器之時既不會刪除卷,也不會對哪怕未被引用的卷做垃圾回收操作
- 卷為docker提供了獨立於容器的數據管理機制
可以把“鏡像”想像成靜態文件,例如“程序”,把卷類比為動態內容,例如“數據”,於是,鏡像可以重用,而卷可以共享
卷實現了“程序(鏡像)"和”數據(卷)“分離,以及”程序(鏡像)“和"制作鏡像的主機”分離,用記制作鏡像時無須考慮鏡像運行在容器所在的主機的環境
描述:有了存儲卷,如果寫在/上,還是存在聯合掛載文件系統中,如果要寫到卷上,就會寫到宿主機關聯的目錄上,程序運行過程生成的臨時數據會寫到tmp目錄中,也就會在容器的可寫層中存儲,隨着容器被刪除而刪除,並沒太大的影響,只有關鍵型的數據才會保存在存儲卷上.
Volume types
- Docker有兩種類型的卷,每種類型都在容器中存在一個掛載點,但其在宿主機上位置有所不同;
綁定掛載卷:在宿主機上的路徑要人工的指定一個特定的路徑,在容器中也需要指定一個特定的路徑,兩個已知的路徑建立關聯關系
docker管理卷: 只需要在容器內指定容器的掛載點是什么,而被綁定宿主機下的那個目錄,是由容器引擎daemon自行創建一個空的目錄,或者使用一個已經存在的目錄,與存儲卷建立存儲關系,這種方式極大解脫用戶在使用卷時的耦合關系,缺陷是用戶無法指定那些使用目錄,臨時存儲比較適合
在容器中使用volumes
- 為docker run 命令使用-v 選項可使用volume docker-managed volume docker run -it -name rbox1 -v /data busybox #/data指定docker的目錄 docker inspect -f {{.Mounts}} rbox1 查看rbox1容器的卷,卷標識符及掛載的主機目錄 bind-mount volume docker run -it -v HOSTDIR:VOLUMEDIR --name rbox2 busybox #宿主機目錄:容器目錄 docker inspect -f {{.Mounts}} rbox2
實操:docker管理卷
[root@node1 ~]# docker run --name b2 -it -v /data busybox / # ls / bin data dev etc home proc root sys tmp usr var #data默認是不存在的 [root@node1 ~]# docker inspect b2|grep -A 5 Mounts "Mounts": [ { "Type": "volume", "Name": "fd6c6484126148c8d2876f068175ba3998b8f442a2635d94daeef87ca8721c90", "Source": "/var/lib/docker/volumes/fd6c6484126148c8d2876f068175ba3998b8f442a2635d94daeef87ca8721c90/_data", 宿主上的目錄 "Destination": "/data", 容器中的目錄
宿主機: 可以很方便實現在宿主機和容器之間共享目錄
[root@node1 ~]# cd /var/lib/docker/volumes/fd6c6484126148c8d2876f068175ba3998b8f442a2635d94daeef87ca8721c90/_data [root@node1 _data]# echo "hello reid" > testreid.html [root@node1 _data]# cat testreid.html hello reid reid
容器
/ # cat /data/testreid.html hello reid / # echo reid >> /data/testreid.html
docker綁定卷
[root@node1 ~]# docker run --name b2 -it --rm -v /docker/volume/b2:/data busybox ##目錄會自動創建 / # ls bin data dev etc home proc root sys tmp usr var [root@node1 _data]# docker inspect b2 |grep Mounts -A 5 "Mounts": [ { "Type": "bind", "Source": "/docker/volume/b2", 宿主機目錄 "Destination": "/data", 容器目錄 "Mode": "", [root@node1 _data]# echo reid > /docker/volume/b2/reid.html [root@node1 _data]# cat /docker/volume/b2/reid.html reid
持久的實現
[root@node1 ~]# docker run --name b2 -it --rm -v /docker/volume/b2:/data busybox / # exit [root@node1 _data]# ls /docker/volume/b2/reid.html 容器退出刪除后,宿主機上的目錄還存在,而還可以基於它的基礎上開啟目錄 /docker/volume/b2/reid.html [root@node1 ~]# docker run --name b2 -it --rm -v /docker/volume/b2:/data/web busybox / # cat /data/web/reid.html reid
使用golong模板來查看
[root@node1 _data]# docker inspect -f {{.Mounts}} b2 #注意{{}}中使用根開始寫 [{bind /docker/volume/b2 /data/web true rprivate}] [root@node1 _data]# docker inspect -f {{.NetworkSettings}} b2 {{ 1d11d66176eefb49868940f3a35cca51842da2518878e4f9eb1a7c1534d4680e false 0 map[] /var/run/docker/netns/1d11d66176ee [] []} {7c465c22a5794bf473ca54a492555832ee0308472b595889e25718cc7a6ddeac 10.0.0.1 0 10.0.0.3 16 02:42:0a:00:00:03} map[bridge:0xc420186180]} [root@node1 _data]# docker inspect -f {{.NetworkSettings.IPAddress}} b2 10.0.0.3
場景:一個docker容器可以關聯到宿主機的目錄中,也可以讓兩個docker容器同時關聯到同一個宿主機的目錄中,實現共享使用同一個存儲卷,容器之間的數據共享
[root@node1 _data]# docker run --name b3 -it --rm -v /docker/volume/b2:/data busybox #再添加一個容器,可以再使用同一個目錄 / # cat /data/reid.html reid / # echo reid2 >> /data/reid.html [root@node1 ~]# docker run --name b2 -it --rm -v /docker/volume/b2:/data/web busybox / # cat /data/web/reid.html reid reid2
場景:需要多個容器同進使用多個卷,卷在那里寫每次初始化時都要使用-v來指定,如果不想記錄這個路徑,docker還支持復制其他的存儲卷路徑
實現:制定一個容器,不執行任何任務,創建時,只要指定它的存儲路徑,作為其他相關聯容器的基礎架構容器,其他的容器啟動時去復制它的存儲卷設置,但是這樣的點浪費,不過使用joined container的基礎的話,幾個容器本來就有密切的關系,如nginx+tomcat,nginx的容器和tomcat容器共享一個底層的網絡,有一個對外的接口,有一個loop接口,這樣80給nginx,在內loop給tomcat,請求進來,nginx作為反射代理轉給tomcat就可以了,再加一個mysql,也是使用loop接口來通訊。
讓它們共享網絡名稱空間中的uts,net,ipc,還可以共享存儲卷,ngInx處理靜態,tomcat處理動態的,在同一個目錄下,使用存儲卷來解決這個問題,這種組織方式使用構建應用。
共享卷
- 多個容器的卷使用同一個主機目錄,如
docker run -it --name c1 -v /docker/volumes/v1:/data/ busybox
- 復制使用其他容器的卷,為docker run 命令使用--volumes-from選項
docker run -it --name bbox1 -v /docker/volumes/v1:/data busybox docker run -it --name bbox2 --volumes-from bbox1 busybox
制定基礎鏡像(網上有專門制作基礎架構容器的,不用啟動,只要創建就可以了)
[root@node1 ~]# docker run --name basiccon -it -v /data/basic/volume/:/data/web/html busybox / # ls /data/ web [root@node1 ~]# docker run --name nginx --network container:basiccon --volumes-from basiccon -it busybox #加入網絡,同時復制卷 / # ls /data/ web