Một tình huống hay gặp trên Website AI: quản trị viên không upload ảnh được vào trình soạn thảo. Một file JPEG vài MB nhưng độ phân giải rất lớn (khoảng 30 megapixel), lần thì nhận lỗi 419, lần thì 502, thử lại nhiều lần cũng không hết. Site chạy trên cluster Kubernetes LKE của Linode, mỗi site một pod, deploy bằng Helm chart chung. Tài liệu này ghi lại quy trình điều tra, kể cả các giả thuyết sai thường gặp, vì lần sau gặp lỗi upload ảnh, đó chính là những chỗ ai cũng muốn nhìn đầu tiên, và cách xử lý hai giới hạn bộ nhớ chồng lên nhau mà không phải rebuild image.
Triệu chứng
Khi upload ảnh từ trang sửa sản phẩm hoặc sửa bài viết trong admin, console trình duyệt hiện:
POST https://site-a.example/admin/media/files/upload-from-editor 419
Failed to load resource: the server responded with a status of 404 ()- Lỗi lặp lại ở nhiều lần thử, reload trang cũng không hết.
- Một số lần thử lại, mã lỗi đổi thành
502 Bad Gateway. - Upload ảnh nhỏ (vài megapixel) vẫn bình thường.
Theo kinh nghiệm với Laravel, 419 gần như luôn là lỗi CSRF hoặc session. Còn 502 khi upload ảnh thường được nghĩ ngay tới giới hạn kích thước body hoặc timeout ở ingress. Cả hai phản xạ đều có thể dẫn đi sai một đoạn.
Quy trình điều tra lỗi upload ảnh
Giả thuyết 1: upload_max_filesize, post_max_size quá nhỏ
Đây là nguyên nhân 419 hay gặp nhất khi upload ảnh: body vượt post_max_size thì PHP bỏ toàn bộ $_POST, kể cả trường _token, và Laravel báo token không khớp. Kiểm tra ngay trong pod, không đoán theo file trong repo:
kubectl -n site-a-example exec deploy/site-a-example -- php -i \
| grep -E 'upload_max_filesize|post_max_size'
upload_max_filesize => 128M => 128M
post_max_size => 128M => 128MNếu cả hai đã lớn hơn nhiều so với file, loại giả thuyết này.
Giả thuyết 2: ingress chặn body của request upload ảnh
Kiểm tra annotation nginx.ingress.kubernetes.io/proxy-body-size và client_max_body_size của nginx trong pod. Nếu body quá lớn, ingress trả 413 chứ không phải 419 hay 502. Log nginx trong pod có dòng a client request body is buffered to a temporary file nghĩa là request đã đi qua ingress và tới được PHP-FPM.
Giả thuyết 3: CSRF/session hết hạn thông thường
Với SESSION_DRIVER=file và một replica, không có chuyện session nằm ở pod này còn request đi pod khác. Nếu người dùng xác nhận lỗi vẫn còn sau khi reload trang, thì không phải chỉ là tab để quá lâu. Giả thuyết này thường không loại được, nhưng cũng không chứng minh được, vì nhiều CMS đưa TokenMismatchException vào danh sách không report.
Giả thuyết 4: phải sửa Dockerfile
Đề xuất thêm file .ini vào Dockerfile để nâng giới hạn upload là kiểu sai dễ lặp lại: đề xuất sửa code hoặc image trước khi có bằng chứng từ môi trường thật. Kết quả php -i ở giả thuyết 1 đủ để rút lại đề xuất này.
Phát hiện 1: container bị OOMKilled
Bước đáng lẽ phải làm sớm nhất: xem pod có khởi động lại không.
kubectl -n site-a-example describe pod site-a-example-<hash>
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Restart Count: ...
Limits:
cpu: 1
memory: 700MiPod Website AI chạy kiểu all-in-one: nginx, PHP-FPM, crond và queue worker cùng dưới supervisord trong một container. Khi tổng bộ nhớ của tất cả vượt limit, kernel giết container. Biểu đồ "Memory Usage (WSS)" trên Grafana có thể vẫn phẳng, không có đỉnh nào. Điều này không phủ định OOMKilled: Prometheus lấy mẫu mỗi 15–30 giây, còn đỉnh bộ nhớ của một lần upload ảnh chỉ kéo dài vài giây rồi bị kill. Reason: OOMKilled trong kubectl describe đáng tin hơn mọi biểu đồ.
Phát hiện 2: hết OOMKilled nhưng upload ảnh vẫn 502
Nâng limit container lên, pod không còn restart, nhưng upload ảnh vẫn 502. Lúc này nhìn vào giới hạn của riêng PHP:
kubectl -n site-a-example exec deploy/site-a-example -- php -i | grep memory_limit
memory_limit => 128M => 128MSau mỗi lần upload ảnh, thư viện GD phải giải nén JPEG thành bitmap thô trước khi resize và tạo thumbnail. Dung lượng file trên đĩa không quan trọng, độ phân giải mới quan trọng:
6000 × 5000 × 4 byte (RGBA) ≈ 120 MB # mới một bản sao trong RAMChỉ một bản sao đã sát 128M. Thêm bản resize cho từng cỡ thumbnail là vượt. PHP báo Allowed memory size exhausted, worker FPM đang xử lý request chết giữa chừng, nginx mất kết nối tới worker và trả 502. Lỗi này không hiện trong kubectl logs, vì PHP-FPM ghi fatal error vào error log riêng chứ không vào stdout.
Nguyên nhân gốc: hai lớp giới hạn bộ nhớ khi upload ảnh
| Lớp | Giới hạn | Ai thực thi | Khi vượt | Người dùng thấy |
|---|---|---|---|---|
| 1 | resources.limits.memory của container | Kernel (cgroup) | Cả container bị kill, exit 137 | Request đang chạy hỏng, pod khởi động lại |
| 2 | memory_limit của mỗi tiến trình PHP | PHP | Fatal error, worker FPM chết | 502 từ nginx trong pod |
Còn 419 đến từ đâu? Nếu session lưu bằng file trong storage/framework/sessions mà thư mục này không nằm trên volume bền, session nằm trong lớp ghi của container. Mỗi lần container bị OOMKilled và khởi động lại, mọi session biến mất. Lần upload ảnh tiếp theo từ trang admin đang mở vẫn gửi token CSRF cũ, Laravel không tìm thấy session tương ứng và trả 419. Giả thuyết CSRF/session đúng là triệu chứng, chỉ là nguyên nhân của nó nằm ở OOMKilled.
Khắc phục
Lớp 1: nâng limit container qua Helm
Sửa resources.limits.memory trong values.yaml của site, commit vào Git rồi áp bằng Helm, để cluster và repo không lệch nhau:
resources:
limits:
cpu: 1
memory: 2048Mi # trước đó 700Mi
requests:
cpu: 100m
memory: 190Mi # giữ nguyên
helm upgrade --install site-a-example ./chart-web -n site-a-example -f values.yamlLớp 2: nâng memory_limit mà không rebuild image
Base image thường dùng chung cho hàng chục site, rebuild để sửa một site là quá tay. Nếu thư mục public/ của site nằm trên volume NFS (xem bài Helm chart với NFS PersistentVolume tĩnh, claimRef và ba loại probe), có thể dùng file .user.ini mà PHP-FPM tự đọc trong webroot:
kubectl -n site-a-example exec deploy/site-a-example -- \
sh -c 'echo "memory_limit = 512M" > /var/www/html/public/.user.ini'PHP-FPM nạp lại file này sau tối đa user_ini.cache_ttl (mặc định 300 giây), không cần restart pod. Upload ảnh lớn lại để xác nhận: không còn 419 hay 502.
Giải pháp bền vững
- Đưa
memory_limitvào chart hoặc base image..user.inichỉ sống trên volume của site: xoá và tạo lại PVC, hoặc cài lại site từ đầu là mất. Có thể ghi file trong hookpostStart, hoặc bake vào image. - Nâng mức mặc định cho mọi site dùng chung image. Với
memory_limit = 128M, site nào nhận ảnh khoảng 15–20 megapixel trở lên cũng có thể gặp đúng lỗi 502 này. Nâng mặc định lên 256–512M tốt hơn vá từng site khi có người báo, nhưng phải tính lại limit container cho tương ứng. - Giảm việc xử lý ảnh đồng bộ: resize phía trình duyệt trước khi upload ảnh, hoặc đẩy việc tạo thumbnail sang queue, để một request không cần vài trăm MB RAM.
- Đưa session ra khỏi container: Redis hoặc database, để pod khởi động lại không làm mọi người dùng bị văng ra.
Checklist khi gặp lỗi upload ảnh trên Kubernetes
Theo thứ tự rẻ tới đắt:
kubectl get pod: cộtRESTARTScó tăng không? Nếu có,kubectl describe podvà tìmOOMKilledtrước mọi giả thuyết khác.php -itrong pod:upload_max_filesize,post_max_size,memory_limit.- Phân biệt mã lỗi: 413 là body quá lớn ở ingress hoặc nginx; 419 là session/CSRF, và có thể là hệ quả của container restart; 502 là upstream chết giữa chừng; 504 mới là timeout.
- Đọc error log của PHP-FPM trong container sau mỗi lần upload ảnh lỗi, vì fatal error không nằm trong
kubectl logs. - Không dùng biểu đồ bộ nhớ lấy mẫu 15–30 giây để bác bỏ OOMKilled.
Về giám sát: cảnh báo khi một container có lý do kết thúc gần nhất là OOMKilled và số restart tăng trong một giờ, thay vì chờ người dùng báo. Cách đặt requests và limits có trong bài Requests và limits, QoS và nodeAffinity.
Bài học
- Mã lỗi là triệu chứng, không phải chẩn đoán. 419 khi upload ảnh có thể là hệ quả của một lần container bị kill.
- Hai giới hạn bộ nhớ, hai kiểu chết. Limit container do kernel thực thi và giết cả pod.
memory_limitdo PHP thực thi và chỉ giết một worker. Sửa một lớp có thể làm lộ lớp kia. - Bằng chứng từ môi trường thật trước, đề xuất sửa sau. Một lệnh
php -itrong pod loại được hai giả thuyết và một đề xuất sửa image. - Session lưu file trong container là trạng thái dễ mất. Mỗi lần pod khởi động lại là mọi người dùng đang đăng nhập bị văng ra.
Tài liệu tham khảo: file .user.ini trong PHP, quản lý tài nguyên container trên Kubernetes và bảo vệ CSRF trong Laravel.
Bài tiếp theo
OOMKilled và 503: memory_limit PHP đấu với limit container. Khi một pod restart liên tục, và phép cộng bộ nhớ bên trong một container all-in-one không khớp với limit.
