Kubernetes 儲存管理是建構雲端原生應用程式的關鍵基礎設施之一。在容器化環境中,應用程式的資料持久化面臨獨特的挑戰:容器本身具有臨時性,隨時可能被終止、重新排程或遷移到不同節點。如何確保資料在這些動態變化中保持完整性和可用性,成為每位 Kubernetes 管理員和開發者必須面對的核心議題。

傳統上,容器技術被視為適合無狀態應用程式的執行環境,有狀態的資料則依賴外部系統進行管理。然而,隨著 Kubernetes 生態系統的成熟,對於直接在叢集內部運行資料庫、訊息佇列、快取系統等有狀態工作負載的需求日益增長。這種需求的驅動力來自多個面向:減少外部依賴可以降低架構複雜度、提升資料存取效能、簡化維運流程,同時也能更充分地利用 Kubernetes 提供的自動化排程與故障恢復能力。

本文將從儲存管理的基礎概念出發,逐步深入探討 Kubernetes 提供的各種儲存抽象機制,包括 Volume、PersistentVolume、PersistentVolumeClaim 和 StorageClass,並詳細說明如何運用 StatefulSet 來部署和管理有狀態應用程式。透過理論說明與實務範例的結合,協助讀者建立完整的 Kubernetes 儲存管理知識體系,並掌握在生產環境中實施最佳實踐的能力。

容器儲存基礎與 Volume 機制

在深入探討 Kubernetes 的進階儲存功能之前,首先需要理解容器儲存的基本原理以及為何需要 Volume 機制。容器的檔案系統預設是臨時性的,當容器終止時,所有寫入容器檔案系統的資料都會遺失。這種設計對於無狀態應用程式來說並無問題,但對於需要持久化資料的應用程式則造成嚴重限制。

Volume 機制的引入正是為了解決這個問題。Volume 本質上是一個目錄,可能包含一些資料,Pod 中的容器可以存取這個目錄。Volume 的生命週期與 Pod 相同,當 Pod 被刪除時,Volume 中的資料是否保留取決於 Volume 的類型。Kubernetes 支援多種 Volume 類型,每種類型都有其特定的使用場景和特性。

最基本的 Volume 類型是 emptyDir,它在 Pod 被分配到節點時建立,初始狀態為空。Pod 中的所有容器都可以讀寫 emptyDir 中的檔案,但當 Pod 從節點上移除時,emptyDir 中的資料會被永久刪除。emptyDir 非常適合用於 Pod 內多個容器之間的臨時資料共享,例如 sidecar 模式中的日誌收集或資料處理。

hostPath 類型的 Volume 則將節點檔案系統中的檔案或目錄掛載到 Pod 中。這種方式允許容器存取節點上的特定路徑,適用於需要存取節點級別資料的場景,例如存取 Docker 內部資料結構、執行需要存取系統日誌的監控代理等。然而,hostPath 的使用需要特別謹慎,因為它會使 Pod 與特定節點產生耦合關係,影響 Kubernetes 的排程彈性和叢集的可攜性。

以下是一個使用 hostPath 掛載靜態網頁內容的 Deployment 範例:

# nginx-hostpath-deployment.yaml
# 此 Deployment 展示如何使用 hostPath 將節點上的網頁內容掛載到 Nginx 容器
# 適用場景:節點上已存在需要提供服務的靜態檔案

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-webserver
  labels:
    app: nginx-webserver
spec:
  replicas: 3  # 設定副本數量為 3
  selector:
    matchLabels:
      app: nginx-webserver  # 選擇器必須與 Pod 模板的標籤匹配
  template:
    metadata:
      labels:
        app: nginx-webserver
    spec:
      containers:
      - name: nginx-webserver
        image: nginx:alpine  # 使用精簡版 Alpine 基礎映像檔
        ports:
        - containerPort: 80  # 容器監聽埠號
        volumeMounts:
        - name: hostvol
          mountPath: /usr/share/nginx/html  # 掛載到 Nginx 預設網頁根目錄
      volumes:
      - name: hostvol
        hostPath:
          path: /home/webcontent  # 節點上的來源目錄
          type: Directory  # 指定類型為目錄,確保路徑存在

在這個範例中,我們將節點上 /home/webcontent 目錄的內容掛載到容器內的 Nginx 網頁根目錄。這種配置方式在開發測試環境中相當便利,但在生產環境中需要考慮幾個重要問題:首先,所有被排程到不同節點的 Pod 副本都需要能夠存取相同的檔案內容,這在多節點叢集中可能需要額外的同步機制;其次,hostPath 的使用會降低 Pod 的可攜性,因為它假設特定路徑在所有可能的目標節點上都存在且包含正確的內容。

除了 emptyDir 和 hostPath,Kubernetes 還原生支援多種網路儲存類型的 Volume,包括 NFS、GlusterFS、Ceph RBD、iSCSI 等。這些網路儲存類型的 Volume 允許資料在叢集中的任何節點上都能被存取,解決了 hostPath 的可攜性問題。然而,直接在 Pod 定義中指定這些儲存類型需要了解底層儲存系統的具體配置細節,這增加了應用程式與基礎設施之間的耦合程度,也使得應用程式的部署配置變得複雜且難以移植。

為了解決這些問題,Kubernetes 引入了更高層次的儲存抽象:PersistentVolume 和 PersistentVolumeClaim。這兩個概念將儲存的配置與消費分離,讓管理員負責配置儲存資源,而開發者只需要聲明對儲存的需求即可。這種關注點分離的設計大大簡化了有狀態應用程式的部署和管理。

PersistentVolume 與 PersistentVolumeClaim 架構

PersistentVolume(PV)和 PersistentVolumeClaim(PVC)是 Kubernetes 儲存管理的核心抽象。PV 代表叢集中的一塊儲存資源,可以由管理員預先配置(靜態配置),也可以透過 StorageClass 動態建立。PVC 則是使用者對儲存資源的需求聲明,它描述了所需的儲存大小、存取模式等屬性。Kubernetes 控制平面會自動將 PVC 與合適的 PV 進行綁定,完成儲存資源的分配。

這種設計模式類似於 Pod 與 Node 的關係:Pod 消費 Node 的計算資源,而 PVC 消費 PV 的儲存資源。正如 Pod 不需要知道它會被排程到哪個具體的 Node 上,PVC 也不需要知道它會綁定到哪個具體的 PV。這種抽象讓應用程式的部署配置與底層儲存實現解耦,提高了可攜性和維護性。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

title PersistentVolume 與 PersistentVolumeClaim 生命週期

participant "管理員" as Admin
participant "Kubernetes API Server" as API
participant "PV Controller" as PVC_Ctrl
participant "儲存系統" as Storage

== 靜態配置流程 ==

Admin -> API : 建立 PersistentVolume
API -> PVC_Ctrl : 通知 PV 建立事件
PVC_Ctrl -> Storage : 驗證儲存資源
Storage --> PVC_Ctrl : 確認資源可用
PVC_Ctrl -> API : 更新 PV 狀態為 Available

== PVC 綁定流程 ==

participant "使用者" as User
participant "Pod" as Pod

User -> API : 建立 PersistentVolumeClaim
API -> PVC_Ctrl : 通知 PVC 建立事件
PVC_Ctrl -> PVC_Ctrl : 搜尋符合條件的 PV
PVC_Ctrl -> API : 綁定 PVC 與 PV
API -> API : 更新 PV 狀態為 Bound

== Pod 使用 PVC ==

User -> API : 建立 Pod(參照 PVC)
API -> Pod : 排程 Pod 到節點
Pod -> Storage : 掛載 PV 到容器

== 資源釋放流程 ==

User -> API : 刪除 Pod
Pod --> Storage : 卸載 Volume
User -> API : 刪除 PVC
API -> PVC_Ctrl : 通知 PVC 刪除事件
PVC_Ctrl -> PVC_Ctrl : 根據 Reclaim Policy 處理
PVC_Ctrl -> API : 更新 PV 狀態

@enduml

PersistentVolume 的定義包含多個重要屬性,這些屬性決定了 PV 的特性和可用性。以下是一個 NFS 類型 PV 的完整定義範例:

# nfs-pv.yaml
# 定義一個使用 NFS 儲存的 PersistentVolume
# 此 PV 提供 5Gi 容量,支援多節點同時讀寫

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv001
  labels:
    tier: "silver"  # 自定義標籤,用於 PVC 選擇器匹配
    environment: "production"
spec:
  capacity:
    storage: 5Gi  # PV 的儲存容量
  accessModes:
    - ReadWriteMany  # 存取模式:多節點讀寫
  persistentVolumeReclaimPolicy: Retain  # 回收策略:保留資料
  storageClassName: nfs  # 儲存類別名稱
  mountOptions:
    - hard  # NFS 掛載選項:硬掛載
    - nfsvers=4.1  # 指定 NFS 協定版本
  nfs:
    path: /exports/data  # NFS 伺服器上的匯出路徑
    server: 172.17.0.2  # NFS 伺服器位址

