Khi chạy MySQL trong Kubernetes, câu hỏi đầu tiên của nhiều người là: dữ liệu của PVC nằm ở đâu, và nó có ăn vào đĩa gốc của node không? Tài liệu này hướng dẫn tìm PVC trên node Kubernetes bằng cách lần từ pod xuống điểm mount CSI, đo dung lượng bằng thí nghiệm ghi 1 GB có đối chứng, rồi gỡ một PVC kẹt ở trạng thái Terminating. Ví dụ dùng LKE của Linode, nhưng cách làm áp dụng được cho driver CSI khác.

Nếu bạn chưa rõ vì sao chúng tôi đưa website lên cluster, hãy đọc Website AI trên VPS: vì sao chúng tôi tính chuyện lên Kubernetes trước. Về phía MySQL, bài Tầng Database: MySQL đa tenant trên VPS cấu hình nhỏ giải thích cách chúng tôi tổ chức dữ liệu trước khi đưa vào cluster. Bài này đi thẳng vào phần chẩn đoán trên node.

Dữ liệu PVC trên node thật sự nằm ở đâu

PersistentVolumeClaim chỉ là một yêu cầu cấp phát. Với driver CSI của block storage, mỗi PVC trên node ứng với một volume riêng ở nhà cung cấp. Volume được gắn vào máy ảo của node đang chạy pod, kubelet format nó nếu còn trống rồi mount vào thư mục của pod. Vì vậy dữ liệu của PVC trên node nằm trên một thiết bị khối riêng, không nằm trên đĩa gốc /.

Điều này khác với emptyDir hoặc lớp ghi của container: hai thứ đó ghi vào đĩa của node. Hiểu sai chỗ lưu PVC trên node dẫn tới hai hệ quả ngược nhau, hoặc lo thừa về đĩa gốc, hoặc quên đặt giới hạn cho phần ghi ngoài volume.

Lần theo chuỗi pod, node, PVC và PV

Để lần ra dữ liệu của PVC, bắt đầu từ pod và đi xuống. Bốn lệnh sau cho bạn node, tên PV, ID volume ở nhà cung cấp và UID của pod. UID dùng để dựng đường dẫn kubelet ở bước sau.

kubectl get pods -o wide        # cột NODE: pod chạy ở node nào
kubectl get pvc                 # cột VOLUME: tên PV của PVC
kubectl get pv <ten-pv> -o jsonpath='{.spec.csi.volumeHandle}{"\n"}'
kubectl get pod <pod> -o jsonpath='{.metadata.uid}{"\n"}'

Đây là định danh hạ tầng, đừng dán vào tài liệu công khai.

Tìm điểm mount CSI của PVC trên node

Cách nhanh nhất để xem PVC trên node mà không cần vào node: chạy df ngay trong pod, tại thư mục đã mount.

kubectl exec <pod> -- df -h /var/lib/mysql
# Filesystem  Size  Used Avail Use% Mounted on
# /dev/sdc    9.8G  ...                /var/lib/mysql

Một volume 10Gi (GiB) thường hiện khoảng 9,8G ở cột Size vì ext4 dùng một phần cho metadata (bảng inode, journal). Đó là bình thường, không phải mất dữ liệu. Phần reserved blocks (mặc định 5%) không làm nhỏ Size mà chỉ làm giảm cột Avail với user thường.

Muốn thấy điểm mount thật của PVC trên node, dùng kubectl debug node thay vì SSH bằng root. Lệnh này tạo một pod tạm với hệ thống tệp của node gắn tại /host. Tài liệu kubectl debug node mô tả đầy đủ cách dùng.

kubectl debug node/<ten-node> -it --image=busybox
chroot /host
df -h | grep kubernetes.io~csi
ls -l /dev/disk/by-id/ | grep -i volume

Cột Mounted on có dạng /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/<ten-pv>/mount. Thiết bị là một symlink trong /dev/disk/by-id, tên chứa chữ Volume và ID volume. Ngoài ra kubelet còn mount volume một lần ở thư mục staging (globalmount); đường dẫn này phụ thuộc phiên bản kubelet và driver, hãy xác nhận bằng findmnt trên cluster thử. Thao tác này chỉ đọc; vẫn nên thử trên cluster thử trước.

