Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn vận hành một sàn thương mại điện tử Việt Nam. Ngày thường, hệ thống chạy trên 4 server ứng dụng, mọi thứ êm ru. Rồi đến 0h ngày sale 11/11, lượng người dùng tăng gấp 20 lần chỉ trong 3 phút. Nếu tất cả traffic đó đổ dồn vào một server duy nhất, server đó "gục" ngay lập tức, và trải nghiệm khách hàng là màn hình trắng đúng lúc họ chuẩn bị bấm "Đặt hàng". Câu hỏi sống còn là: làm sao để phân phối lượng request khổng lồ đó đều ra nhiều server, để không server nào bị quá tải?
Đó chính là công việc của load balancer (bộ cân bằng tải) — thành phần đứng giữa client và các server backend, nhận request và quyết định gửi nó tới server nào. Nghe thì đơn giản, nhưng đằng sau là hàng loạt quyết định kiến trúc quan trọng: cân bằng ở tầng nào (L4 hay L7)? Dùng thuật toán phân phối nào? Làm sao biết server nào đang "khỏe"? Chọn sai, bạn có thể gặp tình trạng một server ngập request trong khi server bên cạnh ngồi chơi, hoặc mất session người dùng giữa chừng, hoặc phơi lỗ hổng bảo mật.
Load balancing là nền tảng của gần như mọi hệ thống có khả năng mở rộng (scale). Nó liên quan trực tiếp tới High Availability (bài 28), auto-scaling (bài 30), và latency optimization (bài 44). Bài này giúp bạn nắm vững sự khác biệt giữa hai tầng cân bằng tải L4 và L7, hiểu rõ các thuật toán phân phối phổ biến, và biết chọn đúng cho từng tình huống thực tế.
Khái niệm cốt lõi
Load balancer làm gì?
Về bản chất, load balancer nhận traffic đến từ nhiều client và phân phối ra một nhóm server backend (thường gọi là backend pool hoặc target group). Nó mang lại bốn giá trị chính:
- Phân tán tải — không server nào bị dồn quá nhiều request.
- Tính sẵn sàng cao — nếu một server chết, load balancer ngừng gửi traffic tới nó và chuyển sang server khỏe.
- Khả năng mở rộng — muốn tăng năng lực, chỉ cần thêm server vào pool.
- Điểm kiểm soát tập trung — nơi lý tưởng để cắt SSL, giới hạn tốc độ (rate limit), ghi log.
L4 — cân bằng tải ở tầng Transport
L4 (layer 4, tầng Transport) làm việc với TCP/UDP, tức là dựa trên IP nguồn, IP đích, port nguồn, port đích. Nó không nhìn vào nội dung của gói tin — không biết bạn đang gọi /api/orders hay /images/logo.png, không biết header HTTP, không biết cookie.
Cách hoạt động điển hình: khi một kết nối TCP đến, L4 load balancer chọn một backend server rồi "khâu" (stitch) luồng dữ liệu giữa client và server đó suốt vòng đời của kết nối. Vì chỉ chuyển tiếp gói tin dựa trên thông tin header TCP/IP, nó cực nhanh, độ trễ thêm vào rất thấp, và tốn ít CPU. Nó cũng trung lập với giao thức — bạn có thể cân bằng tải cho bất kỳ thứ gì chạy trên TCP/UDP: HTTP, gRPC, MQTT, database connection, game server...
Ví dụ dịch vụ: AWS Network Load Balancer (NLB), Google Cloud Network Load Balancer, hoặc chế độ mode tcp của HAProxy.
Điểm mạnh: nhanh, đơn giản, throughput cực cao (NLB xử lý được hàng triệu request/giây), giữ nguyên IP nguồn của client dễ dàng.
Điểm yếu: vì không hiểu nội dung, nó không thể routing theo đường dẫn URL, không cắt SSL để đọc nội dung, không thể chèn header, không hiểu được khái niệm "một request HTTP".
L7 — cân bằng tải ở tầng Application
L7 (layer 7, tầng Application) hiểu giao thức ứng dụng, điển hình là HTTP/HTTPS. Nó đọc được URL path, HTTP header, method, cookie, hostname. Nhờ vậy nó có thể ra quyết định routing thông minh hơn nhiều:
- Gửi request
/api/tới nhóm server API,/static/tới nhóm server phục vụ file tĩnh. - Routing theo hostname:
shop.example.vntới một pool,blog.example.vntới pool khác (host-based routing). - Cắt SSL (SSL termination) — giải mã HTTPS tại load balancer, backend chỉ cần xử lý HTTP thuần.
- Chèn/sửa header (ví dụ
X-Forwarded-Forđể backend biết IP thật của client). - Sticky session dựa trên cookie, nén response, rate limit theo từng route.
Ví dụ dịch vụ: AWS Application Load Balancer (ALB), Nginx, HAProxy mode http, Envoy, Traefik.
Điểm mạnh: routing linh hoạt theo nội dung, nhiều tính năng ứng dụng.
Điểm yếu: chậm hơn L4 (phải parse HTTP), tốn CPU hơn (nhất là khi cắt SSL), phức tạp hơn để cấu hình.
So sánh nhanh L4 vs L7
| Tiêu chí | L4 (Transport) | L7 (Application) |
|---|---|---|
| Nhìn vào nội dung | Không | Có (URL, header, cookie) |
| Tốc độ / độ trễ | Rất nhanh, độ trễ thấp | Chậm hơn, tốn CPU hơn |
| Routing theo path/host | Không | Có |
| SSL termination | Không (chỉ passthrough) | Có |
| Giao thức | Mọi thứ trên TCP/UDP | Chủ yếu HTTP/HTTPS, gRPC |
| Ví dụ | AWS NLB, HAProxy TCP | AWS ALB, Nginx, Envoy |
Các thuật toán phân phối (load balancing algorithms)
Sau khi chọn tầng, câu hỏi tiếp theo là: khi có request đến, load balancer chọn server nào? Đây là vai trò của thuật toán cân bằng tải.
- Round Robin — lần lượt xoay vòng: request 1 → server A, request 2 → server B, request 3 → server C, rồi quay lại A. Đơn giản, công bằng nếu các server có năng lực như nhau và các request tốn tài nguyên tương đương.
- Weighted Round Robin — round robin có trọng số. Nếu server A mạnh gấp đôi server B, gán trọng số A=2, B=1, thì A nhận gấp đôi lượng request. Hữu ích khi cụm server không đồng nhất (một số máy mới, một số máy cũ).
- Least Connections — gửi request tới server đang có ít kết nối đang mở nhất. Tốt hơn round robin khi thời gian xử lý mỗi request chênh lệch lớn (ví dụ một số request là upload file nặng, một số là API nhẹ). Round robin không biết server nào đang "kẹt", còn least connections thì có.
- Least Response Time — gửi tới server có thời gian phản hồi trung bình thấp nhất kết hợp với ít kết nối. Nhạy hơn với sức khỏe thực tế của server.
- IP Hash / Consistent Hashing — băm (hash) IP nguồn (hoặc một khóa nào đó) để quyết định server. Cùng một client sẽ luôn tới cùng một server, tạo ra sticky session ở mức mạng. Consistent hashing đặc biệt quan trọng cho hệ thống cache: khi thêm/bớt server, chỉ một phần nhỏ khóa bị ánh xạ lại thay vì toàn bộ.
/healthz mỗi 5 giây). Nếu server không trả lời hoặc trả mã lỗi liên tiếp vài lần, nó bị đánh dấu "unhealthy" và bị loại khỏi pool cho tới khi khỏe lại. Đây là cơ chế giúp load balancer đóng góp vào tính sẵn sàng cao.Tình huống thực tế
Tình huống 1 — Sàn TMĐT ngày sale: chọn sai thuật toán gây quá tải cục bộ
Một sàn thương mại điện tử tại TP.HCM chạy 8 server ứng dụng sau một AWS ALB (L7), dùng thuật toán mặc định round robin. Ngày thường ổn. Nhưng trong đợt flash sale, họ phát hiện một hiện tượng lạ: 2 trong 8 server thường xuyên bị CPU 95%, trong khi 6 server còn lại chỉ 40%.
Nguyên nhân: một số request là "nặng" (kết xuất trang danh mục có nhiều bộ lọc, tính toán tồn kho thời gian thực), một số "nhẹ" (gọi API lấy giỏ hàng). Round robin phân phối mù quáng theo lượt, không biết server nào đang xử lý request nặng. Kết quả là ngẫu nhiên có server bị dồn nhiều request nặng liền nhau và nghẽn.
Giải pháp: đội vận hành chuyển sang least outstanding requests (biến thể của least connections trên ALB). Load balancer bắt đầu ưu tiên gửi request mới tới server đang xử lý ít request nhất. Sau khi đổi, chênh lệch CPU giữa các server giảm từ khoảng cách 55% xuống dưới 15%, và p99 latency giảm khoảng 30%.
Bài học: round robin chỉ tối ưu khi các request đồng đều về chi phí. Khi thời gian xử lý biến thiên lớn, least connections / least outstanding requests thường tốt hơn nhiều. Đừng để mặc định.
Tình huống 2 — Startup fintech: mất session vì sticky session không đúng
Một startup fintech ở Hà Nội triển khai dịch vụ với phiên đăng nhập lưu trong bộ nhớ (in-memory) trên từng server. Họ đặt một Nginx (L7) phía trước 3 server, round robin. Người dùng phàn nàn: cứ vài phút lại bị đăng xuất ngẫu nhiên.
Nguyên nhân: vì session lưu trong RAM của từng server, khi round robin gửi request tiếp theo của cùng một người dùng sang server khác, server đó không có session → coi như chưa đăng nhập → đá ra.
Có hai hướng xử lý, và đội đã cân nhắc cả hai:
- Sticky session — cấu hình Nginx dùng
ip_hashhoặc cookie-based affinity để cùng một người dùng luôn tới cùng một server. Nhanh, nhưng mong manh: nếu server đó chết, tất cả session trên nó mất hết, và tải có thể lệch khi nhiều người dùng cùng ra IP (qua NAT của một công ty).
- Stateless + session store dùng chung — đưa session ra ngoài, lưu vào Redis. Lúc này server nào cũng đọc được session, không cần sticky, load balancer tự do phân phối. Đây là cách bền vững hơn.
Bài học: sticky session là "băng dán" tiện lợi nhưng che giấu vấn đề gốc là kiến trúc có trạng thái (stateful). Nếu có thể, hãy làm ứng dụng stateless và đưa trạng thái ra store dùng chung. Sticky chỉ nên là giải pháp tạm hoặc cho trường hợp bất khả kháng.
Tình huống 3 — Nền tảng game / IoT: vì sao chọn L4 thay vì L7
Một công ty game mobile tại Singapore phục vụ hàng triệu kết nối realtime qua giao thức TCP tùy chỉnh (không phải HTTP) cho gameplay. Ban đầu họ định dùng một L7 proxy vì "quen thuộc". Nhưng L7 không hiểu giao thức game của họ, và việc terminate mọi kết nối tại proxy tạo độ trễ thêm không chấp nhận được với game realtime, đồng thời proxy trở thành nút thắt CPU khi số kết nối đồng thời lên hàng triệu.
Họ chuyển sang AWS NLB (L4). NLB chỉ chuyển tiếp gói TCP dựa trên IP:port, giữ nguyên IP nguồn của người chơi (quan trọng cho chống gian lận và định vị khu vực), độ trễ thêm gần như bằng 0, và mở rộng tới hàng triệu kết nối đồng thời mà không nghẽn. Với các endpoint HTTP quản trị (dashboard, API mua vật phẩm), họ vẫn dùng một ALB (L7) riêng để tận dụng routing theo path và SSL termination.
Bài học: không có tầng nào "tốt hơn" tuyệt đối. Traffic realtime, non-HTTP, cần độ trễ tối thiểu → L4. Traffic HTTP cần routing thông minh → L7. Kiến trúc trưởng thành thường dùng cả hai, mỗi loại đúng chỗ.
Hướng dẫn từng bước
Đây là quy trình chọn và cấu hình load balancer cho một dịch vụ mới:
- Xác định loại traffic. Là HTTP/HTTPS hay giao thức khác (TCP thô, gRPC, WebSocket, UDP)? Nếu HTTP và cần routing theo path/host, nghiêng về L7. Nếu non-HTTP hoặc cần throughput/độ trễ cực tốt, nghiêng về L4.
- Xác định nhu cầu routing. Bạn có cần gửi
/apivà/statictới các pool khác nhau? Có nhiều hostname trên cùng một IP? Nếu có, bắt buộc L7.
- Quyết định nơi cắt SSL. Cắt tại L7 load balancer (đơn giản cho backend) hay passthrough xuống backend (end-to-end encryption cho yêu cầu bảo mật/tuân thủ nghiêm ngặt)? Passthrough thường đi với L4.
- Chọn thuật toán. Bắt đầu với least connections nếu thời gian xử lý request biến thiên; round robin nếu request đồng đều; weighted nếu server không đồng nhất; consistent hashing nếu là tầng cache.
- Cấu hình health check. Tạo endpoint
/healthznhẹ, kiểm tra được các phụ thuộc quan trọng (DB, cache). Đặt interval hợp lý (ví dụ 5–10 giây), số lần lỗi liên tiếp để loại (ví dụ 3), và số lần khỏe liên tiếp để đưa lại (ví dụ 2).
- Xử lý trạng thái (session). Ưu tiên stateless + session store dùng chung. Chỉ dùng sticky session khi bắt buộc.
- Cấu hình timeout và retry (nếu L7). Đặt idle timeout, connection timeout hợp lý; bật retry cho các request idempotent để che lỗi tạm thời của backend.
- Quan sát và tinh chỉnh. Theo dõi phân bố tải giữa các server, p99 latency, tỉ lệ 5xx. Điều chỉnh thuật toán/trọng số dựa trên dữ liệu thực tế, không phải phỏng đoán.
Lỗi thường gặp & mẹo
- Lỗi: để round robin mặc định cho traffic không đồng đều. Kết quả là tải lệch, một số server nghẽn. Mẹo: nếu chi phí request biến thiên, ưu tiên least connections / least outstanding requests.
- Lỗi: lạm dụng sticky session để che giấu kiến trúc stateful. Khi server chết, session mất hàng loạt và tải lệch. Mẹo: làm ứng dụng stateless, đẩy session vào Redis/store dùng chung.
- Lỗi: health check quá "nông" (chỉ kiểm tra port mở). Server có thể trả lời TCP nhưng ứng dụng bên trong đã hỏng (mất kết nối DB). Mẹo: health check nên gọi tới ứng dụng thật và kiểm tra phụ thuộc quan trọng — nhưng đừng làm nó quá nặng kẻo tự gây tải.
- Lỗi: quên lấy IP thật của client. Ở L7 khi cắt SSL, backend chỉ thấy IP của load balancer. Mẹo: cấu hình để LB gắn
X-Forwarded-Forvà backend đọc header này (cẩn thận với việc giả mạo header từ bên ngoài).
- Lỗi: đặt idle timeout không khớp giữa LB và backend. Nếu LB đóng kết nối sớm hơn backend hoặc ngược lại, sẽ có lỗi 502/504 khó hiểu. Mẹo: đặt idle timeout của backend lớn hơn của LB một chút.
- Mẹo: chống dồn tải khi server vừa hồi phục. Khi một server unhealthy trở lại, đừng đổ toàn bộ tải vào ngay. Dùng cơ chế "slow start" để tăng dần lượng traffic, tránh làm nó "chết" lần nữa.
- Mẹo: xử lý graceful shutdown. Khi gỡ server khỏi pool (deploy, scale-in), hãy cho nó "drain" — ngừng nhận request mới nhưng hoàn tất các request đang xử lý — trước khi tắt, để không cắt ngang người dùng.
Bài tập thực hành
- Phân loại tình huống. Với mỗi hệ thống sau, chọn L4 hay L7 và giải thích: (a) một API HTTP cần gửi
/v1/và/v2/tới hai pool khác nhau; (b) một broker MQTT cho 500.000 thiết bị IoT; (c) một cụm cache Redis cần cùng key luôn tới cùng node.
- Chọn thuật toán. Một dịch vụ có 5 server, trong đó 2 server dùng cấu hình mạnh gấp đôi. Request có thời gian xử lý khá đồng đều. Bạn chọn thuật toán nào và cấu hình trọng số ra sao?
- Thiết kế health check. Viết mô tả (bằng lời) cho một endpoint
/healthzcủa dịch vụ đặt hàng: nó nên kiểm tra những phụ thuộc nào, trả về mã gì khi khỏe/không khỏe, và interval bao nhiêu là hợp lý? Giải thích vì sao không nên kiểm tra quá nhiều phụ thuộc.
- Gỡ lỗi. Người dùng bị đăng xuất ngẫu nhiên sau khi đội bạn thêm server thứ 4 vào pool round robin. Hãy nêu hai nguyên nhân có thể và cách khắc phục bền vững cho mỗi nguyên nhân.
- Thực hành (nếu có môi trường). Dựng thử một Nginx đứng trước 2 backend đơn giản (ví dụ 2 container in ra hostname của mình). Thử lần lượt
round_robin,least_conn,ip_hash; quan sát cách phân phối khác nhau khi bạn gửi nhiều request.
Tóm tắt
- Load balancer phân phối traffic ra nhiều server để tăng khả năng mở rộng và tính sẵn sàng.
- L4 (Transport) routing theo IP:port, không nhìn nội dung — nhanh, độ trễ thấp, trung lập giao thức (ví dụ AWS NLB). Chọn khi cần throughput cao, độ trễ tối thiểu, hoặc traffic non-HTTP.
- L7 (Application) hiểu HTTP — routing theo path/host, cắt SSL, chèn header, sticky cookie (ví dụ AWS ALB, Nginx, Envoy). Chọn khi cần routing thông minh theo nội dung. Đánh đổi: tốn CPU và độ trễ hơn.
- Thuật toán quan trọng: round robin (request đồng đều), weighted (server không đồng nhất), least connections (thời gian xử lý biến thiên), consistent hashing (tầng cache).
- Health check giúp loại bỏ server hỏng khỏi pool — hãy làm health check "sâu" vừa đủ và cẩn thận với graceful drain, slow start.
- Ưu tiên stateless + session store dùng chung thay vì lạm dụng sticky session.
- Kiến trúc trưởng thành thường kết hợp cả L4 và L7, mỗi tầng đúng chỗ. Chọn dựa trên loại traffic và nhu cầu routing, rồi tinh chỉnh bằng dữ liệu quan sát thực tế.