在這個定義中,capacity 指定了 PV 的儲存容量,這是 PVC 在選擇 PV 時的重要匹配條件之一。accessModes 定義了 PV 支援的存取模式,Kubernetes 支援三種存取模式:ReadWriteOnce(RWO)表示 Volume 可以被單個節點以讀寫模式掛載;ReadOnlyMany(ROX)表示 Volume 可以被多個節點以唯讀模式掛載;ReadWriteMany(RWX)表示 Volume 可以被多個節點以讀寫模式掛載。不同的儲存後端支援不同的存取模式,例如 NFS 支援所有三種模式,而大多數區塊儲存只支援 ReadWriteOnce。

persistentVolumeReclaimPolicy 定義了當 PVC 被刪除後 PV 的處理方式。Retain 策略會保留 PV 和其中的資料,需要管理員手動清理;Delete 策略會同時刪除 PV 和底層儲存資源;Recycle 策略(已棄用)會執行基本的清除操作後使 PV 重新可用。在生產環境中,Retain 是最安全的選擇,可以防止意外的資料遺失。

storageClassName 指定了 PV 所屬的儲存類別,這個屬性在 PVC 與 PV 的匹配過程中扮演重要角色。只有具有相同 storageClassName 的 PVC 才能綁定到這個 PV(除非 PVC 指定了空字串的 storageClassName,這種情況下它只會綁定到沒有設定 storageClassName 的 PV)。

相對於 PV 由管理員配置,PVC 是由使用者(應用程式開發者)建立的資源,用於聲明對儲存的需求:

# my-pvc.yaml
# 定義一個 PersistentVolumeClaim
# 此 PVC 請求 5Gi 的 NFS 儲存空間

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
  namespace: default
spec:
  storageClassName: nfs  # 指定儲存類別,必須與目標 PV 匹配
  accessModes:
    - ReadWriteMany  # 請求的存取模式
  resources:
    requests:
      storage: 5Gi  # 請求的儲存容量
  selector:
    matchLabels:
      tier: "silver"  # 標籤選擇器,只綁定具有此標籤的 PV

PVC 的 selector 欄位允許使用者進一步限制可以綁定的 PV 範圍。在這個範例中,PVC 只會綁定到具有 tier: silver 標籤的 PV。這個功能在大型叢集中特別有用,可以將不同等級的儲存資源分配給不同的應用程式。

當 PVC 被建立後,Kubernetes 的 PV Controller 會開始尋找符合條件的 PV。匹配條件包括:storageClassName 必須相同、PV 的容量必須大於或等於 PVC 請求的容量、PV 的存取模式必須包含 PVC 請求的所有存取模式、以及 PV 必須滿足 PVC 的標籤選擇器條件。一旦找到符合條件的 PV,Controller 會將它們綁定在一起,此時 PV 的狀態從 Available 變為 Bound,PVC 也進入 Bound 狀態。

將 PVC 掛載到 Pod 的方式非常直觀,只需要在 Pod 的 volumes 定義中參照 PVC 即可:

# nginx-with-pvc.yaml
# 展示如何在 Deployment 中使用 PersistentVolumeClaim
# 此配置將 PVC 掛載為 Nginx 的網頁根目錄

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-webserver
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-webserver
  template:
    metadata:
      labels:
        app: nginx-webserver
    spec:
      containers:
      - name: nginx-webserver
        image: nginx:alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: web-content
          mountPath: /usr/share/nginx/html  # 掛載點路徑
          readOnly: false  # 設定為可讀寫
      volumes:
      - name: web-content
        persistentVolumeClaim:
          claimName: my-pvc  # 參照之前建立的 PVC

透過 PV 和 PVC 的分離設計,應用程式的部署配置變得簡潔且可攜。開發者只需要知道需要多少儲存空間和什麼樣的存取模式,而不需要了解底層儲存系統的具體實現。這種抽象層的設計使得相同的應用程式配置可以在不同的 Kubernetes 叢集中使用,無論底層使用的是 NFS、Ceph、雲端儲存還是其他儲存系統。

StorageClass 與動態儲存配置

雖然靜態配置 PV 的方式提供了完整的控制能力,但在大規模環境中手動建立和管理 PV 會變得繁瑣且容易出錯。StorageClass 提供了一種動態配置 PV 的機制,讓 Kubernetes 能夠根據 PVC 的需求自動建立 PV,大幅簡化了儲存資源的管理工作。

StorageClass 定義了一個儲存「類別」,包含了用於動態建立 PV 的配置資訊。每個 StorageClass 都會指定一個 Provisioner,這是負責實際建立儲存資源的組件。Kubernetes 內建支援多種雲端供應商的 Provisioner,也支援透過 CSI(Container Storage Interface)使用第三方的 Provisioner。

