Người sắp bàn tiệc khôn ngoan không kê bàn trước khi biết khách ngồi đâu — kê sớm là có nguy cơ đặt bàn xa cửa, xa bếp, xa đúng chỗ khách cần. Kubernetes cấp phát ổ đĩa cho pod theo đúng nguyên tắc ấy: chờ tới khi biết pod chạy ở node nào rồi mới cấp đĩa, thay vì cấp trước rồi ép pod chạy ở nơi đĩa đã nằm. Đó chỉ là một trong những điều bất ngờ khi đo lưu trữ. Pod là thứ tạm thời; dữ liệu thì thường không. Bài này đo bốn cách gắn lưu trữ vào pod và cái nào sống sót qua việc pod bị xoá.

Bốn loại, cùng phép ghi 300 MB

Hệ tệp Thời gian Thông lượng
emptyDir: {} ext4 562 ms 533 MB/giây
emptyDir: {medium: Memory} tmpfs 140 ms 2.142 MB/giây
hostPath tmpfs 119 ms 2.521 MB/giây
lớp ghi của container overlay 362 ms 828 MB/giây
PVC local-path ext4 ~230 ms ~900 MB/giây

emptyDir trong RAM nhanh gấp bốn lần emptyDir trên đĩa. Nhưng có một cái bẫy: medium: Memory tính vào giới hạn bộ nhớ của pod. Ghi 2 GB vào đó với limits.memory: 1Gi là OOMKilled (phần 54) — và nguyên nhân sẽ rất khó lần ra, vì ứng dụng không hề cấp phát bộ nhớ nào.

Chú ý dòng cuối cùng của bảng trên: lớp ghi của container (overlay) nhanh hơn emptyDir trên đĩa. Đó là kết quả tôi không chờ đợi, và nó nhắc rằng "ghi vào container thì chậm" không phải luôn đúng. Nhưng lớp ghi container vẫn là chỗ tệ nhất để ghi dữ liệu, vì lý do khác: nó không giới hạn được, không theo dõi được, và đầy đĩa node thì kubelet đuổi pod (phần 54).

Dữ liệu sống hay chết

Ghi một tệp, xoá pod, tạo lại pod:

emptyDir   ->  số tệp còn lại: 0            MẤT
PVC        ->  "du lieu quan trong"          CÒN

emptyDir gắn với vòng đời pod, không phải vòng đời container. Phân biệt này quan trọng:

  • Container khởi động lại (RESTARTS tăng) → dữ liệu còn.
  • Pod bị xoá và tạo lại → dữ liệu mất.

Nghĩa là emptyDir sống qua một lần OOMKilled nhưng không sống qua một lần triển khai. Điều đó khiến nó hợp lý cho: thư mục tạm, bộ nhớ đệm dựng lại được, và chia sẻ tệp giữa các container trong cùng pod (phần 44).

PVC được cấp phát muộn

StorageClass: standard  rancher.io/local-path  WaitForFirstConsumer

ngay sau khi tạo PVC:   Pending
sau khi pod chạy:       Bound   pvc-f41aa46e-...   1Gi   RWO

PVC nằm ở Pending cho tới khi có pod dùng tới nó. Đây không phải lỗi — đó là volumeBindingMode: WaitForFirstConsumer. Lý do: nếu cấp phát ngay, hệ thống lưu trữ phải chọn vùng hoặc node trước khi biết pod sẽ chạy ở đâu — và có thể chọn sai chỗ, khiến pod không bao giờ xếp được. Chờ tới khi bộ lập lịch quyết định xong rồi mới cấp là đúng thứ tự. Với volumeBindingMode: Immediate thì ngược lại: PVC Bound ngay, và bộ lập lịch buộc phải đặt pod ở nơi volume đã nằm.

PVC bị ghim vào node

nodeAffinity của PV:
  {"key":"kubernetes.io/hostname","operator":"In","values":["sc-worker"]}

Với local-path, PV là một thư mục trên đĩa của đúng một node. PV mang nodeAffinity trỏ vào node đó, và bộ lập lịch buộc phải đặt pod ở đấy. Hệ quả: node chết là pod không chạy được ở đâu cả. Nó nằm Pending với node(s) had volume node affinity conflict cho tới khi node quay lại. Đây chính là điều phần 42 đã nêu về kind. Lưu trữ mạng — NFS, đĩa đám mây, Ceph — không có ràng buộc này, và pod chuyển node tự do.

Ba chế độ truy cập

ReadWriteOnce (RWO)    một NODE gắn được, đọc và ghi
ReadOnlyMany  (ROX)    nhiều node, chỉ đọc
ReadWriteMany (RWX)    nhiều node, đọc và ghi

