Container là tạm thời: pod được tạo lại thì mọi file ghi bên trong mất theo. Với Website AI, đó là vấn đề lớn, vì mỗi site có ảnh khách upload, theme và plugin đã được chỉnh riêng, chứ không chỉ có database. Bài này đi qua các lớp lưu trữ của Kubernetes: volume trong pod, PersistentVolume, PersistentVolumeClaim (PVC) và StorageClass, các chế độ truy cập RWO và RWX, vì sao Block Storage của Linode không dùng chung được cho nhiều pod, và chính sách thu hồi Retain hay Delete ảnh hưởng thế nào tới dữ liệu lẫn hoá đơn. YAML trong bài là mẫu tối thiểu, dùng tên và IP ví dụ.

1. Website AI cần lưu những gì

Nhìn lại pipeline trên Cloud VPS ở bài mở đầu series, bước phân quyền mở quyền ghi cho storage, public/assets, themes và plugins. Đó chính là danh sách thư mục mà site tự ghi vào trong lúc chạy. Cách nhanh để lập danh sách này cho ứng dụng của bạn là xem bước chmod/chown trong script deploy cũ:

  • File upload trong public/ khi site lưu ảnh tại chỗ thay vì object storage.
  • Theme và plugin mà khách hoặc đội triển khai đã chỉnh cho riêng site.
  • storage/: session, cache, log nếu còn ghi file.
  • Database MySQL, nếu chạy trong cluster.

Trên minion, tất cả nằm trên đĩa của máy. Trong Kubernetes, pod có thể bị chuyển sang node khác bất cứ lúc nào, nên dữ liệu phải nằm ở nơi sống lâu hơn pod và gắn được vào node mới.

2. Volume trong pod: sống bao lâu?

Volume là thư mục được gắn vào container. Mỗi loại volume có vòng đời khác nhau:

Loại volumeSống bao lâuDùng cho
emptyDirBằng vòng đời podFile tạm, chia sẻ giữa các container trong pod
configMap, secretTheo object nguồnFile cấu hình, như ở bài ConfigMap và Secret
hostPathTheo đĩa của nodeGần như không nên dùng: pod sang node khác là mất dữ liệu
persistentVolumeClaimĐộc lập với podDữ liệu cần giữ lâu dài

Ba loại đầu không giải quyết được bài toán dữ liệu bền. Loại cuối dẫn tới bộ ba PersistentVolume, PVC và StorageClass.

3. PersistentVolume, PVC và StorageClass

Kubernetes tách việc cung cấp lưu trữ khỏi việc sử dụng lưu trữ, theo tài liệu Persistent Volumes trên kubernetes.io:

  • PersistentVolume (PV): một phần lưu trữ cụ thể đã có trong cluster, như một ổ Block Storage hay một thư mục trên máy NFS. PersistentVolume là tài nguyên cấp cluster, không thuộc namespace nào.
  • PersistentVolumeClaim (PVC): yêu cầu lưu trữ của ứng dụng, nằm trong namespace của site: "tôi cần 10Gi, chế độ ghi từ một node". Pod chỉ tham chiếu tới PVC, không biết phía sau là loại đĩa gì.
  • StorageClass: "loại đĩa" kèm bộ cấp phát (provisioner). Khi PVC chỉ định một StorageClass, bộ cấp phát tự tạo đĩa thật và tự tạo PersistentVolume tương ứng.

Có hai cách để một PVC có PersistentVolume:

  1. Cấp phát động: PVC ghi storageClassName, bộ cấp phát tạo đĩa mới. Đây là cách thường dùng với Block Storage.
  2. Cấp phát tĩnh: người vận hành tạo sẵn PersistentVolume trỏ tới một nơi lưu trữ có sẵn (ví dụ thư mục NFS), PVC gắn vào đúng PersistentVolume đó.

