Một tình huống điển hình: một site Laravel chạy trên cluster, quản trị viên import một file XML khoảng 45 MB và nhận lỗi 413 Request Entity Too Large. Sửa client_max_body_size trong nginx của pod, sửa post_max_size trong php.ini, build lại image, deploy lại: lỗi 413 vẫn còn, kể cả với file chỉ 2 MB. Sau khi qua được lỗi đó, import có thể báo 504. Lỗi 413 và 504 có chung một gốc: trên Kubernetes, request đi qua nhiều lớp proxy, và mỗi lớp có giới hạn riêng. Bài này hướng dẫn xác định lớp nào đang chặn, dựa vào log, rồi đưa giới hạn ra cấu hình.

Nếu bạn chỉ chạy nginx và PHP-FPM trên VPS, hãy xem bài nginx reverse proxy để nắm chuỗi proxy cơ bản. Bài này thêm một lớp nữa là Ingress, đã giới thiệu ở Ingress và NodeBalancer.

Lỗi 413 và 504: request đi qua mấy lớp trước khi tới PHP

Với site dùng ingress-nginx, một request upload đi qua bốn lớp, mỗi lớp có giới hạn về kích thước body, thời gian chờ hoặc kích thước header:

  1. Client, CDN hoặc NodeBalancer ở ngoài cùng: CDN (ví dụ Cloudflare) có giới hạn kích thước body và timeout riêng tuỳ gói, còn NodeBalancer hay proxy phía trước thì tuỳ chế độ; hãy tra tài liệu của dịch vụ đang dùng.
  2. ingress-nginx: một nginx độc lập, chạy ngoài pod của bạn.
  3. nginx trong pod: nginx của image ứng dụng, đứng trước PHP-FPM.
  4. PHP-FPM và PHP: post_max_size, upload_max_filesize, max_execution_time.

Điều dễ bỏ sót là lớp thứ hai. Ingress không phải cấu hình của bạn, nó là một nginx khác với mặc định riêng. Sửa nginx trong pod không thể nâng giới hạn của nginx đứng trước nó, nên lỗi 413 không đổi.

Lỗi 413: vì sao sửa nginx.conf trong pod không tác dụng

Trong tài liệu của ingress-nginx, annotation nginx.ingress.kubernetes.io/proxy-body-size có giá trị mặc định 1m. Mọi request có body lớn hơn mức đó bị ingress trả lỗi 413 mà không chuyển tiếp vào pod. Vì thế sửa file trong pod không đổi được kết quả. Triệu chứng đáng chú ý nhất là file nhỏ như 2 MB cũng lỗi, vượt xa mức bạn đã đặt trong pod.

Cách kiểm tra mức hiệu lực của ingress:

kubectl get ingress app -o yaml | grep -i body-size
kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- grep -n client_max_body_size /etc/nginx/nginx.conf

Annotation đúng là proxy-body-size. Một số hướng dẫn trên mạng nhắc tới client-max-body-size như annotation của ingress-nginx, nhưng nó không nằm trong danh sách annotation chuẩn, nên đừng dựa vào nó. Mặc định 1m theo tài liệu ingress-nginx, nhưng nếu quản trị cluster đã đổi qua ConfigMap thì giá trị hiệu lực khác, nên hãy kiểm bằng hai lệnh ở trên.

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "128m"

Lỗi 413 qua ba mức giới hạn: phép thử và kết luận về ghi đè

Để biết lớp nào thắng khi xảy ra lỗi 413, một phép thử gọn là đặt ba giá trị khác nhau ở ba nơi, rồi upload các file có kích thước nằm giữa chúng:

Nơi đặtGiá trị thử
Ingress (proxy-body-size)10m
nginx trong pod, khối http1m
nginx trong pod, khối server5m
  • File 4,7 MB: qua được. Lớn hơn mức 1m của khối http, nên khối server đã ghi đè.
  • File 7,4 MB: bị nginx trong pod chặn, và có dòng log client intended to send too large body.
  • File 12 MB: bị Ingress chặn, và trong pod không có log nào.