RWO là chế độ hay bị hiểu sai nhất. Nó nghĩa là một node, không phải một pod — nhiều pod trên cùng node gắn được cùng một PVC RWO. Và phần lớn hệ thống lưu trữ khối (đĩa đám mây, iSCSI) chỉ hỗ trợ RWO. RWX cần hệ tệp mạng: NFS, CephFS, hoặc dịch vụ tệp của nhà cung cấp đám mây. Đây là lý do Deployment nhiều bản dùng chung một PVC RWO sẽ kẹt: bản thứ hai xếp lên node khác không gắn được volume. Với ứng dụng có trạng thái, StatefulSet cho mỗi bản một PVC riêng — đó mới là mẫu đúng.

reclaimPolicy: điều gì xảy ra khi xoá PVC

Delete   xoá PVC là xoá luôn PV và dữ liệu       <- mặc định của phần lớn StorageClass
Retain   giữ PV lại, phải dọn tay

Delete là mặc định, và nó nghĩa là kubectl delete pvc xoá dữ liệu vĩnh viễn. Không có thùng rác, không có xác nhận. Với dữ liệu quan trọng, tạo một StorageClass riêng với reclaimPolicy: Retain. Cái giá là PV mồ côi phải dọn tay — và đó là cái giá rẻ so với việc mất dữ liệu vì một lệnh gõ nhầm.

Bốn loại volume khác đáng biết

configMap và secret — phần 61 đã đo, gắn dưới dạng liên kết mềm và cập nhật nguyên tử.

downwardAPI — đưa thông tin về chính pod (tên, namespace, nhãn, giới hạn tài nguyên) vào tệp hoặc biến môi trường.

projected — gộp nhiều nguồn (ConfigMap, Secret, downwardAPI, token) vào một thư mục.

csi — mọi hệ thống lưu trữ hiện đại. Kubernetes không còn nhúng driver cho từng nhà cung cấp; tất cả đều qua giao diện CSI.

Mẫu số chung: ràng buộc muộn, biết đúng ranh giới vòng đời, và cảnh giác tốc độ vay từ ngân sách ẩn

Bài này gói ba nguyên tắc dùng ở mọi hệ. Thứ nhất — ràng buộc muộn: đừng chốt một lựa chọn (đặt ở đâu, cấp cho ai) trước khi có đủ thông tin để chọn đúng. WaitForFirstConsumer giữ PVC ở Pending cho tới khi biết pod chạy node nào, vì cấp sớm là liều đặt đĩa sai chỗ. Đây đúng là lazy initialization (chỉ tính khi thật cần), là late binding / dynamic dispatch, là một trình tối ưu CSDL chọn kế hoạch lúc thực thi với tham số thật thay vì lúc biên dịch, là JIT quyết định biên dịch sau khi thấy nhánh nào nóng. Ràng buộc sớm (Immediate) tiện nhưng khoá bạn vào một quyết định có thể sai — chỉ chọn nó khi thông tin đã đủ ngay từ đầu.

Thứ hai — biết chính xác cái ranh giới mà vòng đời một tài nguyên gắn vào, vì chính ranh giới đó quyết định trạng thái có sống sót hay không. emptyDir gắn với pod chứ không phải container, nên nó sống qua restart mà chết qua recreate — hai từ nghe giống nhau nhưng là hai ranh giới khác hẳn. Cùng câu hỏi ấy quyết định mọi thứ có trạng thái: một try-with-resources/RAII gắn tuổi thọ vào phạm vi khối lệnh, một bean gắn vào phạm vi request hay ứng dụng, một tệp tạm sống theo tiến trình hay theo yêu cầu. Nhầm ranh giới là hoặc rò rỉ, hoặc mất dữ liệu bất ngờ.

Thứ ba, ngắn nhưng đắt: một đường nhanh thường tiêu một tài nguyên bạn không để mắt. emptyDir trên tmpfs nhanh gấp bốn vì nó là RAM — và RAM đó tính vào limits.memory, nên tốc độ vay thẳng từ ngân sách bộ nhớ, rồi cú OOMKilled đổ lỗi nhầm cho ứng dụng. Giống một cache trong bộ nhớ tranh heap, một mmap cộng vào RSS: trước khi ăn mừng một đường tắt nhanh, hỏi nó đang tiêu của ai.

Mẫu số chung: hoãn ràng buộc tới khi đủ thông tin để chọn đúng; xác định rõ ranh giới vòng đời của mỗi thứ có trạng thái (và phân biệt restart với recreate); và với mỗi đường nhanh, tìm cái ngân sách ẩn mà nó đang rút.

Muốn tự soi, xem PVC nào của bạn bị ghim vào node:

kubectl get pv -o custom-columns=\
TEN:.metadata.name,PVC:.spec.claimRef.name,\
NODE:.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0],\
CHINH_SACH:.spec.persistentVolumeReclaimPolicy

Cột NODE có giá trị nghĩa là pod dùng PVC đó chỉ chạy được trên node ấy. Cột CHINH_SACH là Delete nghĩa là xoá PVC sẽ xoá luôn dữ liệu.

Phần sau đo StatefulSet: vì sao cơ sở dữ liệu cần nó thay vì Deployment.