Nếu chroot /host báo thiếu quyền, thêm --profile=sysadmin vào lệnh kubectl debug (kubectl 1.30 trở lên; chỉ dùng trên cluster thử vì pod có quyền rất rộng). Pod debug không tự xoá và nằm ở namespace hiện tại, nên dọn sau khi xem xong:

kubectl get pods | grep node-debugger
kubectl delete pod <node-debugger-...>

Đọc nội dung PVC trên node: lost+found, subPath và owner

Liệt kê thư mục mount của PVC trên node bằng ls -ln (dùng số thay vì tên). Bạn sẽ thấy ba thứ quen thuộc:

  • lost+found, quyền 700, chủ root: do mkfs.ext4 tạo ra, nằm ở gốc volume.
  • Một thư mục con nếu manifest dùng subPath.
  • Tệp dữ liệu MySQL có owner uid 999 và gid 999. Tên user và nhóm hiển thị trên node là tên đang ứng với uid/gid 999 trong /etc/passwd và /etc/group của node, có thể khác tên mysql trong container.

Một lỗi hay gặp trong manifest là ghi hostPath dưới volumeMounts để chọn thư mục con. Đó không phải trường hợp lệ của volumeMounts, kubectl sẽ cảnh báo unknown field hoặc từ chối. Trường đúng là subPath:

volumeMounts:
  - name: mysql-data
    mountPath: /var/lib/mysql
    subPath: mysql          # đúng: subPath, không phải hostPath
volumes:
  - name: mysql-data
    persistentVolumeClaim:
      claimName: pvc-mysql

Dùng subPath còn tránh được việc MySQL từ chối khởi động vì thấy lost+found trong thư mục dữ liệu.

Thí nghiệm ghi 1 GB: đo PVC trên node cho đúng

Cơ chế như sau: ghi vào thư mục không mount volume thì đĩa gốc của node tăng; ghi vào thư mục mount PVC thì đĩa của PVC tăng và đĩa gốc không đổi. Muốn tự kiểm chứng thì dùng một pod thử có gắn PVC (trong ví dụ dưới, pod thử mount PVC tại /data), không dùng pod database đang chạy thật.

# trên node: ghi lại số trước khi ghi (đơn vị byte)
df -B1 / /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/<ten-pv>/mount

# trường hợp 1: thư mục không mount (lớp ghi của container)
kubectl exec <pod> -- dd if=/dev/zero of=/tmp/test.bin bs=1M count=1000 conv=fsync
# đo lại trên node, rồi xoá tệp và đo tiếp
kubectl exec <pod> -- rm /tmp/test.bin

# trường hợp 2: thư mục mount PVC
kubectl exec <pod> -- dd if=/dev/zero of=/data/test.bin bs=1M count=1000 conv=fsync
# đo lại trên node, rồi xoá tệp để trả lại chỗ cho PVC thử
kubectl exec <pod> -- rm /data/test.bin

Vài điểm để số đo có ý nghĩa:

  • Kiểm tra /tmp không phải tmpfs (df /tmp); tmpfs ăn RAM chứ không ăn đĩa.
  • conv=fsync buộc dữ liệu được ghi xuống trước khi dd kết thúc, nên df phản ánh ngay.
  • Chọn một đơn vị. count=1000 với bs=1M là 1000 MiB, tức 1.048.576.000 byte, khác 1000 MB.
  • Đĩa gốc luôn có tiến trình khác ghi (log, layer image), nên dao động vài chục đến vài trăm MB là thường. Chỉ độ tăng khoảng 1 GB mới đáng tin; hãy lặp lại phép đo.

Ngoài ra, phần ghi vào lớp ghi của container được tính vào ephemeral-storage của node. Ghi quá nhiều có thể gây DiskPressure và khiến kubelet đuổi pod. Hãy đặt limits.ephemeral-storage cho container có khả năng ghi ngoài volume.

Bốn nguồn sai lệch khi đo dung lượng PVC