# standard-storageclass.yaml
# 定義一個標準的 StorageClass
# 使用 NFS Provisioner 進行動態配置

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"  # 設為預設儲存類別
provisioner: cluster.local/nfs-client-provisioner  # Provisioner 名稱
parameters:
  archiveOnDelete: "true"  # 刪除 PVC 時保留資料到歸檔目錄
reclaimPolicy: Delete  # 預設的回收策略
allowVolumeExpansion: true  # 允許 Volume 擴容
volumeBindingMode: Immediate  # 立即綁定模式

在這個 StorageClass 定義中,provisioner 指定了負責建立 PV 的 Provisioner。當使用者建立一個指定此 StorageClass 的 PVC 時,Kubernetes 會呼叫這個 Provisioner 來動態建立對應的 PV。parameters 欄位中的參數會被傳遞給 Provisioner,不同的 Provisioner 支援不同的參數。

allowVolumeExpansion 是一個重要的特性,當設為 true 時,允許使用者透過修改 PVC 的 spec.resources.requests.storage 來擴展 Volume 的大小。這個功能對於需要動態增長儲存空間的應用程式非常有用,但要注意不是所有的儲存後端都支援這個功能。

volumeBindingMode 控制了 PV 綁定的時機。Immediate 模式表示 PVC 建立時立即綁定 PV;WaitForFirstConsumer 模式則會延遲綁定,直到有 Pod 使用這個 PVC 時才進行綁定。後者在使用區域性儲存(如雲端供應商的區域性磁碟)時特別重要,可以確保 PV 建立在 Pod 將被排程的同一區域。

以下是使用動態配置的 PVC 範例:

# dynamic-pvc.yaml
# 使用 StorageClass 進行動態儲存配置的 PVC
# Kubernetes 會自動建立對應的 PV

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dynamic-pvc
spec:
  storageClassName: standard  # 指定 StorageClass 名稱
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

當這個 PVC 被建立時,Kubernetes 會尋找名為 standard 的 StorageClass,然後呼叫其指定的 Provisioner 來建立一個 10Gi 的 PV,並自動將其與 PVC 綁定。整個過程對使用者來說是透明的,大大簡化了儲存資源的請求流程。

在實際的生產環境中,通常會定義多個 StorageClass 來滿足不同的需求。例如,可以根據效能等級(standard、premium)、儲存類型(block、file、object)、或資料保護等級(replicated、snapshot-enabled)來建立不同的 StorageClass。管理員可以將其中一個設為預設的 StorageClass,這樣當 PVC 沒有指定 storageClassName 時就會使用預設值。

# premium-storageclass.yaml
# 定義高效能 StorageClass
# 使用 SSD 儲存並啟用 IOPS 限制

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: premium-ssd
provisioner: kubernetes.io/aws-ebs  # AWS EBS Provisioner
parameters:
  type: io1  # 使用 Provisioned IOPS SSD
  iopsPerGB: "50"  # 每 GB 配置 50 IOPS
  fsType: ext4  # 檔案系統類型
reclaimPolicy: Retain  # 保留回收策略
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer  # 延遲綁定以確保區域性

這個範例展示了 AWS EBS 的 StorageClass 配置,使用 io1 類型的 SSD 以獲得高效能,並設定了每 GB 50 IOPS 的配置。volumeBindingMode 設為 WaitForFirstConsumer 是 AWS 環境中的最佳實踐,因為 EBS Volume 只能被同一可用區域的節點存取,延遲綁定可以確保 Volume 建立在正確的區域。

Container Storage Interface(CSI)架構

Container Storage Interface(CSI)是 Kubernetes 儲存系統的現代化擴充機制,它定義了一個標準化的介面,讓儲存供應商能夠開發可跨多個容器協調系統使用的儲存外掛。CSI 的引入解決了早期 Kubernetes 儲存外掛開發的幾個主要問題:外掛程式碼需要包含在 Kubernetes 主專案中、外掛的開發和發布週期受限於 Kubernetes 的發布週期、以及缺乏跨容器協調系統的標準化介面。

CSI 架構由三個主要組件構成:CSI Controller、CSI Node Plugin 和 CSI Identity Service。CSI Controller 負責 Volume 的建立、刪除和快照等控制平面操作;CSI Node Plugin 在每個節點上運行,負責 Volume 的掛載、卸載和裝置格式化等資料平面操作;CSI Identity Service 則提供外掛的身份資訊和功能查詢。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

title Container Storage Interface(CSI)架構

package "Kubernetes 控制平面" {
  component [API Server] as API
  component [Controller Manager] as CM
  component [External Provisioner] as EP
  component [External Attacher] as EA
  component [External Snapshotter] as ES
}

