Sáu Tháng Chạy Kubernetes Storage trên Production — Những Điều Thực Sự Quan Trọng
Khi lần đầu chuyển một workload có trạng thái lên Kubernetes, storage là phần khiến tôi mắc kẹt nhiều nhất. Deployments, Services, ConfigMaps — những thứ đó cảm giác khá trực quan. Còn persistent storage? Các tầng abstraction không có nghĩa lý gì cho đến khi tôi ngồi xuống và truy vết xem chính xác điều gì xảy ra khi một pod cần một ổ đĩa tồn tại qua các lần khởi động lại.
Sau khoảng sáu tháng vận hành PersistentVolumes và PersistentVolumeClaims trên nhiều cluster — bao gồm một setup PostgreSQL và một stack Elasticsearch — tôi đã có cái nhìn rõ ràng hơn nhiều về những gì hoạt động tốt, những gì dễ vỡ, và những cạm bẫy nào ngốn của tôi nhiều giờ nhất ở giai đoạn đầu.
Hai Mô Hình Storage: Static vs Dynamic Provisioning
Chọn đúng mô hình provisioning từ sớm sẽ giúp bạn tránh được nhiều cuộc refactor đau đầu. Có hai lựa chọn, và chúng phù hợp với các môi trường rất khác nhau.
Static Provisioning
Với static provisioning, bạn tự tay tạo một đối tượng PersistentVolume (PV) trỏ đến một tài nguyên storage đã tồn tại — một NFS share, một cloud disk đã được tạo sẵn, hoặc một đường dẫn local. Sau đó một PersistentVolumeClaim (PVC) sẽ yêu cầu storage đó. Kubernetes thực hiện binding dựa trên capacity, access mode và storage class.
# Static PV trỏ đến một NFS share
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv-data
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteMany
nfs:
server: 192.168.1.50
path: /exports/k8s-data
persistentVolumeReclaimPolicy: Retain
# PVC binding với PV ở trên
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data-claim
namespace: production
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 20Gi
Dynamic Provisioning
Dynamic provisioning đảo ngược mô hình. Bạn định nghĩa một StorageClass biết cách tạo storage theo yêu cầu. Khi một PVC xuất hiện trong cluster, Kubernetes gọi provisioner — AWS EBS CSI driver, GCE PD, Longhorn, hay bất cứ thứ gì bạn đã cấu hình — và storage phía sau tự động được tạo ra.
# StorageClass dùng AWS EBS CSI driver
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "3000"
throughput: "125"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
# PVC dùng dynamic provisioning
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: production
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
Ưu và Nhược Điểm của Từng Cách
Static Provisioning
- Ưu điểm: Kiểm soát hoàn toàn nơi dữ liệu lưu trữ. Phù hợp với NFS on-prem hoặc hạ tầng SAN có sẵn. Không cần provisioner bên ngoài.
- Nhược điểm: Quản lý thủ công khi mở rộng quy mô. Bạn phải tự tạo volume và theo dõi chúng. Lỗi binding xảy ra khi capacity hoặc access mode không khớp chính xác — chỉ cần một trường sai là PVC ở trạng thái Pending mãi. Không thực tế khi bạn khởi động hàng chục ứng dụng có trạng thái.
Dynamic Provisioning
- Ưu điểm: Mở rộng quy mô không cần thao tác thủ công. Developer tạo PVC và storage tự xuất hiện. CSI driver xử lý snapshot, resize và mã hóa ngay từ đầu. Trên các cluster cloud, cách này loại bỏ hầu hết công việc vận hành storage.
- Nhược điểm: Yêu cầu CSI driver hoạt động tốt và storage provider tương thích. Chi phí có thể tăng nhanh nếu developer quen tay tạo volume 500Gi. Và reclaim policy mặc định là
Deletesẽ âm thầm xóa cloud disk ngay khi PVC bị xóa — chi tiết này sẽ nói kỹ hơn ở phần sau.
Trên cluster home lab với một NFS server duy nhất, static provisioning vẫn ổn. Nhưng ngay khi chuyển sang AWS EKS, dynamic provisioning với EBS gp3 là lựa chọn hiển nhiên. Sau sáu tháng — trải qua việc thay thế node, nâng cấp cluster từ 1.27 lên 1.30, và một lần xóa nhầm pod hàng loạt — không mất dữ liệu nào.
Setup Được Khuyến Nghị cho Hầu Hết Team
Các cluster trên cloud nên mặc định dùng dynamic provisioning với CSI driver. Tạo ít nhất hai StorageClass:
- standard — đa năng, gp2 hoặc gp3, dùng cho log và dữ liệu không quan trọng
- fast-ssd — IOPS cao (gp3 với 3000 IOPS), dùng cho database và message queue
Đặt volumeBindingMode: WaitForFirstConsumer cho cả hai. Nếu không, Kubernetes sẽ tạo volume trước khi schedule pod — và nếu volume rơi vào us-east-1a trong khi pod lại schedule vào us-east-1b, pod sẽ không bao giờ khởi động được. Thiếu cấu hình này đã khiến tôi mất hai tiếng debug ở giai đoạn đầu.
Một điều nữa: luôn đặt reclaimPolicy: Retain cho các volume chứa dữ liệu thật. Policy mặc định Delete trên PV được tạo động sẽ xóa vĩnh viễn cloud disk khi PVC bị xóa. Không có cảnh báo, không thể khôi phục.
# StorageClass an toàn cho môi trường production
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
Hướng Dẫn Thực Hiện
Bước 1: Kiểm Tra StorageClass Hiện Có
kubectl get storageclass
# Tìm dấu (default) — đây là class được dùng khi không chỉ định storageClassName
Bước 2: Tạo PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: production
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
kubectl apply -f postgres-pvc.yaml
kubectl get pvc -n production
# STATUS phải là Bound sau khi có pod tham chiếu đến (WaitForFirstConsumer)
Bước 3: Mount PVC vào Pod hoặc StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: production
spec:
selector:
matchLabels:
app: postgres
serviceName: postgres
replicas: 1
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
storageClassName: fast-ssd
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi
volumeClaimTemplates bên trong StatefulSet là pattern gọn gàng nhất cho database. Mỗi replica sẽ tự động có PVC riêng — được đặt tên là data-postgres-0, data-postgres-1, v.v. Không cần quản lý PVC thủ công.
Bước 4: Kiểm Tra và Xử Lý Sự Cố
# Kiểm tra trạng thái PVC
kubectl describe pvc postgres-data -n production
# Kiểm tra events nếu bị kẹt ở Pending
kubectl get events -n production --sort-by=.lastTimestamp
# Xem thông tin PV đã được bind
kubectl get pv
kubectl describe pv <pv-name>
PVC bị kẹt ở trạng thái Pending thường do một trong hai nguyên nhân: không có StorageClass nào khớp với yêu cầu, hoặc WaitForFirstConsumer đang được bật và chưa có pod nào được schedule. Chạy kubectl describe pvc — phần Events hầu như luôn cho bạn biết chính xác vấn đề trong vài giây.
Bước 5: Mở Rộng Volume Không Cần Downtime
Nếu StorageClass của bạn có allowVolumeExpansion: true, việc resize chỉ cần một dòng lệnh:
kubectl patch pvc postgres-data -n production \
-p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'
EBS volume resize trực tiếp khi đang chạy — không cần khởi động lại pod. Một số backend khác (như một số setup NFS nhất định) yêu cầu restart pod để kích hoạt resize filesystem sau khi volume nền đã được mở rộng.
Access Mode — Tham Khảo Nhanh
- ReadWriteOnce (RWO): Một node mount volume ở chế độ đọc-ghi. Chuẩn cho database. Hầu hết cloud block storage (EBS, Azure Disk) chỉ hỗ trợ mode này.
- ReadOnlyMany (ROX): Nhiều node mount ở chế độ chỉ đọc. Phù hợp cho file cấu hình dùng chung hoặc static asset.
- ReadWriteMany (RWX): Nhiều node mount đồng thời ở chế độ đọc-ghi. Yêu cầu NFS, CephFS hoặc distributed filesystem. Không khả dụng với cloud block storage thông thường như EBS.
Lỗi phổ biến nhất tôi thường gặp: yêu cầu RWX trên StorageClass dùng EBS. PVC sẽ ở trạng thái Pending mãi mãi, kèm theo thông báo lỗi khó hiểu về access mode không được hỗ trợ. Nếu bạn thực sự cần RWX trên AWS, hãy dùng EFS với EFS CSI driver.
Kết Luận
Kubernetes storage bắt đầu có ý nghĩa với tôi khi tôi thôi nghĩ về nó theo kiểu “gắn một ổ đĩa vào”. PV và PVC là một tầng abstraction — ứng dụng của bạn khai báo những gì nó cần, và cluster tự xử lý việc storage đó đến từ đâu.
Ba quyết định đúng sẽ giúp bạn đi được phần lớn chặng đường: dùng dynamic provisioning, đặt reclaim policy thành Retain cho mọi thứ quan trọng, và dùng StatefulSet với volumeClaimTemplates cho database. Làm đúng những điều đó rồi phần còn lại chỉ là tinh chỉnh.

