jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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.1 trong 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ánKhi nào dùng
round_robin (default)App stateless, request đều nhau
least_connRequest dài ngắn khác nhau
ip_hashCầ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_size1 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; trong location đó, 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_pass chuyể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://name cho bạn cân bằng tải; chọn round_robin / least_conn / ip_hash.
  • max_fails + fail_timeout cho 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

  1. Proxy một app thật: chạy server Node ở :3000 và proxy / tới nó; xác nhận curl http://localhost/ trả JSON từ :3000.
  2. 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;.../; dưới location /api/ bằng cách xem path trong JSON echo.
  3. Cân bằng tải: chạy ba instance (3000/3001/3002), định nghĩa upstream, và xem from xoay vòng qua 9 request.
  4. 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.
  5. IP client thật: thêm bốn dòng proxy_set_header, log X-Forwarded-For trong app Node, và xác nhận bạn thấy IP thật thay vì 127.0.0.1.
  6. 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.