k8s-volume

張鐵牛發表於2022-01-16

1. 簡介

我們都知道 Container 中的檔案在磁碟上是臨時存放的,這給 Container 中執行的較重要的應用 程式帶來一些問題。

  1. 是當容器崩潰時檔案丟失。(kubelet 會重新啟動容器, 但容器會以乾淨的狀態重啟)
  2. 在同一 Pod 中執行多個容器如何共享檔案

Kubernetes 卷(Volume)這一抽象概念能夠解決這兩個問題。

2. 背景

Docker 也有 卷(Volume)的概念,但對它只有少量且鬆散的管理。 Docker 卷是磁碟上或者另外一個容器內的一個目錄。 Docker 提供卷驅動程式,但是其功能非常有限。

Kubernetes 支援很多型別的卷。 Pod 可以同時使用任意數目的卷型別。 臨時卷型別的生命週期與 Pod 相同,但持久卷可以比 Pod 的存活期長。 當 Pod 不再存在時,Kubernetes 也會銷燬臨時卷;不過 Kubernetes 不會銷燬 持久卷。對於給定 Pod 中任何型別的卷,在容器重啟期間資料都不會丟失。

卷的核心是一個目錄,其中可能存有資料,Pod 中的容器可以訪問該目錄中的資料。 所採用的特定的卷型別將決定該目錄如何形成的、使用何種介質儲存資料以及目錄中存放 的內容。

使用卷時, 在 .spec.volumes 欄位中設定為 Pod 提供的卷,並在 .spec.containers[*].volumeMounts 欄位中宣告卷在容器中的掛載位置。 容器中的程式看到的是由它們的 Docker 映象和卷組成的檔案系統檢視。 Docker 映象 位於檔案系統層次結構的根部。各個卷則掛載在映象內的指定路徑上。 卷不能掛載到其他卷之上,也不能與其他卷有硬連結。Pod 配置中的每個容器必須獨立指定各個卷的掛載位置。

3. emptyDir

當 Pod 分派到某個 Node 上時,emptyDir 卷會被建立,並且在 Pod 在該節點上執行期間,卷一直存在。 就像其名稱表示的那樣,卷最初是空的。 儘管 Pod 中的容器掛載 emptyDir 卷的路徑可能相同也可能不同,這些容器都可以讀寫 emptyDir 卷中相同的檔案。 當 Pod 因為某些原因被從節點上刪除時,emptyDir 卷中的資料也會被永久刪除。

說明: 容器崩潰並會導致 Pod 被從節點上移除,因此容器崩潰期間 emptyDir 卷中的資料是安全的。

資源模板如下:

apiVersion: v1
kind: Pod
metadata:
  name: producer-consumer
spec:
  containers:
  - image: busybox
    name: producer
    volumeMounts:
    - mountPath: /producer_dir
      name: shared-volume
    args:
    - /bin/sh
    - -c
    - echo "hello world" > /producer_dir/hello; sleep 3000
  - image: busybox
    name: consumer
    volumeMounts:
    - mountPath: /consumer_dir
      name: shared-volume
    args:
    - /bin/sh
    - -c
    - cat /consumer_dir/hello; sleep 3000
  volumes:
  - name: shared-volume
    emptyDir: {}

建立一個pod,其中有兩個container,一個是producer,另一個是consumer,producer負責生成hello檔案並且寫入內容hello world,consumer 則負責讀取hello檔案中的內容,驗證同一pod中的container儲存共享機制。

因為 emptyDir 是 Docker Host 檔案系統裡的目錄,其效果相當於在k8s-woker-01上執行了 docker run -v /producer_dirdocker run -v /consumer_dir。通過 docker inspect 檢視容器的詳細配置資訊,我們發現兩個容器都 mount 了同一個目錄:

$ docker inspect 52305cbb0dec

$ docker inspect c946762ccc8c

這裡Source指定的路徑就是 emptyDir 在 Host 上的真正路徑。

emptyDir 是 Host 上建立的臨時目錄,其優點是能夠方便地為 Pod 中的容器提供共享儲存,不需要額外的配置。但它不具備永續性,如果 Pod 不存在了,emptyDir 也就沒有了。根據這個特性,emptyDir 特別適合 Pod 中的容器需要臨時共享儲存空間的場景,比如前面的生產者消費者用例。

4. hostPath

警告:

HostPath 卷存在許多安全風險,最佳做法是儘可能避免使用 HostPath。 當必須使用 HostPath 卷時,它的範圍應僅限於所需的檔案或目錄,並以只讀方式掛載。