package "CSI Controller Pod" {
  component [CSI Controller Plugin] as CCP
}

package "每個節點" {
  component [Kubelet] as KL
  component [CSI Node Plugin] as CNP
}

database "儲存系統" as Storage

API --> CM : Watch PVC/PV
CM --> EP : 觸發配置
EP --> CCP : CreateVolume
CCP --> Storage : 建立 Volume

CM --> EA : 觸發附加
EA --> CCP : ControllerPublish
CCP --> Storage : 附加 Volume

KL --> CNP : NodeStageVolume
KL --> CNP : NodePublishVolume
CNP --> Storage : 掛載 Volume

API --> ES : Watch VolumeSnapshot
ES --> CCP : CreateSnapshot
CCP --> Storage : 建立快照

note bottom of CCP
  CSI Controller 負責:
  - Volume 生命週期管理
  - 快照管理
  - Volume 擴容
end note

note bottom of CNP
  CSI Node Plugin 負責:
  - Volume 格式化
  - 掛載/卸載操作
  - Volume 狀態報告
end note

@enduml

使用 CSI 驅動程式需要在叢集中部署對應的組件。大多數 CSI 驅動程式會以 DaemonSet 的形式在每個節點上部署 Node Plugin,並以 Deployment 或 StatefulSet 的形式部署 Controller Plugin。以下是一個使用 CSI 驅動程式的 StorageClass 範例:

# csi-storageclass.yaml
# 使用 CSI 驅動程式的 StorageClass 定義
# 此範例使用假設的 CSI 驅動程式

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: csi-standard
provisioner: csi.example.com  # CSI 驅動程式名稱
parameters:
  csi.storage.k8s.io/fstype: ext4  # 檔案系統類型
  replicationType: async  # 驅動程式特定參數
  compressionEnabled: "true"  # 啟用壓縮
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

CSI 的另一個重要特性是支援 Volume 快照功能。VolumeSnapshot 資源允許使用者建立 PV 的時間點副本,這對於資料備份、災難復原和複製生產環境進行測試等場景非常有用。使用 Volume 快照需要儲存系統和 CSI 驅動程式的支援,並且需要在叢集中部署 VolumeSnapshot CRD 和 External Snapshotter 組件。

# volumesnapshot.yaml
# 建立 Volume 快照
# 此快照可用於備份或建立新的 PVC

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: my-snapshot
spec:
  volumeSnapshotClassName: csi-snapclass  # 快照類別名稱
  source:
    persistentVolumeClaimName: my-pvc  # 來源 PVC

從快照還原資料只需要建立一個新的 PVC 並指定快照作為資料來源:

# restore-from-snapshot.yaml
# 從快照還原建立新的 PVC
# 新 PVC 會包含快照時間點的資料

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: restored-pvc
spec:
  storageClassName: csi-standard
  dataSource:
    name: my-snapshot
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi  # 必須大於或等於快照大小

CSI 架構的引入使 Kubernetes 的儲存生態系統更加開放和豐富。目前已有數百個 CSI 驅動程式可供使用,涵蓋了幾乎所有主流的儲存系統,包括公有雲服務(AWS EBS、Azure Disk、GCP Persistent Disk)、企業級儲存陣列(NetApp、Dell EMC、Pure Storage)、軟體定義儲存(Ceph、Portworx、OpenEBS)等。

StatefulSet 與有狀態應用程式管理

在討論了 Kubernetes 的儲存機制之後,接下來探討如何運用這些機制來部署和管理有狀態應用程式。StatefulSet 是 Kubernetes 專門為有狀態應用程式設計的工作負載控制器,它提供了幾個對於有狀態應用程式至關重要的特性:穩定且唯一的網路識別符、穩定且持久的儲存、有序的部署和擴展、以及有序的終止和刪除。

首先需要理解無狀態應用程式與有狀態應用程式之間的根本差異。無狀態應用程式不依賴本地儲存的狀態,任何一個副本都可以處理任何請求,副本之間完全可以互相替換。這種特性使得無狀態應用程式非常容易水平擴展:只需要增加副本數量即可提升處理能力,Kubernetes 的 ReplicaSet 或 Deployment 能夠很好地管理這類應用程式。

然而,有狀態應用程式的情況則完全不同。以分散式資料庫為例,叢集中的每個節點通常都有獨特的角色和身份(如主節點和從節點)、每個節點儲存不同的資料分片、節點之間需要透過穩定的網路位址進行通訊、以及節點的啟動和關閉順序可能會影響資料的一致性。這些需求是 Deployment 和 ReplicaSet 無法滿足的,因為它們建立的 Pod 具有隨機的名稱、可能被任意終止或重新排程、且沒有穩定的網路識別符。

