ALB Trả Về 502 Bad Gateway Dù Target Group Healthy: Nguyên Nhân Thực Sự Từ Phía Ứng Dụng

Bạn nhìn vào console, target group xanh lá cây hoàn toàn — tất cả instance đều Healthy — nhưng người dùng vẫn nhận được 502 Bad Gateway. Đây là một trong những tình huống gây bối rối nhất khi vận hành ALB, bởi vì 502 từ ALB không có nghĩa là instance chết, mà có nghĩa là ALB đã kết nối được đến instance nhưng nhận về một phản hồi không hợp lệ hoặc kết nối bị đóng đột ngột.

TL;DR: ALB 502 Bad Gateway — Tóm Tắt Nhanh

Nguyên nhânTriệu chứng quan sát đượcHướng xử lý
Ứng dụng đóng kết nối trước khi gửi response hoàn chỉnh502 xuất hiện ngẫu nhiên, không theo patternKiểm tra application log, tăng keep-alive timeout
HTTP response header không hợp lệ502 nhất quán trên một endpoint cụ thểKiểm tra raw response bằng curl hoặc tcpdump
Keep-alive timeout của ứng dụng ngắn hơn ALB idle timeout502 xuất hiện trên các request sau khi kết nối idle một thời gianTăng keep-alive timeout phía ứng dụng
Target trả về HTTP/1.0 thay vì HTTP/1.1502 trên một số loại request nhất địnhCấu hình ứng dụng dùng HTTP/1.1
Response body không khớp Content-Length502 sau khi nhận được một phần responseKiểm tra logic streaming hoặc chunked encoding

Cơ Chế Hoạt Động: ALB Xử Lý Response Như Thế Nào

Health check và request routing là hai luồng độc lập trong ALB. Health check chỉ xác nhận rằng target đang lắng nghe trên port được cấu hình và trả về status code nằm trong dải cho phép. Nó không kiểm tra tính hợp lệ của HTTP response header, không kiểm tra keep-alive behavior, và không phát hiện race condition trong connection handling.

Khi ALB forward một request thực đến target, nó kỳ vọng nhận về một HTTP response hoàn chỉnh và hợp lệ theo RFC. Nếu target đóng kết nối TCP trước khi gửi xong response, gửi header không đúng format, hoặc vi phạm HTTP/1.1 spec, ALB sẽ phát sinh 502 — trong khi health check vẫn tiếp tục pass bình thường vì hai luồng này không liên quan đến nhau.

