PersistentVolume và PersistentVolumeClaim

K
Kai··6 min read

Mọi volume ở Bài 41 đều ephemeral, chết theo pod. Database, upload của người dùng, dữ liệu cần sống lâu hơn pod thì cần lưu trữ bền. Kubernetes giải bài này bằng cách tách vai. Đây là chỗ phải dò kỹ cái gì tạo ra cái gì, cái gì bind cái gì, vì nhầm vai là rối toàn bộ.

Hai object, hai người tạo:

  • PersistentVolume (PV)"a piece of storage in the cluster that has been provisioned by an administrator ... It is a resource in the cluster just like a node ... PVs have a lifecycle independent of any individual Pod." Đây là miếng lưu trữ thật, do admin tạo (hoặc CSI sinh ra động — Bài 43). PV là cluster-scoped (không thuộc namespace, như Node).
  • PersistentVolumeClaim (PVC)"a request for storage by a user ... Pods consume node resources and PVCs consume PV resources." Đây là tờ yêu cầu, do user tạo. PVC là namespaced.

Phép loại suy của tài liệu: Pod tiêu thụ tài nguyên Node, PVC tiêu thụ tài nguyên PV. Giờ dò từng bước trên cụm.

Bước 1 — admin tạo PV

PV mang chi tiết lưu trữ thật. Ở đây dùng hostPath trên worker-0 cho đơn giản (chưa có CSI):

apiVersion: v1
kind: PersistentVolume
metadata: {name: pv-static}
spec:
  capacity: {storage: 1Gi}
  accessModes: ["ReadWriteOnce"]
  persistentVolumeReclaimPolicy: Retain
  hostPath: {path: /mnt/data}        # lưu trữ thật trên node
kubectl get pv pv-static
NAME        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM
pv-static   1Gi        RWO            Retain           Available   <unset>

PV mới sinh ở phase Available, CLAIM=<unset>chưa ai yêu cầu nó. Đây là kho hàng admin dựng sẵn, chờ người đến nhận. (Bốn phase của PV: AvailableBoundReleasedFailed.)

Bước 2 — user tạo PVC

User không biết (và không cần biết) PV cụ thể nào; họ chỉ khai nhu cầu: bao nhiêu dung lượng, access mode gì:

apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: pvc-app}
spec:
  accessModes: ["ReadWriteOnce"]
  resources: {requests: {storage: 500Mi}}

PVC này nói "tôi cần 500Mi, đọc-ghi bởi một node". Nó không trỏ tới PV nào; việc ghép là của hệ thống.

Bước 3 — control loop bind PVC ↔ PV (hai chiều)

Đây là mấu chốt: ai ghép PVC với PV? Tài liệu: "A control loop in the control plane watches for new PVCs, finds a matching PV (if possible), and binds them together." Đó là controller persistentvolume-binder bên trong kube-controller-manager (thành phần ta dựng ở Bài 8). Nó thấy PVC pvc-app mới, tìm một PV Available thỏa mãn (≥500Mi, RWO) → thấy pv-static (1Gi, RWO) → bind:

kubectl get pv pv-static  -o jsonpath='PV.status={.status.phase} PV.claimRef={.spec.claimRef.namespace}/{.spec.claimRef.name}{"\n"}'
kubectl get pvc pvc-app   -o jsonpath='PVC.status={.status.phase} PVC.volumeName={.spec.volumeName}{"\n"}'
PV.status=Bound  PV.claimRef=default/pvc-app
PVC.status=Bound PVC.volumeName=pv-static

Bind là hai chiều và 1-1, đúng như tài liệu: "a PVC to PV binding is a one-to-one mapping, using a ClaimRef which is a bi-directional binding." Controller ghi claimRef lên PV (trỏ tới default/pvc-app) volumeName lên PVC (trỏ tới pv-static). Từ giờ hai bên khóa với nhau: "Once bound, PersistentVolumeClaim binds are exclusive." Không PVC nào khác chiếm được pv-static nữa, dù PVC chỉ xin 500Mi mà PV có 1Gi (PVC nhận trọn capacity của PV nó bind). Chuỗi nhân-quả tới đây:

admin ──tạo──▶ PV (Available, kho hàng thật)
user  ──tạo──▶ PVC (tờ yêu cầu: 500Mi RWO)
                 │
kube-controller-manager / persistentvolume-binder
                 │  watch PVC → tìm PV khớp → ghi claimRef(PV) + volumeName(PVC)
                 ▼
            PV.Bound ◀──── 1-1 ────▶ PVC.Bound

Bước 4 — pod dùng claim (và data sống lâu hơn pod)

Pod không trỏ thẳng PV mà trỏ PVC (đúng tầng trừu tượng: app khai "tôi cần claim này", không cần biết storage backend):

  volumes:
  - name: v
    persistentVolumeClaim: {claimName: pvc-app}

Tài liệu: "Pods use claims as volumes. The cluster inspects the claim to find the bound volume and mounts that volume for a Pod." Kubelet đọc PVC, lần ra PV bound, rồi mount storage thật vào pod. Ghi data rồi xóa pod, tạo pod mới dùng cùng claim:

kubectl exec user-pod  -- sh -c 'echo "du-lieu-ben-vung-001" > /data/file.txt'
kubectl delete pod user-pod --now
# ... tạo user-pod2 dùng cùng pvc-app ...
kubectl exec user-pod2 -- cat /data/file.txt
du-lieu-ben-vung-001

Pod mới đọc lại đúng dữ liệu pod ghi, vì PV "có lifecycle independent of any individual Pod". Đây là điều emptyDir (Bài 41) không làm được: xóa pod là mất. Storage bền sống ngoài vòng đời pod.

Bước 5 — reclaim: chuyện gì xảy ra khi user xong việc

User xong thì xóa PVC. Phase tiếp theo của PV do persistentVolumeReclaimPolicy quyết:

kubectl delete pvc pvc-app
kubectl get pv pv-static
ssh worker-0 'sudo cat /mnt/data/file.txt'
NAME        ... RECLAIM POLICY   STATUS     CLAIM
pv-static   ... Retain           Released   default/pvc-app

du-lieu-ben-vung-001

PVC mất, PV chuyển phase Released (vẫn ghi claim=default/pvc-app cũ), không về Available. Và data trên host vẫn còn. Đó là Retain: "The reclaim policy ... tells the cluster what to do with the volume after it has been released." Retain giữ nguyên data và không cho PV tái dùng tự động, admin phải tự dọn rồi tạo PV mới (an toàn, chống mất data). Lựa chọn kia là Delete (xóa luôn cả storage thật khi PVC mất, tiện cho dynamic provisioning, Bài 43).

Access modes — đọc-ghi kiểu nào

accessModes ở cả PV lẫn PVC quy định cách mount, theo tài liệu:

Mode Viết tắt Ý nghĩa
ReadWriteOnce RWO đọc-ghi bởi một node (nhiều pod cùng node vẫn được)
ReadOnlyMany ROX chỉ-đọc bởi nhiều node
ReadWriteMany RWX đọc-ghi bởi nhiều node (cần backend hỗ trợ, vd NFS)
ReadWriteOncePod RWOP đọc-ghi bởi đúng một pod duy nhất

Đa số block storage (EBS — Bài 43) chỉ làm được RWO (gắn vào một node một lúc); file storage (NFS, EFS) mới làm RWX. PVC phải xin mode mà PV/backend hỗ trợ, kẻo không bind được.

🧹 Dọn dẹp

kubectl delete pod user-pod2 --now
kubectl delete pvc pvc-app          # -> PV Released
kubectl delete pv pv-static         # admin dọn PV
ssh worker-0 'sudo rm -rf /mnt/data'  # dọn data thật trên host (Retain không tự xóa)

Retain, data thật phải xóa tay trên host, đúng tinh thần "PV sống ngoài pod". Cụm về CoreDNS + metrics-server. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 42-pv-pvc.

Tổng kết

Lưu trữ bền tách làm hai vai và một bộ ghép. Admin tạo PV (miếng lưu trữ thật, cluster-scoped, sinh ở phase Available); user tạo PVC (tờ yêu cầu, namespaced, chỉ khai dung lượng + access mode, không trỏ PV nào). Control loop persistentvolume-binder trong kube-controller-manager watch PVC, tìm PV khớp, bind hai chiều 1-1 bằng claimRef (trên PV) + volumeName (trên PVC) → cả hai thành Bound, khóa độc quyền. Pod trỏ PVC (không trỏ PV), kubelet lần ra PV bound rồi mount, và data sống lâu hơn pod (xóa pod tạo lại vẫn đọc được). Xóa PVC → PV về Released; reclaimPolicy: Retain giữ data (admin tự dọn), Delete xóa luôn. Access modes (RWO/ROX/RWX/RWOP) quy định ai mount được. Tất cả mới là static, admin tạo PV tay. Bài 43 cho StorageClass + CSI tự sinh PV: user tạo PVC, hệ thống tự đẻ PV khớp (dynamic provisioning), và ta sẽ cài EBS CSI driver thật để thấy AWS tạo volume.