StatefulSet 透過以下機制來滿足有狀態應用程式的需求:

Pod 識別符的穩定性是 StatefulSet 最重要的特性之一。StatefulSet 中的每個 Pod 都會獲得一個從 0 開始的序號索引,這個索引會附加到 StatefulSet 名稱後面形成 Pod 名稱。例如,名為 mysql 的 StatefulSet 會建立名為 mysql-0、mysql-1、mysql-2 的 Pod。即使 Pod 被重新排程到不同節點,它的名稱也會保持不變。這種穩定的命名使得其他系統能夠可靠地識別和連接到特定的 Pod。

與穩定的 Pod 名稱相配合,StatefulSet 還提供穩定的網路識別符。當與 Headless Service 搭配使用時,每個 Pod 都會獲得一個可預測的 DNS 名稱,格式為 <pod-name>.<service-name>.<namespace>.svc.cluster.local。例如,mysql-0 Pod 可以透過 mysql-0.mysql.default.svc.cluster.local 被存取。這讓應用程式能夠透過穩定的 DNS 名稱進行相互通訊,而不需要依賴會變化的 IP 位址。

# mysql-headless-service.yaml
# 為 StatefulSet 建立 Headless Service
# 提供穩定的網路識別符給每個 Pod

apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
spec:
  ports:
  - port: 3306
    name: mysql
  clusterIP: None  # Headless Service,不分配 ClusterIP
  selector:
    app: mysql

StatefulSet 的有序操作特性確保了 Pod 的部署、擴展、更新和刪除都按照定義的順序進行。預設情況下,Pod 會按照 0、1、2… 的順序依次建立,只有前一個 Pod 進入 Running 和 Ready 狀態後,下一個 Pod 才會開始建立。類似地,在縮減規模或刪除時,Pod 會按照相反的順序(…2、1、0)依次終止。這種有序操作對於需要主從關係或叢集初始化順序的應用程式非常重要。

以下是一個完整的 MongoDB Replica Set StatefulSet 定義:

# mongodb-statefulset.yaml
# 部署 MongoDB Replica Set
# 使用 StatefulSet 確保穩定的網路識別符和持久儲存

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
spec:
  serviceName: mongodb  # 必須與 Headless Service 名稱匹配
  replicas: 3
  selector:
    matchLabels:
      app: mongodb
  template:
    metadata:
      labels:
        app: mongodb
    spec:
      terminationGracePeriodSeconds: 10  # 優雅終止等待時間
      containers:
      - name: mongodb
        image: mongo:5.0
        command:
        - mongod
        - "--replSet"
        - rs0  # Replica Set 名稱
        - "--bind_ip_all"  # 綁定所有網路介面
        ports:
        - containerPort: 27017
          name: mongodb
        volumeMounts:
        - name: mongodb-data
          mountPath: /data/db  # MongoDB 資料目錄
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1000m"
        readinessProbe:
          exec:
            command:
            - mongo
            - --eval
            - "db.adminCommand('ping')"
          initialDelaySeconds: 30
          periodSeconds: 10
        livenessProbe:
          exec:
            command:
            - mongo
            - --eval
            - "db.adminCommand('ping')"
          initialDelaySeconds: 30
          periodSeconds: 10
  volumeClaimTemplates:
  - metadata:
      name: mongodb-data
    spec:
      accessModes: ["ReadWriteOnce"]  # MongoDB 需要獨佔存取
      storageClassName: premium-ssd  # 使用高效能儲存類別
      resources:
        requests:
          storage: 20Gi

volumeClaimTemplates 是 StatefulSet 特有的功能,它定義了 PVC 的範本。當 StatefulSet 建立新的 Pod 時,會根據這個範本為每個 Pod 建立一個獨立的 PVC。這些 PVC 的名稱也是穩定的,格式為 <volumeClaimTemplate-name>-<pod-name>,例如 mongodb-data-mongodb-0。即使 Pod 被刪除並重新建立,它仍會綁定到原來的 PVC,從而保留資料。

這種設計確保了每個 Pod 都有專屬的持久儲存,且資料在 Pod 重新排程後仍然可用。這對於資料庫等需要持久化資料的應用程式來說是必不可少的。需要注意的是,當 StatefulSet 被刪除時,volumeClaimTemplates 建立的 PVC 不會自動刪除,這是為了防止資料意外遺失。如果需要刪除這些 PVC,必須手動進行。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

title StatefulSet 部署流程與持久儲存管理

participant "使用者" as User
participant "API Server" as API
participant "StatefulSet Controller" as SSC
participant "PV Controller" as PVC
participant "Scheduler" as Sched
participant "Kubelet" as KL

