Load balancer — L4 vs L7, thuật toán phân tải
Vì sao cần load balancer, L4 (TCP) vs L7 (HTTP), thuật toán round-robin/least-conn/IP hash, health check, sticky session và vì sao session cần external store.
TL;DR: Load balancer (LB) phân phối request đến nhiều server backend, giúp scale ngang, tăng tính sẵn sàng, và hỗ trợ rolling deploy không downtime. Có hai tầng: L4 hoạt động ở tầng Transport (TCP/IP) — nhanh, không đọc nội dung HTTP; L7 hoạt động ở tầng Application (HTTP) — chậm hơn nhưng định tuyến thông minh theo URL/header/cookie. Nginx và HAProxy làm được cả hai; cloud LB (AWS ALB/NLB, GCP) phân chia rõ ràng hơn. Bài này giải thích khi nào dùng tầng nào, thuật toán phân tải nào, và pitfall lớn nhất: session state không được chia sẻ tự động giữa các instance.
Ở bài 08 — Reverse proxy với Nginx bạn đã thấy upstream block với nhiều server — đó chính là L7 load balancing đơn giản nhất. Bài này phóng to toàn bộ bức tranh.
1. Vì sao cần load balancer
Một ứng dụng web chạy trên một server đơn có ba giới hạn cứng:
- Giới hạn throughput: một process/server chỉ xử lý được hữu hạn request/giây. Khi traffic tăng gấp đôi, không thể "tăng tốc" server đơn lên gấp đôi mãi được (vertical scaling có trần).
- Single point of failure (SPOF): server đơn chết → toàn bộ dịch vụ chết.
- Deploy tốn downtime: restart app để deploy code mới → user thấy 502 trong vài giây.
Load balancer giải quyết cả ba bằng cách đứng trước một pool nhiều server backend:
| Bài toán | Giải pháp với LB |
|---|---|
| Throughput giới hạn | Thêm server mới vào pool (scale ngang / horizontal scaling) |
| SPOF | LB detect server chết → không route vào đó nữa (health check + failover) |
| Deploy downtime | Rolling deploy: tắt từng server, deploy, bật lại — LB route vòng qua server còn lại |
LB phân phối request — thêm/bớt server mà client không hay biết
2. L4 vs L7 — hai tầng, hai triết lý
L4 và L7 là số tầng trong mô hình OSI: L4 là tầng Transport (TCP/UDP), L7 là tầng Application (HTTP). Bạn đã học ánh xạ OSI ↔ TCP/IP ở khoá Foundations bài 02.
L4 load balancer — định tuyến ở tầng Transport
L4 LB hoạt động ở mức IP + port + TCP. Nó nhìn vào IP đích, port, và các flag TCP — nhưng không đọc nội dung HTTP bên trong. Khi nhận một TCP connection, nó forward toàn bộ byte stream đó sang một server backend.
- Nhanh: không cần parse HTTP, chỉ xử lý header IP/TCP nhỏ.
- TLS passthrough: gói TLS đi qua nguyên vẹn, backend tự decrypt — LB không thấy nội dung HTTP.
- Không thể định tuyến theo URL/header: tất cả traffic tới port 443 đi vào một pool duy nhất.
- Ứng dụng: database connection pooling (PostgreSQL/MySQL), raw TCP service, game server.
L7 load balancer — định tuyến ở tầng Application
L7 LB đọc được HTTP (URL, header, cookie, method). Nó parse request, quyết định backend, rồi tạo một kết nối HTTP mới tới backend đó — đây là lý do L7 cũng là reverse proxy.
- Định tuyến thông minh:
/api/*vào service A,/static/*vào service B,User-Agent: mobilevào server mobile. - TLS termination: LB decrypt HTTPS — backend nhận HTTP thuần (xem bài 08).
- Chậm hơn L4: phải parse HTTP trước mỗi request.
- Ứng dụng: web app, REST API, microservice routing — đây là default trong hầu hết stack.
3. Thuật toán phân tải
Sau khi LB quyết định pool backend, nó cần chọn server nào nhận request tiếp theo. Các thuật toán phổ biến:
Round-robin (mặc định)
Phân request lần lượt: request 1 → server A, request 2 → server B, request 3 → server C, request 4 → server A, ...
Đơn giản, phân đều về số lượng request. Phù hợp khi các request có thời gian xử lý tương đương và server đồng nhau.
upstream backend_pool {
server 10.0.0.1:8080; # round-robin mac dinh
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
Weighted round-robin
Server mạnh hơn nhận nhiều request hơn tương ứng với trọng số. Server A có weight=3 nhận 3 request trên mỗi 4 request của chu kỳ.
upstream backend_pool {
server 10.0.0.1:8080 weight=3; # may manh hon, nhan 3x
server 10.0.0.2:8080 weight=1;
}
Least connections
LB chọn server đang có ít connection nhất tại thời điểm đó. Hiệu quả khi request có thời gian xử lý khác nhau nhiều — server đang bận (nhiều kết nối mở) sẽ không nhận thêm cho tới khi bớt tải.
upstream backend_pool {
least_conn;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
IP hash / Consistent hashing
Hash IP (hoặc một thuộc tính khác) của client để luôn route request từ cùng client tới cùng server. Đây là nền tảng của sticky session theo IP.
upstream backend_pool {
ip_hash; # hash theo IP client
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
Consistent hashing là phiên bản nâng cao: khi thêm/bớt server, chỉ một phần nhỏ client bị "xáo" sang server mới, thay vì rehash toàn bộ. Phổ biến trong cache layer (Memcached cluster, CDN).
4. Health check — phát hiện server chết
LB phải biết server nào còn sống để không route vào đó khi chết. Có hai loại health check:
Active health check: LB định kỳ gửi probe tới backend (ví dụ HTTP GET /health) và kiểm tra response. Nếu server không trả 200 trong một số lần liên tiếp, LB đánh dấu server đó là unhealthy và ngừng route.
Passive health check: LB quan sát response thật của traffic. Nếu một server liên tục trả 5xx hoặc timeout, LB tạm loại nó ra. Nginx dùng cơ chế này với max_fails và fail_timeout.
upstream backend_pool {
server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
# Neu server tra loi 3 lan trong 30s -> danh dau down 30s
}
Failover: khi server A được đánh dấu unhealthy, LB tự động chuyển toàn bộ traffic sang các server còn lại — không cần can thiệp thủ công. Khi server A recover (health check pass trở lại), LB đưa nó vào pool.
sequenceDiagram
participant LB as Load Balancer
participant A as Server A (down)
participant B as Server B (up)
LB->>A: Health check GET /health
A-->>LB: Timeout / 500
Note over LB: max_fails dat -> danh dau A unhealthy
LB->>B: Route request (A bi bo qua)
B-->>LB: 200 OK
LB-->>LB: Dinh ky check A...
LB->>A: Health check GET /health
A-->>LB: 200 OK
Note over LB: A recover -> them lai vao pool5. Sticky session — khi state nằm trong server
Sticky session (hay session affinity) là kỹ thuật đảm bảo tất cả request từ cùng một user luôn được route tới cùng một server backend. LB nhìn vào một giá trị định danh (thường là cookie hoặc IP) để quyết định.
Vì sao cần? Nếu app lưu session state trong bộ nhớ của server (ví dụ Java HttpSession in-memory, Express session in-memory store), request thứ hai từ cùng user mà rơi vào server khác sẽ không tìm thấy session — user bị logout hoặc thấy lỗi.
upstream backend_pool {
ip_hash; # Sticky theo IP -- don gian nhat
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
Nginx Enterprise và HAProxy hỗ trợ sticky theo cookie (chính xác hơn IP hash vì IP có thể chia sẻ qua NAT):
# HAProxy config (sticky cookie)
backend app_servers
balance roundrobin
cookie SERVERID insert indirect nocache
server app1 10.0.0.1:8080 check cookie app1
server app2 10.0.0.2:8080 check cookie app2
Khi nhiều user bị "ghim" vào một server, server đó có thể quá tải trong khi các server khác rảnh. Sticky session là giải pháp tình thế, không phải kiến trúc tốt. Giải pháp đúng là external session store — lưu session trong Redis/Memcached thay vì in-memory server, để mọi server đều đọc được. Khi đó LB có thể dùng round-robin hoặc least-conn mà không cần sticky.
6. Liên hệ các bài khác
- Bài 08 — Reverse proxy với Nginx:
upstreamblock trong Nginx là L7 LB tích hợp — reverse proxy và LB thường là cùng một component. - Bài 03 — Cookie, session & CORS: hiểu session cookie là nền tảng để hiểu vì sao sticky session tồn tại và vì sao external session store (Redis) là giải pháp đúng.
- Khoá Foundations bài 02 — Mô hình TCP/IP 4 tầng: L4/L7 là số tầng OSI ánh xạ vào TCP/IP — Transport và Application.
- Module 3 — TLS & Công cụ Debug: L7 LB làm TLS termination — handshake TLS dừng tại LB, không đi vào backend. Module 3 đào sâu cơ chế TLS.
7. ⚠️ Pitfall — lỗi thường gặp với load balancer
Pitfall 1 — Session state in-memory + round-robin = user bị đăng xuất ngẫu nhiên
Đây là lỗi kinh điển nhất. App lưu session trong HashMap hoặc in-memory store trên mỗi server. LB round-robin gửi request 1 tới server A (tạo session), request 2 tới server B (không có session) → app trả 401 hoặc redirect về login. User báo "tôi bị đăng xuất liên tục mà không làm gì". Fix: dùng Redis/Memcached làm external session store, hoặc chuyển sang stateless JWT (không cần session server-side).
Pitfall 2 — Health check trả 200 nhưng app thật sự bị lỗi
Health check endpoint /health chỉ trả {"status":"ok"} mà không kiểm tra DB connection, cache, queue — app trông healthy nhưng không xử lý được request thật. LB vẫn route vào server đó → user thấy lỗi. Fix: health check sâu hơn — kiểm tra dependency thật (SELECT 1 tới DB, ping Redis).
Pitfall 3 — Sticky session ghim user vào server chết
Server A down, nhưng cookie sticky của user vẫn trỏ vào A. LB cần logic để fallback sang server khác khi server được ghim không available — không phải mọi LB đều xử lý đúng case này. HAProxy có option redispatch; Nginx cần cấu hình thêm.
Pitfall 4 — L4 LB không thể làm TLS termination
L4 LB không đọc HTTP, nên cũng không thể làm TLS termination (vì TLS nằm bên trong TCP). Nếu bạn cần TLS termination + routing theo URL, phải dùng L7 LB. Chọn L4 khi cần passthrough TLS tới backend tự xử lý.
Pitfall 5 — Round-robin với request nặng nhẹ không đều
Round-robin chia đều số request, không chia đều tải. Một request upload file 100MB và một request query nhỏ đều được đếm là "1 request". Server A có thể đang xử lý 5 upload nặng trong khi server B đang nhàn — nhưng LB vẫn chia đều. Fix: dùng least_conn khi request có thời gian xử lý khác biệt lớn.
8. 📚 Deep Dive — tài liệu gốc
- Nginx upstream module —
least_conn,ip_hash,weight,max_fails,fail_timeout. - HAProxy documentation — load balancing algorithms, health checks, sticky session với cookie.
- AWS ALB vs NLB — Application LB (L7) vs Network LB (L4) trong AWS, use case từng loại.
- Google Cloud Load Balancing — global vs regional, HTTP(S) vs TCP/UDP LB.
Ghi chú: HAProxy documentation section 4 (proxies) và 5 (server) là reference đầy đủ nhất về thuật toán và health check options. Đọc khi cần tune production LB.
9. Tóm tắt
- Load balancer phân phối request vào pool backend — giải quyết scale ngang, HA, và rolling deploy.
- L4 LB hoạt động ở tầng Transport: chỉ thấy IP/port/TCP, không đọc HTTP, nhanh, TLS passthrough. Dùng cho database, raw TCP.
- L7 LB hoạt động ở tầng Application: đọc HTTP URL/header/cookie, định tuyến thông minh, TLS termination. Dùng cho web và API — đây là default trong stack hiện đại.
- Thuật toán: round-robin (phân đều request), weighted (theo sức mạnh server), least-conn (chọn server ít tải nhất), IP hash (sticky theo IP).
- Health check phát hiện server chết và loại khỏi pool (active probe hoặc passive observation); failover tự động.
- Sticky session ghim user vào một server — giải pháp tình thế khi session in-memory. Giải pháp đúng: external session store (Redis) để mọi server dùng chung.
- Pitfall lớn nhất: session không chia sẻ giữa instance khi dùng round-robin — biểu hiện là user bị đăng xuất ngẫu nhiên.
10. Tự kiểm tra
Q1L4 và L7 load balancer khác nhau căn bản ở điểm nào? Tại sao L7 đắt hơn về CPU?▸
Q2Khi nào chọn L4, khi nào chọn L7?▸
X-Forwarded-For), hay cần A/B testing theo cookie. Trong hầu hết web app hiện đại, L7 là lựa chọn mặc định vì linh hoạt hơn và chi phí CPU tăng thêm không đáng kể so với lợi ích.Q3Vì sao `least_conn` tốt hơn `round_robin` khi request có thời gian xử lý không đều?▸
least_conn chọn server đang có ít kết nối active nhất tại thời điểm quyết định — server bận (nhiều connection mở) sẽ không nhận thêm cho tới khi xong việc. Kết quả: tải phân phối theo sức chứa thực tế, không theo số đếm request.Q4Sticky session là gì và tại sao nó là giải pháp tình thế thay vì kiến trúc đúng?▸
Q5Passive health check trong Nginx hoạt động thế nào? Khác gì active health check?▸
max_fails lần trong fail_timeout giây, LB đánh dấu server đó là down và không route vào trong thời gian fail_timeout. Không cần gửi probe riêng. Active health check (Nginx Plus, HAProxy): LB định kỳ gửi request tới endpoint kiểm tra (như /health) và kiểm tra response chủ động, không cần đợi traffic thật gây lỗi. Active phát hiện server chết sớm hơn và chính xác hơn (có thể kiểm tra DB connection, cache...), nhưng tốn thêm probe traffic.Q6Tại sao cần health check kiểm tra dependency thật (DB, Redis) thay vì chỉ trả status ok?▸
{"status":"ok"} cứng không phản ánh tình trạng thật. LB vẫn route traffic vào server đó → mọi request thật trả 500 hoặc timeout. Health check sâu (deep health check) thực hiện SELECT 1 tới DB, ping Redis, check queue backlog — nếu bất kỳ dependency nào fail thì trả 503. LB nhận 503 → đánh dấu server unhealthy → không route. User không bị lỗi; team được báo để fix dependency.Bài tiếp theo: Module 2 — Tổng kết & cheat sheet
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên