Cross-Zone Load Balancing trên AWS NLB: Bài Học Từ Triển Khai RabbitMQ Trên EKS
Tác giả: Lê Phan Tấn Lộc — DevOps Engineer
Tags:AWS,NLB,EKS,RabbitMQ,Kubernetes,Load Balancing,Networking
Mở Đầu
Trong một lần triển khai RabbitMQ lên Amazon EKS theo mô hình Kubernetes Operator, tôi gặp phải một lỗi kỳ lạ: kết nối từ ứng dụng ngoài vào RabbitMQ qua AWS Network Load Balancer (NLB) lúc được, lúc không — hoàn toàn không nhất quán. Test port 5672 bằng bash /dev/tcp thì TCP handshake thành công một lần, thất bại lần tiếp theo, rồi lại thành công. Không có lỗi ứng dụng, không có log rõ ràng, NLB target health đều healthy.
Sau khi đào sâu vào tài liệu AWS và kiến trúc mạng, tôi phát hiện ra nguyên nhân gốc rễ: Cross-Zone Load Balancing bị tắt mặc định trên NLB. Bài này là phân tích kỹ thuật về cơ chế đó, tại sao nó gây ra vấn đề trong kiến trúc này, và cách khắc phục.
Kiến Trúc Triển Khai
Trước khi đi vào vấn đề, hãy hiểu bức tranh tổng thể của hệ thống.
Yêu Cầu
- Mỗi project cần một RabbitMQ cluster riêng, độc lập
- Ứng dụng ngoài VPC (qua VPC Peering) cần kết nối vào cổng AMQP 5672
Quyết Định Kiến Trúc
┌─────────────────────────────────────────────────────────────────┐
│ EKS Cluster │
│ (ap-northeast-1) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Namespace: devops │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌────────────────────────┐ │ │
│ │ │ RabbitmqCluster │ │ RabbitmqCluster │ │ │
│ │ │ rabbitmq-1 │ │ rabbitmq-2 │ │ │
│ │ │ (2 replicas) │ │ (2 replicas) │ │ │
│ │ └─────────────────────┘ └────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ RabbitMQ Cluster Operator (controller) │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ ALB │ │ NLB Internal│
│(Layer 7) │ │ (Layer 4) │
│ port 443 │ │ port 5672+ │
└──────────┘ └──────────────┘
│ │
▼ ▼
rabbitmq-1.abc.io App từ VPC Peering
(Management UI) (10.21.0.0/16)
Hai cách expose dịch vụ:
| ALB (Layer 7) | NLB (Layer 4) | |
|---|---|---|
| Dùng cho | Management UI (HTTP 15672) | AMQP protocol (TCP 5672) |
| Tại sao | ALB hiểu HTTP, hỗ trợ host-based routing, sticky sessions | AMQP là TCP thuần — ALB không hỗ trợ TCP tùy ý |
| Cách expose | Ingress (ALB Ingress Controller) | TargetGroupBinding |
Tại Sao Dùng TargetGroupBinding?
Thay vì tạo một Service: LoadBalancer mới (sẽ sinh ra một NLB mới, tốn tiền), chúng tôi dùng TargetGroupBinding — một CRD của AWS Load Balancer Controller cho phép bind một Kubernetes Service vào một Target Group có sẵn của NLB. Mỗi project dùng một port khác nhau trên cùng một NLB:
# TargetGroupBinding cho project 1
apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
name: rabbitmq-tgb
namespace: devops
spec:
serviceRef:
name: rabbitmq-1
port: 5672
targetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:...
targetType: ip
networking:
ingress:
- from:
- ipBlock:
cidr: 10.0.0.0/16 # VPC EKS
ports:
- port: 5672
protocol: TCP
- from:
- ipBlock:
cidr: 10.21.0.0/16 # VPC Peering
ports:
- port: 5672
protocol: TCP
Vấn Đề: Kết Nối Lúc Được, Lúc Không
Mô Tả Triệu Chứng
Khi test từ EC2 trong VPC peering (10.21.x.x) vào NLB internal:
# Lần 1: OK
$ bash -c "echo >/dev/tcp/internal-nlb.amazonaws.com/5672" && echo "OPEN" || echo "CLOSED"
OPEN
# Lần 2: Fail
$ bash -c "echo >/dev/tcp/internal-nlb.amazonaws.com/5672" && echo "OPEN" || echo "CLOSED"
CLOSED
# Lần 3: OK lại
$ bash -c "echo >/dev/tcp/internal-nlb.amazonaws.com/5672" && echo "OPEN" || echo "CLOSED"
OPEN
NLB Target Health: Tất cả targets đều healthy
RabbitMQ pods: Running, không có crash hay restart
Security Group / NACL: Đã verify, rules đều đúng
Vấn đề không nằm ở ứng dụng, mà nằm ở tầng network, cụ thể là cách NLB định tuyến traffic qua các Availability Zone.
Cross-Zone Load Balancing: Cơ Chế Và Hành Vi
NLB Phân Phối Traffic Như Thế Nào?
AWS NLB là Layer 4 load balancer. Khác với ALB, NLB không kiểm tra HTTP headers hay URL — nó chỉ nhìn vào IP và port để quyết định routing.
Khi bạn tạo một NLB, AWS deploy một load balancer node (là một ENI thực tế với IP của AWS) tại mỗi Availability Zone mà NLB được enable. DNS của NLB trả về tất cả các IP này.
NLB DNS: internal-xxxx.elb.ap-northeast-1.amazonaws.com
→ 10.0.1.45 (AZ: ap-northeast-1a — node NLB)
→ 10.0.2.67 (AZ: ap-northeast-1c — node NLB)
→ 10.0.3.89 (AZ: ap-northeast-1d — node NLB)
Client connect tới NLB → DNS resolver trả về một trong các IP trên (round-robin DNS hoặc dựa vào TTL cache) → NLB node nhận request.
Cross-Zone Load Balancing Là Gì?
Đây là thiết lập quyết định NLB node có thể route traffic sang AZ khác không.
Cross-Zone = OFF (mặc định):
Client → NLB node AZ-A → CHỈ forward đến targets trong AZ-A
Client → NLB node AZ-C → CHỈ forward đến targets trong AZ-C
Cross-Zone = ON:
Client → NLB node AZ-A → có thể forward đến targets ở BẤT KỲ AZ nào
Client → NLB node AZ-C → có thể forward đến targets ở BẤT KỲ AZ nào
AWS Documentation: "By default, each Network Load Balancer node distributes traffic across the registered targets in its Availability Zone only."
— AWS NLB Documentation
Vấn Đề Xảy Ra Khi Không Có Target Trong Một AZ
Đây là điểm mấu chốt. Giả sử:
- NLB được enable trên 3 AZs: ap-northeast-1a, 1c, 1d
- RabbitMQ pods chạy trên các nodes thuộc ap-northeast-1a và ap-northeast-1c
- Không có pod nào trong ap-northeast-1d
Với Cross-Zone OFF:
NLB node 1a → healthy targets ✓ → forward OK
NLB node 1c → healthy targets ✓ → forward OK
NLB node 1d → KHÔNG có targets → Connection REFUSED/TIMEOUT
Và AWS DNS phân phối đều cả 3 IP → 1/3 requests fail.
Tại Sao NLB Không Tự Xóa AZ Có Vấn Đề?
Câu hỏi tự nhiên: nếu 1d không có target, NLB không tự remove IP đó khỏi DNS sao?
Câu trả lời: Có — nhưng chỉ khi health check fails, không phải khi không có targets.
Hành vi chính xác:
| Trạng thái AZ | Behavior |
|---|---|
| Có targets, đều healthy | DNS giữ IP, traffic normal |
| Có targets, tất cả unhealthy | DNS xóa IP của AZ đó |
| Không có targets nào | DNS vẫn giữ IP của AZ đó |
| Tất cả AZ đều unhealthy | Fail-open: DNS trả về tất cả IP |
Đây là điểm bẫy quan trọng: NLB không "biết" rằng một AZ trống. Health check chỉ chạy trên registered targets — nếu không có target, không có health check, không có fail signal → DNS không thay đổi.
DNS TTL Và Hiệu Ứng Caching
Dù AZ có vấn đề đã bị xóa khỏi DNS (trường hợp targets unhealthy), vẫn còn một tầng vấn đề: DNS TTL caching.
NLB DNS có TTL = 60 giây:
"The DNS entry also specifies the time-to-live (TTL) of 60 seconds. This helps ensure that the IP addresses can be remapped quickly in response to changing traffic."
— AWS ELB Documentation
Nhưng 60 giây là TTL của DNS record ở phía AWS. Trong thực tế:
-
OS DNS cache — nhiều Linux distros không cache DNS theo TTL, nhưng
nscd,systemd-resolvedthì có - Application DNS cache — JVM nặng về DNS caching (default là 30s, có thể config đến vĩnh viễn)
- NLB internal propagation — khi NLB thay đổi IP set, propagation mất vài chục giây
Timeline khi một AZ bị remove:
T+0s: AZ bị mark unhealthy/empty → NLB starts removing from DNS
T+30s: NLB DNS propagated, nhưng client vẫn dùng cached IP cũ
T+60s: DNS TTL expire → client refresh → IP cũ không còn trong response
T+90s: Tất cả connections mới đi đúng AZ có targets
Trong khoảng T+0s đến T+60s: intermittent failures — đúng như những gì tôi quan sát.
Phân Tích Kiến Trúc Của Chúng Tôi
Quay lại deployment cụ thể. NLB internal được gắn với các private subnet trải đều trên 3 AZ:
NLB internal:
├── ap-northeast-1a → subnet: 10.0.1.0/24 (private)
├── ap-northeast-1c → subnet: 10.0.2.0/24 (private)
└── ap-northeast-1d → subnet: 10.0.3.0/24 (private)
Karpenter chạy RabbitMQ pods trên node-on-demand pool. Pods được schedule vào các node mà Karpenter đang chạy — không đảm bảo phân bố đều 3 AZ. Khi pods chỉ rơi vào 2 trong 3 AZs:
AZ 1a: rabbitmq-pod-0 → target healthy ✓
AZ 1c: rabbitmq-pod-1 → target healthy ✓
AZ 1d: (không có pod) → NLB node 1d không có target → connection refused
Client từ VPC peering (10.21.x.x) resolve DNS → nhận cả 3 IP → đôi khi hit IP của AZ 1d → CLOSED.
Giải Pháp
Option 1: Bật Cross-Zone Load Balancing (Recommended)
Đây là giải pháp đơn giản nhất:
# Bật cross-zone trên NLB
aws elbv2 modify-load-balancer-attributes \
--load-balancer-arn arn:aws:elasticloadbalancing:ap-northeast-1:xxx:loadbalancer/net/... \
--attributes Key=load_balancing.cross_zone.enabled,Value=true
Hoặc trong Terraform:
resource "aws_lb" "rabbitmq_nlb" {
name = "rabbitmq-internal"
internal = true
load_balancer_type = "network"
enable_cross_zone_load_balancing = true # Quan trọng!
subnets = [
aws_subnet.private_1a.id,
aws_subnet.private_1c.id,
aws_subnet.private_1d.id,
]
}
Trade-off: AWS tính phí data transfer cross-AZ (~$0.01/GB). Với traffic AMQP thông thường, con số này nhỏ và đáng để đổi lấy sự ổn định.
Kết quả sau khi bật:
Client → NLB node 1d (không có target local) → cross-zone → forward đến 1a hoặc 1c → OK ✓
Option 2: Giới Hạn NLB Chỉ Dùng AZ Có Pods
Nếu biết trước pods chạy ở AZ nào, chỉ enable NLB subnet cho những AZ đó:
# Chỉ enable 2 subnets thay vì 3
aws elbv2 set-subnets \
--load-balancer-arn arn:aws:elasticloadbalancing:... \
--subnets subnet-1a subnet-1c
Nhược điểm: phải maintain manually, không phù hợp với Karpenter dynamic scheduling.
Option 3: Dùng Pod Topology Spread Constraints
Đảm bảo pods phân bố đúng AZ với NLB subnets. Trong RabbitmqCluster spec:
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: rabbitmq-1
topologyKey: kubernetes.io/hostname
# Kết hợp với topology spread để đảm bảo phân bố đều AZ
Nhưng với Karpenter, cách này vẫn không đảm bảo 100% pods rải đều sang AZ có NLB subnet.
→ Recommendation: Option 1 (bật cross-zone) + Option 3 (anti-affinity) kết hợp.
TargetGroupBinding: Pattern Kết Nối Kubernetes Với NLB
Phần này tôi muốn chia sẻ thêm về TargetGroupBinding — pattern ít được biết đến nhưng rất hữu ích trong môi trường EKS production.
Vấn Đề Với Service Type: LoadBalancer
Cách thông thường để expose TCP service ra ngoài trong Kubernetes là:
service:
type: LoadBalancer
AWS Load Balancer Controller sẽ tự tạo một NLB mới. Nhưng khi bạn có nhiều services cần expose TCP:
- 1 NLB/service = tốn tiền (NLB ~$16/tháng + data transfer)
- Khó quản lý port khi nhiều projects dùng chung
TargetGroupBinding: Dùng Chung NLB, Port Khác Nhau
NLB: internal-rabbitmq.example.com
├── Port 5672 → Target Group 1 → rabbitmq-1 pods
├── Port 5673 → Target Group 2 → rabbitmq-2 pods
├── Port 5674 → Target Group 3 rabbitmq-3 pods
└── Port 5675 → Target Group 4 → rabbitmq-4 pods
Mỗi project có một TargetGroupBinding riêng:
apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
name: rabbitmq-2-tgb
namespace: devops
spec:
serviceRef:
name: rabbitmq-2 # ClusterIP service của RabbitmqCluster
port: 5672
targetGroupARN: arn:aws:elasticloadbalancing:...:targetgroup/rabbitmq-2/xxx
targetType: ip
networking:
ingress:
- from:
- ipBlock:
cidr: 10.21.0.0/16 # VPC peering
ports:
- port: 5673 # Port trên NLB cho project này
protocol: TCP
AWS Load Balancer Controller tự động:
- Discover pods của
rabbitmq-2service - Register pod IPs vào target group ARN tương ứng
- Deregister pods khi chúng terminate
- Cập nhật khi pods scale up/down
Không cần tạo NLB mới. Tiết kiệm chi phí đáng kể cho môi trường nhiều project.
ALB Cho Management UI: Sticky Sessions Là Bắt Buộc
Một bài học khác từ lần triển khai này: RabbitMQ Management UI yêu cầu session affinity.
Tại Sao?
RabbitMQ Management UI là một ứng dụng multi-node. Khi bạn login vào node A, session token được lưu trên node A. Nếu request tiếp theo route đến node B → 401 Unauthorized hoặc 502.
Cấu Hình Sticky Sessions Trên ALB
# Trong Ingress annotations
alb.ingress.kubernetes.io/target-group-attributes: >
stickiness.enabled=true,
stickiness.lb_cookie.duration_seconds=86400
ALB dùng cookie AWSALBTG để pin session về một target cụ thể trong 24 giờ.
Lưu ý: Đây chỉ cần thiết cho Management UI (HTTP). AMQP connections (port 5672) không cần sticky session vì chúng duy trì persistent TCP connection — khi connection đã được establish đến một node, nó sẽ duy trì kết nối đó suốt vòng đời của connection.
Bảng Tóm Tắt: ALB vs NLB Cho RabbitMQ
| Tiêu chí | ALB | NLB |
|---|---|---|
| Layer | 7 (HTTP/HTTPS) | 4 (TCP/UDP) |
| Dùng cho RabbitMQ | Management UI (15672) | AMQP (5672) |
| Sticky Sessions | ✓ Cookie-based | ✓ Source IP-based |
| SSL Termination | ✓ ACM Certificate | ✗ (pass-through) |
| Cross-Zone Default | ✓ Enabled | ✗ Disabled |
| Cost | Cao hơn (LCU) | Thấp hơn cho TCP |
| Latency | ~1ms thêm (HTTP processing) | Cực thấp (Layer 4) |
Checklist Triển Khai RabbitMQ Với NLB Trên EKS
Dựa trên kinh nghiệm thực tế, đây là checklist cần check trước khi go-live:
□ Cross-Zone Load Balancing bật trên NLB
□ TargetGroupBinding dùng targetType: ip (không phải instance)
□ Security Group / NACL cho phép traffic từ nguồn cần thiết
□ PodAntiAffinity: required (không phải preferred) để tránh 2 pods cùng node
□ ALB sticky sessions bật cho Management UI
□ PodDisruptionBudget: minAvailable >= 1 để tránh mất quorum khi drain
□ Persistence: storageClassName phải explicit (không để trống)
□ RabbitMQ additionalConfig: consumer_timeout để tránh timeout cho long-running consumers
□ Test connectivity từ tất cả subnets/VPCs sẽ dùng
Kết Luận
Vấn đề lúc được lúc không trong network thường là triệu chứng của một trong ba nguyên nhân: DNS resolution không đồng nhất, Load Balancing không đều qua AZ, hoặc asymmetric routing trong VPC Peering.
Trong trường hợp này, Cross-Zone Load Balancing mặc định bị tắt trên NLB là root cause. Khi Karpenter schedule pods vào 2 trong 3 AZs mà NLB được enable, 1/3 requests sẽ hit NLB node không có local target → connection refused.
Bài học chính:
- Luôn bật Cross-Zone Load Balancing cho NLB khi workload không đảm bảo phủ đều tất cả AZs — đặc biệt với dynamic schedulers như Karpenter
- TargetGroupBinding là pattern mạnh để share NLB giữa nhiều services, tiết kiệm chi phí trong môi trường multi-project
- ALB sticky sessions là bắt buộc cho RabbitMQ Management UI, không cần thiết cho AMQP
- NLB DNS TTL = 60s — sau khi thay đổi cấu hình NLB, cần đợi DNS propagate trước khi kết luận fix đã có hiệu quả
- PodAntiAffinity: required thay vì preferred để đảm bảo HA thực sự — scheduler đã từng pack cả 2 replicas vào cùng một node trong môi trường thực
Tài Liệu Tham Khảo
- AWS NLB Documentation — Cross-Zone Load Balancing
- AWS ELB — How Elastic Load Balancing Works
- AWS Load Balancer Controller — TargetGroupBinding
- RabbitMQ Cluster Operator — Installation
- RabbitMQ Cluster Operator — GitHub
- AWS Advanced Multi-AZ Resilience Patterns
- Optimizing Data Transfer Costs with AWS NLB
Top comments (0)