Hình dung theo kiểu Cloud VPS: StorageClass là bảng giá các loại đĩa, PersistentVolume là cái đĩa đã mua, PVC là phiếu yêu cầu của một site, còn pod là người dùng đĩa theo phiếu.

4. Access mode: RWO, RWX và vì sao nó quan trọng

Mỗi PersistentVolume khai báo các chế độ truy cập mà nó hỗ trợ:

Chế độViết tắtÝ nghĩa
ReadWriteOnceRWOĐọc ghi từ một node tại một thời điểm. Nhiều pod trên cùng node đó vẫn dùng được
ReadOnlyManyROXNhiều node cùng đọc, không ai ghi
ReadWriteManyRWXNhiều node cùng đọc ghi
ReadWriteOncePodRWOPChỉ đúng một pod (beta từ Kubernetes 1.27, ổn định từ 1.29)

Một hiểu lầm phổ biến: RWO giới hạn theo node, không theo pod. Hai pod trên cùng node vẫn gắn chung một volume RWO được. Nhưng khi pod mới được xếp lên node khác, nó sẽ không gắn được volume đang nằm ở node cũ.

5. Block Storage của Linode: chỉ RWO

Trên LKE, Block Storage được nối vào Kubernetes qua CSI driver cho Linode Block Storage. Cluster có sẵn hai StorageClass: linode-block-storage (chính sách Delete) và linode-block-storage-retain (chính sách Retain, là mặc định). Một PVC cấp phát động trông như sau:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data
  namespace: database
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: linode-block-storage-retain
  resources:
    requests:
      storage: 10Gi

Block Storage là một ổ đĩa mạng gắn vào một máy tại một thời điểm, nên chỉ hỗ trợ RWO. Điều này hoàn toàn ổn cho workload chỉ chạy một bản như MySQL, Prometheus: một pod, một đĩa. Còn những gì cần giữ cho database, từ buffer pool tới backup, đã được bàn trong bài MySQL đa tenant trên VPS cấu hình nhỏ.

Nhưng với code và file upload của Website AI, RWO là trở ngại. Khi chạy hai bản pod của một site trên hai node, hoặc khi rolling update xếp pod mới sang node khác, pod thứ hai sẽ không gắn được đĩa. Dữ liệu dùng chung giữa nhiều pod trên nhiều node cần RWX, và cách phổ biến nhất để có RWX là NFS.

6. NFS cho dữ liệu dùng chung: PersistentVolume tĩnh

Một phương án đơn giản là một máy NFS riêng, mỗi site một thư mục, và mỗi site một PersistentVolume tĩnh trỏ vào thư mục đó (IP ví dụ):

apiVersion: v1
kind: PersistentVolume
metadata:
  name: site-a-example-com-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 10.0.1.50
    path: /export/site-a-example-com
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: site-a-example-com-pvc
  namespace: site-a-example-com
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: ""
  volumeName: site-a-example-com-pv
  resources:
    requests:
      storage: 10Gi

Ba chi tiết đáng chú ý:

  • storageClassName: "" trong PVC là bắt buộc. Bỏ trống trường này, cluster sẽ gán StorageClass mặc định cho PVC, và PVC không còn khớp với PersistentVolume tĩnh không có class, nên nằm mãi ở trạng thái Pending.
  • capacity của PersistentVolume NFS chỉ để khớp với PVC. NFS không giới hạn dung lượng theo con số này, một site vẫn có thể ghi đầy đĩa của máy NFS.
  • Trong pod, chỉ mount đúng các thư mục cần giữ (public, themes, plugins) bằng subPath, không mount đè cả thư mục code của image.

Đổi lại, NFS mang tới những rủi ro mới: máy NFS thành điểm lỗi đơn của mọi site dùng nó, hiệu năng đọc ghi file nhỏ qua mạng kém hơn đĩa local, và quyền file phải khớp giữa user trong container với chủ sở hữu thư mục trên máy NFS. Với dữ liệu dùng chung, đây thường là đánh đổi chấp nhận được nếu máy NFS được sao lưu và giám sát. Database thì nên cân nhắc riêng giữa Block Storage trong cluster và máy riêng ngoài cluster.

7. Reclaim policy: Retain hay Delete và cái giá của mỗi lựa chọn

Khi PVC bị xoá (trực tiếp, hoặc vì cả namespace của site bị xoá), số phận của PersistentVolume phía sau do persistentVolumeReclaimPolicy quyết định:

Chính sáchĐiều gì xảy raRủi ro
DeletePersistentVolume và đĩa thật bị xoá theo PVCXoá nhầm namespace là mất dữ liệu ngay, không có thùng rác
RetainPersistentVolume chuyển sang trạng thái Released, đĩa thật vẫn còn nguyênĐĩa vẫn tồn tại và vẫn bị tính tiền tới khi có người dọn tay

Vòng đời của một PersistentVolume đi qua các trạng thái Available (chưa ai dùng), Bound (đã gắn với PVC), Released (PVC đã bị xoá, nhưng PV chưa được thu hồi) và Failed. Một PV ở trạng thái Released không tự gắn lại với PVC mới, vì nó vẫn giữ tham chiếu tới PVC cũ trong claimRef. Muốn dùng lại, phải xoá tham chiếu đó hoặc tạo PV mới trỏ vào cùng nơi lưu trữ.

Với Block Storage, hệ quả của Retain rất cụ thể: mỗi PVC bị xoá để lại một ổ đĩa trên tài khoản Linode, không pod nào dùng nhưng hoá đơn vẫn tính. Gỡ nhiều site mà không ai dọn, chi phí cứ thế cộng dồn âm thầm. Vì vậy nên đặt hai quy tắc: dữ liệu khách hàng dùng Retain để không bao giờ mất vì xoá nhầm; và quy trình gỡ site phải có bước kiểm tra kubectl get pv, xử lý các PV Released, sau khi đã sao lưu.

8. Lỗi thường gặp với PersistentVolume và PVC

  • PVC Pending mãi: không có StorageClass phù hợp, không có PersistentVolume tĩnh nào khớp dung lượng và access mode, hoặc quên storageClassName: "" như mục 6. kubectl describe pvc cho biết lý do.
  • Xin RWX từ StorageClass chỉ hỗ trợ RWO: PVC không bao giờ được cấp. Block Storage không làm được RWX, đổi sang NFS.
  • Multi-Attach error: pod mới ở node khác đòi gắn volume RWO đang được pod cũ giữ. Thường gặp khi rolling update một Deployment dùng Block Storage. Với workload kiểu này, đặt chiến lược Recreate hoặc dùng StatefulSet.
  • Pod Pending vì volume: pod chờ PVC chưa Bound sẽ không được xếp lịch, xem lại PVC trước.
  • Permission denied trên NFS: user chạy PHP trong container không có quyền ghi thư mục export. Thống nhất UID/GID giữa image và máy NFS.

9. Đánh đổi và khuyến nghị cho Website AI

Bộ ba PersistentVolume, PVC, StorageClass tách bạch rõ ai cung cấp và ai dùng lưu trữ, nhưng chọn đúng loại đĩa và chính sách thu hồi vẫn là quyết định của mình. Một cách sắp xếp hợp lý cho sản phẩm kiểu Website AI: code nằm trong image, dữ liệu site tự ghi (upload, theme, plugin) nằm trên NFS qua PersistentVolume tĩnh RWX với chính sách Retain, cấu hình nạp từ ConfigMap và Secret. Database đặt trong hay ngoài cluster là quyết định riêng, tuỳ yêu cầu sao lưu và hiệu năng. Mảnh còn thiếu của bộ khung là đưa traffic từ Internet vào các Service bên trong cluster.

Bài tiếp theo

Ingress và NodeBalancer: đưa traffic vào cluster Kubernetes. Thay thế cho balancer nginx với hàng trăm file cấu hình domain trên Cloud VPS.