Load Balancing (cân bằng tải)
Xem thửTình huống: scale lên 3 bản mà một bản vẫn gánh gần hết tải
Đội GDev tăng gdev-api từ 1 lên 3 bản để chịu flash sale. Sau khi scale, dashboard cho thấy một bản nhận gần hết traffic, hai bản kia gần như rảnh. Tệ hơn, lần deploy kế tiếp làm rơi vài request đang xử lý, và khi một bản chết, một số khách chờ đúng 1 giây rồi mới được phục vụ.
Cả ba hiện tượng đều là hành vi bình thường của bộ cân bằng tải (load balancer, viết tắt LB) khi bạn chưa hiểu nó đang làm việc ở tầng nào, nhìn sức khỏe backend bằng cách nào và rút một backend ra sao. Bài này dựng một LB nginx thật trong Docker, ba backend, rồi tự tái hiện và đọc từng hiện tượng qua access log.
Sau bài này bạn làm được các việc sau:
- Phân biệt LB tầng 4 (L4) và tầng 7 (L7) và đoán đúng nó cân bằng theo kết nối hay theo request.
- Chọn thuật toán phân phối (round robin, least connections, hash) và nói được cạm bẫy của từng cái.
- Giải thích health check thụ động (passive) và chủ động (active), và cái giá người dùng phải trả với kiểu thụ động.
- Rút một backend ra khỏi LB mà không làm rơi request đang chạy (connection draining).
- Đọc access log của LB để biết request đi đâu, thất bại ở đâu và mất bao lâu.
Bạn cần có sẵn: Docker, curl, hiểu mã trạng thái HTTP cơ bản và đã đọc bài Availability (health check, failover) ngay trước bài CAP.
Ba mô hình cần nhớ
- LB cân bằng theo đơn vị nó nhìn thấy: L4 thấy kết nối TCP nên cân bằng theo kết nối, L7 đọc được HTTP nên cân bằng theo request. Một client giữ kết nối lâu có thể dồn tất cả vào một backend ở L4.
- Health check là tín hiệu trễ: kiểu thụ động học từ lỗi của request thật nên người dùng đầu tiên trả giá; kiểu chủ động hỏi trước nên không ai trả giá, nhưng cần công cụ hỗ trợ.
- Rút backend phải từ từ: ngừng gửi request mới, để request cũ chạy xong, rồi mới tắt. Làm ngược lại là nguồn rơi request sau mỗi lần deploy.
Chuẩn bị môi trường lab
Lab dùng nginx làm LB và ba container hashicorp/http-echo làm backend. Mỗi backend trả một chuỗi cố định (backend-1, backend-2, backend-3) nên bạn biết ngay request rơi vào đâu. Tên container dùng tiền tố dv3c-, không publish cổng cố định (Docker chọn cổng ngẫu nhiên trên 127.0.0.1).
docker pull nginx:1.27-alpine
docker pull hashicorp/http-echo:1.0
mkdir -p dv3c-lb/conf && cd dv3c-lb
docker network create dv3c-net
for i in 1 2 3; do
docker run -d --name dv3c-be$i --network dv3c-net hashicorp/http-echo:1.0 -text="backend-$i"
done
Lưu cấu hình sau thành conf/default.conf. Đây là toàn bộ LB:
log_format lb '$remote_addr "$request" $status upstream=$upstream_addr ust=$upstream_status rt=$request_time';
access_log /dev/stdout lb;
upstream gdev_api {
zone gdev_api 64k;
server dv3c-be1:5678 max_fails=2 fail_timeout=10s;
server dv3c-be2:5678 max_fails=2 fail_timeout=10s;
server dv3c-be3:5678 max_fails=2 fail_timeout=10s;
}
server {
listen 80;
location / {
proxy_pass http://gdev_api;
proxy_connect_timeout 1s;
}
}
Đọc cấu hình:
- Khối
upstreamliệt kê nhóm backend. Mặc định nginx dùng round robin, nghĩa là lần lượt từng backend. max_fails=2 fail_timeout=10slà kiểu health check thụ động: hai lần giao tiếp thất bại trong 10 giây thì coi backend là không khả dụng trong 10 giây tiếp theo. Theo tài liệu nginx mặc định là 1 lần và 10 giây.zone gdev_api 64kcho các worker process của nginx dùng chung trạng thái của nhóm. Thiếu dòng này, mỗi worker giữ bộ đếm riêng.proxy_connect_timeout 1srút thời gian chờ kết nối từ mặc định 60 giây xuống 1 giây để lab chạy nhanh. Production thường đặt vài giây tùy mạng.log_formatthêm ba trường quan trọng:upstreamlà backend nhận request,ustlà mã mà backend trả,rtlà tổng thời gian xử lý.
Mount thư mục conf (không mount từng file) để sửa file bằng sed rồi nginx -s reload vẫn có hiệu lực:
docker run -d --name dv3c-lb --network dv3c-net -p 127.0.0.1:0:80 -v "$PWD/conf:/etc/nginx/conf.d:ro" nginx:1.27-alpine
PORT=$(docker port dv3c-lb 80 | cut -d: -f2)
Lab 1: phân phối request và các thuật toán
Dự đoán trước khi chạy: 9 request liên tiếp từ một máy qua round robin sẽ rơi thế nào vào ba backend?
for i in $(seq 1 9); do curl -s localhost:$PORT/; done | sort | uniq -c
docker logs dv3c-lb 2>&1 | grep upstream= | tail -3
Kết quả minh họa (địa chỉ IP của bạn sẽ khác):
3 backend-1
3 backend-2
3 backend-3
192.168.96.1 "GET / HTTP/1.1" 200 upstream=192.168.96.2:5678 ust=200 rt=0.000
192.168.96.1 "GET / HTTP/1.1" 200 upstream=192.168.96.3:5678 ust=200 rt=0.000
192.168.96.1 "GET / HTTP/1.1" 200 upstream=192.168.96.4:5678 ust=200 rt=0.001
Mỗi backend nhận đúng 3 request. Ba dòng log cho thấy ba địa chỉ upstream khác nhau liên tiếp và trường ust=200 nghĩa là backend trả 200.
Một chi tiết đáng chú ý: khi tôi bỏ dòng zone, trên máy 4 core, 9 request cho kết quả 5, 2, 2. Lý do là mỗi worker process của nginx giữ bộ đếm round robin riêng, nên các request đi vào các worker khác nhau lệch tải nhau. Con số cụ thể phụ thuộc số worker và máy của bạn, nhưng nguyên tắc là: nhóm upstream chia sẻ giữa worker cần zone.
Các thuật toán thông dụng, thử bằng cách thêm một dòng vào đầu khối upstream rồi chạy docker exec dv3c-lb nginx -s reload:
| Thuật toán | Dòng cấu hình | Chọn backend thế nào | Hợp khi | Cạm bẫy |
|---|---|---|---|---|
| Round robin | mặc định, hoặc weight=3 trên từng server | Lần lượt, có trọng số | Request đồng đều, backend giống nhau | Request nặng nhẹ chênh lệch thì tải lệch |
| Least connections | least_conn; | Backend đang giữ ít kết nối nhất | Thời gian xử lý mỗi request khác nhau nhiều | Khi mọi backend đều rảnh, nó chọn gần như lần lượt |
| Hash theo IP | ip_hash; | Cùng địa chỉ client luôn tới cùng backend | Cần dính phiên (sticky) đơn giản | Nhiều người sau một NAT dồn vào một backend |
| Hash theo khóa | hash $request_uri consistent; | Cùng URI tới cùng backend | Cache cục bộ theo URL | Backend thêm hay bớt thì một phần khóa bị chuyển |
Thử ip_hash rồi gọi lại 6 request:
sed -i 's/ zone gdev_api 64k;/ zone gdev_api 64k;\n ip_hash;/' conf/default.conf
docker exec dv3c-lb nginx -s reload
for i in $(seq 1 6); do curl -s localhost:$PORT/; done | sort | uniq -c
Kết quả minh họa:
6 backend-1
Cả 6 request vào một backend vì chúng cùng một địa chỉ client. Theo tài liệu nginx, ip_hash dùng ba octet đầu của địa chỉ IPv4 làm khóa, nên mọi client trong cùng một dải /24 cũng dồn chung vào một backend. Với gdev-api đứng sau cổng NAT của văn phòng đối tác, toàn bộ nhân viên của họ có thể chạm đúng một bản. Đó là một cách để có tình huống "một bản gánh hết tải".
Đưa cấu hình về như cũ trước khi làm tiếp:
sed -i '/ip_hash;/d' conf/default.conf
docker exec dv3c-lb nginx -s reload
Về dính phiên (sticky session) nói chung: nó che giấu việc ứng dụng còn lưu trạng thái trong bộ nhớ của từng bản. Khi bản đó chết hoặc bị scale in, phiên mất, và tải không còn đều. Lựa chọn tốt hơn là đưa phiên ra ngoài (Redis, database) để backend nào cũng phục vụ được. Dính phiên bằng cookie (sticky) theo tài liệu nginx chỉ có trong bản thương mại cho tới trước phiên bản 1.29.6; từ phiên bản đó nó có trong bản mã nguồn mở, còn image 1.27 dùng trong lab thì chưa có, nên lab chỉ dùng hash.
L4 và L7: cân bằng theo kết nối hay theo request
Nginx ở trên làm việc ở L7: nó đọc request HTTP nên biết đường dẫn, header, và có thể chia từng request. Một LB L4 chỉ thấy kết nối TCP, chuyển nguyên các byte mà không hiểu bên trong là gì.
| L4 | L7 | |
|---|---|---|
| Nhìn thấy | Địa chỉ IP và cổng, kết nối TCP hoặc UDP | Phương thức, đường dẫn, host, header, cookie |
| Đơn vị cân bằng | Một kết nối | Một request |
| Làm được | Chuyển tiếp rất nhanh, giao thức nào cũng được, giữ nguyên TLS nếu muốn | Định tuyến theo đường dẫn, kết thúc TLS, ghi đè header, thử lại sang backend khác, cache |
| Ví dụ | NLB của AWS, stream của nginx, kube-proxy ở chế độ IPVS | ALB của AWS, nginx http, Envoy, Ingress controller |
Hãy dự đoán: một client mở một kết nối và gửi 6 request trên cùng kết nối đó (keep-alive). Qua LB L4 chúng chia thế nào, qua LB L7 chia thế nào?
Để thử, dựng một LB L4 bằng chính nginx với khối stream. File này thay toàn bộ nginx.conf, lưu thành nginx-l4.conf:
events {}
stream {
upstream gdev_tcp {
zone gdev_tcp 64k;
server dv3c-be1:5678;
server dv3c-be2:5678;
server dv3c-be3:5678;
}
server {
listen 9000;
proxy_pass gdev_tcp;
}
}
docker run -d --name dv3c-l4 --network dv3c-net -p 127.0.0.1:0:9000 -v "$PWD/nginx-l4.conf:/etc/nginx/nginx.conf:ro" nginx:1.27-alpine
PORT4=$(docker port dv3c-l4 9000 | cut -d: -f2)
curl -s $(for i in 1 2 3 4 5 6; do printf "localhost:$PORT4/ "; done) | sort | uniq -c
for i in 1 2 3 4 5 6; do curl -s localhost:$PORT4/; done | sort | uniq -c
curl -s $(for i in 1 2 3 4 5 6; do printf "localhost:$PORT/ "; done) | sort | uniq -c
Kết quả minh họa cho ba lệnh (6 request trên một kết nối qua L4, 6 kết nối riêng qua L4, rồi 6 request trên một kết nối qua L7):
6 backend-1
2 backend-1
2 backend-2
2 backend-3
2 backend-1
2 backend-2
2 backend-3
Phân tích:
- Lệnh thứ nhất: curl gọi cùng một địa chỉ nhiều lần trong một lệnh nên dùng lại kết nối. Qua L4 cả 6 request cùng đi vào
backend-1vì LB chỉ chọn backend một lần khi mở kết nối. - Lệnh thứ hai: sáu lần curl riêng là sáu kết nối riêng, nên L4 chia đều 2, 2, 2. Quy luật không đổi, chỉ đơn vị được đếm là kết nối.
- Lệnh thứ ba: qua L7 chính các request giữ kết nối vẫn chia đều, vì nginx đọc từng request và chọn backend cho mỗi request.
- Hệ quả vận hành: gRPC, HTTP/2 và các client dùng connection pool giữ kết nối hàng giờ. Đứng sau LB L4, các kết nối ấy dính chặt vào một backend, và một bản mới được scale thêm sẽ không nhận được traffic nào cho tới khi client mở kết nối mới. Với loại giao thức này nên dùng LB L7 hoặc cân bằng ở phía client.
Điều gì xảy ra khi một backend chết giữa chừng
Dự đoán trước khi xem sơ đồ: backend-2 bị dừng nhưng nginx vẫn chưa biết. Request kế tiếp tới đúng backend này sẽ nhận lỗi ngay, nhận lỗi sau một lúc, hay vẫn thành công?
Đang tải sơ đồ…
Cách đọc sơ đồ:
- Container đã dừng nên gói tin tới địa chỉ của nó không được ai trả lời. Nginx chờ hết
proxy_connect_timeout(1 giây ở lab) rồi mới coi lần thử là thất bại. Đây là câu trả lời cho dự đoán: không lỗi ngay, mà chờ rồi thành công. - Mặc định
proxy_next_upstreamlàerror timeout, nên nginx tự thử lại cùng request ở backend kế tiếp. Người dùng nhận 200, chỉ chậm hơn đúng bằng thời gian chờ. - Các request dùng phương thức không idempotent (
POST,PATCH,LOCK) theo mặc định không bị thử lại nếu đã gửi đi. Đây là cố ý: gửi lại một lệnh tạo đơn hàng có thể tạo hai đơn. - Mỗi lần thất bại được nginx ghi nhớ. Tới
max_failslần trongfail_timeout, backend bị tạm loại. Phần sau trong lab mô tả chính xác chuyện đó và cái giá của nó.
Hãy xem health check thụ động trả giá bằng người dùng ra sao, và kiểm tra chủ động thay đổi điều đó thế nào. Dự đoán trước khi bấm: sau khi backend-2 chết, bao nhiêu request của người dùng bị chậm trước khi nginx ngừng gửi tới nó?
Đang tải Animation…
Lab 2 (failure lab): dừng một backend và đọc log
Dừng backend-2 rồi gửi 8 request, ghi lại mã và thời gian từng request:
docker stop dv3c-be2
for i in $(seq 1 8); do curl -s -o /dev/null -w "%{http_code} %{time_total}\n" localhost:$PORT/; done
docker logs --since 30s dv3c-lb 2>&1 | grep -E "ust=504|\[warn\]|\[error\]" | tail -6
Kết quả minh họa (giờ, IP và độ trễ nhỏ của bạn sẽ khác):
200 0,001705
200 1,002995
200 0,002083
200 0,001722
200 1,004818
200 0,002807
200 0,001745
200 0,001809
2026/10/23 23:50:01 [error] 21#21: *28 upstream timed out (110: Operation timed out) while connecting to upstream, client: 192.168.96.1, server: , request: "GET / HTTP/1.1", upstream: "http://192.168.96.3:5678/", host: "localhost:32779"
192.168.96.1 "GET / HTTP/1.1" 200 upstream=192.168.96.3:5678, 192.168.96.4:5678 ust=504, 200 rt=1.001
2026/10/23 23:50:02 [warn] 21#21: *35 upstream server temporarily disabled while connecting to upstream, client: 192.168.96.1, server: , request: "GET / HTTP/1.1", upstream: "http://192.168.96.3:5678/", host: "localhost:32779"
2026/10/23 23:50:02 [error] 21#21: *35 upstream timed out (110: Operation timed out) while connecting to upstream, client: 192.168.96.1, server: , request: "GET / HTTP/1.1", upstream: "http://192.168.96.3:5678/", host: "localhost:32779"
192.168.96.1 "GET / HTTP/1.1" 200 upstream=192.168.96.3:5678, 192.168.96.4:5678 ust=504, 200 rt=1.002
Triệu chứng: không có lỗi nào tới người dùng, nhưng đúng hai request chậm 1 giây rồi mọi thứ trở lại bình thường. Đây là câu trả lời cho dự đoán ở phần animation: 2 request, đúng bằng max_fails.
Đọc log từng chỗ:
upstream=192.168.96.3:5678, 192.168.96.4:5678là hai địa chỉ cách nhau dấu phẩy: nginx thử backend thứ nhất rồi thử lại backend thứ hai.ust=504, 200là mã tương ứng: lần thử đầu bị 504 (hết thời gian kết nối), lần thử hai được 200. Mã trả cho người dùng ở cột$statuslà 200.rt=1.001là tổng thời gian, bằng đúngproxy_connect_timeoutcộng thời gian xử lý ở backend thứ hai.- Dòng
[warn] upstream server temporarily disabledxuất hiện ở lần thất bại thứ hai: nginx vừa đánh dấu backend-2 không khả dụng. Sau đó sáu request còn lại nhanh vì nginx không gửi gì tới nó.
Để đếm nhanh số request từng bị thử mà backend không đáp, lọc các dòng có ust=504:
docker logs dv3c-lb 2>&1 | grep -c "ust=504"
Kết quả minh họa (tính ngay sau 8 request ở trên):
2
Hai dòng, đúng bằng hai request chậm. Con số này rất hữu ích khi đối chiếu với báo cáo "thỉnh thoảng bị chậm 1 giây".
Phần đắt giá nhất của kiểu thụ động nằm ở sau 10 giây. Chờ 11 giây rồi gửi 4 request:
sleep 11
for i in 1 2 3 4; do curl -s -o /dev/null -w "%{http_code} %{time_total}\n" localhost:$PORT/; done
Kết quả minh họa:
200 0,002317
200 0,002576
200 1,003796
200 0,003314
Một request lại chậm 1 giây. Hết fail_timeout, nginx thử lại backend-2, và vật thử là chính request của một người dùng. Backend vẫn chết thì người đó chờ thêm 1 giây. Với fail_timeout=10s, mỗi 10 giây có một người dùng trả giá, mãi tới khi backend hồi phục. Bật lại backend để xem nó quay lại:
docker start dv3c-be2
sleep 11
for i in $(seq 1 6); do curl -s localhost:$PORT/; done | sort | uniq -c
Kết quả minh họa:
2 backend-1
2 backend-2
2 backend-3
Hai điều cần lưu ý. Một là nginx phân giải tên container khi khởi động, nên nếu container đổi IP khi khởi động lại thì nginx vẫn dùng IP cũ; trong lab này IP không đổi nên chạy đúng, còn production dùng service discovery hoặc resolver. Hai là với nhóm chỉ có một server, tài liệu nginx nói các tham số max_fails và fail_timeout bị bỏ qua và server đó không bao giờ bị coi là không khả dụng.
Lab 3: connection draining khi deploy
Draining là cách rút một backend: ngừng giao request mới, để request đang xử lý chạy xong. Ta tái hiện bằng một backend chậm, mỗi request mất 6 giây (image python:3.12-slim, khoảng 120 MB), cộng backend-1 làm dự phòng.
docker run -d --name dv3c-slow --network dv3c-net python:3.12-slim python -c "
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
time.sleep(6)
self.send_response(200)
self.end_headers()
self.wfile.write(b'slow-done\n')
ThreadingHTTPServer(('', 8000), Handler).serve_forever()
"
Thay cấu hình nginx bằng nhóm chỉ có dv3c-slow và dv3c-be1 làm dự phòng (backup chỉ nhận request khi các server thường không khả dụng):
log_format lb '$remote_addr "$request" $status upstream=$upstream_addr ust=$upstream_status rt=$request_time';
access_log /dev/stdout lb;
upstream gdev_drain {
server dv3c-slow:8000;
server dv3c-be1:5678 backup;
}
server {
listen 80;
location / {
proxy_pass http://gdev_drain;
}
}
Lưu thành conf/default.conf, nạp lại, rồi gửi một request đang chạy, và trong lúc nó chạy thì đánh dấu dv3c-slow là down và nạp lại cấu hình:
docker exec dv3c-lb nginx -s reload
curl -s -w "in-flight: code=%{http_code} time=%{time_total}s\n" localhost:$PORT/ &
sleep 2
sed -i 's/server dv3c-slow:8000;/server dv3c-slow:8000 down;/' conf/default.conf
docker exec dv3c-lb nginx -s reload
curl -s -w "new request: code=%{http_code}\n" localhost:$PORT/
wait
Kết quả minh họa:
backend-1
new request: code=200
slow-done
in-flight: code=200 time=6.002821s
Phân tích:
- Request đã đi vào backend chậm từ trước lúc reload vẫn chạy tới cuối, trả
slow-donevới mã 200 sau 6 giây. Nó không bị cắt. - Request mới sau reload đi vào
backend-1(dự phòng) và nhận 200 ngay, vì backend chậm đã được đánh dấudown. - Cơ chế: theo tài liệu điều khiển của nginx, khi nạp lại cấu hình, worker cũ ngừng nhận kết nối mới và tiếp tục phục vụ các request hiện có cho tới khi xong. Đây chính là draining ở mức LB.
Với bản chạy thật, quy trình deploy an toàn lặp lại ba bước đó: đưa bản cũ về trạng thái không nhận request mới, đợi request đang chạy xong (hoặc tới thời hạn tối đa), rồi mới tắt. Trong Kubernetes việc đó do Pod bị gỡ khỏi endpoint cộng terminationGracePeriodSeconds đảm nhiệm như đã học ở bài Pod. Nếu bỏ bước giữa, tiến trình bị giết giữa chừng, và đó là cái "rơi vài request sau mỗi lần deploy" ở đầu bài.
Dọn dẹp toàn bộ lab:
docker rm -f dv3c-lb dv3c-l4 dv3c-slow dv3c-be1 dv3c-be2 dv3c-be3
docker network rm dv3c-net
Lỗi thường gặp
| Triệu chứng | Nguyên nhân hay gặp | Cách kiểm chứng |
|---|---|---|
| Một backend nhận gần hết tải | Dùng ip_hash sau NAT, hoặc LB L4 với kết nối giữ lâu | Đếm theo trường upstream trong log; thử L7 hoặc round robin |
| Thỉnh thoảng một request chậm đúng bằng connect timeout | Health check thụ động đang thử lại backend chết | Tìm ust=504, 200 trong access log; xem [warn] temporarily disabled |
| Chia tải lệch dù dùng round robin | Thiếu zone nên mỗi worker đếm riêng | Thêm zone vào khối upstream và đo lại |
| Rớt request mỗi lần deploy | Tắt backend ngay, không draining | Đếm mã 502 hoặc 504 quanh thời điểm deploy |
| Thử lại tạo trùng đơn hàng | Bật proxy_next_upstream non_idempotent cho POST không có khóa chống trùng | Dùng idempotency key hoặc để mặc định |
| Health check xanh nhưng người dùng lỗi | Chỉ kiểm tra cổng TCP, không kiểm tra HTTP | So sánh nc -z và curl như ở bài Availability |
| Backend mới scale thêm không nhận traffic | LB L4 và client giữ kết nối lâu | Cho client đóng kết nối định kỳ, hoặc dùng L7 |
Tự kiểm tra
Hãy trả lời trước khi đọc đáp án ngay bên dưới mỗi câu.
Câu 1. Client gRPC mở một kết nối HTTP/2 tới LB L4 rồi gửi 10.000 request trên đó. Có bao nhiêu backend nhận request, và làm sao sửa?
Đáp án: một backend, vì LB L4 chọn backend một lần khi mở kết nối. Sửa bằng LB L7 hiểu HTTP/2, hoặc cân bằng phía client, hoặc bắt client đóng và mở lại kết nối định kỳ.
Câu 2. Với max_fails=3 fail_timeout=30s và proxy_connect_timeout 2s, một backend vừa chết. Tối đa bao nhiêu người dùng bị chậm 2 giây trước khi nó bị loại, và mỗi bao lâu có thêm một người bị chậm?
Đáp án: 3 người bị chậm 2 giây (đúng bằng max_fails). Sau khi bị loại, cứ mỗi 30 giây nginx lại thử bằng một request thật, nên mỗi 30 giây thêm một người chờ nếu backend vẫn chết.
Câu 3. Vì sao nên dùng nginx -s reload thay vì khởi động lại container nginx khi thay đổi nhóm upstream?
Đáp án: reload để worker cũ phục vụ nốt request đang chạy rồi mới thoát, còn khởi động lại cắt mọi kết nối đang có.
Nếu chỉ nhớ 3 điều
- L4 cân bằng theo kết nối, L7 theo request: client giữ kết nối lâu sẽ dồn vào một backend sau LB L4, và
ip_hashsau NAT cũng gây hiệu ứng tương tự. - Health check thụ động dùng người dùng thật làm vật thử: số request chậm bằng
max_fails, rồi mỗifail_timeoutthêm một lần. Kiểm tra chủ động tránh được điều này nhưng nginx mã nguồn mở không có. - Rút backend phải qua draining: ngừng nhận request mới, đợi request đang chạy hoàn tất, rồi mới tắt. Hãy đọc log với
upstream,ustvàrtđể biết chuyện gì thật sự xảy ra.
Bài tiếp theo là Proxy Server, vai trò rộng hơn của chính nginx: reverse proxy, TLS, cache, rate limit và hai mã 502 với 504.
Tài liệu chính thức: module upstream của nginx, module proxy của nginx, điều khiển nginx bằng tín hiệu và hướng dẫn HTTP load balancer của nginx.