Load Balancer và Reverse Proxy

K
Kai··5 min read

Một server có giới hạn: chịu được bấy nhiêu kết nối, và nếu nó chết thì cả dịch vụ chết. Để phục vụ nhiều người và không có điểm chết đơn lẻ, ta đặt nhiều server và một load balancer phân phối lưu lượng giữa chúng. Bài này giải thích load balancer, reverse proxy, và các khái niệm đi kèm — những thứ trung tâm của hệ thống có khả năng mở rộng.

Vấn đề: một server là không đủ

Hai vấn đề khi chỉ có một server:

  • Tải: lượng truy cập vượt sức một máy.
  • Điểm chết đơn lẻ (single point of failure): máy đó chết là dịch vụ sập.

Giải pháp: chạy nhiều bản của ứng dụng (nhớ "replicas" ở series Docker Swarm) và đặt một thứ phía trước để phân phối request giữa chúng — đó là load balancer (LB).

                          ┌─► Server A
   Client ──► Load        ├─► Server B     (nhiều bản giống nhau)
             Balancer ────┤
                          └─► Server C
        phân phối request, bỏ qua server đang chết

Lợi ích: chia tải ra nhiều máy (mở rộng ngang — scale out), và nếu một server chết, LB ngừng gửi cho nó, dịch vụ vẫn chạy.

L4 và L7: phân tải ở tầng nào

Nhớ mô hình phân tầng (Bài 1)? Load balancer hoạt động ở một trong hai tầng, và đây là phân biệt quan trọng:

  • L4 (tầng giao vận — TCP/UDP): phân phối dựa trên IP và cổng, không nhìn nội dung. Nhanh, đơn giản, hợp mọi giao thức. Nó chỉ chuyển tiếp gói TCP tới một backend.
  • L7 (tầng ứng dụng — HTTP): hiểu nội dung HTTP (URL, header, cookie), nên định tuyến thông minh được — ví dụ /api/* tới nhóm server này, /images/* tới nhóm khác; hoặc theo host. Linh hoạt hơn nhưng tốn xử lý hơn.
   L4 LB:  nhìn   [ IP:cổng đích ]            → chọn backend
   L7 LB:  nhìn   [ GET /api/users  Host: ... ]→ chọn backend theo URL/host

Ví dụ thực tế (series AWS): NLB (Network Load Balancer) của AWS là L4; ALB (Application Load Balancer) là L7. Nói "load balancer tầng 7" là nhắc đúng cách gọi theo OSI ở Bài 1.

Thuật toán phân tải

LB chọn backend nào cho mỗi request theo một thuật toán:

   Round-robin       lần lượt A, B, C, A, B, C...  (mặc định phổ biến)
   Least connections gửi tới server đang ít kết nối nhất
   IP hash           cùng client IP luôn tới cùng server (sticky theo IP)
   Weighted          server mạnh nhận tỉ lệ nhiều hơn

Round-robin đơn giản và đủ cho phần lớn. Least-connections tốt khi các request nặng nhẹ khác nhau.

Health check: bỏ qua server chết

Làm sao LB biết một server đã chết để ngừng gửi? Bằng health check: LB định kỳ "thăm" mỗi backend (ví dụ gọi GET /health — nhớ endpoint /health ta đặt trong app ở series Docker/AWS). Backend trả 200 = khỏe, tiếp tục nhận request; không trả lời/trả lỗi = bị loại tạm thời. Khi nó khỏe lại, LB đưa vào lại.

Đây là cơ chế làm nên tính sẵn sàng cao (high availability): server hỏng được bỏ qua tự động, người dùng không thấy gián đoạn. (Swarm/Kubernetes cũng dùng health check để tự thay bản chết — nhớ series Docker.)

Reverse proxy: họ hàng gần

Reverse proxy là một server đứng trước các server thật, nhận request thay cho chúng rồi chuyển tiếp vào trong. Load balancer L7 thực ra là một dạng reverse proxy. Phân biệt hai loại "proxy":

  • Forward proxy đứng trước client (đại diện cho client đi ra Internet — ví dụ proxy công ty).
  • Reverse proxy đứng trước server (đại diện cho server nhận request từ ngoài).
   Forward:  Client ──► [Forward proxy] ──► Internet
   Reverse:  Internet ──► [Reverse proxy] ──► Server backend

Reverse proxy (như nginx, HAProxy, Caddy, Traefik) làm nhiều việc hữu ích ngoài phân tải:

  • TLS termination: giải mã HTTPS ở proxy (nhớ Bài 9), nói HTTP thường với backend bên trong — backend khỏi lo chứng chỉ.
  • Định tuyến: theo đường dẫn/host tới các dịch vụ khác nhau.
  • Cache nội dung tĩnh, nén, giới hạn tốc độ (rate limit) — chính nginx của blog này làm rate limit /api/auth mà ta gặp khi deploy.
  • Che giấu kiến trúc bên trong (client chỉ thấy proxy).

Đây là lý do gần như mọi hệ thống web thật đều có nginx/reverse proxy ở phía trước.

Sticky session: khi cần "dính" server

Vì LB rải request ra nhiều server, hai request của cùng một người có thể vào hai server khác nhau. Nếu trạng thái phiên (session) lưu trong server (in-memory), điều này gây lỗi "lúc đăng nhập lúc không". Hai cách xử lý:

  • Sticky session (session affinity): LB ghim một client vào cùng một server (qua cookie hoặc IP hash). Đơn giản nhưng kém linh hoạt khi scale.
  • Tách trạng thái ra ngoài (khuyến nghị): lưu session ở nơi dùng chung (Redis, database), server nào cũng đọc được. Khi đó server stateless, scale thoải mái — đây là kiến trúc hiện đại nên hướng tới.

Tổng kết

Load balancer phân phối lưu lượng tới nhiều bản server để chia tải và tránh điểm chết đơn lẻ. Nó hoạt động ở L4 (theo IP/cổng — nhanh) hoặc L7 (hiểu HTTP — định tuyến thông minh), chọn backend bằng thuật toán (round-robin, least-conn...), và dùng health check để bỏ qua server chết (nền của high availability). Reverse proxy (nginx...) đứng trước server lo TLS termination, định tuyến, cache, rate limit. Để scale tốt, giữ server stateless (đẩy session ra Redis/DB) thay vì sticky session.

Bạn đã có toàn bộ bức tranh từ gói tin tới hệ thống mở rộng. Bài 12 gom các công cụ chẩn đoán lại thành quy trình gỡ lỗi mạng thực chiến.