User -> API : 建立 StatefulSet
API -> SSC : 通知 StatefulSet 建立

== 建立第一個 Pod (mongodb-0) ==

SSC -> API : 建立 PVC (mongodb-data-mongodb-0)
API -> PVC : 處理 PVC 請求
PVC -> PVC : 動態配置 PV
PVC -> API : PVC 已綁定

SSC -> API : 建立 Pod (mongodb-0)
API -> Sched : 排程 Pod
Sched -> API : 綁定到節點
API -> KL : 建立 Pod
KL -> KL : 掛載 Volume
KL -> KL : 啟動容器
KL -> API : Pod Ready

== 建立第二個 Pod (mongodb-1) ==

SSC -> SSC : 確認 mongodb-0 Ready
SSC -> API : 建立 PVC (mongodb-data-mongodb-1)
SSC -> API : 建立 Pod (mongodb-1)

note over SSC
  等待 mongodb-1 Ready
  然後建立 mongodb-2
end note

== Pod 重新排程場景 ==

note over API
  mongodb-1 節點故障
end note

SSC -> API : 重新建立 Pod (mongodb-1)
API -> Sched : 排程到新節點
KL -> KL : 綁定原有 PVC (mongodb-data-mongodb-1)
KL -> KL : 資料完整保留

@enduml

StatefulSet 還支援多種更新策略。OnDelete 策略是最保守的選擇,StatefulSet 不會自動更新 Pod,而是等待使用者手動刪除舊的 Pod 後才建立新的。RollingUpdate 策略則會自動進行滾動更新,按照 Pod 序號的逆序依次更新,可以透過 partition 參數實現分段更新(金絲雀發布)。

# 在 StatefulSet spec 中配置更新策略
spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 1  # 只更新序號 >= 1 的 Pod

當 partition 設為 1 時,只有 mongodb-1 和 mongodb-2 會被更新,mongodb-0 會保持舊版本。這種方式允許管理員先在部分 Pod 上測試新版本,確認沒有問題後再將 partition 設為 0 完成全部更新。

儲存管理最佳實踐

在實際的生產環境中實施 Kubernetes 儲存管理時,遵循最佳實踐能夠確保系統的可靠性、效能和可維護性。以下是基於實務經驗整理的關鍵建議。

首先,關於 StorageClass 的設計,建議根據效能需求和資料重要性定義多個 StorageClass。例如,可以建立 standard(標準 HDD)、premium(SSD)、high-availability(跨區域複製)等不同等級。每個 StorageClass 應該有清晰的命名和文件說明其特性和適用場景。同時,設定一個合理的預設 StorageClass,讓不需要特殊儲存需求的應用程式可以直接使用預設值,簡化部署配置。

# 多層級 StorageClass 設計範例
---
# 標準儲存,適用於一般應用程式
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
    description: "標準 HDD 儲存,適用於非效能敏感的工作負載"
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

---
# 高效能儲存,適用於資料庫
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: premium
  annotations:
    description: "高效能 SSD 儲存,適用於資料庫和效能敏感應用"
provisioner: kubernetes.io/aws-ebs
parameters:
  type: io2
  iopsPerGB: "50"
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

對於存取模式的選擇,需要根據應用程式的實際需求來決定。如果應用程式只需要單一 Pod 存取儲存,使用 ReadWriteOnce 即可,這也是大多數區塊儲存支援的模式。如果需要多個 Pod 同時讀寫相同的資料,則需要使用 ReadWriteMany,這通常需要 NFS 或其他共享檔案系統的支援。避免過度請求存取模式,因為這可能限制可用的 PV 選擇,並可能增加成本。

資料保護是儲存管理中最重要的考量之一。對於生產環境中的重要資料,強烈建議使用 Retain 回收策略,這樣即使 PVC 被意外刪除,資料仍然保留在 PV 中。同時,應該建立定期備份機制,可以利用 CSI 快照功能或外部備份工具(如 Velero)來實現。備份策略應該包括備份頻率、保留期限、以及災難復原程序的定期演練。

# 自動快照排程範例(使用 VolumeSnapshotClass)
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: daily-snapshot
driver: csi.example.com
deletionPolicy: Retain  # 保留快照即使刪除 VolumeSnapshot 資源
parameters:
  csi.storage.k8s.io/snapshotter-secret-name: csi-secret
  csi.storage.k8s.io/snapshotter-secret-namespace: default