graph TD HC["Health Check Probe
(độc lập)"] -->|"GET /health → 200 OK"| TG["Target Instance
(Healthy)"] Client["Client Request"] --> ALB["ALB"] ALB -->|"Forward request"| TG TG -->|"Response không hợp lệ
hoặc kết nối đóng đột ngột"| ALB ALB -->|"502 Bad Gateway"| Client HC -.->|"Vẫn pass bình thường"| Status["Status: Healthy ✓"]
  1. Health Check (độc lập): ALB định kỳ gửi request đến target, nhận status 200, đánh dấu Healthy. Luồng này không liên quan đến request thực của người dùng.
  2. Client Request: Người dùng gửi request, ALB chọn target và thiết lập kết nối TCP.
  3. 502 tại ALB: Target trả về response không hợp lệ hoặc đóng kết nối đột ngột — ALB không thể forward response này và trả về 502 cho client.
  4. Healthy status không thay đổi: Vì health check vẫn pass, instance tiếp tục nhận traffic và tiếp tục gây 502.

Bước 1: Đọc Access Log Của ALB Để Xác Định Loại 502

Trước khi làm bất cứ điều gì, bạn cần biết ALB đang log gì. ALB access log ghi lại trường error_reason — đây là điểm khởi đầu duy nhất đáng tin cậy. Nếu chưa bật access log, đây là bước đầu tiên cần làm ngay.

Bật access log cho ALB:

aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/1234567890abcdef \
  --attributes Key=access_logs.s3.enabled,Value=true \
               Key=access_logs.s3.bucket,Value=my-alb-logs-bucket \
               Key=access_logs.s3.prefix,Value=my-alb

Sau khi có log, lọc các dòng 502:

aws s3 cp s3://my-alb-logs-bucket/my-alb/ . --recursive --exclude "*" --include "*.log.gz"
zcat *.log.gz | awk '$9 == 502 {print $0}' | head -50

Chú ý trường thứ 9 (status code) và trường error_reason ở cuối mỗi dòng. Các giá trị phổ biến: RESPONSE_INVALID chỉ ra header không hợp lệ; CONNECTION_ERROR chỉ ra kết nối bị đóng đột ngột từ phía target.

Bước 2: Kiểm Tra Raw HTTP Response Trực Tiếp Từ Target

ALB log cho bạn biết loại lỗi, nhưng để biết tại sao, bạn cần xem response thực tế từ ứng dụng — bypass hoàn toàn ALB. SSH vào một instance trong target group hoặc dùng một EC2 khác trong cùng VPC:

# Thay thế 10.0.1.50 bằng private IP của target instance
# Thay thế 8080 bằng port mà target group đang dùng
curl -v --http1.1 http://10.0.1.50:8080/your-endpoint 2>&1 | head -100

Những dấu hiệu cần chú ý trong output:

  • Server trả về HTTP/1.0 thay vì HTTP/1.1 — ALB yêu cầu HTTP/1.1 cho backend connection.
  • Header Transfer-Encoding: chunked xuất hiện cùng với Content-Length — đây là vi phạm RFC 7230.
  • Kết nối bị đóng (* Connection #0 to host ... left intact không xuất hiện) trước khi response hoàn chỉnh.
  • Header chứa ký tự không hợp lệ hoặc format sai.
ALB hoạt động như một HTTP/1.1 proxy nghiêm ngặt. Nó không cố gắng 'sửa' response không hợp lệ — nó reject ngay lập tức và trả về 502. Đây là hành vi đúng theo spec, nhưng khác với một số reverse proxy khác có thể tolerant hơn.

Bước 3: Kiểm Tra Application Log Tại Thời Điểm 502 Xảy Ra

Correlation theo timestamp là cách duy nhất để biết ứng dụng có nhận được request hay không. Nếu ALB log ghi nhận 502 nhưng application log không có entry tương ứng, kết nối đã bị đóng trước khi request đến được application layer — thường là do process crash, OOM kill, hoặc socket backlog đầy.

# Kiểm tra system log để phát hiện OOM kill hoặc process restart
sudo journalctl -u your-app-service --since "2024-01-15 10:00:00" --until "2024-01-15 11:00:00"

# Hoặc kiểm tra kernel log
sudo dmesg | grep -i "oom\|killed" | tail -20
# Kiểm tra socket backlog và connection state
ss -tlnp | grep :8080
ss -s

Nếu Send-Q trong output của ss -tlnp liên tục cao, socket backlog đang bị overflow — ứng dụng không xử lý connection đủ nhanh.

Bước 4: Xác Định Race Condition Giữa Keep-Alive Timeout Của Ứng Dụng Và ALB

Đây là nguyên nhân phổ biến nhất gây ra 502 intermittent mà rất khó debug. ALB tái sử dụng kết nối TCP đến target (connection pooling) để tối ưu hiệu năng. Nếu ứng dụng đóng kết nối keep-alive đúng vào lúc ALB đang gửi request mới trên kết nối đó, ALB nhận về RST hoặc FIN và phát sinh 502.

sequenceDiagram participant ALB as ALB participant App as Application Note over ALB,App: Kết nối TCP được tái sử dụng (connection pool) ALB->>App: Request #1 App->>ALB: Response #1 (OK) Note over App: Idle... keep-alive timer chạy Note over App: Timer hết → App quyết định đóng kết nối ALB->>App: Request #2 (gửi trên kết nối cũ) App->>ALB: TCP RST / FIN (kết nối đã đóng) ALB->>ALB: Không có HTTP response Note over ALB: Trả về 502 Bad Gateway
  1. ALB duy trì connection pool đến các target instance.
  2. Ứng dụng quyết định đóng kết nối sau X giây idle (keep-alive timeout của ứng dụng).
  3. Đúng lúc đó, ALB gửi request mới trên kết nối sắp bị đóng — race condition xảy ra.
  4. ALB nhận về TCP RST, không có HTTP response — 502 được trả về cho client.

Để tránh race condition này, keep-alive timeout của ứng dụng phải lớn hơn idle timeout của ALB. Kiểm tra idle timeout hiện tại của ALB:

aws elbv2 describe-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/1234567890abcdef \
  --query 'Attributes[?Key==`idle_timeout.timeout_seconds`]'

Giá trị mặc định của idle_timeout.timeout_seconds là 60 giây. Nếu ứng dụng của bạn (ví dụ: Nginx, Node.js, Gunicorn) có keep-alive timeout mặc định thấp hơn — ví dụ Nginx mặc định là 75 giây nhưng nhiều framework khác mặc định thấp hơn nhiều — race condition sẽ xảy ra thường xuyên dưới tải cao.

Nếu cần điều chỉnh idle timeout của ALB xuống thấp hơn để phù hợp với ứng dụng:

aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/1234567890abcdef \
  --attributes Key=idle_timeout.timeout_seconds,Value=30

Bước 5: Phân Biệt Idle Timeout Của ALB Và Health Check Timeout Của Target Group

Một điểm dễ nhầm lẫn: có hai loại timeout hoàn toàn khác nhau liên quan đến ALB và target group. Chúng kiểm soát hai thứ khác nhau và được cấu hình ở hai nơi khác nhau.

Loại 1 — Idle Timeout của ALB (idle_timeout.timeout_seconds): Kiểm soát thời gian tối đa một kết nối TCP giữa client và ALB (hoặc giữa ALB và target) có thể idle mà không có dữ liệu truyền qua. Đây là thuộc tính của Load Balancer, không phải Target Group.

# Xem idle timeout của ALB
aws elbv2 describe-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/1234567890abcdef \
  --query 'Attributes[?Key==`idle_timeout.timeout_seconds`]'

Loại 2 — Health Check Timeout của Target Group (HealthCheckTimeoutSeconds): Kiểm soát thời gian ALB chờ response từ target trong mỗi lần thực hiện health check. Đây là thuộc tính của Target Group.

# Xem cấu hình health check của target group, bao gồm HealthCheckTimeoutSeconds
aws elbv2 describe-target-groups \
  --target-group-arns arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-tg/1234567890abcdef \
  --query 'TargetGroups[0].{HealthCheckTimeoutSeconds:HealthCheckTimeoutSeconds,HealthCheckIntervalSeconds:HealthCheckIntervalSeconds,HealthyThresholdCount:HealthyThresholdCount,UnhealthyThresholdCount:UnhealthyThresholdCount}'

HealthCheckTimeoutSeconds không ảnh hưởng đến request thực của người dùng — nó chỉ liên quan đến health check probe. Khi debug 502, idle_timeout.timeout_seconds mới là giá trị cần quan tâm để đối chiếu với keep-alive timeout của ứng dụng.

Bước 6: Kiểm Tra Security Group Và Network ACL Không Chặn Response

Security group của target instance phải cho phép inbound traffic từ security group của ALB trên port mà target group đang dùng. Đây là điều kiện cơ bản, nhưng thay đổi security group rule sau khi deploy là nguyên nhân phổ biến gây ra 502 đột ngột.

# Lấy security group của ALB
aws elbv2 describe-load-balancers \
  --load-balancer-arns arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/1234567890abcdef \
  --query 'LoadBalancers[0].SecurityGroups'

# Kiểm tra inbound rules của security group target instance
aws ec2 describe-security-groups \
  --group-ids sg-0123456789abcdef0 \
  --query 'SecurityGroups[0].IpPermissions'

Ngoài security group, kiểm tra Network ACL của subnet chứa target instance — không giống security group, NACL là stateless và cần có cả inbound lẫn outbound rule cho ephemeral ports (1024-65535) để response traffic đi được từ target về ALB.

# Lấy subnet của target instance, sau đó kiểm tra NACL
aws ec2 describe-network-acls \
  --filters Name=association.subnet-id,Values=subnet-0123456789abcdef0 \
  --query 'NetworkAcls[0].Entries'

Kinh Nghiệm Thực Tế: Khi Gunicorn Gây Ra 502 Intermittent Dưới Tải Cao

Triệu chứng: 502 xuất hiện với tần suất khoảng 0.1-0.3% request, không có pattern rõ ràng, tăng lên dưới tải cao. ALB access log ghi CONNECTION_ERROR. Application log của Gunicorn không có gì bất thường — không có exception, không có timeout.

Chẩn đoán ban đầu sai: Team nghĩ đây là vấn đề về resource — CPU hoặc memory của instance. Scale up instance type, 502 vẫn tiếp tục.

Nguyên nhân thực sự: Gunicorn mặc định có keepalive là 2 giây. ALB idle timeout là 60 giây (mặc định). Dưới tải cao, ALB liên tục tái sử dụng kết nối đến Gunicorn worker. Gunicorn đóng kết nối sau 2 giây idle, nhưng ALB không biết điều này và tiếp tục gửi request mới trên kết nối đã bị đóng. Race condition xảy ra thường xuyên hơn khi số lượng concurrent connection tăng.

Fix: Tăng keepalive trong Gunicorn config lên 75 giây (lớn hơn ALB idle timeout 60 giây). 502 biến mất hoàn toàn.

Insight quan trọng: Health check pass không có nghĩa là keep-alive behavior của ứng dụng tương thích với ALB connection pooling. Đây là hai thứ hoàn toàn tách biệt.

IAM Policy Cần Thiết Để Thực Hiện Các Bước Trên

Nếu bạn đang chạy các lệnh CLI trên từ một IAM role hoặc user không phải admin, policy tối thiểu cần có:

🔽 Click để xem IAM policy tối thiểu
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "elasticloadbalancing:DescribeLoadBalancers",
        "elasticloadbalancing:DescribeLoadBalancerAttributes",
        "elasticloadbalancing:ModifyLoadBalancerAttributes",
        "elasticloadbalancing:DescribeTargetGroups",
        "elasticloadbalancing:DescribeTargetHealth"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeNetworkAcls"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-alb-logs-bucket",
        "arn:aws:s3:::my-alb-logs-bucket/*"
      ]
    }
  ]
}

Lưu ý: Các action Describe* của ELBv2 yêu cầu "Resource": "*" vì chúng không hỗ trợ resource-level permission. Kiểm tra AWS Service Authorization Reference để xác nhận trước khi áp dụng vào môi trường production.

Wrap-Up: ALB 502 Bad Gateway — Checklist Và Bước Tiếp Theo

Khi ALB trả về 502 dù target group healthy, thứ tự kiểm tra hiệu quả nhất là: đọc ALB access log để xác định loại lỗi → kiểm tra raw response từ target → đối chiếu keep-alive timeout của ứng dụng với idle timeout của ALB → kiểm tra security group và NACL.

Phần lớn các trường hợp 502 intermittent trong môi trường production đều liên quan đến keep-alive timeout mismatch — không phải lỗi ứng dụng, không phải resource exhaustion. Kiểm tra idle_timeout.timeout_seconds của ALB bằng aws elbv2 describe-load-balancer-attributes và đảm bảo keep-alive timeout của ứng dụng lớn hơn giá trị đó.

Tài liệu tham khảo chính thức:

Glossary: Các Thuật Ngữ Quan Trọng

Thuật ngữGiải thích
502 Bad GatewayALB nhận được response không hợp lệ hoặc không nhận được response từ target — khác với 504 (timeout).
idle_timeout.timeout_secondsThuộc tính của ALB, kiểm soát thời gian tối đa một kết nối TCP có thể idle. Mặc định 60 giây.
HealthCheckTimeoutSecondsThuộc tính của Target Group, chỉ ảnh hưởng đến health check probe — không liên quan đến request thực của người dùng.
Keep-alive timeoutThời gian ứng dụng duy trì kết nối TCP mở sau khi xử lý xong một request, cho phép tái sử dụng kết nối.
Connection poolingALB tái sử dụng kết nối TCP đến target thay vì tạo kết nối mới cho mỗi request — hiệu quả hơn nhưng nhạy cảm với keep-alive timeout mismatch.

Nhận xét

Bài đăng phổ biến từ blog này

EC2 Không Có Internet Trong Custom VPC: Cách Gắn Internet Gateway và Cập Nhật Route Table

Route 53 Alias vs CNAME: Khi Nào Dùng Alias Record Cho Zone Apex?