Hai sự thật va nhau ở mọi hệ thống lớn: một máy chủ không chịu nổi tải (nên phải nhiều máy), và máy chủ thì có lúc chết (nên không được phụ thuộc một cái nào). Lớp giải cả hai là load balancer. Bài này mổ xẻ cách một bộ cân bằng tải như HAProxy (Stack Overflow nổi tiếng chạy trên HAProxy) chia tải và tự loại máy chủ hỏng, rồi dựng thật HAProxy với nhiều backend để xem failover xảy ra trực tiếp.

Bài toán: chia tải, và đừng gửi request vào máy đã chết

Load balancer đứng trước nhiều backend, nhận mọi request và phân phối. Nó phải làm hai việc:

  1. Chia tải theo một thuật toán (đều, hoặc theo tải hiện tại).
  2. Health check: liên tục kiểm mỗi backend còn sống không; nếu chết thì ngừng gửi request vào đó — mà người dùng không thấy lỗi.

Cách giải: HAProxy round-robin + health check

Cấu hình HAProxy khai backend và cách kiểm sức khoẻ:

frontend fe
  bind :80
  default_backend be
backend be
  balance roundrobin              # chia đều lần lượt; hoặc leastconn, source...
  option httpchk GET /            # health check: GET / định kỳ
  server b1 be1:5678 check inter 500 fall 2 rise 1
  server b2 be2:5678 check inter 500 fall 2 rise 1
  server b3 be3:5678 check inter 500 fall 2 rise 1

check inter 500 = kiểm mỗi 500ms; fall 2 = hỏng 2 lần liên tiếp thì đánh DOWN; rise 1 = khoẻ lại 1 lần thì đưa vào lại pool. HAProxy tự làm toàn bộ, không cần can thiệp tay.

Ảnh chụp đoạn mã nền tối minh hoạ Stack Overflow cân bằng tải với HAProxy round-robin cộng health check HAProxy 3.4 chia tải nhiều backend tự loại máy chủ hỏng không rớt request, một bài toán một máy chủ không chịu nổi tải và máy chủ có lúc chết cần chia request ra nhiều backend khi 1 backend chết tự dừng gửi vào nó mà không rớt request không trả 502 503, hai cấu hình HAProxy round-robin cộng health check mỗi backend frontend fe bind 80 default_backend be backend be balance roundrobin chia đều lần lượt hoặc leastconn source option httpchk GET dấu gạch chéo health check GET định kỳ server b1 be1 5678 check inter 500 fall 2 rise 1 server b2 be2 5678 check server b3 be3 5678 check inter 500 kiểm mỗi 500ms fall 2 hỏng 2 lần liên tiếp đánh DOWN rise 1 khoẻ 1 lần đưa lại vào pool HAProxy tự làm, ba vài thuật toán cân bằng roundrobin lần lượt từng backend đơn giản đều khi request đồng cỡ leastconn gửi vào backend đang ít kết nối nhất hợp khi request dài ngắn khác nhau source hash theo IP nguồn cùng client về cùng backend sticky nhẹ

Hình 1: Cấu hình HAProxy — balance roundrobin chia tải, option httpchk + check inter/fall/rise cho health check tự động; và vài thuật toán cân bằng (roundrobin, leastconn, source).

Đo THẬT: round-robin và failover

Ta dựng thật HAProxy 3.4 trước 3 backend (mỗi backend trả tên mình), rồi bắn request qua HAProxy.

Round-robin — 12 request chia đều:

4  backend-1
4  backend-2
4  backend-3

Chia đều tuyệt đối, mỗi backend nhận 1/3. Giết backend-2 (giả lập máy chủ chết) rồi bắn tiếp:

docker stop htl-be2   # máy chủ chết
12 request sau đó:
   6  backend-1
   6  backend-3

HAProxy tự dồn sang 2 backend còn sống, không trả lỗi 5xx — health check (fall 2, inter 500ms) phát hiện DOWN trong ~1 giây và ngừng gửi vào backend-2. Bật lại backend-2:

docker start htl-be2
12 request:
   4  backend-1
   4  backend-2   (đã quay lại)
   4  backend-3

rise 1: khoẻ lại 1 lần là HAProxy đưa backend-2 vào lại pool — hoàn toàn tự động. Đây là bằng chứng trực tiếp về điều làm nên tính sẵn sàng: máy chủ có thể chết và hồi, người dùng không hề hay biết.