監控和可觀測性對於維護健康的儲存系統不可或缺。建議監控以下指標:PV 和 PVC 的使用量和可用空間、儲存 I/O 延遲和吞吐量、CSI 驅動程式的健康狀態、以及 StorageClass 的動態配置成功率。可以使用 Prometheus 和 Grafana 來收集和視覺化這些指標,並設定適當的告警閾值。

對於有狀態應用程式的部署,除了使用 StatefulSet 外,還需要考慮以下幾點:確保 Pod 的 readinessProbe 和 livenessProbe 配置正確,以便 Kubernetes 能夠準確判斷 Pod 的健康狀態;設定合理的 terminationGracePeriodSeconds,讓應用程式有足夠的時間完成正在進行的操作和優雅關閉;對於資料庫等需要特定啟動順序的應用程式,可以使用 init containers 來處理初始化邏輯。

效能最佳化也是儲存管理的重要面向。使用區域性儲存時,設定 volumeBindingMode 為 WaitForFirstConsumer 以避免跨區域資料傳輸。根據工作負載特性選擇合適的儲存類型,例如隨機讀寫密集的資料庫應該使用 SSD,而大量循序寫入的日誌系統可以使用 HDD。對於需要極低延遲的應用,可以考慮使用本地 SSD(Local Persistent Volume),但要注意這會降低 Pod 的調度彈性。

多租戶環境中的儲存隔離也需要特別注意。可以使用 ResourceQuota 來限制每個 Namespace 能夠請求的儲存總量,使用 LimitRange 來限制單個 PVC 的大小範圍。對於需要嚴格隔離的場景,可以為不同租戶建立專用的 StorageClass,並透過 RBAC 控制對 StorageClass 的存取權限。

# ResourceQuota 範例:限制 Namespace 的儲存使用
apiVersion: v1
kind: ResourceQuota
metadata:
  name: storage-quota
  namespace: tenant-a
spec:
  hard:
    requests.storage: "100Gi"  # 總儲存請求上限
    persistentvolumeclaims: "10"  # PVC 數量上限
    premium.storageclass.storage.k8s.io/requests.storage: "50Gi"  # 特定 StorageClass 上限

最後,關於維運流程的建議:建立清晰的儲存資源命名規範和標籤策略,方便管理和追蹤;定期審查未使用的 PV 和 PVC,清理不再需要的儲存資源以節省成本;記錄重要的儲存操作(如擴容、快照、還原)以便追蹤和審計;在進行任何破壞性操作之前,確保有完整的備份和經過測試的還原程序。

結語

Kubernetes 儲存管理是一個複雜但至關重要的領域,它為雲端原生應用程式提供了資料持久化的基礎設施。透過本文的探討,我們從最基本的 Volume 概念出發,逐步深入理解了 PersistentVolume 和 PersistentVolumeClaim 的抽象設計、StorageClass 的動態配置機制、CSI 的擴充架構,以及 StatefulSet 在有狀態應用程式管理中的關鍵角色。

PV 和 PVC 的分離設計體現了 Kubernetes 一貫的關注點分離原則,讓管理員能夠專注於儲存資源的配置,而開發者只需要聲明對儲存的需求。這種抽象不僅提高了應用程式的可攜性,也使得儲存資源的管理更加系統化和自動化。StorageClass 和動態配置進一步簡化了這個流程,讓儲存資源能夠隨需建立,大幅提升了運維效率。

CSI 的引入使 Kubernetes 的儲存生態系統更加開放和豐富。標準化的介面讓儲存供應商能夠快速開發和發布新的功能,使用者也能夠更容易地在不同的儲存系統之間選擇和切換。Volume 快照、克隆、擴容等進階功能的支援,讓 Kubernetes 能夠滿足更多企業級的儲存需求。

StatefulSet 為有狀態應用程式提供了必要的保障,包括穩定的網路識別符、穩定的持久儲存,以及有序的生命週期管理。這些特性使得資料庫、訊息佇列、分散式系統等複雜的有狀態應用程式能夠在 Kubernetes 上可靠地運行,擴展了 Kubernetes 的應用範圍。

在實際應用中,儲存管理的成功不僅取決於對技術機制的理解,更需要根據具體需求制定合適的策略。選擇正確的儲存類型和存取模式、設計合理的 StorageClass 層級、實施完善的資料保護措施、建立有效的監控和維運流程,這些都是確保儲存系統可靠運行的關鍵因素。

隨著雲端原生技術的持續發展,Kubernetes 儲存管理也在不斷演進。新的 CSI 功能、更強大的資料保護機制、更智慧的資源管理能力正在陸續推出。保持對新技術的關注,並結合自身環境的特點進行評估和採用,將有助於建構更加高效、可靠的儲存基礎設施,為業務應用提供堅實的資料保障。