Sau khi hoàn tất bước tách hệ quản trị cơ sở dữ liệu sang VPS chuyên dụng qua Private VPC theo lộ trình chuyển dịch Multi-Server, hệ thống của bạn đã giải tỏa được điểm nghẽn tranh chấp tài nguyên giữa tầng ứng dụng và cơ sở dữ liệu. Dù vậy, khi lượng truy cập đồng thời tiếp tục tăng, một máy chủ web đơn lẻ (Web Node) sẽ sớm chạm ngưỡng xử lý CPU của các tiến trình thực thi như PHP-FPM, Node.js hoặc Python Gunicorn.
Để tăng năng lực tiếp nhận truy vấn mà không bị giới hạn bởi cấu hình của một máy chủ vật lý đơn lẻ, bạn nên mở rộng hệ thống theo chiều ngang (scale-out). Mô hình này bổ sung các máy chủ web chạy song song và sử dụng một máy chủ Nginx đóng vai trò Load Balancer (Reverse Proxy tầng 7) đặt ở phía trước.
Khi triển khai cân bằng tải, việc cấu hình danh sách địa chỉ IP trong khối upstream mới chỉ là bước khởi đầu. Nếu chưa xử lý thỏa đáng việc đồng bộ phiên làm việc (session), giải mã SSL tại proxy (SSL Offloading) và cơ chế tự động cô lập máy chủ gặp sự cố (healthcheck và failover), hệ thống sẽ dễ phát sinh lỗi đăng xuất bất thường, vòng lặp chuyển hướng HTTPS (Infinite Redirect Loop) hoặc trả về mã lỗi 502 Bad Gateway cho người dùng.
Bài viết này hướng dẫn bạn cách thiết lập Nginx Load Balancer theo hướng tối ưu tài nguyên: lựa chọn thuật toán phân phối tải phù hợp, duy trì kết nối TCP keepalive để tránh cạn kiệt cổng mạng và cấu hình kịch bản chuyển đổi dự phòng nhanh chóng khi có sự cố phát sinh.
Thuật toán phân phối tải trong khối upstream
Nginx cung cấp nhiều thuật toán để điều phối lưu lượng từ khối upstream tới các máy chủ ứng dụng phía sau. Việc chọn thuật toán phù hợp với đặc thù ứng dụng giúp tận dụng tối đa năng lực của toàn bộ cụm máy chủ.
Thuật toán least_conn
Với các ứng dụng web động như WordPress, Laravel hay Next.js, thời gian xử lý mỗi yêu cầu thường có sự chênh lệch đáng kể. Một yêu cầu tải tệp tĩnh có thể chỉ mất 2ms, nhưng một thao tác xử lý giỏ hàng hoặc truy vấn dữ liệu phức tạp có thể kéo dài 300ms – 500ms.
Nếu sử dụng thuật toán phân phối luân phiên mặc định (Round-Robin), có thời điểm một máy chủ phải tiếp nhận liên tiếp nhiều yêu cầu nặng, dẫn tới nghẽn hàng đợi CPU trong khi máy chủ còn lại vẫn còn nhiều tài nguyên rảnh rỗi.
Thuật toán least_conn giải quyết vấn đề này bằng cách theo dõi và chuyển yêu cầu mới đến máy chủ đang có số lượng kết nối hoạt động (active connections) ít nhất. Nhờ vậy, tải xử lý của toàn cụm được phân bổ đồng đều hơn theo thời gian thực.
Rủi ro khi dùng ip_hash
Khi gặp hiện tượng người dùng bị mất phiên đăng nhập lúc chuyển máy chủ, một số người vận hành thường chọn giải pháp kích hoạt chỉ thị ip_hash. Thuật toán này băm địa chỉ IP của client để cố định các kết nối từ một IP về đúng một máy chủ phía sau.
Tuy nhiên, ip_hash mang lại nhiều hạn chế đáng lưu ý:
- Nếu hệ thống đặt sau mạng phân phối nội dung (Cloudflare, Fastly) hoặc người dùng truy cập từ mạng nội bộ công ty (nơi hàng trăm người dùng chung một địa chỉ IP NAT ra ngoài), Nginx sẽ điều hướng toàn bộ lưu lượng đó về duy nhất một máy chủ web. Máy chủ này dễ rơi vào tình trạng quá tải cục bộ, trong khi các máy chủ khác lại dư thừa tài nguyên.
- Do đó,
ip_hashchỉ nên coi là phương án xử lý tình thế. Hướng xử lý căn bản và bền vững cho kiến trúc cụm là tách riêng tầng lưu trữ phiên làm việc như hướng dẫn bên dưới.
Quản lý phiên làm việc tập trung
Khi người dùng gửi yêu cầu đầu tiên, họ kết nối tới Web Node 01 và tạo phiên đăng nhập. Ở yêu cầu kế tiếp, Load Balancer có thể điều phối họ sang Web Node 02. Nếu Web Node 02 chỉ tìm kiếm session trên ổ đĩa cục bộ (/var/lib/php/sessions), hệ thống sẽ không tìm thấy phiên tương ứng và yêu cầu người dùng đăng nhập lại.
Bạn có thể xử lý bài toán này theo hai hướng tiếp cận:
Lưu trữ session trên Redis
Các Web Node trong cụm sẽ chuyển toàn bộ thao tác đọc và ghi session về một máy chủ Redis đặt trong mạng nội bộ Private VPC. Theo quy chuẩn đã nêu tại bài viết triển khai Redis an toàn trên Docker, độ trễ truy xuất dữ liệu từ bộ nhớ RAM của Redis qua Private VPC thường dưới 0.4ms, đáp ứng tốt các hệ thống có lưu lượng truy cập lớn.
Trên từng Web Node (Web-01, Web-02), bạn điều chỉnh tệp cấu hình PHP (php.ini):
; Chỉ định Redis làm trình điều khiển Session
session.save_handler = redis
session.save_path = "tcp://10.0.0.30:6379?auth=MatKhauBaoMatRedisCapCao123!&database=2&timeout=2.0"Khởi động lại tiến trình PHP-FPM để áp dụng thay đổi:
sudo systemctl restart php8.2-fpmSau khi cấu hình, dù yêu cầu được phân bổ tới Web Node nào, ứng dụng vẫn truy xuất đúng dữ liệu phiên làm việc từ máy chủ Redis nội bộ.
Dùng cơ chế xác thực không trạng thái qua JWT
Với các ứng dụng phát triển trên nền Node.js, Go hoặc Python REST API, một giải pháp hiệu quả khác là thiết kế kiến trúc phi trạng thái (stateless) bằng JSON Web Token (JWT).
Thông tin định danh và quyền hạn của người dùng được ký điện tử và lưu trữ an toàn phía trình duyệt (thông qua HTTP-Only Cookie hoặc Authorization Header). Khi tiếp nhận yêu cầu, bất kỳ Web Node nào cũng có thể kiểm tra tính hợp lệ của token mà không cần truy vấn bộ nhớ cục bộ hay gọi vào cơ sở dữ liệu.
SSL Offloading và xử lý vòng lặp chuyển hướng
Vai trò của SSL Offloading
Trong mô hình kiến trúc này, Nginx Load Balancer đóng vai trò là điểm kết thúc mã hóa SSL (SSL Termination). Toàn bộ quá trình bắt tay TLS (TLS 1.2 / TLS 1.3 Handshake) cùng thao tác mã hóa và giải mã dữ liệu HTTPS đều được xử lý tập trung tại Load Balancer.
Từ Load Balancer, các yêu cầu được chuyển tiếp tới Web Node 01 và Web Node 02 bằng giao thức HTTP thông thường (cổng 80) qua mạng nội bộ Private VPC.
Mô hình này mang lại hai lợi ích chính:
- Tiết kiệm tài nguyên xử lý: Giảm tải khoảng 15% đến 25% công suất CPU cho các Web Node, giúp tài nguyên này tập trung phục vụ logic nghiệp vụ của ứng dụng.
- Tập trung quản trị chứng chỉ: Bạn chỉ cần cài đặt và cấu hình gia hạn chứng chỉ SSL (Let's Encrypt / Certbot) trên một máy chủ Load Balancer duy nhất thay vì thao tác phân tán trên từng máy chủ web.
Xử lý lỗi vòng lặp chuyển hướng
Khi Nginx Load Balancer chuyển tiếp yêu cầu tới Web Node qua giao thức HTTP, một số framework (như WordPress hay Laravel) nhận thấy kết nối gửi đến là HTTP sẽ tự động phát sinh mã chuyển hướng 301 Moved Permanently để yêu cầu đổi sang https://....
Trình duyệt tiếp tục gửi lại yêu cầu HTTPS tới Load Balancer, Load Balancer lại gọi HTTP tới Web Node, và Web Node lại phản hồi mã 301. Vòng lặp này tiếp diễn cho đến khi trình duyệt dừng lại và thông báo lỗi ERR_TOO_MANY_REDIRECTS.
Để xử lý dứt điểm tình trạng này, bạn cần cấu hình đồng bộ ở cả hai phía:
Cấu hình header ủy quyền trên Nginx Load Balancer
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;Khai báo proxy tin cậy trên Web Node
Trên tệp cấu hình Nginx của từng Web Node, bạn cần cho phép Nginx nhận diện địa chỉ IP thực của client thông qua header X-Forwarded-For do Load Balancer chuyển tiếp:
# Khai báo địa chỉ IP nội bộ của máy chủ Load Balancer
set_real_ip_from 10.0.0.10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# Báo cho FastCGI / PHP biết request gốc là HTTPS
fastcgi_param HTTPS on;Với ứng dụng Laravel, bạn khai báo IP 10.0.0.10 vào danh sách protected $proxies trong tệp app/Http/Middleware/TrustProxies.php.
Tối ưu kết nối nội bộ bằng keepalive
Theo mặc định, mỗi khi chuyển tiếp yêu cầu tới backend, Nginx sẽ khởi tạo một kết nối TCP mới và đóng lại ngay sau khi nhận xong phản hồi (theo cơ chế HTTP/1.0 không keepalive).
Khi hệ thống tiếp nhận lưu lượng lớn (chẳng hạn khoảng 1.000 request/giây), thao tác mở và đóng socket liên tục sẽ tạo ra hàng chục nghìn kết nối ở trạng thái TIME_WAIT trên hệ điều hành. Khi số lượng socket chạm ngưỡng giới hạn cổng tạm thời (Ephemeral Ports), máy chủ Load Balancer sẽ không còn cổng kết nối khả dụng và trả về thông báo lỗi Cannot assign requested address.
Để hạn chế tình trạng này, bạn nên cấu hình nhóm kết nối tái sử dụng (connection pooling) trong khối upstream:
upstream web_cluster {
least_conn;
server 10.0.0.11:80 max_fails=3 fail_timeout=10s;
server 10.0.0.12:80 max_fails=3 fail_timeout=10s;
# Duy trì sẵn một nhóm 64 kết nối TCP rảnh rỗi tới mỗi worker
keepalive 64;
}
server {
...
location / {
proxy_pass http://web_cluster;
# Chuyển sang HTTP/1.1 và xóa header Connection để tái sử dụng socket
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}Cấu hình failover và kiểm tra sức khỏe
Bản Nginx mã nguồn mở hỗ trợ cơ chế kiểm tra sức khỏe thụ động (passive healthcheck) thông qua hai tham số max_fails và fail_timeout, kết hợp cùng chỉ thị proxy_next_upstream.
Cơ chế chuyển tiếp khi máy chủ gặp sự cố
Xét tình huống Web Node 01 gặp sự cố gián đoạn dịch vụ, ví dụ ứng dụng phát sinh lỗi dừng tiến trình hoặc container bị sập.
Nếu không có cấu hình dự phòng, những yêu cầu gửi đến Web Node 01 sẽ lập tức nhận mã lỗi 502 Bad Gateway.
Khi cấu hình proxy_next_upstream, Nginx load balancer sẽ tự động kiểm soát phản hồi từ backend: nếu Web Node 01 trả về các mã lỗi 502, 503, 504 hoặc bị quá thời gian chờ (timeout), Nginx sẽ chuyển tiếp yêu cầu đó sang Web Node 02 để xử lý tiếp trong nền:
proxy_next_upstream error timeout invalid_header http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 5s;Người dùng cuối chỉ thấy độ trễ tăng nhẹ khoảng 0.2 giây, dữ liệu vẫn phản hồi đầy đủ và hầu như không gặp thông báo lỗi gián đoạn nào.
Tạo endpoint /healthz trên web node
Để phục vụ hệ thống giám sát và giúp bộ cân bằng tải xác định trạng thái thực tế của từng node, bạn nên tạo một endpoint nhẹ /healthz trên ứng dụng hoặc trong cấu hình Nginx cục bộ để trả về mã HTTP 200:
# Cấu hình trên Web-01 và Web-02
location = /healthz {
access_log off;
default_type text/plain;
return 200 "healthy\n";
}Cấu hình hoàn chỉnh cho Nginx load balancer
Dưới đây là nội dung tệp cấu hình chuẩn hóa tại /etc/nginx/conf.d/load-balancer.conf trên máy chủ load balancer:
# Khối định nghĩa cụm máy chủ Web backend
upstream web_cluster {
least_conn; # Sử dụng thuật toán kết nối ít nhất
server 10.0.0.11:80 max_fails=3 fail_timeout=10s;
server 10.0.0.12:80 max_fails=3 fail_timeout=10s;
# Nhóm kết nối TCP Keepalive tái sử dụng
keepalive 64;
}
# Chuyển hướng toàn bộ HTTP sang HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# Máy chủ HTTPS chính tiếp nhận lưu lượng
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
# Cấu hình chứng chỉ SSL
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Tối ưu hóa phiên làm việc SSL
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# Giao thức và mã hóa an toàn hiện đại
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Kích hoạt HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# Định tuyến tới cụm Web Node
location / {
proxy_pass http://web_cluster;
# Chuyển sang HTTP/1.1 và xóa header Connection cho keepalive
proxy_http_version 1.1;
proxy_set_header Connection "";
# Truyền đầy đủ các thông tin ủy quyền cần thiết
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Cấu hình giới hạn thời gian chờ
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# Cơ chế Failover tự động
proxy_next_upstream error timeout invalid_header http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;
# Gắn header chẩn đoán để biết request do ai xử lý
add_header X-LB-Proxy "Nginx-Cluster-LB" always;
}
}Kiểm tra cú pháp cấu hình và nạp lại dịch vụ:
sudo nginx -t
sudo systemctl reload nginxKiểm tra và diễn tập sự cố
Sau khi hoàn tất cấu hình, bạn nên tiến hành kiểm tra thực tế để xác nhận hệ thống vận hành đúng thiết kế:
Kiểm tra phân phối tải luân phiên
Gửi liên tiếp 10 truy vấn bằng lệnh curl từ máy cá nhân và quan sát thông tin máy chủ xử lý:
for i in {1..10}; do curl -sI https://example.com | grep -i "x-served-by"; done(Ghi chú: Bạn có thể thêm chỉ thị add_header X-Served-By $hostname always; vào khối server của từng web node để phục vụ bước xác nhận này).
Kết quả phản hồi luân phiên giữa các node:
X-Served-By: web-node-01
X-Served-By: web-node-02
X-Served-By: web-node-01
X-Served-By: web-node-02Diễn tập dừng dịch vụ trên một web node
Đăng nhập SSH vào máy chủ Web Node 01 và chủ động dừng dịch vụ Nginx:
sudo systemctl stop nginxTrong lúc Web Node 01 tạm dừng hoạt động, bạn gửi liên tục các truy vấn hoặc sử dụng công cụ kiểm thử tải ab (Apache Benchmark):
ab -n 500 -c 10 https://example.com/Kết quả mong đợi:
- Tỷ lệ lỗi (Failed requests): Bằng 0.
- Toàn bộ lưu lượng được Nginx load balancer tự động chuyển tiếp sang Web Node 02.
- Người dùng truy cập website không gặp thông báo lỗi gián đoạn.
Khi bạn khởi động lại Web Node 01 bằng lệnh sudo systemctl start nginx, bộ cân bằng tải Nginx sẽ tự động đưa máy chủ này trở lại cụm xử lý sau khi hết thời gian fail_timeout (10 giây).
Lộ trình triển khai tiếp theo
Làm chủ kỹ thuật cân bằng tải với Nginx giúp hạ tầng website nâng cao độ tin cậy và sẵn sàng đáp ứng lưu lượng truy cập gia tăng. Để hoàn thiện chu trình phát hành ứng dụng trên cụm máy chủ mà không làm gián đoạn dịch vụ, bạn có thể tiếp tục tìm hiểu kỹ thuật triển khai Zero-Downtime Deployment với Docker Blue/Green và xử lý tính phi trạng thái của dữ liệu tĩnh qua bài viết về giải pháp lưu trữ tệp tải lên cho cụm Multi-Server.



