Pod MySQL trên LKE đứng yên ở trạng thái ContainerCreating sau khi bạn xoá pod hoặc đổi node. Không có log ứng dụng vì container chưa hề chạy, và kubectl get pod chỉ hiện ContainerCreating mà không nói thêm gì. Đằng sau trạng thái ContainerCreating đó có thể là ba nhóm lỗi khác nhau, mỗi nhóm cần cách xử lý riêng và có thể chồng lên nhau trong cùng một lần xếp lịch pod. Bài này là quy trình chẩn đoán theo từng tầng: đọc Events, phân biệt hai dạng Multi-Attach, hiểu lỗi Unable to find device path, và tách lỗi mount với lỗi MySQL crash.

Nếu cần ôn lại khái niệm, xem Volume, PersistentVolume, PVC và StorageClass. Nếu pod kẹt vì bạn tăng số replica, bài nhân bản Laravel với PVC ReadWriteOnce đi vào trường hợp đó.

Ba nhóm lỗi cùng đứng sau ContainerCreating

Trạng thái ContainerCreating nghĩa là pod đã được gán node nhưng kubelet chưa tạo xong container. Với pod có PVC, một nguyên nhân thường gặp nằm ở volume; các nguyên nhân khác gồm kéo image chậm hoặc lỗi, lỗi CNI, và Secret hoặc ConfigMap không mount được. Với nguyên nhân thuộc volume, Events cho thấy ba nhóm:

  1. Multi-Attach (hai dạng): already used by pod(s) khi volume đang gắn cho pod khác, hoặc exclusively attached to one node khi volume vẫn gắn ở node cũ.
  2. Device path: MountVolume.MountDevice failed ... Unable to find device path, attach đã báo thành công nhưng node chưa thấy thiết bị.
  3. Crash sau mount: không còn ở ContainerCreating. Sau khi mount xong, MySQL tự crash ở bước khởi động InnoDB, pod là CrashLoopBackOff, nhưng hay bị nhầm chung một sự cố.

Bước 1: đọc Events của pod ContainerCreating và phân biệt hai dạng Multi-Attach

Việc đầu tiên luôn là kubectl describe pod và nhìn phần Events từ dưới lên:

kubectl describe pod mysql-<hash>
Events:
  Normal   Scheduled           default-scheduler        Successfully assigned default/mysql-<hash> to node-b
  Warning  FailedAttachVolume  attachdetach-controller  Multi-Attach error for volume "pvc-..." Volume is already exclusively attached to one node and can't be attached to another

Hai dạng thông báo có nguyên nhân khác nhau. Dạng already used by pod(s) <tên pod> nghĩa là có pod khác đang dùng volume; nguyên nhân điển hình là Deployment replicas: 2 dùng chung một PVC. Dạng exclusively attached to one node thường xuất hiện khi pod mới được xếp sang node khác so với pod vừa bị xoá và volume chưa kịp detach khỏi node cũ. Điểm chung là ReadWriteOnce được tính theo node, không theo pod (xem access modes).

Dạng thứ hai thường tự hết sau một khoảng ngắn, khi volume được detach và attach lại. Đừng xoá gì vội. Kiểm tra bằng:

kubectl get volumeattachment | grep <ten-pv>
kubectl get pv,pvc -o wide

VolumeAttachment cho biết volume đang gắn vào node nào và đã attach thành công chưa. Đây là bước nhẹ hơn nhiều so với việc xoá PVC, nên hãy thử các bước nhẹ này trước khi dựng lại PVC.

Bước 2: PV, PVC, claimRef và finalizer khi pod ContainerCreating

Một phản xạ hay gặp với pod ContainerCreating là mở PV bằng kubectl edit rồi xoá khối claimRef với hy vọng "giải phóng" volume. Với PV đang Bound, thao tác này chỉ đưa PV sang Available, PVC vẫn ở Bound, còn volume vẫn gắn vào node cũ. Nó không gỡ attach, nên pod vẫn ContainerCreating. PV có hai finalizer. kubernetes.io/pv-protection ngăn xoá PV đang được PVC dùng, còn finalizer external-attacher/<driver> bảo đảm PV được detach trước khi xoá; đừng gỡ chúng khi chưa hiểu hệ quả.

