Skip to content

PV/PVC Lifecycle & Dynamic Provisioning

Vì sao vòng đời quan trọng hơn cấu hình tĩnh

Hầu hết tài liệu storage dạy bạn cách tạo một PVC. Nhưng sự cố production hiếm khi nằm ở lúc tạo — chúng nằm ở các chuyển trạng thái: lúc disk được tạo nhưng Pod không bind được, lúc PVC bị xóa và disk biến mất theo (hoặc không), lúc node chết và volume kẹt trong trạng thái Terminating, lúc một PV Released không bao giờ bind lại được. Hiểu storage ở mức production nghĩa là hiểu máy trạng thái của PV/PVC, không chỉ cú pháp YAML.

Vòng đời này cũng là nơi quyết định lớn nhất về độ bền dữ liệu được đưa ra một cách âm thầm: reclaimPolicy quyết định một lệnh kubectl delete pvc sẽ chỉ tháo liên kết hay xóa vĩnh viễn cả terabyte dữ liệu. File này mổ xẻ toàn bộ máy trạng thái, từ provisioning đến reclaiming, và các điểm mà một sai lầm dẫn tới mất dữ liệu hoặc Pod kẹt Pending.

Năm pha của vòng đời: provisioning → binding → mounting → releasing → reclaiming

Pha 1: Provisioning — PV ra đời

PV được tạo theo một trong hai con đường:

  • Static provisioning: admin tạo trước một object PersistentVolume trỏ tới một disk đã tồn tại (ví dụ một PD đã tạo bằng gcloud). PV nằm chờ trong pool, sẵn sàng để bind. Dùng khi disk được quản lý ngoài Kubernetes, khi migrate dữ liệu có sẵn, hoặc khi cần kiểm soát chính xác disk nào gắn với PVC nào.

  • Dynamic provisioning: phổ biến hơn nhiều. Workload chỉ tạo một PVC trỏ tới một StorageClass; Kubernetes tự động gọi CSI driver để tạo disk thật và sinh PV tương ứng — không cần admin can thiệp. Đây là cơ chế mặc định của GKE và là trọng tâm phần sau (GKE Persistent Volumes).

Pha 2: Binding — PVC tìm thấy PV của nó

Kubernetes control loop ghép một PVC với một PV thỏa mãn yêu cầu của nó (dung lượng ≥, access mode tương thích, StorageClass khớp). Binding là một-một và độc quyền: một khi PVC bind vào một PV, PV đó thuộc về PVC đó, không ai khác dùng được, kể cả khi PVC chỉ yêu cầu dung lượng nhỏ hơn PV.

Với dynamic provisioning, binding gần như tức thì vì PV được tạo riêng cho PVC đó. Với static provisioning, nếu không PV nào khớp, PVC ở trạng thái Pending cho đến khi có PV phù hợp xuất hiện. Một bẫy static phổ biến: PVC yêu cầu 100Gi nhưng pool chỉ có PV 50Gi và PV 500Gi — PVC bind vào PV 500Gi (đủ điều kiện ≥) và lãng phí 400Gi, vì binding không chia nhỏ PV.

Pha 3: Mounting — volume vào được Pod

Khi Pod dùng PVC được scheduler đặt lên một node, hai bước CSI xảy ra:

  • ControllerPublishVolume (attach): với block storage, CSI controller gọi Compute Engine API để attach disk vào VM của node. Đây là thao tác ở tầng GCP, có độ trễ (giây tới chục giây) và bị giới hạn bởi per-node attach limit (file 3).
  • NodeStageVolume + NodePublishVolume (mount): CSI node plugin trên node format disk (nếu chưa) và mount nó vào filesystem của Pod. Với file storage (NFS), không có "attach" — chỉ mount NFS export.

Chuỗi attach→mount này là lý do Pod stateful khởi động chậm hơn stateless: nó phải chờ disk attach và mount xong trước khi container chạy. Khi node chết đột ngột, disk vẫn "attached" vào VM đã chết ở tầng GCP, và phải được detach (hoặc force-detach) trước khi attach vào node mới — đây là nguồn gốc của tình trạng Pod stateful kẹt khi node failure (giải bằng Stateful HA Operator, file 3).

Pha 4: Releasing — PVC biến mất

Khi PVC bị xóa, PV chuyển sang trạng thái Released. Lúc này dữ liệu vẫn còn trên disk, nhưng PV không còn bind với PVC nào. Điều xảy ra tiếp theo phụ thuộc hoàn toàn vào reclaimPolicy.

Pha 5: Reclaiming — số phận của disk

Đây là quyết định quan trọng nhất về độ bền:

  • reclaimPolicy: Delete (mặc định cho dynamic provisioning): khi PVC bị xóa, CSI driver gọi GCP API xóa luôn disk thật và toàn bộ dữ liệu (GKE Persistent Volumes). Tiện cho dữ liệu tạm, nhưng là quả mìn cho dữ liệu quan trọng: một kubectl delete pvc nhầm, hoặc một helm uninstall cuốn theo PVC, là mất sạch.

  • reclaimPolicy: Retain: khi PVC bị xóa, PV chuyển Released nhưng disk và dữ liệu vẫn còn. PV không tự bind lại PVC mới (vì nó còn giữ tham chiếu tới PVC cũ trong claimRef); admin phải can thiệp thủ công để giải phóng và tái sử dụng. An toàn hơn nhiều cho dữ liệu sản xuất.

Quy luật production: với mọi dữ liệu không thể tạo lại, hoặc dùng StorageClass có reclaimPolicy: Retain, hoặc bật deletion protection ở tầng dưới, kèm Backup for GKE (file 9). Mặc định Delete chỉ an toàn cho dữ liệu thật sự ephemeral hoặc tái tạo được.

yaml
# StorageClass an toàn cho dữ liệu production
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: pd-ssd-retain
provisioner: pd.csi.storage.gke.io
parameters:
  type: pd-ssd
reclaimPolicy: Retain          # giữ disk khi xóa PVC
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

Dynamic provisioning end-to-end: từ PVC tới disk thật

Đây là chuỗi đầy đủ khi một PVC được tạo với dynamic provisioning — hiểu nó giúp debug mọi lỗi provisioning:

  1. Workload tạo PVC trỏ tới một StorageClass (hoặc StorageClass mặc định nếu không chỉ định).
  2. StorageClass xác định provisioner (CSI driver nào, ví dụ pd.csi.storage.gke.io), parameters (loại disk, IOPS...), reclaimPolicy, volumeBindingMode.
  3. CSI external-provisioner thấy PVC chưa bind, gọi CreateVolume của CSI driver.
  4. CSI driver gọi GCP API (Compute Engine disks.insert, hoặc Filestore API...) để tạo disk thật.
  5. GCP tạo tài nguyên; CSI driver sinh object PV trỏ tới disk đó, với đúng zone/region và topology.
  6. Control loop bind PVC ↔ PV. Khi Pod được đặt lên node, chuỗi attach→mount (pha 3) chạy.

Mỗi mắt xích là một điểm có thể hỏng: thiếu quota disk ở GCP → bước 4 fail (PVC Pending, event ProvisioningFailed); CSI driver chưa enable → bước 3 không xảy ra; topology không khớp → PV tạo sai zone (xem binding mode bên dưới). Lệnh debug đầu tiên luôn là kubectl describe pvc <name> để đọc event, rồi kubectl describe pv và log của CSI controller.

Volume binding modes: Immediate vs WaitForFirstConsumer

Đây là tham số StorageClass quan trọng nhất mà người mới thường bỏ qua, và là nguyên nhân số một của Pod stateful kẹt Pending.

Immediate: tạo disk ngay khi PVC ra đời

Với volumeBindingMode: Immediate, disk được tạo và PV được bind ngay khi PVC được tạo, trước khi scheduler biết Pod sẽ chạy ở đâu. Với một zonal disk (PD/Hyperdisk thường), điều này nghĩa là disk bị "đóng đinh" vào một zone tùy CSI driver chọn. Sau đó scheduler phải đặt Pod đúng zone đó — nếu zone đó hết tài nguyên, hoặc Pod có affinity tới zone khác, Pod kẹt Pending vĩnh viễn vì zonal disk không attach cross-zone được.

Đây là một lỗi kinh điển: StorageClass dùng Immediate, disk tạo ở us-central1-a, nhưng node pool còn chỗ chỉ ở us-central1-b — Pod không bao giờ chạy, dù cluster còn dư công suất.

WaitForFirstConsumer: để scheduler quyết định trước

Với volumeBindingMode: WaitForFirstConsumer, Kubernetes hoãn việc tạo disk cho đến khi có một Pod dùng PVC đó được scheduler xét. Lúc đó scheduler chọn node (dựa trên đủ ràng buộc: resource, affinity, taint), rồi CSI mới tạo disk đúng zone của node đã chọn. Thứ tự bị đảo: scheduling trước, provisioning sau. Nhờ vậy disk luôn ở đúng zone Pod chạy, loại bỏ hoàn toàn lớp lỗi zone-mismatch (Kubernetes Volume Binding Mode).

Đây là lý do WaitForFirstConsumer là mặc định đúng cho mọi zonal storage trên GKE, và là binding mode của StorageClass mặc định do GKE tạo. Theo tài liệu Google Cloud, GKE khuyến nghị WaitForFirstConsumer để việc gán zone tôn trọng ràng buộc scheduling của Pod (GKE Persistent Volumes).

Khi nào dùng Immediate? Hiếm: chủ yếu cho storage không zonal (regional disk dùng được nhiều zone, hoặc Filestore/GCS không gắn zone), hoặc khi cần disk tồn tại trước khi có Pod (một số pattern pre-provision). Mặc định, luôn ưu tiên WaitForFirstConsumer.

allowedTopologies: ràng buộc zone tường minh

StorageClass có thể giới hạn zone được phép qua allowedTopologies. Kết hợp với regional PD (cần đúng hai zone), đây là cách ép disk được tạo trong tập zone cụ thể:

yaml
allowedTopologies:
- matchLabelExpressions:
  - key: topology.gke.io/zone
    values:
    - us-central1-a
    - us-central1-b

Với WaitForFirstConsumer, thường không cần allowedTopologies vì scheduler đã quyết định zone; nhưng với regional PD nó hữu ích để đảm bảo hai bản sao nằm ở đúng hai zone mong muốn.

StorageClass mặc định của GKE

GKE tạo sẵn vài StorageClass. Hiểu chúng để biết PVC không chỉ định StorageClass sẽ nhận gì:

StorageClassProvisionerLoại diskAccess modeGhi chú
standard-rwopd.csi.storage.gke.iopd-balancedRWOMặc định trên cluster mới, dùng CSI driver
premium-rwopd.csi.storage.gke.iopd-ssdRWOSSD, IOPS cao hơn
standardkubernetes.io/gce-pdpd-standard (HDD)RWOIn-tree provisioner cũ, tránh dùng cho cluster mới

Theo tài liệu, StorageClass mặc định của GKE dùng pd-balanced (ext4) với volumeBindingMode: WaitForFirstConsumer (GKE Persistent Volumes). standard (in-tree, HDD) tồn tại vì lý do lịch sử — nó dùng provisioner cũ kubernetes.io/gce-pd đã deprecated; cluster mới nên dùng standard-rwo/premium-rwo (CSI). Một bẫy phổ biến: PVC không ghi storageClassName nhận StorageClass mặc định, và nếu mặc định là HDD standard (trên cluster cũ), database sẽ chạy trên HDD mà không ai chủ ý chọn — luôn ghi rõ storageClassName.

Bạn có thể đổi StorageClass mặc định bằng annotation storageclass.kubernetes.io/is-default-class: "true", nhưng chỉ nên có đúng một mặc định; hai mặc định gây hành vi không xác định.

Real-world scenario: vì sao một Helm chart làm mất database

Một tình huống thật minh họa nhiều bài học của file này. Một team deploy PostgreSQL bằng Helm chart, dùng PVC với StorageClass mặc định (reclaimPolicy: Delete). Vài tháng sau, họ chạy helm uninstall để dọn một release tưởng là không dùng — nhưng nhầm release. Vì chart định nghĩa PVC như tài nguyên của release, helm uninstall xóa PVC; vì reclaimPolicy: Delete, CSI driver xóa luôn PD; toàn bộ database biến mất, không backup.

Bài học rút ra, áp thẳng vào policy:

  1. Dữ liệu production phải dùng StorageClass Retain, để xóa PVC không xóa disk.
  2. StatefulSet volumeClaimTemplates không bị Helm xóa tự động (file 9) — một lý do nữa để dùng StatefulSet cho database thay vì PVC rời.
  3. Backup for GKE là lưới an toàn cuối — không phụ thuộc hoàn toàn vào reclaimPolicy (file 9).

Common mistakes / anti-patterns

  • Dùng Immediate cho zonal disk. Disk tạo sai zone, Pod kẹt Pending dù cluster còn chỗ. Hệ quả ở scale: stateful workload không deploy được, tốn giờ debug. Phòng tránh: luôn WaitForFirstConsumer cho zonal storage.

  • Để reclaimPolicy: Delete cho dữ liệu quan trọng. Một thao tác xóa nhầm là mất vĩnh viễn. Hệ quả: mất dữ liệu không phục hồi. Phòng tránh: Retain + Backup for GKE cho dữ liệu sản xuất.

  • Không ghi storageClassName rõ ràng. PVC nhận mặc định, có thể là HDD trên cluster cũ. Hệ quả: hiệu năng tệ không lý giải được. Phòng tránh: luôn chỉ định StorageClass tường minh.

  • Quên allowVolumeExpansion: true từ đầu. Khi cần mở rộng disk, không expand được nếu StorageClass không cho phép (và đổi StorageClass của PVC đã tạo là không thể). Hệ quả: phải migrate dữ liệu sang PVC mới. Phòng tránh: bật allowVolumeExpansion ngay từ đầu (file 9).

  • Hiểu lầm binding là chia nhỏ PV (static). Một PVC nhỏ bind vào PV lớn và lãng phí phần dư. Hệ quả: trả tiền cho dung lượng không dùng. Phòng tránh: với static, ghép PV-PVC đúng kích thước; ưu tiên dynamic provisioning.

Official references