PV, PVC: binding, access modes và reclaim policy
Bài này là phần thực hành. Mình sẽ không giảng lý thuyết dài, mà đi thẳng vào 5 bài tập nhỏ. Làm xong 5 bài này là bạn xử lý được gần như mọi câu PV/PVC trong đề CKA.
Mục tiêu bài học
Sau khoảng 30 phút, bạn làm được:
- Phân biệt PV (cluster-scoped) và PVC (namespaced), hiểu ai "xin" và ai "cấp".
- Tạo một cặp PV + PVC khớp nhau và đưa PVC sang trạng thái
Bound. - Đọc đúng 4 access modes, và nhớ rằng
ReadWriteOncekhông phải "một pod". - Chọn
persistentVolumeReclaimPolicyphù hợp và biết cách đưa PV từReleasedvềAvailable.
Ôn nhanh từ bài trước
Bài trước bạn đã dùng emptyDir, hostPath và volume từ ConfigMap/Secret. Cả ba đều buộc vào vòng đời pod: pod bị xóa, volume bị xóa hoặc mồ côi.
PV/PVC ra đời để tách vòng đời dữ liệu khỏi vòng đời pod. Pod chết, PVC còn, dữ liệu còn. Đó là toàn bộ lý do cặp đôi này tồn tại.
Ví dụ mẫu từng bước
Cách mình hay hình dung: PV là ổ đĩa có sẵn, PVC là đơn xin cấp ổ đĩa, control plane (PersistentVolume controller) là người se duyên. Bạn viết đơn, nó đi tìm ổ đĩa khớp, khớp thì gắn hai bên lại.
flowchart TD
A["📝 Tạo PVC"] --> B{"🔍 Có PV khớp?"}
B -->|"storageClassName + accessModes + capacity"| C["🔗 PVC Bound"]
B -->|"lệch một điều kiện"| D["⏳ PVC Pending"]
D --> E["🛠️ Sửa PVC hoặc tạo PV to hơn"]
E --> B
Bước 1: tạo PV
Không có lệnh imperative kiểu kubectl create pv, nên bạn phải viết YAML. Tạo file pv-demo.yaml:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-demo
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
hostPath:
path: /mnt/data/pv-demo
type: DirectoryOrCreate
kubectl apply -f pv-demo.yaml
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS AGE
pv-demo 1Gi RWO Retain Available 10s
Bước 2: tạo PVC
Tạo file pvc-demo.yaml. Xin ít hơn PV đang có, đây là chuyện bình thường:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-demo
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
storageClassName: ""
kubectl apply -f pvc-demo.yaml
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-demo Bound pv-demo 1Gi RWO 5s
Để ý: PVC xin 500Mi nhưng nhận cả 1Gi. ==Capacity của PV phải lớn hơn hoặc bằng requests.storage của PVC==, không cần bằng nhau.
Bước 3: dùng PVC trong pod
apiVersion: v1
kind: Pod
metadata:
name: pod-demo
spec:
containers:
- name: app
image: busybox
command: ["sh", "-c", "sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: pvc-demo
kubectl apply -f pod-demo.yaml
kubectl exec pod-demo -- sh -c "echo hello-cka > /data/note.txt && cat /data/note.txt"
Nếu in ra hello-cka, PVC đã được mount đúng.
Bước 4: xem reclaim policy hành động
kubectl delete pod pod-demo
kubectl delete pvc pvc-demo
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS AGE
pv-demo 1Gi RWO Retain Released default/pvc-demo 3m
PV không bị xóa, nó chuyển sang Released. Dữ liệu vẫn nằm trên đĩa nhưng PV chưa bind lại được, vì spec.claimRef vẫn trỏ tới PVC đã chết. Bước 5 là cách cứu:
kubectl patch pv pv-demo -p '{"spec":{"claimRef":null}}'
kubectl get pv pv-demo
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS AGE
pv-demo 1Gi RWO Retain Available 4m
Luyện tập có hướng dẫn
Làm đúng thứ tự, mỗi bài tự bấm giờ. Bài 1 và 2 là ví dụ trên, bài 3 đến 5 là phần ăn điểm trong đề thi.
Bài 1 (5 phút): tạo cặp PV/PVC Bound
Tạo pv-web 2Gi, ReadWriteOnce, reclaim policy Retain, storageClassName: "", hostPath /mnt/data/pv-web, và pvc-web xin 1Gi cùng access mode, cùng storageClassName.
Kiểm tra:
kubectl get pvc pvc-web -o wide
kubectl get pvc pvc-web -o jsonpath='{.spec.volumeName}{"\n"}'
Kỳ vọng: STATUS = Bound, và lệnh jsonpath in ra pv-web. Đây là cách nhanh nhất để biết PVC đang cắm vào PV nào.
Bài 2 (5 phút): ghi dữ liệu qua PVC
Tạo pod pod-web dùng image nginx, mount pvc-web vào /usr/share/nginx/html, sau đó ghi một file:
kubectl exec pod-web -- sh -c "echo 'CKA storage ok' > /usr/share/nginx/html/index.html"
kubectl exec pod-web -- cat /usr/share/nginx/html/index.html
Xóa pod, tạo lại pod mới cùng PVC, đọc lại file. Dữ liệu vẫn còn, đó chính là điểm khác biệt so với emptyDir ở bài trước.
Bài 3 (7 phút): cố tình lệch accessModes
Tạo pvc-rwx.yaml xin ReadWriteMany, 1Gi, storageClassName: "":
kubectl apply -f pvc-rwx.yaml
kubectl get pvc pvc-rwx
kubectl describe pvc pvc-rwx | tail -n 8
Kỳ vọng: STATUS = Pending, event kiểu:
Warning ProvisioningFailed persistentvolume-controller no persistent volumes available for this claim and no storage class is set
Giờ sửa. Bẫy ở đây: bạn không thể kubectl edit để đổi accessModes, API sẽ trả về Forbidden: updates to persistentvolumeclaims spec are forbidden. Cách đúng là xóa và tạo lại:
kubectl delete pvc pvc-rwx
# sửa accessModes thành ReadWriteOnce rồi apply lại
Tự kiểm tra: nếu xóa PVC rồi apply lại với ReadWriteOnce và 1Gi, PVC có tự bind vào pv-web (2Gi, RWO) không? Đáp án là có.
Bài 4 (6 phút): lệch capacity
Tạo PVC pvc-big xin 3Gi, ReadWriteOnce, storageClassName: "". Không có PV nào đủ lớn, nên PVC Pending.
Đừng sửa PVC. Hãy tạo thêm một PV đủ lớn:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-big
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
hostPath:
path: /mnt/data/pv-big
type: DirectoryOrCreate
kubectl apply -f pv-big.yaml
kubectl get pvc pvc-big
PV mới xuất hiện và binder tự gắn ngay, không cần can thiệp. Bài học: bạn chỉ cần làm cho phía PV khớp, không nhất thiết phải sửa đơn xin. Và nếu có nhiều PV cùng khớp, Kubernetes chọn PV nhỏ nhất vẫn đủ lớn.
Bài 5 (7 phút): Retain, Released và claimRef
kubectl delete pod pod-web --ignore-not-found
kubectl delete pvc pvc-web
kubectl get pv
Kỳ vọng: pv-web ở trạng thái Released, cột CLAIM vẫn ghi default/pvc-web.
Giờ thử bind lại mà chưa xử lý gì:
kubectl apply -f pvc-web.yaml
kubectl get pvc pvc-web
PVC mới sẽ Pending, vì PV đang Released chứ không phải Available. Sửa bằng cách gỡ claimRef:
kubectl patch pv pv-web -p '{"spec":{"claimRef":null}}'
kubectl get pv pv-web
PV về Available, và PVC Pending ở trên sẽ tự bind trong vài giây. Đây là mẹo rất hay bị hỏi trong đề: Retain + Released + xóa claimRef là bộ ba đi liền nhau.
Lỗi thường gặp và cách xử lý
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
PVC Pending, event no persistent volumes available for this claim and no storage class is set |
Không có PV nào khớp accessModes, capacity hoặc storageClassName |
So kubectl describe pv với kubectl describe pvc, sửa bên lệch |
PVC Pending dù đã tạo PV đúng |
PV có storageClassName: manual nhưng PVC để trống, hoặc ngược lại |
Hai bên phải ghi giống hệt nhau, kể cả chuỗi rỗng "" |
kubectl edit pvc báo Forbidden: updates to persistentvolumeclaims spec are forbidden |
spec.accessModes, storageClassName gần như immutable |
Xóa PVC và tạo lại, hoặc tạo PV đúng ý |
PV Released mãi, tạo PVC mới vẫn Pending |
spec.claimRef còn trỏ tới PVC cũ |
kubectl patch pv <ten> -p '{"spec":{"claimRef":null}}' |
Xóa PVC báo lỗi hoặc treo ở Terminating |
Finalizer kubernetes.io/pvc-protection vì còn pod đang dùng |
Xóa pod trước, rồi xóa PVC |
Pod Pending dù PVC đã Bound |
Pod có thể đang thiếu node hoặc sai claimName |
kubectl describe pod xem event, đối chiếu tên PVC |
[!WARNING] Có một trường hợp rất dễ sửa sai: PVC
Pendingkhông phải lúc nào cũng do PV. Nếu cluster của bạn có StorageClass mặc định, PVC không ghistorageClassNamesẽ bị dynamic provisioning "cướp" và bind vào một PV khác hoàn toàn. Trong bài này mình luôn ghistorageClassName: ""để tránh chuyện đó.
[!NOTE]
ReadWriteOncenghĩa là một node mount read-write, không phải một pod. Hai pod trên cùng một node vẫn dùng chung một PVC RWO bình thường. ChỉReadWriteOncePodmới giới hạn đúng một pod trong toàn cluster.
Góc chuyên gia
Bảng access modes, học đúng 4 dòng này là đủ:
| Access mode | Viết tắt | Ý nghĩa thực tế |
|---|---|---|
ReadWriteOnce |
RWO | Một node mount read-write |
ReadOnlyMany |
ROX | Nhiều node mount read-only |
ReadWriteMany |
RWX | Nhiều node mount read-write, cần backend hỗ trợ (NFS, CephFS) |
ReadWriteOncePod |
RWOP | Đúng một pod trong cluster mount read-write |
Bảng reclaim policy:
| Policy | Sau khi PVC bị xóa | Dùng khi |
|---|---|---|
Retain |
PV sang Released, dữ liệu còn, phải xử lý tay |
Dữ liệu quan trọng, an toàn khi thi |
Delete |
Xóa PV object và backing storage | Lab, dữ liệu tạm |
Recycle |
Xóa nội dung rồi về Available |
Đã deprecated, đừng dùng |
Mặc định: PV tạo thủ công là Retain, PV do dynamic provisioning tạo là Delete. Cứ ghi rõ ra trong YAML cho chắc.
Mẹo thi:
kubectl explain pv.spec --recursivelà phao cứu sinh khi quên field. Không cần nhớ hết, chỉ cần nhớ lệnh này.- Muốn chắc chắn PVC bind đúng PV, thêm
volumeName: pv-demovàospeccủa PVC. Nó ép binder chỉ xét đúng PV đó, tránh bị PV khác "giành". - Thứ tự khớp khi bind:
storageClassName→accessModes→ capacity →selector. Lệch ở bước nào,describe pvcsẽ dừng ở bước đó. - Trước khi nộp, luôn chạy
kubectl get pvcxemSTATUS.Boundmới là xong,Pendinglà mất điểm. - Đừng dùng
hostPathcho câu hỏi mô tả production. NHƯNG trong lab CKA thìhostPathlà cách nhanh nhất để có một PV hợp lệ, vì bạn không có NFS server.
Tóm tắt và bước tiếp theo
Năm dòng để bạn ghi ra giấy nháp:
- PV là cluster-scoped, PVC là namespaced.
- Bind thành công khi
storageClassName,accessModeskhớp và capacity PV lớn hơn hoặc bằng request. - RWO là per-node, không phải per-pod.
Retainđể lại PV ởReleased, muốn dùng lại thì xóaclaimRef.- PVC gần như immutable: sai access mode thì xóa tạo lại.
Kiểm tra miệng 60 giây:
PV là resource cấp {{c1::cluster}}, còn PVC là resource cấp {{c2::namespace}}.
Access mode cho phép nhiều node mount read-write là {{c3::ReadWriteMany}}.
Reclaim policy mặc định của PV tạo thủ công là {{c4::Retain}}, còn PV do StorageClass tạo động mặc định là {{c5::Delete}}.
PVC ở trạng thái {{c6::Pending}} nghĩa là chưa tìm được PV phù hợp.
Muốn PV đang ở trạng thái Released bind lại được, ta xóa {{c7::claimRef}} trong spec của PV.
Bộ thẻ ôn tập:
[
{ "front": "PV là resource cấp cluster hay namespace?", "back": "Cluster-scoped. PVC mới là namespaced." },
{ "front": "Kể tên 4 access modes của PV", "back": "ReadWriteOnce, ReadOnlyMany, ReadWriteMany, ReadWriteOncePod." },
{ "front": "ReadWriteOnce giới hạn theo node hay theo pod?", "back": "Theo node. Nhiều pod cùng node vẫn dùng chung được." },
{ "front": "PVC chuyển sang Bound khi nào?", "back": "Khi controller tìm được PV khớp storageClassName, accessModes và capacity." },
{ "front": "PV 1Gi có bind được với PVC xin 2Gi không?", "back": "Không. Capacity PV phải lớn hơn hoặc bằng requests.storage." },
{ "front": "Có nhiều PV cùng khớp thì chọn cái nào?", "back": "PV nhỏ nhất vẫn đủ lớn." },
{ "front": "Ba reclaim policy của PV?", "back": "Retain, Delete và Recycle (Recycle đã deprecated)." },
{ "front": "Reclaim policy mặc định của PV tạo thủ công?", "back": "Retain." },
{ "front": "Reclaim policy mặc định của PV do dynamic provisioning tạo?", "back": "Delete." },
{ "front": "PV ở trạng thái Released nghĩa là gì?", "back": "PVC đã bị xóa, dữ liệu còn nhưng PV chưa bind lại được." },
{ "front": "Làm sao đưa PV từ Released về Available?", "back": "Gỡ spec.claimRef bằng kubectl patch pv <ten> -p '{\"spec\":{\"claimRef\":null}}'." },
{ "front": "Sửa accessModes của PVC đã tạo được không?", "back": "Không. Spec gần như immutable, phải xóa và tạo lại." },
{ "front": "storageClassName rỗng (\"\") trên PVC có ý nghĩa gì?", "back": "Chỉ bind với PV không có StorageClass, bỏ qua StorageClass mặc định." },
{ "front": "Lệnh xem PVC đang bind vào PV nào?", "back": "kubectl get pvc <ten> -o jsonpath='{.spec.volumeName}'." }
]
Bài tiếp theo: bạn đã tự tay tạo PV, nghĩa là đang làm static provisioning. Bài 5.3 sẽ bỏ hẳn bước đó: chỉ cần một StorageClass, PVC tự sinh PV. Bạn sẽ thấy vì sao hầu hết cluster thật không ai viết PV bằng tay, và vì sao WaitForFirstConsumer là field khiến nhiều người mất điểm oan.