Tương tự, accessModes của PVC đã bound là bất biến. nhiều phiên bản kubectl báo lỗi immutable khi kubectl edit; trong ghi nhận gốc, giá trị không đổi. Còn kubectl apply trả spec is immutable after creation except resources.requests for bound claims. Đổi sang ReadWriteMany cũng vô ích vì driver block storage không hỗ trợ.

Bước 3: ContainerCreating vì Unable to find device path, attach xong nhưng chưa thấy thiết bị

Đây là lỗi hay gây hoang mang nhất. Chuỗi event điển hình:

Normal   SuccessfulAttachVolume  AttachVolume.Attach succeeded for volume "pvc-..."
Warning  FailedMount             MountVolume.MountDevice failed for volume "pvc-..." :
  rpc error: code = Internal desc = Unable to find device path out of attempted paths:
  [/dev/disk/by-id/linode-pvc... /dev/disk/by-id/scsi-0Linode_Volume_pvc...]

Attach thành công nghĩa là control plane đã yêu cầu gắn volume vào máy ảo. Nhưng kubelet cần thấy thiết bị khối xuất hiện trong /dev/disk/by-id/ thì mới format và mount được. Nếu thiết bị chưa hiện, driver báo Unable to find device path và kubelet thử lại.

Trong nhiều trường hợp, lỗi này chỉ xuất hiện vài lần ở những lần thử đầu rồi tự hết: container vẫn được tạo và Started ngay sau đó. Đó là lỗi thoáng qua, không đáng lo. Nhưng cũng có trường hợp pod ContainerCreating kẹt lâu, kéo dài và không tự hết, thậm chí buộc phải dựng lại PVC. Nguyên nhân gốc của kiểu kẹt lâu chưa được xác nhận. Điều đáng chú ý là cùng một cấu hình, có lần đổi node chạy bình thường, có lần kẹt, và chưa có lời giải thích nào cho khác biệt này. Khi gặp, hãy ghi lại thời điểm attach và tự kiểm tra từng tầng:

ls -l /dev/disk/by-id/ | grep -i linode        # trên node: có thiết bị linode-pvc... chưa
kubectl get volumeattachment -o wide           # volume gắn vào node nào
kubectl -n kube-system logs <pod csi-linode-node trên node đó>

Nếu thiết bị chưa hiện trên node dù VolumeAttachment báo đã attach, hãy kiểm tra thêm phía nhà cung cấp xem volume đã gắn vào đúng máy chưa.

Những thử nghiệm không gỡ được ContainerCreating

  • Đổi accessModes của PVC: bất biến, như trên.
  • Bỏ claimRef trong PV: chỉ đổi trạng thái PV, không detach volume.
  • Đặt strategy: Recreate rồi xoá pod bằng tay: Recreate chỉ tác động khi Deployment cập nhật, không liên quan đến việc xoá pod thủ công, nên pod vẫn ContainerCreating. Lỡ đã thử mà không thấy gì thay đổi thì đó là đúng. Tuy vậy Recreate có ích khi rollout Deployment dùng PVC ReadWriteOnce, vì pod cũ dừng trước pod mới, tránh pod mới bị xếp sang node khác rồi dính Multi-Attach.
  • Chuyển sang StatefulSet: không làm Multi-Attach biến mất khi pod đổi node, vì ràng buộc nằm ở volume, không ở loại workload.

Mount xong nhưng MySQL vẫn crash: InnoDB assertion trên ibdata1

Cần tách rõ: lỗi device path không gây crash MySQL. Trong tình huống dùng mysql:5.7, sau vài lần thử lại, container được mount và khởi động, rồi MySQL tự dừng với log:

[ERROR] [FATAL] InnoDB: Table flags are 0 in the data dictionary but the flags in file ./ibdata1 are 0x4800!
InnoDB: Assertion failure in thread ... We intentionally generate a memory trap.

Đây là vấn đề của dữ liệu trong thư mục datadir, không phải của mount. Hai cách đã ghi nhận: đổi image sang bản mới hơn, hoặc thêm subPath sau mountPath. Nhiều khả năng subPath hết crash vì MySQL dùng một thư mục con sạch trên volume, thay vì thư mục gốc của volume. Lý do không phải lost+found, đó là chủ đề khác trong bài manifest Kubernetes đầu tiên. Giả thuyết dữ liệu cũ do một bản MySQL khác ghi là suy luận có cơ sở, chưa được kiểm chứng. Bạn có thể kiểm tra bằng cách liệt kê nội dung gốc volume trước khi khởi động MySQL, xem cách xem dữ liệu PVC trên node.

volumeMounts:
- name: mysql-data
  mountPath: /var/lib/mysql
  subPath: mysql

Cần hiểu hệ quả: subPath: mysql làm MySQL khởi tạo một datadir mới và trống. Dữ liệu cũ (ibdata1 do bản MySQL khác ghi) vẫn nằm ở gốc volume nhưng không được dùng, nên cách này chỉ tránh được crash vì bỏ qua dữ liệu cũ, không sửa gì ở dữ liệu. Chỉ dùng khi chấp nhận bỏ dữ liệu đó hoặc đã có backup. Nếu cần giữ dữ liệu, hãy chạy đúng phiên bản MySQL đã ghi datadir: ghim tag cụ thể, không dùng latest, và không hạ phiên bản MySQL trên datadir đã được nâng cấp.

Phương án cuối khi ContainerCreating kéo dài: xoá Deployment, PVC và PV Released rồi dựng lại

Khi pod ContainerCreating mãi không qua và mọi bước nhẹ đều không có tác dụng, nếu dữ liệu không quan trọng hoặc đã có bản sao, có thể dựng lại. Backup trước. Với StorageClass Retain, xoá PVC để lại PV Released và volume ngoài nhà cung cấp vẫn tồn tại, xoá PV không xoá volume thật, nên volume đó sẽ thành volume mồ côi, phải xoá tay ở phía nhà cung cấp sau khi chắc chắn không cần. PVC mới cấp một volume trống mới, dữ liệu cũ chỉ còn trên volume cũ:

kubectl delete -f mysql.yaml
kubectl delete -f mysql-pvc.yaml     # hoặc: kubectl delete pvc mysql-pvc
kubectl get pv                       # PV ở trạng thái Released
kubectl delete pv <ten-pv>
kubectl apply -f mysql-pvc.yaml
kubectl apply -f mysql.yaml

Nếu muốn dùng lại volume cũ, hãy bỏ claimRef của PV đang Released rồi đặt volumeName trỏ vào PV đó trong PVC mới, thay vì xoá PV. Một lỗi hay gặp là gõ kubectl delete pvc mysql-pvc.yaml; lệnh này tìm PVC tên mysql-pvc.yaml và sẽ không thấy. Dùng delete -f với file, hoặc delete pvc <tên> với tên.

Giảm rủi ro để pod không kẹt ContainerCreating

  • Với một replica và một PVC, StatefulSet và Deployment đều dùng được, nhưng StatefulSet diễn đạt đúng ý định hơn. Nó không làm hết Multi-Attach khi đổi node.
  • Không dựa vào việc các replica nằm cùng node khi dùng chung PVC ReadWriteOnce; cần nhiều replica thì dùng ReadWriteMany qua NFS hoặc object storage.
  • Cân nhắc đưa database ra ngoài cluster; xem MySQL đa tenant trên VPS cho mô hình máy database riêng.

Checklist chẩn đoán pod ContainerCreating

  1. Pod ContainerCreating: chạy kubectl describe pod, đọc Events từ dưới lên và phân loại: Multi-Attach, device path, hay crash sau mount.
  2. Multi-Attach: tìm pod đang dùng volume; kiểm tra VolumeAttachment; chờ detach trước khi xoá gì.
  3. Device path: ghi lại có Started sau đó không; nếu có thì đó là lỗi thoáng qua.
  4. CrashLoopBackOff sau mount: đọc kubectl logs --previous, kiểm tra datadir và subPath.
  5. Chỉ xoá PVC và PV khi đã có backup, kể cả khi pod ContainerCreating kéo dài.

Bài tiếp theo

MySQL replication trong StatefulSet: vì sao xtrabackup 2.4 hỏng với MySQL 8. Khi mỗi pod đã có volume riêng ổn định, bước kế tiếp là cho các pod MySQL sao chép dữ liệu cho nhau.