Một lỗi hay gặp khi tự đo là kết luận đúng nhưng bảng df đi kèm lại không ủng hộ nó, thậm chí đĩa gốc tăng ở lần lẽ ra không tăng. Có bốn nguồn sai lệch thường gặp:

  • Tệp thử của lần đầu không được xoá trước lần hai, nên hai phép đo chồng lên nhau.
  • df -h làm tròn tới 0,1G, che mất phần chênh lệch nhỏ.
  • Tải tệp thử qua mạng bằng curl kéo theo cache và log, trộn vào số đo.
  • Lẫn 1000 MB với 1000 MiB, hoặc đo ở hai thời điểm khi node đang có việc khác.

Cách khắc phục là đo từng lần bằng dd và df -B1, xoá tệp giữa hai lần, ghi chênh lệch theo byte.

PVC kẹt Terminating: tìm pod còn dùng PVC trên node trước khi patch

Sau kubectl delete pvc, PVC có thể treo ở trạng thái Terminating. Nguyên nhân kẹt phụ thuộc từng trường hợp. Phần dưới là cơ chế chuẩn của Kubernetes; giả thuyết thường gặp nhất là còn pod dùng PVC.

Kubernetes gắn finalizer kubernetes.io/pvc-protection vào mọi PVC. Nó chỉ được gỡ khi không còn pod nào dùng PVC. Xem cơ chế bảo vệ storage đang dùng trong tài liệu chính thức. Vì vậy hãy kiểm tra xem còn pod nào đang mount PVC không:

kubectl describe pvc <ten-pvc> | grep -E "Finalizers|Used By"
kubectl get pods -n <namespace> -o json | jq -r '.items[]
  | select(.spec.volumes[]?.persistentVolumeClaim.claimName=="<ten-pvc>")
  | .metadata.namespace + "/" + .metadata.name'

Nếu còn pod dùng PVC trên node, hạ Deployment hoặc StatefulSet xuống 0 replica. PVC sẽ tự biến mất. Chỉ khi không còn pod mà PVC vẫn kẹt mới dùng phương án cuối:

kubectl patch pvc <ten-pvc> -p '{"metadata":{"finalizers":null}}' --type=merge

Patch finalizer khi pod còn chạy là rủi ro: đối tượng PVC biến mất nhưng volume có thể vẫn đang gắn với node, và lần gắn sau dễ báo lỗi attach.

StorageClass Retain: PV Released và đĩa vẫn tính phí

Với StorageClass có reclaimPolicy: Retain, xoá PVC không xoá volume, kể cả khi PVC trên node đã hết pod dùng. PV chuyển sang Released, volume ở nhà cung cấp vẫn còn và vẫn bị tính phí. Quy trình dọn gợi ý, theo phần reclaiming của tài liệu PV:

  1. Sao lưu dữ liệu cần giữ ra ngoài.
  2. kubectl get pv để thấy PV ở trạng thái Released, đối chiếu volumeHandle với volume ở nhà cung cấp.
  3. kubectl delete pv <ten-pv>: với Retain, lệnh này chỉ xoá đối tượng PV.
  4. Xoá volume ở phía nhà cung cấp, sau khi chắc chắn đã sao lưu.

Bước 3 là bước hay bị quên. Lệnh xoá ở phía nhà cung cấp cần làm theo tài liệu của nhà cung cấp, và nên thử trên volume không quan trọng trước.

Checklist

  • Muốn tìm dữ liệu PVC trên node, từ pod lấy node, tên PV, volumeHandle và UID pod.
  • Xem PVC trên node: dùng df trong pod trước; chỉ vào node bằng kubectl debug node khi cần.
  • Dùng ls -ln để đọc owner theo số uid.
  • Đo dung lượng PVC trên node bằng dd và df -B1, xoá tệp thử giữa các lần.
  • Đặt ephemeral-storage limit cho container ghi ngoài volume.
  • PVC trên node kẹt Terminating: xem Used By trước, patch finalizer là phương án cuối.
  • Retain: với PVC trên node đã xoá, dọn PV Released rồi mới xoá volume ở nhà cung cấp.

Bài tiếp theo

Pod Pending vì Insufficient cpu: tính chỗ trống thật từ kubectl describe node. Sau khi nắm chuyện lưu trữ, bài sau hướng dẫn tính tay phần CPU còn trống trên node, để hiểu vì sao pod Pending dù biểu đồ CPU có thể trông còn rảnh.