Bài toán: một server chịu không nổi, nhiều server lại cần ai điều phối
Khi app bắt đầu chịu tải cao, cách đơn giản nhất là scale ngang: chạy nhiều instance cùng lúc. Nhưng ngay lập tức bạn gặp vấn đề tiếp theo:
- Client gọi vào đâu?
- Nếu một instance chết, request vẫn đổ vào nó thì sao?
- Làm sao biết instance nào đang khoẻ?
Đây là lúc bạn cần một load balancer đứng trước các backend để nhận request rồi phân phối đều ra.
HAProxy là công cụ làm chuyện đó. Nó nhẹ, nhanh, cấu hình rõ ràng và đã được dùng ở production lớn từ hơn hai mươi năm nay.
Cân bằng tải là gì, giải thích ngắn
Cân bằng tải (load balancing) là kỹ thuật phân phối request đến nhiều server backend thay vì dồn vào một chỗ.
Client
│
▼
Load Balancer (HAProxy)
├── Backend 1 (app:8001)
├── Backend 2 (app:8002)
└── Backend 3 (app:8003)
HAProxy nhận toàn bộ request ở port đầu vào, sau đó quyết định chuyển request đó đến backend nào dựa trên thuật toán đã cấu hình.
Ba thuật toán phổ biến nhất:
| Thuật toán | Cách hoạt động | Dùng khi nào |
|---|---|---|
roundrobin |
Luân phiên theo thứ tự | Request nhẹ, tương đương nhau |
leastconn |
Gửi đến server ít connection nhất | Request nặng, xử lý lâu |
source |
Cùng IP client → cùng backend | Cần session sticky |
Điều kiện trước khi bắt đầu
- Máy đã cài Docker và Docker Compose
- Hiểu cơ bản về Docker networking
- Có sẵn một app để test (bài này dùng Nginx làm backend giả)
Cấu trúc project
haproxy-demo/
├── docker-compose.yml
├── haproxy/
│ └── haproxy.cfg
Bước 1: Viết file cấu hình HAProxy
Tạo file haproxy/haproxy.cfg:
global
log stdout format raw local0
maxconn 4096
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
frontend http_front
bind *:80
default_backend web_backends
backend web_backends
balance roundrobin
option httpchk GET /
http-check expect status 200
server backend1 app1:80 check inter 5s fall 3 rise 2
server backend2 app2:80 check inter 5s fall 3 rise 2
server backend3 app3:80 check inter 5s fall 3 rise 2
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 5s
stats show-node
stats auth admin:admin123
Giải thích các phần quan trọng:
frontend http_front: HAProxy lắng nghe port 80, nhận mọi request HTTP vàobackend web_backends: định nghĩa nhóm backend và cách phân phốibalance roundrobin: luân phiên đều qua 3 serveroption httpchk GET /: health check bằng cách gửi GET đến/check inter 5s fall 3 rise 2: kiểm tra mỗi 5 giây, down sau 3 lần thất bại liên tiếp, up lại sau 2 lần thành cônglisten stats: Stats UI trên port 8404, bảo vệ bằng basic auth
Bước 2: Viết docker-compose.yml
services:
haproxy:
image: haproxy:2.9-alpine
ports:
- "80:80"
- "8404:8404"
volumes:
- ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
depends_on:
- app1
- app2
- app3
restart: unless-stopped
app1:
image: nginx:alpine
volumes:
- ./app1:/usr/share/nginx/html:ro
app2:
image: nginx:alpine
volumes:
- ./app2:/usr/share/nginx/html:ro
app3:
image: nginx:alpine
volumes:
- ./app3:/usr/share/nginx/html:ro
Bước 3: Tạo nội dung backend giả để phân biệt
mkdir -p app1 app2 app3
echo "<h1>Backend 1</h1>" > app1/index.html
echo "<h1>Backend 2</h1>" > app2/index.html
echo "<h1>Backend 3</h1>" > app3/index.html
Bước 4: Chạy stack
docker compose up -d
Kiểm tra tất cả container đang chạy:
docker compose ps
Kết quả mong đợi:
NAME STATUS PORTS
haproxy-demo-haproxy-1 Up 0.0.0.0:80->80/tcp, 0.0.0.0:8404->8404/tcp
haproxy-demo-app1-1 Up 80/tcp
haproxy-demo-app2-1 Up 80/tcp
haproxy-demo-app3-1 Up 80/tcp
Bước 5: Kiểm tra cân bằng tải
Gửi vài request liên tiếp và xem response thay đổi:
for i in $(seq 1 6); do curl -s http://localhost/; echo; done
Kết quả sẽ luân phiên:
<h1>Backend 1</h1>
<h1>Backend 2</h1>
<h1>Backend 3</h1>
<h1>Backend 1</h1>
<h1>Backend 2</h1>
<h1>Backend 3</h1>
Bước 6: Xem Stats UI
Mở trình duyệt vào http://localhost:8404/stats, đăng nhập với admin / admin123.
Stats UI cho thấy:
- Trạng thái từng backend (UP / DOWN)
- Số request đã xử lý
- Số connection hiện tại
- Số lần health check thất bại
Kiểm tra health check hoạt động đúng chưa
Tắt một backend thủ công:
docker compose stop app2
Gửi request tiếp:
for i in $(seq 1 4); do curl -s http://localhost/; echo; done
HAProxy sẽ bỏ app2 ra khỏi pool và chỉ xoay giữa app1 và app3. Không có request nào bị lỗi.
Bật lại:
docker compose start app2
Sau 2 lần health check thành công (khoảng 10 giây), HAProxy tự đưa app2 trở lại pool.
Các lỗi hay gặp
HAProxy khởi động xong nhưng request 503
Health check chạy ngay khi HAProxy start. Nếu backend chưa sẵn sàng, HAProxy đánh dấu chúng là DOWN và trả 503. Đợi 10–15 giây cho backend khởi xong rồi thử lại. Hoặc thêm healthcheck vào service backend trong Compose.
Lỗi cannot bind socket khi start
Port 80 hoặc 8404 đang bị chiếm bởi process khác. Kiểm tra:
sudo ss -tlnp | grep ':80\|:8404'
Config parse error
HAProxy validate config rất nghiêm. Xem log để biết dòng nào lỗi:
docker compose logs haproxy
Lỗi thường gặp: thiếu dấu cách trước check, sai tên server trong backend, hoặc timeout không đúng đơn vị.
Backend tên không resolve được
Trong Docker Compose, HAProxy dùng tên service (vd: app1) làm hostname. Nếu bạn đổi tên service thì phải đổi trong haproxy.cfg theo.
Chuyển sang leastconn khi nào
roundrobin phù hợp khi các request nhẹ và xử lý nhanh. Với bài toán như:
- WebSocket
- Upload file lớn
- API xử lý lâu
Đổi sang leastconn để tránh một backend bị chất đống request trong khi backend kia rảnh:
backend web_backends
balance leastconn
...
Session sticky với source
Nếu app cần user luôn vào cùng một backend (ví dụ: dùng local session, không có shared cache), dùng source:
backend web_backends
balance source
hash-type consistent
...
hash-type consistent giúp khi thêm hoặc bớt backend, chỉ một phần nhỏ request bị chuyển backend thay vì toàn bộ bị xáo trộn.
Khi nào không dùng HAProxy theo cách này
- App đang trên Kubernetes: dùng Service + Ingress thay vì tự quản lý HAProxy
- Cần SSL termination phức tạp với nhiều domain: Nginx hoặc Caddy dễ cấu hình hơn
- Chỉ có một backend duy nhất: không cần load balancer, tốn tài nguyên vô ích
Checklist trước khi lên production
- Đổi
stats authsang password mạnh hơn, hoặc tắt Stats UI nếu không cần - Bật SSL trên
frontendnếu app expose ra Internet - Cấu hình log ra file hoặc log collector thay vì stdout
- Điều chỉnh
timeoutphù hợp với đặc thù của app - Test health check bằng cách kill từng backend thủ công trước khi deploy thật