Cơ chế đúng của nginx là directive đặt ở khối cụ thể hơn thì ghi đè khối bao ngoài, không phải kiểu "giá trị hẹp hơn thắng". Giữa Ingress và pod thì hai lớp độc lập, nên giá trị hiệu lực là giá trị nhỏ nhất trong chuỗi. Chưa có thử nghiệm ở cấp location. Về lý thuyết, mỗi request chỉ được xử lý trong một location, nên directive đặt ở location / không áp dụng cho request đã chuyển sang location ~ \.php$. Đó là suy luận, hãy tự thử trên cluster thử.

Lưu ý thêm về lớp thứ tư: với PHP thuần, body vượt post_max_size làm $_POST và $_FILES rỗng, kèm cảnh báo POST Content-Length ... exceeds the limit trong log PHP, nên upload "thành công" nhưng không có file. Với Laravel thì khác: middleware ValidatePostSize (mặc định nằm trong global middleware) so Content-Length với post_max_size và ném PostTooLargeException, ứng dụng trả lỗi 413 với trang "Payload Too Large". Vì vậy 413 vẫn có thể do ứng dụng sinh ra, không chỉ do nginx.

Nhận biết lớp gây lỗi 413 qua log

Quan sátLớp chặn
Trả 413, log ingress có dòng request, log nginx trong pod không có gìingress-nginx
Trả 413, log nginx trong pod có client intended to send too large bodynginx trong pod
Trả 413 (trang "Payload Too Large"), access log nginx trong pod có dòng request, error log nginx không có too large body, log Laravel có PostTooLargeExceptionỨng dụng Laravel (post_max_size)
Upload xong nhưng file rỗng, không có lỗi HTTP (PHP thuần, không có ValidatePostSize)PHP (post_max_size)

Vì vậy câu hỏi đầu tiên khi gặp lỗi 413 là có thấy dòng log nào trong pod không. Không có log nghĩa là request chưa tới pod, và không nên mất thời gian sửa nginx trong pod.

Bẫy log nginx: file log riêng không ra kubectl logs

Nhiều image khai báo log riêng cho từng site, ví dụ error_log /var/log/nginx/web.error. File này nằm trong pod, kubectl logs không thấy nó và bạn chỉ đọc được bằng kubectl exec. Khi chẩn đoán lỗi 413, một hiểu lầm hay gặp là thấy access.log được trỏ symlink tới /dev/stdout rồi cho rằng "không đọc được" và gỡ symlink đi. Trong trường hợp nginx là tiến trình chính (PID 1) của container, chính symlink đó giúp log ra kubectl logs, nên nên giữ nguyên.

access_log /dev/stdout;
error_log  /dev/stderr warn;

Điều kiện quan trọng là image chạy nginx thế nào. Nếu nginx chạy dưới supervisord, /dev/stdout của nginx là đường ống tới supervisord chứ không phải stdout của container, nên log có thể vẫn không ra kubectl logs. Khi đó hãy chuyển log của chương trình nginx trong supervisor ra /dev/fd/1 và /dev/fd/2:

[program:nginx]
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0

Cũng cần kiểm tra quyền ghi: worker nginx chạy bằng user không phải root có thể gặp permission denied khi mở /dev/stdout. Đây là một lý do có thể khiến log "không đọc được", cần tự xác nhận trên image của bạn.

Sau lỗi 413 là lỗi 504: timeout ở hai tầng

Sau khi qua lỗi 413, bước import thường chạy lâu và gặp 504 Gateway Time-out. Timeout cũng nằm ở hai nơi độc lập:

  • Ingress: proxy-read-timeout (mặc định 60 giây), cùng proxy-send-timeout.
  • nginx trong pod chờ PHP-FPM: fastcgi_read_timeout (mặc định 60 giây).
  • PHP: max_execution_time, và request_terminate_timeout của FPM nếu được đặt.

Một lỗi hay gặp với import lớn là chỉ nâng proxy-read-timeout ở Ingress lên rất cao mà vẫn không đủ. Log của pod sau đó có dòng upstream timed out (110: Operation timed out) while reading response header from upstream với upstream là fastcgi://127.0.0.1:9000. Dòng này cho biết request đã qua ingress, và nginx trong pod mới là nơi hết giờ. Cần nâng đủ cả hai tầng. Timeout hiệu lực cũng có thể đã là giá trị tuỳ chỉnh: nếu image đã đặt fastcgi_read_timeout 180 mà import vẫn quá thời gian, giá trị phải lớn hơn thời gian xử lý thực tế chứ không chỉ lớn hơn mặc định. Nếu phía trước còn CDN, timeout của nó có thể hiển thị mã khác 504 của nginx (ví dụ Cloudflare trả 524).

Một lỗi hay gặp là thêm proxy_connect_timeout vào nginx trong pod. Directive này chỉ áp dụng cho proxy_pass, không áp dụng cho fastcgi_pass. Directive đúng cho PHP-FPM là fastcgi_read_timeout, đi cùng max_execution_time. Nếu import thành công sau thay đổi như vậy, điều đó chưa chứng minh được quan hệ nhân quả; hãy tự thử với fastcgi_read_timeout.

location ~ \.php$ {
    # giữ nguyên các dòng include fastcgi_params và SCRIPT_FILENAME hiện có
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_read_timeout 300;
}

Đoạn trên chỉ là phần trích của khối location sẵn có, không phải cấu hình đầy đủ.

Với tác vụ chạy hàng phút như import, cách bền hơn là đẩy sang queue (xem chạy artisan trên Kubernetes) thay vì kéo dài timeout của mọi lớp.

Đưa giới hạn ra cấu hình thay vì sửa trong pod

Sửa trực tiếp trong pod mất sau lần restart đầu tiên, và lỗi 413 sẽ quay lại, nên mọi giới hạn cần đi vào cấu hình khai báo. Với Helm, đặt chúng trong values.yaml để Ingress được sinh ra đã có sẵn annotation:

ingress:
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "128m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "600"

Còn nginx và PHP trong pod nên lấy từ ConfigMap hoặc từ image, với các giá trị cùng thứ bậc: Ingress lớn hơn hoặc bằng nginx pod, và post_max_size lớn hơn upload_max_filesize. Cấu hình ingress-nginx nói chung nằm trong bài ingress-nginx và cert-manager.

Lỗi 400: header hoặc cookie quá lớn

Ngoài lỗi 413 và 504, lỗi thứ ba cùng họ là 400 Bad Request: Request Header Or Cookie Too Large. Đây là thông báo của nginx khi header vượt bộ đệm. Mặc định của large_client_header_buffers là 4 8k. Một hướng xử lý là tăng trong khối server của nginx pod, ví dụ large_client_header_buffers 4 32k;. Chưa có xác nhận lớp nào sinh lỗi trong từng trường hợp cụ thể. Nếu lỗi do Ingress, cần chỉnh khoá large-client-header-buffers trong ConfigMap của ingress-nginx. Hãy áp dụng cùng phương pháp: xem lớp nào có dòng request trong log.

Checklist

  • Gặp lỗi 413: kiểm tra log pod trước, không có log thì sửa Ingress.
  • Có log request ở pod mà error log nginx không có dòng too large body: kiểm tra post_max_size và log ứng dụng (Laravel trả 413 qua ValidatePostSize).
  • Đặt proxy-body-size ở Ingress, client_max_body_size ở khối server của pod, và PHP lớn tương ứng.
  • Gặp 504: tìm dòng upstream timed out, rồi nâng cả hai tầng timeout.
  • Đưa mọi giá trị vào values hoặc ConfigMap, không sửa tay trong pod.
  • Cho log của site ra stdout và stderr; nếu nginx chạy dưới supervisord thì dùng /dev/fd/1 và /dev/fd/2.

Tài liệu gốc: annotation của ingress-nginx và client_max_body_size trên nginx.org.

Bài tiếp theo

Kubeconfig cho dev theo namespace: ServiceAccount, Role và những rủi ro. Khi dev cần tự xem log pod như trong bài này, bạn cấp quyền cho họ thế nào mà không trao toàn bộ cluster.