Nginx from Zero to Production · Part 3 — Reverse Proxy & Load Balancing
Put Nginx in front of a real app: proxy_pass, the headers you must forward, upstream pools, load-balancing algorithms, health checks and failover, and WebSocket proxying — with a runnable Node demo and exercises.
Đến giờ Nginx mới phục vụ file từ đĩa. Giờ tới việc thật: Nginx làm reverse proxy đứng trước ứng dụng của bạn. Đây là cách gần như mọi app Node, Python, Go, Rails hay Java được đưa ra internet.
Ta cũng sẽ biến một proxy thành load balancer qua nhiều instance app.
1. “Reverse proxy” nghĩa là gì
Forward proxy đứng trước client (vd proxy công ty che nhân viên). Reverse proxy đứng trước server — client tưởng đang nói chuyện với một server, nhưng Nginx lặng lẽ chuyển tiếp tới backend phía sau.
Browser ──HTTP──► NGINX :80 ──HTTP──► Node app :3000
◄──────── (public) ◄──────── (private, not exposed)
Tại sao đặt Nginx trước một app vốn đã nói được HTTP?
- Đầu cuối TLS — Nginx xử lý HTTPS để app của bạn đơn giản (Phần 4).
- Một cổng public — phục vụ file tĩnh, nhiều app, và API dưới một domain.
- Cân bằng tải — toả ra nhiều instance app.
- Bảo vệ — giới hạn tốc độ, giới hạn kích thước request, giấu chi tiết backend.
2. Một backend để proxy
Hãy tạo một backend nhỏ để phần sau cụ thể. Server HTTP nào cũng được; đây là một cái Node + TypeScript không cần thư viện:
// server.ts — run several copies on different ports to test load balancing
import { createServer } from 'node:http';
// PORT lets us launch the same code on 3000, 3001, 3002…
const port = Number(process.env.PORT ?? 3000);
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
// Echo the port so we can SEE which instance answered.
res.end(JSON.stringify({ from: port, path: req.url }));
});
server.listen(port, () => console.log(`backend up on :${port}`));
npx tsx server.ts # one instance on :3000
PORT=3001 npx tsx server.ts # in another terminal
PORT=3002 npx tsx server.ts # and another
3. proxy_pass — chuyển tiếp tới backend
Reverse proxy tối giản chỉ một directive:
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://127.0.0.1:3000; # forward everything to the Node app
}
}
sudo nginx -t && sudo nginx -s reload
curl http://localhost/hello
# → {"from":3000,"path":"/hello"}
Nginx nhận request ở :80 và chuyển tới app ở :3000. Trình duyệt không bao giờ thấy :3000.
Cái bẫy dấu gạch chéo cuối
Cái này bẫy tất cả mọi người:
location /api/ {
proxy_pass http://127.0.0.1:3000; # NO trailing slash → keeps /api/ prefix
# request /api/users → backend gets /api/users
}
location /api/ {
proxy_pass http://127.0.0.1:3000/; # WITH trailing slash → strips /api/
# request /api/users → backend gets /users
}
Quy tắc: dấu / cuối trên proxy_pass cắt bỏ prefix location đã khớp; không có dấu / thì giữ lại. Chọn tuỳ theo backend của bạn có mong đợi prefix /api hay không.
4. Các header bạn BẮT BUỘC chuyển tiếp
Mặc định backend mất thông tin về request gốc — nó thấy kết nối đến từ Nginx (127.0.0.1), không phải client thật. Chuyển tiếp sự thật một cách tường minh:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host; # original Host header
proxy_set_header X-Real-IP $remote_addr; # real client IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # http or https
}
Vì sao mỗi cái quan trọng:
- app của bạn cần hostname thật để routing, tạo link, cookie.
- thiếu chúng, mọi khách trông như
127.0.0.1trong log và rate limiter của app. - báo cho app rằng user vào qua https dù Nginx→app là http thường; framework dùng cái này để tạo URL redirect đúng và đặt cookie
Secure.
Một phần rất lớn lỗi “vòng lặp redirect” và “sai IP trong log” đến từ việc quên bốn dòng này. Đặt chúng vào một snippet rồi
includeở mọi nơi (ta sẽ làm ở Phần 5).
5. upstream — định nghĩa nhóm backend
Để cân bằng tải, đặt tên một nhóm server bằng block upstream, rồi proxy_pass tới tên đó:
# in the http context, OUTSIDE server { }
upstream app_pool {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
server {
listen 80;
location / {
proxy_pass http://app_pool; # proxy to the pool, not one host
proxy_set_header Host $host;
}
}
sudo nginx -t && sudo nginx -s reload
curl http://localhost/ ; curl http://localhost/ ; curl http://localhost/
# → {"from":3000,...} {"from":3001,...} {"from":3002,...}
Giá trị from xoay vòng — Nginx đang chia request qua cả ba instance.
6. Thuật toán cân bằng tải
Chọn cách Nginx chọn backend tiếp theo:
upstream app_pool {
# round_robin; # DEFAULT — rotate evenly through servers
least_conn; # send to the server with fewest active connections
# ip_hash; # same client IP always → same server (sticky sessions)
server 127.0.0.1:3000 weight=3; # gets 3× the traffic (beefier box)
server 127.0.0.1:3001;
server 127.0.0.1:3002 backup; # only used when others are down
}
| Thuật toán | Khi nào dùng |
|---|---|
round_robin (default) | App stateless, request đều nhau |
least_conn | Request dài ngắn khác nhau |
ip_hash | Cần dính phiên mà không có session store chung |
Bổ ngữ: weight=N gửi nhiều traffic hơn theo tỉ lệ tới server mạnh; backup đánh dấu dự phòng; down loại một cái ra mà không xoá dòng.
Ưu tiên backend stateless + session store chung (Redis) để mọi instance phục vụ được mọi user — khi đó bạn chẳng cần
ip_hash.
7. Health check & chuyển dự phòng
Nginx bản mã nguồn mở làm health check bị động: nếu một backend lỗi quá nhiều lần, nó bị đánh dấu không khả dụng một lúc:
upstream app_pool {
server 127.0.0.1:3000 max_fails=3 fail_timeout=10s;
server 127.0.0.1:3001 max_fails=3 fail_timeout=10s;
}
- sau 3 lần thử thất bại…
- …ngừng gửi traffic tới đó 10 giây, rồi thử lại.
Test trực tiếp: tắt instance :3000 và cứ curl tiếp — sau vài lần lỗi Nginx dồn mọi thứ sang các cái còn sống, không hiện lỗi cho user. Health check chủ động — Nginx thăm dò /health theo lịch — là tính năng của Nginx Plus.)
8. Timeout, buffering & kích thước body
Mặc định proxy hợp lý mà bạn sẽ muốn ở gần như mọi nơi:
location / {
proxy_pass http://app_pool;
proxy_connect_timeout 5s; # max time to CONNECT to backend
proxy_send_timeout 60s; # max time to SEND the request
proxy_read_timeout 60s; # max time to wait for a response chunk
client_max_body_size 10m; # reject uploads bigger than 10 MB (413)
proxy_buffering on; # buffer the backend response (default on)
}
Hai cái bẫy:
- Mặc định
client_max_body_sizelà 1 MB — upload lớn hơn sẽ lỗi 413 Request Entity Too Large cho tới khi bạn nâng lên. - Với response streaming (Server-Sent Events, log trực tiếp), tắt
proxy_buffering off;tronglocationđó, nếu không Nginx sẽ giữ luồng và client không thấy gì cho tới khi kết thúc.
9. Proxy WebSocket
WebSocket bắt đầu là HTTP rồi nâng cấp kết nối. Nginx sẽ không chuyển tiếp việc nâng cấp trừ khi bạn bảo nó:
location /ws/ {
proxy_pass http://app_pool;
proxy_http_version 1.1; # upgrades require HTTP/1.1
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; # the magic that allows ws://
proxy_read_timeout 3600s; # keep idle sockets alive (1h)
}
Quên header Upgrade/Connection thì WebSocket của bạn “kết nối rồi đóng ngay” — một lỗi rất phổ biến, rất khó hiểu.
10. Tóm tắt
proxy_passchuyển request tới backend; dấu gạch chéo cuối quyết định prefix có bị cắt không.- Luôn chuyển tiếp
Host,X-Real-IP,X-Forwarded-For,X-Forwarded-Proto. upstream+proxy_pass http://namecho bạn cân bằng tải; chọnround_robin/least_conn/ip_hash.max_fails+fail_timeoutcho chuyển dự phòng bị động.- Để ý
client_max_body_size, buffering cho streaming, và header nâng cấp WebSocket.
Tiếp — Phần 4: TLS/HTTPS, bảo mật & hiệu năng: chứng chỉ, HTTP/2, gzip/brotli, caching, và giới hạn tốc độ.
Bài tập
- Proxy một app thật: chạy server Node ở :3000 và proxy
/tới nó; xác nhậncurl http://localhost/trả JSON từ :3000. - Dấu gạch chéo cuối: chứng minh khác biệt giữa
proxy_pass http://127.0.0.1:3000;và.../;dướilocation /api/bằng cách xempathtrong JSON echo. - Cân bằng tải: chạy ba instance (3000/3001/3002), định nghĩa
upstream, và xemfromxoay vòng qua 9 request. - Chuyển dự phòng: với
max_fails=2 fail_timeout=10s, tắt một instance và xác nhận traffic chuyển sang các cái khác mà client không lỗi. - IP client thật: thêm bốn dòng
proxy_set_header, logX-Forwarded-Fortrong app Node, và xác nhận bạn thấy IP thật thay vì127.0.0.1. - Nâng cao: đặt
client_max_body_size 1m, upload file 2 MB, quan sát 413, rồi nâng giới hạn và thành công.