TCP, HTTP & Web cho Backend/Load balancer — L4 vs L7, thuật toán phân tải
20/29
Bài 20 / 29~20 phútHTTP & WebMiễn phí lượt xem

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ánGiải pháp với LB
Throughput giới hạnThêm server mới vào pool (scale ngang / horizontal scaling)
SPOFLB detect server chết → không route vào đó nữa (health check + failover)
Deploy downtimeRolling deploy: tắt từng server, deploy, bật lại — LB route vòng qua server còn lại
Client
Client
Load Balancer
Load Balancer
App 1
App 1
App 2
App 2
App 3
App 3

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ý

L4L7 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: mobile và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.
L4 — Transport
Nhìn IP + Port + TCP flags
Không đọc HTTP
TLS passthrough
Cực nhanh, overhead thấp
Dùng cho: DB, raw TCP, game
L7 — Application
Đọc URL, header, cookie
Định tuyến theo content
TLS termination
Chậm hơn L4 một chút
Dùng cho: web, API, microservice

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_failsfail_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 pool

5. 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
⚠️ Sticky session phá cân bằng tải

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

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

📚 Tài liệu tham khảo

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

Tự kiểm tra
Q1
L4 và L7 load balancer khác nhau căn bản ở điểm nào? Tại sao L7 đắt hơn về CPU?
L4 LB hoạt động ở tầng Transport — chỉ nhìn IP, port, và TCP flags, không đọc nội dung HTTP. Nó forward byte stream TCP nguyên vẹn sang backend. L7 LB hoạt động ở tầng Application — phải parse toàn bộ HTTP request (method, URL, header, cookie) trước mỗi quyết định routing. Việc parse HTTP tốn CPU hơn so với chỉ nhìn header TCP. Ngoài ra L7 phải maintain hai kết nối TCP (client↔LB và LB↔backend) thay vì chỉ forward một kết nối như L4. Đổi lại, L7 có thể định tuyến thông minh, làm TLS termination, và rewrite header.
Q2
Khi nào chọn L4, khi nào chọn L7?
Chọn L4 khi: cần tốc độ tối đa (game server, financial trading), dịch vụ không phải HTTP (PostgreSQL, Redis, raw TCP), hoặc cần TLS passthrough (backend tự xử lý TLS, LB không được đọc nội dung). Chọn L7 khi: cần routing theo URL hoặc header (microservice), cần TLS termination tại LB, cần header injection (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.
Q3
Vì sao `least_conn` tốt hơn `round_robin` khi request có thời gian xử lý không đều?
Round-robin chia đều số lượng request, không chia đều tải thực tế. Nếu một request upload video mất 30 giây còn các request query nhỏ mất 50ms, server đang xử lý upload có thể đang quá tải trong khi LB vẫn tiếp tục gửi request mới tới theo thứ tự vòng tròn. 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.
Q4
Sticky session là gì và tại sao nó là giải pháp tình thế thay vì kiến trúc đúng?
Sticky session (session affinity) là cấu hình LB để route tất cả request từ cùng một user tới cùng một server backend — thường dựa theo IP hash hoặc cookie. Cần thiết khi app lưu session state trong bộ nhớ server (in-memory), vì request tới server khác sẽ không tìm thấy session đó. Tuy nhiên nó là giải pháp tình thế vì: phá vỡ cân bằng tải (nhiều user ghim vào một server làm nó quá tải), phức tạp khi server chết (session mất theo server), và cản trở scale ngang. Giải pháp đúng là dùng external session store như Redis — mọi server đọc được session của nhau, LB có thể dùng round-robin tự do.
Q5
Passive health check trong Nginx hoạt động thế nào? Khác gì active health check?
Passive health check (Nginx open-source): LB quan sát response của traffic thật. Khi server trả lỗi (timeout, kết nối từ chối) vượt ngưỡng 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.
Q6
Tại sao cần health check kiểm tra dependency thật (DB, Redis) thay vì chỉ trả status ok?
App có thể đang chạy bình thường (process sống, port lắng nghe) nhưng không xử lý được request thật vì dependency hỏng — DB unreachable, Redis timeout, queue đầy. Health check chỉ trả {"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

Đặt 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

Bài tiếp theo

Module 2 — Tổng kết & cheat sheet