Một pod Laravel không chịu nổi lượng truy cập lớn, nên bạn tăng replicas lên 2. Pod đầu chạy, pod thứ hai kẹt ở Init:0/1 và báo Multi-Attach. Nguyên nhân gần như luôn giống nhau: cả hai pod dùng chung một PVC ReadWriteOnce và rơi vào hai node khác nhau. Bài này đi theo chuỗi thử và sai thường gặp khi nhân bản Laravel: đọc event, cô lập nguyên nhân, thử ReadWriteMany, chuyển sang StatefulSet, rồi gặp lỗi 419 vì session. Mục tiêu là để bạn biết trước điểm dừng, thay vì lặp lại từng bước.
Nếu cần ôn khái niệm, xem Volume, PersistentVolume, PVC và StorageClass. Phần image sạch cho Laravel nằm ở bài kiểm kê image Laravel.
Tăng replicas với PVC ReadWriteOnce: một pod kẹt Init và báo Multi-Attach
Deployment mẫu có replicas: 2, một initContainer chạy php artisan migrate --force, chiến lược Recreate, và một PVC ReadWriteOnce duy nhất được mount vào ba thư mục (public, themes, resources) bằng subPath:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2
strategy: {type: Recreate}
selector:
matchLabels: {app: web}
template:
metadata:
labels: {app: web}
spec:
initContainers:
- name: migrate
image: registry.example.com/web:1.0
command: ["sh", "-c", "php artisan migrate --force"]
containers:
- name: web
image: registry.example.com/web:1.0
volumeMounts:
- {name: data, mountPath: /var/www/html/public, subPath: public}
- {name: data, mountPath: /var/www/html/themes, subPath: themes}
- {name: data, mountPath: /var/www/html/resources, subPath: resources}
volumes:
- name: data
persistentVolumeClaim:
claimName: web-pvcSau kubectl apply, kết quả là một pod Running và một pod Init:0/1 mãi không đi tiếp. Hãy đọc phần Events của pod đang kẹt:
kubectl describe pod web-<hash>
Warning FailedAttachVolume attachdetach-controller Multi-Attach error for volume "pvc-..." Volume is already used by pod(s) web-<hash-kia>
Warning FailedMount kubelet Unable to attach or mount volumes: ... timed out waiting for the conditionDòng Multi-Attach error ... already used by pod(s) là thông tin quan trọng nhất. Nó nói rằng volume đang gắn cho pod khác, và Kubernetes không cho gắn thêm. Event này cho biết volume đang gắn vào node của pod kia, tức hai pod nằm ở hai node khác nhau. Dòng FailedMount bên dưới chỉ là hệ quả: kubelet chờ hết thời gian. Pod hiện Init:0/1 dù initContainer không dùng volume, vì kubelet gắn mọi volume của pod trước khi chạy initContainer.
Cô lập nguyên nhân: thử bỏ PVC ReadWriteOnce khỏi manifest
Để chắc rằng thủ phạm là volume chứ không phải image hay initContainer, hãy bỏ phần volumes và volumeMounts rồi apply lại. Khi đó cả hai pod đều lên Running. Phép thử này không phải cách sửa, vì ứng dụng mất dữ liệu ghi lúc chạy. Nó chỉ chứng minh rằng PVC ReadWriteOnce là điểm nghẽn: một PVC ReadWriteOnce chỉ gắn được vào một node.
Thử ReadWriteMany: CSI từ chối, accessModes bất biến
Phản xạ tiếp theo khi PVC ReadWriteOnce không đủ dùng là đổi accessModes sang ReadWriteMany. Với driver block storage của Linode thì cách này không chạy. Ở một thử nghiệm riêng với MySQL, event cho thấy driver từ chối ngay lúc attach:
Warning FailedAttachVolume attachdetach-controller
AttachVolume.Attach failed ... ControllerPublishVolume Volume capability is not compatible:
... access_mode:<mode:MULTI_NODE_MULTI_WRITER>Tài liệu của Linode ghi rõ driver Block Storage CSI chỉ hỗ trợ ReadWriteOnce, nghĩa là volume gắn được vào một node tại một thời điểm. Hai điều cần nhớ. Thứ nhất, accessModes của PVC đã bound là bất biến: kubectl edit sẽ không đổi gì, còn kubectl apply báo spec is immutable after creation. Muốn đổi phải tạo PVC mới. Thứ hai, trường này chỉ là yêu cầu; việc volume có thực sự chia sẻ được hay không do driver quyết định.
StatefulSet với volumeClaimTemplates: mỗi pod một PVC ReadWriteOnce
Một cách khiến cả hai pod chạy là chuyển sang StatefulSet với volumeClaimTemplates. Controller tạo cho mỗi pod một PVC ReadWriteOnce riêng (data-web-0, data-web-1), nên không còn tranh chấp attach:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: web-headless
replicas: 2
selector:
matchLabels: {app: web}
template:
# ... giữ nguyên containers và initContainers như Deployment ở trên, chỉ bỏ khối volumes
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: web-headless
spec:
clusterIP: None
selector: {app: web}
ports:
- port: 80Hai ghi chú. serviceName phải trỏ tới một headless Service (clusterIP: None) để pod có DNS ổn định; một lỗi hay gặp là trỏ vào Service thường, thường vẫn chạy nhưng không đúng ý nghĩa của StatefulSet. Và Service để người dùng truy cập vào web nên là một Service riêng (ví dụ một Service ClusterIP có selector: {app: web} và port: 80), vì headless không cân bằng tải theo cách thông thường. Khi scale xuống, PVC data-web-1 vẫn còn (mặc định không tự xoá, xem persistentVolumeClaimRetentionPolicy), nên lần scale lên sau pod sẽ dùng lại dữ liệu cũ. Đổi số bản sao bằng kubectl scale statefulset web --replicas=1, xem hướng dẫn scale StatefulSet.
Nhưng cái giá là dữ liệu lệch nhau. Mỗi pod có PVC riêng, nên ảnh upload, theme và plugin cài ở pod 0 không tồn tại ở pod 1. Đây là hành vi đúng của volumeClaimTemplates. Cái sai là kỳ vọng rằng hai bản sao sẽ dùng chung dữ liệu.
Deployment dùng chung PVC ReadWriteOnce khi hai pod ở cùng node
Theo tài liệu access modes, ReadWriteOnce giới hạn theo node, không phải theo pod: nhiều pod trên cùng một node vẫn có thể mount chung volume. Vì vậy về lý thuyết, nếu hai pod rơi vào cùng node thì không có lỗi. Đừng dựa vào điều này. Scheduler có thể đặt pod sang node khác bất cứ lúc nào, node bị thay hoặc drain là lỗi quay lại, và cả hai pod ghi vào cùng một ổ ext4 mà ứng dụng không hề phối hợp. Chiến lược Recreate chỉ quyết định thứ tự thay pod khi cập nhật, không giải quyết replicas > 1. Phần này chưa được thử với nodeAffinity ép cùng node, nên coi đó là suy luận từ tài liệu.
Lỗi 419 sau khi nhân bản: SESSION_DRIVER=file
Khi hai pod chạy, người dùng bắt đầu gặp 419 Page Expired (CSRF token mismatch). Lý do: SESSION_DRIVER=file lưu session trong file cục bộ của từng pod. Request đăng nhập tới pod 0 tạo session ở pod 0; request kế tiếp tới pod 1 không thấy session đó và token không khớp. Theo tài liệu session của Laravel, driver database, redis hoặc memcached là lựa chọn dùng chung cho nhiều máy chủ.
Cách tối thiểu là chuyển session và cache sang database trong ConfigMap:
data:
SESSION_DRIVER: "database"
CACHE_DRIVER: "database" # Laravel 10 trở xuống; Laravel 11 trở lên dùng CACHE_STOREVà các lệnh artisan để tạo bảng:
php artisan session:table
php artisan cache:table
php artisan migrate --forceHai lệnh session:table và cache:table tạo file migration. Nhưng đừng chạy chúng trong container đang chạy rồi quên: file sinh ra nằm trong filesystem của pod và mất khi pod khởi động lại. Cách đúng là commit hai migration vào repository, đưa vào image, và để initContainer hoặc Job chạy migrate. Mẹo export CACHE_DRIVER=file để chạy artisan khi bảng cache chưa tồn tại chỉ là cách tạm. Hai điều kiện nữa: APP_KEY phải giống nhau giữa các pod (dùng chung qua Secret), nếu không cookie và session cũng lỗi và có thể sinh 419; và khi nhiều replica cùng chạy initContainer migrate thì dễ đua nhau, nên dùng php artisan migrate --isolated hoặc một Job riêng. Database driver cũng chỉ là mức tối thiểu; Redis là hướng thay thế phù hợp hơn khi tải lớn, nhưng chưa được thử trong tình huống này.
Gỡ PVC ReadWriteOnce và PV Released khi mount kẹt
Nếu volume kẹt đến mức pod không bao giờ mount được và bạn quyết định xoá, hãy hiểu rằng đó là phương án mất dữ liệu, backup trước. Với Retain, kubectl delete pv chỉ xoá đối tượng Kubernetes; volume thật trên Linode vẫn còn nguyên và tính phí cho đến khi bạn xoá thủ công, còn PVC mới sẽ tạo volume mới, trống. Muốn giữ dữ liệu cũ, hãy tạo PV trỏ tới volume cũ (claimRef) trước khi xoá. Với StorageClass có reclaimPolicy: Retain, xoá PVC ReadWriteOnce để lại PV ở trạng thái Released và phải xoá thủ công:
kubectl delete -f web.yaml # xoá Deployment, PVC, Service
kubectl get pv # PV ở trạng thái Released
kubectl delete pv <ten-pv>
kubectl apply -f web.yamlCách này không giúp gì khi bạn muốn một PVC ReadWriteOnce gắn vào hai node, vì lỗi nằm ở thiết kế chứ không ở trạng thái kẹt. Nguyên nhân gốc của những lần mount kẹt ngẫu nhiên khác (không liên quan tới hai pod dùng chung) chưa được xác định; chi tiết sẽ nằm ở bài kế tiếp của series.
Hướng đúng thay cho PVC ReadWriteOnce khi dữ liệu chung nhiều replica
- Dữ liệu người dùng upload, theme, plugin: thay PVC ReadWriteOnce bằng NFS (ReadWriteMany) hoặc object storage như S3. Phương án này là hướng nghiên cứu tiếp theo, chưa phải kết quả đã thử trong chuỗi trên.
- Session và cache: Redis hoặc database, không dùng file.
- Mã nguồn: nằm trong image, không copy vào volume lúc khởi động. Mẫu
postStartchạycp -rfvào PVC có rủi ro vì hook này không đảm bảo chạy xong trước khi tiến trình chính nhận request. - Cơ sở dữ liệu: không nhân bản bằng cách tăng replicas của pod dùng PVC ReadWriteOnce; xem manifest MySQL StatefulSet.
Checklist trước khi tăng replicas
- Ứng dụng có ghi file lúc chạy không? Nếu có, nó nằm ở đâu, và có thể đưa ra object storage không.
SESSION_DRIVERvàCACHE_DRIVERkhông phảifile.- Không có PVC ReadWriteOnce dùng chung giữa các pod của một Deployment; PVC ReadWriteOnce chỉ dành cho một pod hoặc các pod cùng node.
- Migration session và cache đã nằm trong image, chạy bằng initContainer hoặc Job.
- Nếu dùng StatefulSet,
serviceNametrỏ vào headless Service. - Đã backup trước khi xoá bất kỳ PVC ReadWriteOnce hoặc PV nào.
Bài tiếp theo
Pod kẹt ContainerCreating trên LKE: Multi-Attach và Unable to find device path. Khi volume vẫn kẹt dù bạn không nhân bản gì, hãy đọc event theo từng tầng.



