存儲卷理論
docker存儲卷
在以往的docker應用中,容器如果跨多節點運行,容器本身也擁有一個文件系統,通常而言,將容器從A節點遷至B節點,容器就需要關閉或刪除已有鏡像,並且會在被遷移的節點上重新建立一個容器,而此次容器的所有數據都會被遺留在原有節點上;
因爲docker容器運行的方式依賴於其底層的鏡像構建方式,而底層的鏡像都是隻讀的,寫操作只能發生在鏡像棧最上面所添加的可寫層,這個可寫層保存在我們對應容器所運行的節點上, 如果刪除容器,底層的只讀部分鏡像不會改變,但最上層的可寫層被刪除,那麼寫入的數據也將被改變,必然會丟失;
爲了改變這種膠離容器的生命週期而存儲數據,即使容器刪除或刪除數據依然存在,在docker中我們應該使用存儲卷,docker的存儲卷有兩種: 綁定掛載卷、docker管理卷,不管是綁定的那種卷保存的數據都還是在當前節點上, 而不同的是綁定掛載卷可以由用戶手動指定,而docker管理的卷,其路徑是由docker進行管理和創建的,一般而言如果沒有特殊指定刪除容器時並不會刪除其使用的存儲卷;
而到了k8s時代,當我們Pod被調度運行時,它會運行於多個數據節點中的任意一個節點中,假設我們有一個k8s集羣,當調度一個Pod時,它運行在A節點我們仍然使用本地存儲卷,那麼該卷就是被構建在A節點上,但可能因爲各種原因導致節點故障,那麼K8S會自動重新創建一個Pod,而創建的這個新的Pod會被隨便調度到集羣中任意一個節點,而這個節點是不會存儲原先數據的,那麼在這種只能存儲於本地的數據卷只在本地使用時似乎沒有問題,但一旦跨多節點那麼就無法得到穩妥的解決了
k8s存儲卷
所以我們期望在分佈式集羣管理的Kubernetes模型中,我們的存儲卷不但應該要脫離容器的生命週期,也需要與節點的生命週期相分離,那麼我們就需要引入能通過網絡訪問的網絡存儲空間了, 比如NFS,只要讓集羣中每一個節點都掛載NFS,而後集羣無論將Pod調度至何意一個節點上,所構建的存儲卷只要NFS不宕機,哪怕因爲各種原因Pod被重新構建到其它節點,只要重構的節點能訪問到NFS,那就能讓該Pod訪問到NFS存儲數據,而對於NFS來說,它只是一個網絡存儲系統,我們只需要確保節點能夠訪問,那麼基本上都可以爲我們的k8s集羣提供脫離節點生命週期的外圍存儲系統;
Pause容器
對於Kubernetes來講,存儲卷不屬於容器,而屬於Pod,Pod可能會運行多個容器,而其中一個容器會做爲主容器,其它的皆爲輔助容器,一個Pod中的多個容器是可以使用同一個存儲卷的,因爲存儲卷屬於Pod,Pod有一個底層基礎架構容器,運行於kubeadm部署之上的k8s集羣之上,這個基礎架構容器鏡像名字就叫pause,pause在每一個node節點上都存在,它主要爲是集羣中的每一個Pod提供一個底層的基礎支撐設備,而我們運行的每一個Pod其內部也都會有一個pause容器存在,但對我們而言不用關注它;
容器藉助於內核中的六個網絡名稱空間提供服務,pause提供了一個基礎的容器,只要加入到這個Pod的容器,都將共享底層pause的網絡名稱空間、IPC以及UTS,同一個Pod內的所有容器,不但能通過lo通信,而且還共享同一個主機名,除此之外還有一個功能,當我們給Pod添加一個存儲卷,只要這個卷是屬於Pause的,後續每一個加入這個Pause的容器都可以複製Pause容器中的存儲卷,類似於docker中的–from-volume功能,對於Pod來說只需要指定這個容器需要複製Pause容器的存儲卷就行,但它與docker不同的是,k8s的Pod中容器要想使用存儲卷,我們需要明確定義掛載這個存儲捲到本地的某個目錄下,它才能被使用;
如果我們期望在Pause使用存儲卷,而這個存儲有時候會屬於NFS\ceph\samba\local,而每一種存儲設備所提供的訪問方式是各不一樣的,因此這裏就需要兩個非常重要的步驟,
- 以NFS爲例,假如該節點無法通過NFS連接至NFS存儲,那麼該節點的Pod也必然不能訪問NFS之上的存儲空間,所以想要每個節點能夠適配到目標存儲系統上就必須要有對應的驅動,每一個容器都是共享所在節點底層的內核的,而驅動是屬於內核的功能,所以要確保節點的內核先能適配外部的存儲系統;
- 節點內核可能會適配N種存儲系統,假設k8s集羣之外部署了samba\nfs\ceph,那麼當啓動Pod時,這個Pod就可能會使用NFS或其它存儲設備,從這個角度上來講,Pause在接入這個存儲卷時,需要明確指明它自己要連接至節點級,已經關聯到哪一種類型的存儲設備上, Pause在關聯存儲卷時,Pod必須要以指定存儲設備的訪問接口匹配,說白了,真正去驅動並調用這個服務的客戶端不是你的節點的內核,內核只是負責把他們二者之間橋接起來而已,而真正去驅動它的,必須要去成爲這個存儲服務的客戶端纔可以;
CSI
容器存儲接口 (Container Storage Interface)
當Pause要連接到NFS,必須要在連接的時候指明,文件系統類型,NFS訪問地址,和服務導出來的文件系統路徑,這樣一來就麻煩了,目前來講,我們基於可用的存儲類型,是很多的,那爲了儘可能支持不同的存儲設備,我們就不得不爲Kubernetes中的Pod適配每一種存儲系統內置驅動客戶端;
這樣一來,我們的整個系統就會變得龐大無比,因此爲了避免這種情形,甚至於在必要的情況下,我們允許用戶自定義存儲,Kubernetes爲了爲了使得這種功能更加靈活,提供了一種特殊的類型叫做CSI,CSI是k8s的一個插件接口,叫做容器存儲接口,利用CSI用戶可以方便開發自己的存儲驅動插件,可以自定義使用任何類型的存儲插件,爲了降低用戶使用的k8s的複雜度,Kubernetes內置了很多標準類型,只有標準類型滿足不了我們的需要時,我們纔有必要去使用CSI擴展
關鍵點: 第一:節點需要適配的存儲設備需要內核驅動支持, 第二:自身也需要扮演成爲這個存儲系統的客戶端,爲了能夠驅動這個客戶端,它必須內置一些插件鏈接不同的存儲系統,而這個插件其實就是存儲驅動;
存儲卷狀態
根據應用本身是否存在持久存儲數據,以及某一次請求與此次的請求有否關連性, 根據這種關連性分爲四種狀態: 1、有狀態要存儲, 2、有狀態無持久存儲, 3、無狀態有存儲,4、無狀態無存儲,而大多數跟數據存儲服務相關應用跟有狀態應用幾乎都是需要持久存儲的。
如docker, 容器本身有命令週期,爲了使得容器將來終結以後可以將它刪除,或是編排至其它節點上運行,意味着數據不能放在容器本地或容器自已名稱空間中,
掛載說明
如k8s, pod運行時應運行在某一個節點上,節點正常pod將會始終運行在這一個節點之上, 節點故障或刪除纔會重構pod,當數據存放在pod上時,一旦被重構 數據 將會隨着pod結束而結束,爲了解決數據丟失, 此時應當將數據放置於pod外的節點存儲卷中使之數據持久性,這樣使得pod結束數據也不會被丟失,而某個單點的存儲卷只有一定程度的持久存儲性,如節點down或損壞數據都將被丟失,爲了實現更強大的存儲 應當使用脫離節點的共享存儲設備
k8s也提供了各種的存儲卷功能,對pod來講,同一個pod內的多個容器可共享訪問同一組存儲卷,而對k8s來說存儲卷不屬於容器而屬於pod, 如果兩個容器都掛載了,就說明兩個容器都掛載了說明共享數據了,pod底層有一個基礎容器不會被啓動而是靠一個獨特的鏡像來創建的 (pause), 此時掛載應當爲: 多個Pod --> 同一個掛載目錄 --> 同一個節點目錄 --> 底層存儲卷
存儲卷類型-掛載
Kubernetes支持的存儲驅動 ]# kubectl explain pod.spec.volumes, 官方掛載說明
雲存儲:awsEastocBlockStore、azureDisk、azureFile、gcePersistentDisk、vshpere Volume
分佈式存儲:cephfs、glusterfs、rbd
網絡存儲:nfs、iscsi、fc
臨時存儲:emptyDir、gitRpo(deprecated)
本地存儲:hostPath、local
emptyDir|hostPath, 只在節點本地使用,一旦pod被刪除,該存儲卷也會被一併刪除,無法數據持久只能當一個臨時目錄使用,或當緩存使用,emptyDir背後關連的宿主機目錄也可以是內存當成是硬盤掛載使用,
特殊存儲:configMap、secret、downwardAPI
自定義存儲:CSI
持久卷申請:persistentVolumeClaim
...
創建流程
pod --> pvc --> pv (存儲空間) ,
- 創建: 當用戶創建pvc需要用到pv時,它能夠向存儲類申請創建對應用戶請求大小的pv, 由用戶請求而動態生成,pv動態攻擊
- 掛載: 需要指定volumemount掛載點
本地存儲卷
spac:
conteiners:
# 2、配置掛載路徑
volumeMounts
- name: # 這裏爲掛載卷的掛載名稱
mountPath: # 要掛載的路徑
# 如果有多個容器,哪個容器用就掛載哪個,如果有多個容器只掛載一個,其它容器無法訪問該掛載點的目錄
# 1、配置掛載類型
volumes: # 掛載卷
- name: 掛載名稱
掛載類型: {} # 空鍵值對關連數組,使用磁盤空間大小不控制
emptyDir
空目錄,只要Pod一刪除,數據就沒了,它大多數場景都是用在緩存的情況下,就比如我們在這個Pod中的容器可能需要用到緩存的功能,如Nginx,nginx的temp目錄可能會存儲一些臨時文件,這樣的情況,我們就可以使用emptyDir來做,讓緩存存儲在指定的存儲文件系統之上,更好用的是emptyDir還支持直接將數據存儲在內存中,直接在節點的內存當中切割一段空間出來,把這個空間模擬成一個硬盤,然後把它當緩存用,速度更快;
場景二: 我們當前這個Pod沒有文件存儲的需求,但是Pod內部有兩個容器,這兩個容器之間需要共享一些數據,那這個時候我們就可以使用emptyDir這個空間,讓第一個容器在裏面讀寫,第二個容器也可用讀寫,這就實現的共享數據,或者說我們的php-fpm有一個sock文件,當構建lnmp的時候,我們可以直接將這個php-fpm的sock文件放在emptyDir裏面。然後供nginx使用unix://emptydir_volumes/data/php-fpm.sock;
# k8s 當中 $() 表示變量引用
apiVersion: v1
kind: Pod
metadata:
name: empty-storage
labels:
app: empty-storage
release: qa
spec:
containers:
- name: empty-nginx
image: ikubernetes/myapp:v1
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
readinessProbe:
failureThreshold: 3
initialDelaySeconds: 3
httpGet:
port: http
path: /index.html
volumeMounts: # 掛載到容器目錄中
- name: test # 如果有多個容器,訪問的內容相同
mountPath: /usr/share/nginx/html # 掛載點爲 ikubernetes的訪問目錄
- name: empty-busybox
image: busybox
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-c"] # 注意 $() 表示變量被引號 如果用command得使用 $$()
args:
- while true; do echo $(date) >> /data/index.html;sleep 2;done
volumeMounts: # 掛載到容器目錄中
- name: test # 與上一個容器共享同一個掛載點
mountPath: /data # 當這裏內容被修改,上一個容器的內容也會變更
volumes:
- name: test # 掛載名稱是test,
emptyDir: {} # 類型emptyDir 容器刪除內容也會被清空
hostPath
將主機節點文件系統上的文件或目錄掛載至pod中, 注意是 主機節點
type類型
取值 | 行爲 |
---|---|
空字符串(默認)用於向後兼容,這意味着在安裝 hostPath 卷之前不會執行任何檢查。 | |
DirectoryOrCreate |
如果在給定路徑上什麼都不存在,那麼將根據需要創建空目錄,權限設置爲 0755,具有與 Kubelet 相同的組和所有權。 |
Directory |
在給定路徑上必須存在的目錄。 |
FileOrCreate |
如果在給定路徑上什麼都不存在,那麼將在那裏根據需要創建空文件,權限設置爲 0644,具有與 Kubelet 相同的組和所有權。 |
File |
在給定路徑上必須存在的文件。 |
Socket |
在給定路徑上必須存在的 UNIX 套接字。 |
CharDevice |
在給定路徑上必須存在的字符設備。 |
BlockDevice |
在給定路徑上必須存在的塊設備。 |
apiVersion: v1
kind: Pod
metadata:
name: host-storage
spec:
containers:
- name: host-busybox
image: ikubernetes/myapp:v1
imagePullPolicy: IfNotPresent
volumeMounts:
- name: mydir
mountPath: /usr/share/nginx/html
volumes:
- name: mydir
hostPath:
path: /data
type: Directory
# 然後在node 主機節點上創建對應的 /data目錄
# MountVolume.SetUp failed for volume "mydir" : hostPath type check failed: /data is not a directory, 這個錯是指,在節點上沒有這個目錄 而不是k8s mstaer的目錄
nfs
nfs
卷能將 NFS (網絡文件系統) 掛載到您的 Pod 中。 不像emptyDir
那樣會在刪除 Pod 的同時也會被刪除,nfs
卷的內容在刪除 Pod 時會被保存,卷只是被卸載掉了。 這意味着nfs
卷可以被預先填充數據,並且這些數據可以在 Pod 之間”傳遞”。
[root@slave1 ~]# yum install -y nfs-utils
[root@slave1 ~]# cat /etc/exports
/data/nfs 192.168.2.0/24(rw)
[root@slave1 ~]# systemctl start nfs
[root@slave1 ~]# mkdir -p /data/nfs/{1,2,3,4,5}
[root@slave1 ~]# echo '11111111' >> /data/nfs/1/index.html
[root@slave1 ~]# echo '22222222' >> /data/nfs/2/index.html
# 在節點級手動測試是否能夠成功掛載
[root@slave2 ~]# mount -t nfs 192.168.2.221:/data/nfs /data/nfs
# 查看nfs服務端掛載情況 exportfs -arv
# nfs客戶端查看服務端口nfs掛載路徑 showmount -e NFS服務端地址
[root@master ~] cat nfs-stornge.yaml
apiVersion: v1
kind: Pod
metadata:
name: nfs-storage
spec:
containers:
- name: nfs-busybox
image: ikubernetes/myapp:v1
imagePullPolicy: IfNotPresent
volumeMounts:
- name: nfssystem
mountPath: /usr/share/nginx/html
volumes:
- name: nfssystem
nfs:
path: /data/nfs
server: 192.168.2.221
]# kubectl get pods nfs-storage -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nfs-storage 1/1 Running 0 35s 10.244.1.103 slave1
[root@master ~]# curl 10.244.1.103/1/index.html
11111111
[root@master ~]# curl 10.244.1.103/2/index.html
22222222
總結
在k8s上使用存儲卷,大致分爲如下步驟,首先我們先在Pod上定義存儲卷,然後在Pod的spec中定義volume將存儲卷定義起來,而後我們需要在容器中去定義要使用的存儲卷,但這種方式必須要在spec.volumes中定義存儲卷,並且必須要明確且清晰的給出訪問存儲系統的客戶端配置信息;
但這對大多數終端用戶來說,幾乎是不可能完成的任務,尤其是遇到非常複雜的存儲系統時更是如此,那麼這種方案其實違背於K8s提出的生產者消費者模式,而生產消費指的是存儲能力的一方提供存儲,消費者只需要向存儲端說明它要使用多少存儲就可以了,而無需更多的關注存儲系統的細節;
很顯然,spec.volumes手動定義的方式是無法滿足這種需求的,爲此k8s系統爲了使得存儲服務也能夠轉爲生產消費者模型機制,它爲Kubernetes的volumes和volumeMounts以及和後端的存儲設備之間增加了一箇中間層,這個中間層就稱之爲PV;