hostPath 卷能將主機節點檔案系統上的檔案或目錄掛載到你的 Pod 中。 雖然這不是大多數 Pod 需要的,但是它為一些應用程式提供了強大的逃生艙。

例如,hostPath 的一些用法有:

  • 執行一個需要訪問 Docker 內部機制的容器;可使用 hostPath 掛載 /var/lib/docker 路徑。
  • 允許 Pod 指定給定的 hostPath 在執行 Pod 之前是否應該存在,是否應該建立以及應該以什麼方式存在。

除了必需的 path 屬性之外,使用者可以選擇性地為 hostPath 卷指定 type

支援的 type 值如下:

取值 行為
空字串(預設)用於向後相容,這意味著在安裝 hostPath 卷之前不會執行任何檢查。
DirectoryOrCreate 如果在給定路徑上什麼都不存在,那麼將根據需要建立空目錄,許可權設定為 0755,具有與 kubelet 相同的組和屬主資訊。
Directory 在給定路徑上必須存在的目錄。
FileOrCreate 如果在給定路徑上什麼都不存在,那麼將在那裡根據需要建立空檔案,許可權設定為 0644,具有與 kubelet 相同的組和所有權。
File 在給定路徑上必須存在的檔案。
Socket 在給定路徑上必須存在的 UNIX 套接字。

hostPath 配置示例:

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # 宿主上目錄位置
      path: /data
      # 此欄位為可選
      type: Directory

注意: FileOrCreate 模式不會負責建立檔案的父目錄。 如果欲掛載的檔案的父目錄不存在,Pod 啟動會失敗。 為了確保這種模式能夠工作,可以嘗試把檔案和它對應的目錄分開掛載,如 FileOrCreate 配置所示。

hostPath FileOrCreate 配置示例

apiVersion: v1
kind: Pod
metadata:
  name: test-webserver
spec:
  containers:
  - name: test-webserver
    image: k8s.gcr.io/test-webserver:latest
    volumeMounts:
    - mountPath: /var/local/aaa
      name: mydir
    - mountPath: /var/local/aaa/1.txt
      name: myfile
  volumes:
  - name: mydir
    hostPath:
      # 確保檔案所在目錄成功建立。
      path: /var/local/aaa
      type: DirectoryOrCreate
  - name: myfile
    hostPath:
      path: /var/local/aaa/1.txt
      type: FileOrCreate

5. local

local 卷所代表的是某個被掛載的本地儲存裝置,例如磁碟、分割槽或者目錄。

local 卷只能用作靜態建立的持久卷。尚不支援動態配置。

hostPath 卷相比,local 卷能夠以持久和可移植的方式使用,而無需手動將 Pod 排程到節點。系統通過檢視 PersistentVolume 的節點親和性配置,就能瞭解卷的節點約束。

然而,local 卷仍然取決於底層節點的可用性,並不適合所有應用程式。 如果節點變得不健康,那麼local 卷也將變得不可被 Pod 訪問。使用它的 Pod 將不能執行。 使用 local 卷的應用程式必須能夠容忍這種可用性的降低,以及因底層磁碟的耐用性特徵 而帶來的潛在的資料丟失風險。

下面是一個使用 local 卷和 nodeAffinity 的持久卷示例:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  capacity:
    storage: 100Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/ssd1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - example-node

使用 local 卷時,你需要設定 PersistentVolume 物件的 nodeAffinity 欄位。 Kubernetes 排程器使用 PersistentVolume 的 nodeAffinity 資訊來將使用 local 卷的 Pod 排程到正確的節點。

PersistentVolume 物件的 volumeMode 欄位可被設定為 "Block" (而不是預設值 "Filesystem"),以將 local 卷作為原始塊裝置暴露出來。

使用 local 卷時,建議建立一個 StorageClass 並將其 volumeBindingMode 設定為 WaitForFirstConsumer。 延遲卷繫結的操作可以確保 Kubernetes 在為 PersistentVolumeClaim 作出繫結決策時, 會評估 Pod 可能具有的其他節點約束,例如:如節點資源需求、節點選擇器、Pod 親和性和 Pod 反親和性。

你可以在 Kubernetes 之外單獨運行靜態驅動以改進對 local 卷的生命週期管理。 請注意,此驅動尚不支援動態配置。

說明: 如果不使用外部靜態驅動來管理卷的生命週期,使用者需要手動清理和刪除 local 型別的持久卷。

6. subPath

  1. 同一個pod中多容器掛載同一個卷時提供隔離
  2. 將configMap和secret作為檔案掛載到容器中而不覆蓋掛載目錄下的檔案

6.1 同一pod中多容器掛載同一個卷時提供隔離

先建立一個共享資源的 pod 用於 設定subPath的前後對照比較

暫未設定subPath,通過hostPath的方式將檔案掛載到了叢集節點上

apiVersion: v1
kind: Pod
metadata:
  name: hostpath-test
spec:
  containers:
  - image: busybox
    name: test-c-01
    volumeMounts:
    - mountPath: /opt/test
      name: shared-volume
    args:
    - /bin/sh
    - -c
    - sleep 3000
  - image: busybox
    name: test-c-02
    volumeMounts:
    - mountPath: /opt/test
      name: shared-volume
    args:
    - /bin/sh
    - -c
    - sleep 3000
  volumes:
  - name: shared-volume
    hostPath:
     path: /test
     type: DirectoryOrCreate

演示步驟如圖所示:

此時pod 中容器掛載的卷是共享的

宿主機上的掛載情況如下

接下來加入subPath配置,再來檢視卷共享的情況

資原始檔內容如下:

apiVersion: v1
kind: Pod
metadata:
  name: hostpath-test
spec:
  containers:
  - image: busybox
    name: test-c-01
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - mountPath: /opt/test
      name: shared-volume
      subPath: c01
    args:
    - /bin/sh
    - -c
    - sleep 3000
  - image: busybox
    name: test-c-02
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - mountPath: /opt/test
      name: shared-volume
      subPath: c02
    args:
    - /bin/sh
    - -c
    - sleep 3000
  volumes:
  - name: shared-volume
    hostPath:
     path: /test02
     type: DirectoryOrCreate

演示步驟如圖所示:

此時pod 中容器掛載的卷是不共享的

宿主機上的掛載情況如下

通過檢視宿主機上的掛載目錄可以判斷出,其實是通過制定的subPath的建立了各自的子目錄,每個容器都是用各自的subPath,所以實現了掛載的隔離。

6.2 將configMap/secret作為檔案掛載到容器中而不覆蓋掛載目錄下的檔案

首先我們run一個普通的nginx作為前後的對照

apiVersion: v1
kind: Pod
metadata:
  name: helloworld
spec:
  containers:
  - image: nginx
    name: helloworld
    imagePullPolicy: IfNotPresent

演示步驟如下:

可以看到 正常的nginx配置檔案有很多

接下來 我們通過ConfigMap方式掛載一下nginx.conf檔案

  1. 建立一個cm資源

    普普通通,極其簡單的一個配置檔案

    apiVersion: v1
    data:
      nginx-conf: |
        worker_processes  1;
        events {
          worker_connections  1024;
        }
        http {
          server {
            listen       80;
            location / {
                root   html;
                index  index.html index.htm;
            }
          }
        }
    kind: ConfigMap
    metadata:
      name: conf-nginx
    
  2. 建立nginx-pod

    也是一個很簡單的nginx-pod,掛載了上面的cm資源

    apiVersion: v1
    kind: Pod
    metadata:
      name: helloworld
    spec:
      containers:
      - image: nginx
        name: helloworld
        imagePullPolicy: IfNotPresent
        volumeMounts:
          - name: config-vol
            # 這裡不能寫成 /etc/nginx/nginx.conf 是因為這樣寫會被解析成nginx.conf資料夾,從而導致啟動失敗
            mountPath: /etc/nginx
      volumes:
        - name: config-vol
          configMap:
            name: conf-nginx
            items:
              - key: nginx-conf
                path: nginx.conf
    
  3. 演示步驟如下

這時就發現直接通過cm掛載配置檔案其實是有問題的,自己想掛載的檔案雖然掛載成功了,但是掛載點目錄下的其他資源會丟失,這其實是個很大的隱患,因為有的服務少了部分配置檔案會導致啟動失敗。

如果我們既想要掛載cm資源,又不想把掛載點中的其他配置檔案丟失掉,這時subPath就有作用了

apiVersion: v1
kind: Pod
metadata:
  name: helloworld
spec:
  containers:
  - image: nginx
    name: helloworld
    imagePullPolicy: IfNotPresent
    volumeMounts:
      - name: config-vol
        # 修改掛載點
        mountPath: /etc/nginx/nginx.conf
        # 新增subPath資訊
        subPath: nginx.conf
  volumes:
    - name: config-vol
      configMap:
        name: conf-nginx
        items:
          - key: nginx-conf
            path: nginx.conf

演示步驟如下: