Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn vừa deploy một database PostgreSQL lên Kubernetes cho một startup fintech ở Sài Gòn. Mọi thứ chạy mượt mà, khách hàng đăng ký ầm ầm, dữ liệu giao dịch đổ về. Rồi một buổi sáng, node chứa Pod database bị lỗi phần cứng, Kubernetes tự động reschedule Pod sang node khác — và bạn phát hiện toàn bộ dữ liệu khách hàng đã biến mất sạch. Không phải Kubernetes "ăn cắp" dữ liệu của bạn, mà vì bạn chưa hiểu một sự thật cốt lõi: container vốn dĩ ephemeral (phù du) — nó không được sinh ra để giữ dữ liệu lâu dài.
Đây chính là lý do bài học này quan trọng đến vậy. Trong các bài trước, chúng ta đã nói về Pod, Deployment, Service — những thứ giúp ứng dụng chạy và giao tiếp. Nhưng gần như mọi ứng dụng thực tế đều cần lưu trữ dữ liệu bền vững (persistent storage): database cần giữ record, hệ thống upload file cần giữ ảnh người dùng, service cache đôi khi muốn giữ warm data qua các lần restart. Nếu không nắm vững cách Kubernetes xử lý storage, bạn sẽ hoặc là mất dữ liệu, hoặc là xây một kiến trúc chắp vá không thể mở rộng.
Bài này sẽ giúp bạn hiểu từ gốc rễ ba khái niệm: Volume, PersistentVolume (PV) và PersistentVolumeClaim (PVC) — cùng với cơ chế StorageClass giúp mọi thứ tự động hóa. Sau bài này, bạn sẽ tự tin trả lời câu hỏi kinh điển trong phỏng vấn DevOps: "Làm sao để dữ liệu sống sót qua việc Pod bị xóa?"
Khái niệm cốt lõi
Vấn đề gốc: container filesystem là ephemeral
Mỗi container có một filesystem riêng, được tạo ra từ image. Khi container ghi dữ liệu vào filesystem đó (ví dụ ghi vào /var/lib/mysql), dữ liệu nằm trong writable layer của container. Vấn đề: writable layer này gắn liền với vòng đời của container. Container chết, restart, hoặc Pod bị reschedule — writable layer bị xóa và tạo lại từ đầu. Dữ liệu bay hơi.
Thêm một tầng phức tạp nữa: một Pod có thể chứa nhiều container (ví dụ main app + sidecar). Các container này cần một cách để chia sẻ dữ liệu với nhau. Filesystem riêng của từng container không giải quyết được điều này.
Kubernetes giải quyết cả hai vấn đề bằng khái niệm Volume.
Volume — lớp trừu tượng cơ bản
Volume trong Kubernetes là một thư mục có thể truy cập được bởi các container trong một Pod. Điểm mấu chốt: Volume gắn với vòng đời của Pod, không phải container. Nghĩa là container trong Pod có restart bao nhiêu lần thì dữ liệu trong Volume vẫn còn — miễn là Pod còn tồn tại.
Kubernetes hỗ trợ rất nhiều loại Volume. Hãy nắm chắc những loại quan trọng nhất:
emptyDir — Đây là loại đơn giản nhất. Khi Pod được tạo, Kubernetes tạo một thư mục trống trên node. Các container trong Pod đều mount được thư mục này. Dùng để làm gì? Không gian tạm (scratch space): sort dữ liệu lớn, checkpoint tính toán, hoặc chia sẻ file giữa hai container trong cùng Pod (ví dụ một container ghi log, một container đọc log gửi đi). Nhưng lưu ý: khi Pod bị xóa, emptyDir bay theo. Nó chỉ sống bằng đúng vòng đời của Pod. Đây là temp space, không phải persistent storage.
volumes:
- name: cache-temp
emptyDir:
sizeLimit: 500Mi
hostPath — Mount một thư mục từ chính node vào Pod. Nghe tiện, nhưng đây là con dao hai lưỡi: nếu Pod bị reschedule sang node khác, nó sẽ nhìn thấy một thư mục hoàn toàn khác (hoặc rỗng). hostPath cũng tạo rủi ro bảo mật. Thực tế chỉ nên dùng cho các DaemonSet hệ thống (như agent thu thập log, monitoring) cần đọc dữ liệu của node đó.
Persistent volume types — Đây mới là thứ dùng cho production: các volume được cấp phát từ hệ thống lưu trữ bên ngoài, độc lập với node, ví dụ AWS EBS, Google Persistent Disk, Azure Disk, hoặc network storage như NFS, Ceph. Chính nhóm này được quản lý qua cơ chế PV/PVC.
PersistentVolume (PV) — tài nguyên lưu trữ trong cluster
Hãy nghĩ về PersistentVolume như một "miếng đĩa cứng" đã được cung cấp sẵn trong cluster. Nó là một tài nguyên cấp cluster (cluster-scoped), do admin tạo ra hoặc do hệ thống tự động cấp phát. PV mô tả: dung lượng bao nhiêu, kiểu truy cập gì, nằm ở backend storage nào (EBS volume ID nào, NFS server ở đâu...).
Điểm quan trọng: PV có vòng đời độc lập với bất kỳ Pod nào. Pod chết, PV vẫn còn. Đây chính là chìa khóa giải quyết bài toán persistent.
PersistentVolumeClaim (PVC) — yêu cầu lưu trữ từ ứng dụng
Nếu PV là "nguồn cung", thì PersistentVolumeClaim là "đơn đặt hàng". Developer không cần biết đĩa cứng vật lý nằm ở đâu — họ chỉ viết một PVC nói rằng: "Tôi cần 20Gi dung lượng, kiểu truy cập ReadWriteOnce." Kubernetes sẽ tìm một PV phù hợp và bind (gắn kết) PVC với PV đó. Sau đó Pod chỉ cần tham chiếu tới PVC.
Đây là sự phân tách trách nhiệm rất đẹp: admin lo hạ tầng (PV), developer lo nhu cầu (PVC). Giống như bạn thuê phòng khách sạn — bạn chỉ cần đặt "phòng đôi có view", không cần quan tâm khách sạn xây phòng đó bằng gạch gì.
Access Modes — chế độ truy cập
Ba chế độ bạn phải nhớ:
- ReadWriteOnce (RWO): Volume được mount đọc-ghi bởi một node duy nhất. Đây là mode phổ biến nhất cho database. Lưu ý là "một node", nghĩa là nhiều Pod trên cùng node đó vẫn dùng chung được.
- ReadOnlyMany (ROX): Nhiều node mount ở chế độ chỉ đọc. Hợp cho việc phân phối config hoặc dữ liệu tĩnh dùng chung.
- ReadWriteMany (RWX): Nhiều node mount đọc-ghi cùng lúc. Cần backend hỗ trợ như NFS, CephFS, EFS. Block storage như EBS không hỗ trợ RWX.
StorageClass — tự động hóa việc cấp phát
Nếu phải tạo tay từng PV thì quá cực. StorageClass cho phép dynamic provisioning: khi có PVC yêu cầu, Kubernetes tự động gọi provisioner (ví dụ AWS EBS CSI driver) để tạo ra một PV mới đúng theo yêu cầu, rồi bind luôn. Developer chỉ cần viết PVC trỏ tới StorageClass, phần còn lại tự động.
StorageClass còn định nghĩa reclaim policy — chuyện gì xảy ra với dữ liệu khi PVC bị xóa: Delete (xóa luôn đĩa vật lý) hay Retain (giữ lại để cứu dữ liệu). Với production database, hãy dùng Retain để tránh thảm họa.
Tình huống thực tế
Ví dụ 1: Startup fintech mất dữ liệu vì dùng emptyDir cho database
Một startup fintech ở TP.HCM (gọi là PayFlow) mới chuyển sang Kubernetes. Team dev còn non kinh nghiệm, deploy PostgreSQL bằng một Deployment với volume kiểu emptyDir vì "thấy nó chạy được, lưu được data". Suốt hai tuần staging mọi thứ ổn. Đến khi họ update image của database (đổi version), Kubernetes rolling update — tạo Pod mới, xóa Pod cũ. Toàn bộ dữ liệu test của 200 tài khoản demo biến mất.
Diễn giải: emptyDir sống bằng vòng đời Pod. Rolling update = Pod cũ bị xóa = emptyDir bị xóa. May mà đây mới là staging. Họ sửa lại bằng cách dùng PVC với StorageClass gp3 trên AWS EBS, reclaim policy Retain, và quan trọng nhất là đổi từ Deployment sang StatefulSet — vì StatefulSet mỗi Pod có PVC riêng ổn định, đúng chuẩn cho stateful workload như database.
Bài học: Không bao giờ dùng emptyDir cho dữ liệu cần giữ. Database trên Kubernetes phải đi cùng PV/PVC bền vững và nên chạy bằng StatefulSet.
Ví dụ 2: Sàn TMĐT chọn sai access mode cho ảnh sản phẩm
Một sàn thương mại điện tử ở Đông Nam Á cần lưu ảnh sản phẩm do seller upload. Backend chạy 5 replica, tất cả cần đọc-ghi cùng một thư mục ảnh. Kỹ sư dùng PVC với StorageClass mặc định trỏ tới AWS EBS (access mode RWO). Kết quả: chỉ Pod nào nằm cùng node với EBS volume mới mount được. 4 Pod còn lại ở node khác báo lỗi Multi-Attach error, không start nổi. Trang upload ảnh sập từng phần.
Diễn giải: EBS là block storage, chỉ hỗ trợ ReadWriteOnce — không thể attach vào nhiều node cùng lúc. Với nhu cầu nhiều Pod trên nhiều node cùng đọc-ghi, phải dùng ReadWriteMany, tức là chuyển sang AWS EFS (NFS-based) qua EFS CSI driver. Về sau team thậm chí chọn giải pháp tốt hơn cho ảnh: đẩy lên object storage (S3), tách hẳn khỏi filesystem của Pod. Nhưng bài học về access mode thì đắt giá.
Bài học: Hiểu access mode trước khi chọn backend. RWX cần NFS/EFS/CephFS, không phải block storage. Và với file tĩnh dùng chung như ảnh, object storage thường là lựa chọn kiến trúc đúng hơn PV.
Ví dụ 3: Ngân hàng số suýt mất data vì reclaim policy Delete
Một ngân hàng số ở Hà Nội có một cluster staging và một production. Junior engineer dọn dẹp staging, chạy kubectl delete pvc cho vài PVC "không dùng nữa". Nhưng vì StorageClass để reclaim policy mặc định là Delete, nên khi PVC bị xóa, EBS volume phía sau bị hủy vật lý luôn. Xui hơn nữa, một trong các PVC đó thực ra đang giữ snapshot dữ liệu test tài chính quan trọng chưa backup.
Diễn giải: Reclaim policy Delete khiến việc xóa PVC lan xuống tận đĩa vật lý. Với dữ liệu quan trọng, luôn dùng Retain: khi đó xóa PVC, PV chuyển sang trạng thái Released và giữ nguyên đĩa, cho phép admin cứu dữ liệu thủ công. Sau sự cố này, team đặt policy: mọi StorageClass cho dữ liệu có giá trị đều phải Retain, và bật thêm volume snapshot định kỳ.
Bài học: Reclaim policy là ranh giới giữa "dọn dẹp" và "thảm họa". Production data dùng Retain, kết hợp snapshot backup.
Hướng dẫn từng bước
Ta sẽ triển khai một PostgreSQL có persistent storage bằng dynamic provisioning.
Bước 1 — Kiểm tra StorageClass hiện có. Hầu hết cloud provider tạo sẵn một StorageClass mặc định.
kubectl get storageclass
Bạn sẽ thấy ví dụ gp3 (default) trên EKS. Nếu tự quản cluster, có thể cần cài CSI driver tương ứng.
Bước 2 — Tạo PVC. Đây là đơn đặt hàng lưu trữ.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: gp3
resources:
requests:
storage: 20Gi
Apply và kiểm tra:
kubectl apply -f postgres-pvc.yaml
kubectl get pvc postgres-pvc
Trạng thái sẽ chuyển từ Pending sang Bound khi provisioner tạo xong PV.
Bước 3 — Gắn PVC vào Pod/Deployment. Trong phần spec.template.spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
subPath: pgdata
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
Chú ý subPath: pgdata — mẹo nhỏ tránh lỗi khi PostgreSQL đòi thư mục data rỗng nhưng volume có sẵn thư mục lost+found.
Bước 4 — Xác minh dữ liệu bền vững. Tạo một bảng, insert dữ liệu, rồi xóa Pod:
kubectl delete pod <postgres-pod-name>
Kubernetes tạo Pod mới, mount lại đúng PVC cũ. Kết nối vào và kiểm tra — dữ liệu vẫn còn. Đó là khoảnh khắc bạn thực sự hiểu persistent storage.
Bước 5 — (Production) Dùng StatefulSet thay Deployment cho database, và cấu hình StorageClass với reclaimPolicy: Retain cùng volume snapshot định kỳ.
Lỗi thường gặp & mẹo
PVC kẹt ở trạng thái Pending mãi. Nguyên nhân phổ biến: không có StorageClass phù hợp, hoặc access mode yêu cầu không được backend hỗ trợ, hoặc không đủ dung lượng. Chạy kubectl describe pvc <name> và đọc phần Events — nó nói rất rõ lý do.
Multi-Attach error volume. Bạn đang cố mount một PV RWO (như EBS) vào nhiều node. Hoặc giảm số node, hoặc chuyển sang backend RWX (EFS/NFS), hoặc dùng StatefulSet để mỗi Pod có PVC riêng.
Dùng Deployment cho stateful workload. Deployment coi mọi Pod như nhau và chia sẻ cùng một PVC — với database nhiều replica sẽ gây corruption. Stateful workload phải dùng StatefulSet với volumeClaimTemplates để mỗi Pod có storage riêng ổn định.
Quên subPath khiến app không khởi động. Nhiều database từ chối khởi tạo nếu thư mục data không "sạch". Dùng subPath để tạo thư mục con riêng bên trong volume.
Reclaim policy Delete trên dữ liệu quý. Mẹo vàng: production data luôn Retain. Kiểm tra bằng kubectl get sc <name> -o yaml xem reclaimPolicy.
Không set resource requests/limits và không giám sát dung lượng. PV đầy thì database ngừng ghi, ứng dụng sập. Hãy giám sát kubelet_volume_stats_used_bytes và cân nhắc volume expansion (allowVolumeExpansion: true).
Mẹo cuối: Với file dùng chung như ảnh, video, tài liệu — cân nhắc object storage (S3, GCS) thay vì cố ép vào PV. Đúng công cụ cho đúng việc.
Bài tập thực hành
- Cơ bản — emptyDir chia sẻ: Tạo một Pod có hai container. Container A mỗi 5 giây ghi thời gian hiện tại vào file trong một emptyDir. Container B đọc và in file đó ra. Quan sát chúng chia sẻ dữ liệu. Sau đó xóa Pod, tạo lại, xác nhận dữ liệu cũ đã mất — hiểu vì sao emptyDir không phải persistent.
- Trung cấp — PVC bền vững: Tạo một PVC 5Gi với StorageClass mặc định. Gắn vào một Deployment chạy
nginx, mount vào/usr/share/nginx/html. Tạo một fileindex.htmltùy chỉnh trong volume. Xóa Pod, để Kubernetes tạo Pod mới, và xác nhận fileindex.htmlvẫn còn. Sau đó chạykubectl describe pvđể xem PV được dynamic provisioning ra sao.
- Nâng cao — StatefulSet + reclaim policy: Triển khai PostgreSQL bằng StatefulSet với
volumeClaimTemplates. Cấu hình một StorageClass córeclaimPolicy: RetainvàallowVolumeExpansion: true. Insert dữ liệu, scale StatefulSet từ 1 lên 2 replica, quan sát mỗi replica có PVC riêng. Cuối cùng, thử mở rộng dung lượng PVC bằng cách sửaspec.resources.requests.storagevà xác nhận volume expansion hoạt động.
Tóm tắt
Container là ephemeral — dữ liệu ghi vào filesystem của nó sẽ mất khi container/Pod chết. Kubernetes giải quyết bằng nhiều tầng trừu tượng:
- Volume gắn với vòng đời Pod. emptyDir là temp space (mất theo Pod), hostPath chỉ hợp cho agent hệ thống.
- PersistentVolume (PV) là miếng lưu trữ độc lập với Pod, sống sót qua mọi lần Pod bị xóa.
- PersistentVolumeClaim (PVC) là đơn đặt hàng lưu trữ của developer, được bind vào PV phù hợp — tách bạch trách nhiệm giữa admin và dev.
- Access modes (RWO / ROX / RWX) quyết định backend nào dùng được: block storage chỉ RWO, muốn RWX phải dùng NFS/EFS/CephFS.
- StorageClass cho phép dynamic provisioning tự động và định nghĩa reclaim policy — dùng
Retaincho dữ liệu quý. - Stateful workload (database) nên chạy bằng StatefulSet, không phải Deployment.