Ảnh chụp bảng kết quả chạy thật trên HAProxy 3.4 cộng 3 backend output thật, một round-robin 12 request chia đều 3 backend curl x12 qua HAProxy 4 backend-1 4 backend-2 4 backend-3 chia đều tuyệt đối mỗi backend nhận 1 phần 3, hai giết backend-2 health check tự loại failover docker stop htl-be2 giả lập máy chủ chết 12 request sau đó 6 backend-1 6 backend-3 HAProxy tự dồn sang 2 backend còn sống không trả lỗi 5xx health check fall 2 inter 500ms phát hiện DOWN trong 1s ngừng gửi vào be2, ba bật lại backend-2 tự quay lại pool docker start htl-be2 12 request 4 backend-1 4 backend-2 đã quay lại 4 backend-3 rise 1 khoẻ lại 1 lần là HAProxy đưa be2 vào lại không cần can thiệp tay, Stack Overflow hệ thống lớn dùng HAProxy thế nào một cửa vào client chỉ biết HAProxy backend thêm bớt chết trong suốt với client health check tự loại backend hỏng không ai gửi request vào máy chết thuật toán roundrobin leastconn source tuỳ hình dạng tải tầng L4 L7 cân bằng ở tầng TCP hoặc HTTP SSL termination sticky session

Hình 2: Chạy thật trên HAProxy — round-robin chia 4/4/4; giết backend-2 thì health check tự dồn 6/6 (không 5xx); bật lại thì backend-2 tự quay vào pool 4/4/4. Kèm cách hệ thống lớn dùng HAProxy.

Vì sao đây là mảnh không thể thiếu ở quy mô lớn

  • Một cửa vào duy nhất. Client chỉ biết địa chỉ HAProxy; backend được thêm/bớt/chết trong suốt với client. Muốn scale thì thêm backend rồi khai vào HAProxy — không client nào phải đổi gì.
  • Health check tách "chết" khỏi "người dùng thấy lỗi". Không có nó, một backend chết nghĩa là 1/N request trả lỗi cho tới khi ai đó can thiệp. Có nó, lỗi được cô lập trong ~1 giây, tự động.
  • Nhiều tầng, nhiều thuật toán. Cân bằng ở tầng TCP (L4) hay HTTP (L7); leastconn cho request dài-ngắn khác nhau; source cho sticky session; SSL termination; giới hạn tốc độ. HAProxy là một điểm điều phối giàu tính năng.

Đánh đổi cần cân nhắc

Load balancer là một điểm phụ thuộc — phải nhân đôi nó. Nếu chỉ có một HAProxy, nó thành single point of failure. Thực tế chạy ≥2 HAProxy với một IP nổi (keepalived/VRRP) hoặc DNS/anycast phía trước. Cân bằng tải giải tính sẵn sàng của backend, nhưng tự nó cũng cần được làm sẵn sàng.

Health check nông có thể đánh lừa. GET / chỉ biết tiến trình còn sống, không biết nó có phục vụ đúng không (DB phía sau chết, đĩa đầy...). Health check tốt nên kiểm một endpoint phản ánh sức khoẻ thật (như bài REST API capstone của sê-ri Go). Health check sai làm HAProxy giữ backend hỏng hoặc loại backend khoẻ.

Round-robin không phải luôn tối ưu. Nó giả định request đồng cỡ. Nếu request dài ngắn rất khác nhau, leastconn (gửi vào máy ít kết nối nhất) cân tải tốt hơn. Chọn thuật toán theo hình dạng tải thật, đo trước khi chọn.

Ba ý mang về

  1. Load balancer chia tải nhiều backend sau một cửa vào duy nhất: đo thật HAProxy round-robin chia 12 request thành 4/4/4 — thêm/bớt backend trong suốt với client.
  2. Health check biến "máy chủ chết" thành chuyện vô hình với người dùng: đo thật giết một backend thì HAProxy tự dồn sang 2 máy còn sống (6/6, không lỗi 5xx) trong ~1s, và tự đưa lại vào pool khi hồi — hoàn toàn tự động.
  3. Bản thân LB phải được làm sẵn sàng và health check phải sâu: chạy ≥2 HAProxy (tránh single point of failure), kiểm endpoint phản ánh sức khoẻ thật, và chọn thuật toán (roundrobin/leastconn/source) theo hình dạng tải.

Nguồn

Phần sau ta xét một nút thắt âm thầm: vì sao hàng nghìn kết nối tới PostgreSQL làm sập database, và PgBouncer gộp kết nối cứu thế nào — dựng thật và đo.