Load balancer L4/L7, NAT và VNet: order-service chạy 2 instance
1 VM chết là sập hết. Giờ muốn chạy 2 instance order-service thì làm sao?
Tới giờ ta có: domain api.shop.example, HTTPS, một Azure VM chạy order-service, Postgres ở VM khác. Giờ nâng lên 2 instance sau load balancer, đặt trong mạng riêng cho đúng.
1. Bài toán
Internet ──► VM1 (public IP) : order-service :8080 ──► VM-DB (public IP?) :5432
Black Friday, VM1 quá tải. Tuần sau Azure bảo trì host, VM1 reboot, API chết 5 phút.
2. Vấn đề
- Single point of failure, không scale ngang được.
- Thêm VM2 thì client gọi IP nào? DNS round-robin không biết VM nào đang chết.
- Postgres có public IP là rủi ro bảo mật lớn.
3. Giải pháp: Load balancer + mạng riêng (VNet/VPC)
Load balancer nhận traffic ở một địa chỉ, chia cho nhiều backend, và bỏ backend không khỏe ra.
| L4 (Transport) | L7 (Application) | |
|---|---|---|
| Nhìn thấy | IP, port, TCP/UDP | HTTP: host, path, header, cookie |
| Routing | Theo 5-tuple (hash) | /orders/** → pool A, /images → pool B |
| TLS | Pass-through | Terminate / re-encrypt, WAF |
| Hiệu năng | Rất nhanh, ít overhead | Tốn hơn vì parse HTTP |
| Ví dụ | Azure Load Balancer, AWS NLB | Azure Application Gateway, AWS ALB, Nginx, HAProxy |
VNet (Azure) / VPC (AWS): mạng ảo riêng, dải IP private (RFC 1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), chia subnet, mỗi subnet có NSG/security group.
VNet shop-vnet 10.0.0.0/16
├── snet-edge 10.0.1.0/24 Application Gateway / LB (có public IP)
├── snet-app 10.0.2.0/24 vm-order-1 10.0.2.4, vm-order-2 10.0.2.5 (KHÔNG public IP)
└── snet-db 10.0.3.0/24 vm-postgres 10.0.3.4 (KHÔNG public IP)
4. Code từng bước
Bước 1: Health endpoint cho probe (Actuator)
management:
endpoints.web.exposure.include: health
endpoint.health.probes.enabled: true # /actuator/health/readiness, /liveness
server:
shutdown: graceful # đang xử lý dở thì làm xong rồi mới tắt
spring:
lifecycle.timeout-per-shutdown-phase: 20s
datasource:
url: jdbc:postgresql://10.0.3.4:5432/orders # private IP
username: ${DB_USER}
password: ${DB_PASSWORD}
Bước 2: Tạo NSG chỉ cho phép đúng luồng (Azure CLI)
# App subnet: chỉ nhận 8080 từ subnet edge
az network nsg rule create -g shop-rg --nsg-name nsg-app -n allow-edge-8080 \
--priority 100 --source-address-prefixes 10.0.1.0/24 \
--destination-port-ranges 8080 --access Allow --protocol Tcp
# DB subnet: chỉ nhận 5432 từ subnet app
az network nsg rule create -g shop-rg --nsg-name nsg-db -n allow-app-5432 \
--priority 100 --source-address-prefixes 10.0.2.0/24 \
--destination-port-ranges 5432 --access Allow --protocol Tcp
Bước 3: Đọc IP thật của client. Sau LB/Nginx, request.getRemoteAddr() là IP của proxy. Proxy thêm header:
X-Forwarded-For: 203.0.113.7, 10.0.1.5 # client gốc, rồi các proxy
X-Forwarded-Proto: https
server:
forward-headers-strategy: native
tomcat:
remoteip:
internal-proxies: 10\\.0\\.1\\.\\d{1,3} # chỉ tin XFF từ subnet edge
@GetMapping("/whoami")
public String whoami(HttpServletRequest req) {
return req.getRemoteAddr(); // đã là IP client thật nhờ RemoteIpValve
}
Bước 4: Test
for i in 1 2 3 4; do curl -s https://api.shop.example/actuator/info | grep host; done
# host: vm-order-1 / vm-order-2 xen kẽ
sudo systemctl stop order-service # trên vm-order-1
# sau vài lần probe fail, mọi request đi vm-order-2, không lỗi
5. Cách hoạt động
Client 203.0.113.7
│ HTTPS api.shop.example → public IP của App Gateway
▼
┌──────────── snet-edge ────────────┐
│ Application Gateway (L7, TLS) │ probe GET /actuator/health/readiness mỗi 5s
└───────┬────────────────┬──────────┘
│ HTTP + XFF │
▼ ▼
vm-order-1 10.0.2.4 vm-order-2 10.0.2.5 (snet-app, không public IP)
└───────┬────────┘
▼ JDBC 10.0.3.4:5432
vm-postgres (snet-db)
Outbound (app gọi API thanh toán bên ngoài):
10.0.2.4 ──► NAT Gateway (public IP 20.x.x.x) ──► Internet
đổi src IP private → public, nhớ bảng mapping để trả về
NAT (Network Address Translation): IP private không đi được trên Internet. NAT đổi địa chỉ nguồn sang public IP (SNAT, ra ngoài) hoặc đổi đích từ public sang private (DNAT, LB/port forward vào trong). Với NAT gateway, mọi VM đi ra bằng một public IP cố định, tiện cho đối tác whitelist IP, và VM vẫn không nhận được kết nối từ ngoài vào.
6. So sánh: public IP vs private IP
| Public IP | Private IP | |
|---|---|---|
| Định tuyến trên Internet | Có | Không |
| Dùng cho | LB, App Gateway, NAT gateway, bastion | App VM, DB, cache |
| Rủi ro | Bị scan, brute-force | Chỉ vào được từ trong VNet / VPN / peering |
7. Khi nào nên dùng / không nên dùng
- L7 khi cần route theo path/host, TLS termination, WAF. L4 cho TCP thuần (Postgres, gRPC pass-through) hoặc cần cực nhanh.
- Không đặt public IP cho DB. SSH vào qua Azure Bastion hoặc VPN.
- Chỉ tin
X-Forwarded-Fortừ proxy của mình; client có thể tự gửi header giả.
Chạy 2 instance xong admin cứ bị logout. Bật sticky session là xong đúng không?
Bật sticky thì hết lỗi, nhưng đó là chữa triệu chứng. Nguyên nhân: session (bài 9.6) nằm trong memory của vm-order-1, request sau rơi vào vm-order-2 không có session đó.
Sticky session (cookie affinity):
Client ──► LB ──(cookie ApplicationGatewayAffinity)──► luôn vm-order-1
Nhược điểm của sticky:
- vm-order-1 chết → mọi user dính vào nó mất session.
- Tải lệch: user nặng dồn vào một máy.
- Deploy rolling, scale-in đều làm user bị logout.
Cách tốt hơn: stateless. Mọi instance giống hệt nhau, state để ở ngoài.
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
spring:
data.redis.host: 10.0.2.10
session.timeout: 30m
Hoặc với API cho mobile: dùng JWT, server không giữ session. Khi đó LB chọn instance nào cũng được, scale và failover thoải mái.
Health check fail thì LB làm gì? Liveness với readiness khác gì?
Đúng hướng rồi. LB gọi probe định kỳ; fail liên tiếp N lần (unhealthy threshold) thì ngừng gửi traffic mới tới instance đó, khỏe lại thì đưa vào lại. LB không restart VM.
| Probe | Hỏi | Fail thì |
|---|---|---|
| Readiness | Sẵn sàng nhận traffic chưa? | LB / K8s ngừng gửi request |
| Liveness | Process còn sống không, có bị treo? | Orchestrator restart (K8s, VMSS repair) |
Lưu ý: đừng cho readiness phụ thuộc cứng vào service bên ngoài (ví dụ payment API). Payment chậm sẽ làm cả 2 instance bị gỡ khỏi LB, sập toàn bộ.
Phỏng vấn hỏi về load balancer, NAT thì trả lời sao?
Q1. Load balancer L4 và L7 khác nhau thế nào?
🇻🇳 Tiếng Việt:
L4 làm việc ở tầng TCP/UDP, chỉ nhìn IP và port nên rất nhanh nhưng không hiểu HTTP. L7 parse HTTP nên route theo path, host, header, terminate TLS và gắn WAF được, đổi lại tốn tài nguyên hơn. Trên Azure: Load Balancer là L4, Application Gateway là L7.
🇬🇧 English:
An L4 load balancer works at the TCP/UDP level and only sees IPs and ports, so it is very fast but HTTP-unaware. An L7 load balancer parses HTTP, so it can route by path, host or header, terminate TLS and run a WAF, at a higher cost. On Azure, Load Balancer is L4 and Application Gateway is L7.
Q2. Sticky session có nên dùng không?
🇻🇳 Tiếng Việt:
Chỉ nên là giải pháp tạm. Nó làm tải lệch và mất session khi instance chết hoặc deploy. Em thiết kế service stateless, session để ở Redis qua Spring Session hoặc dùng token như JWT.
🇬🇧 English:
Only as a stopgap. It causes uneven load and loses sessions when an instance dies or is redeployed. I keep services stateless and store sessions in Redis via Spring Session, or use tokens such as JWT.
Q3. Sau load balancer làm sao lấy IP thật của client?
🇻🇳 Tiếng Việt:
Proxy thêm X-Forwarded-For và X-Forwarded-Proto. Trong Spring Boot em bật forward-headers-strategy và cấu hình internal-proxies để chỉ tin header từ proxy của mình, tránh client giả mạo IP.
🇬🇧 English:
The proxy adds X-Forwarded-For and X-Forwarded-Proto. In Spring Boot I enable forward-headers-strategy and restrict trusted internal proxies, so clients cannot spoof their IP by sending the header themselves.
Q4. NAT gateway để làm gì?
🇻🇳 Tiếng Việt:
Cho các VM chỉ có IP private đi ra Internet bằng một public IP cố định, mà không cho ai từ ngoài kết nối vào. Đối tác có thể whitelist đúng IP đó.
🇬🇧 English:
It lets VMs with only private IPs reach the Internet through a fixed public IP while blocking inbound connections. Partners can whitelist that single IP.
Cách nhớ ngắn gọn
- L4 = IP/port, nhanh; L7 = HTTP, thông minh.
- Health probe fail → LB gỡ instance khỏi pool.
- Stateless > sticky session.
- XFF chỉ tin khi đến từ proxy của mình.
- Public IP chỉ ở edge; app và DB ở subnet private; ra ngoài qua NAT gateway.
Tự kiểm tra
Muốn route /orders và /payments tới 2 pool khác nhau thì cần L4 hay L7?
L7, vì phải đọc path HTTP.
Sau LB, getRemoteAddr() trả về gì nếu không cấu hình gì?
IP của LB/proxy, không phải client.
Vì sao readiness không nên kiểm tra API bên ngoài?
API ngoài chậm sẽ làm mọi instance cùng bị gỡ khỏi LB, gây sập toàn bộ.
SNAT và DNAT khác nhau thế nào?
SNAT đổi địa chỉ nguồn (đi ra, ví dụ NAT gateway). DNAT đổi địa chỉ đích (đi vào, ví dụ LB/port forward).
Postgres VM nên đặt thế nào trong VNet?
Subnet riêng, không public IP, NSG chỉ cho 5432 từ subnet app.