Nginx reverse proxy đứng trước order-service

4 câu hỏi~25 phútTừ khóa: reverse proxy, upstream, proxy_pass, proxy_set_header, gzip, limit_req, proxy_read_timeout, 502, 504, event loop

Nginx và Tomcat khác nhau thế nào? Sao làm web lại cần cả hai?

Đây là cặp rất hay gặp khi deploy Spring Boot. Điểm quan trọng nhất: Nginx và Tomcat không làm cùng một việc. Ở bài 9.5 ta đã dùng Nginx để terminate TLS cho api.shop.example. Giờ nhìn kỹ nó.

1. Bài toán

order-service là Spring Boot, chạy bằng java -jar, bên trong có embedded Tomcat nghe port 8080:

Client ── GET /orders/42 ──► Tomcat :8080 ──► Spring MVC ──► OrderController ──► Postgres

Tomcat = Web Server + Servlet Container: nhận HTTP và đưa vào code Java của bạn. Project nhỏ thì chỉ Tomcat là đủ.

2. Vấn đề khi cho Internet vào thẳng Tomcat:8080

3. Giải pháp: Nginx làm reverse proxy

                    ┌──────────────┐
Internet ──────────►│ Nginx :443   │  process riêng
                    └──────┬───────┘
                           ▼
                    ┌──────────────────────┐
                    │ JVM: Tomcat :8080    │
                    │      Spring Boot     │
                    └──────────────────────┘
NginxTomcat
Reverse proxy, routing requestChạy Java web application
Load balancingServlet container
SSL/TLS terminationChạy Servlet / Spring MVC
Serve static files, gzipXử lý business logic qua app
Rate limiting, giới hạn bodyKết nối DB, service khác

Nhớ bằng 2 câu hỏi: Nginx: "request này nên chuyển tới đâu?". Tomcat: "request đã vào Java rồi, chạy nó theo Servlet/Spring thế nào?".

4. Code từng bước

Bước 1: Cài Nginx (đã cài ở bài 9.5)

sudo apt install -y nginx
sudo nginx -v

Bước 2: Config /etc/nginx/conf.d/order-service.conf

# Ngoài server{}: vùng nhớ rate limit, 10 req/s mỗi IP
limit_req_zone $binary_remote_addr zone=api_rl:10m rate=10r/s;

upstream order_backend {
    server 10.0.2.4:8080 max_fails=3 fail_timeout=10s;
    server 10.0.2.5:8080 max_fails=3 fail_timeout=10s;
    keepalive 32;                       # giữ connection tới backend
}

server {
    listen 443 ssl;
    http2 on;
    server_name api.shop.example;
    ssl_certificate     /etc/letsencrypt/live/api.shop.example/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.shop.example/privkey.pem;

    client_max_body_size 2m;

    gzip on;
    gzip_types application/json text/css application/javascript;
    gzip_min_length 1024;

    # File tĩnh: Nginx trả luôn, không qua JVM
    location /static/ {
        root /var/www/shop;
        expires 7d;
    }

    location / {
        limit_req zone=api_rl burst=20 nodelay;

        proxy_pass http://order_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";              # cần cho keepalive upstream
        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_connect_timeout 3s;
        proxy_send_timeout    30s;
        proxy_read_timeout    30s;     # chờ backend trả byte tiếp theo tối đa 30s
    }
}

Bước 3: Spring Boot phía sau

server:
  port: 8080
  forward-headers-strategy: native
  tomcat:
    threads.max: 200
    connection-timeout: 5s
  shutdown: graceful

Bước 4: Kiểm tra và test

sudo nginx -t && sudo systemctl reload nginx     # reload không rớt connection
curl -I https://api.shop.example/orders/42
curl -sI -H "Accept-Encoding: gzip" https://api.shop.example/orders | grep -i content-encoding
for i in $(seq 1 50); do curl -s -o /dev/null -w "%{http_code}\n" https://api.shop.example/orders/42; done | sort | uniq -c
#   30 200
#   20 503    <- limit_req chặn (đổi bằng limit_req_status 429;)
tail -f /var/log/nginx/error.log

5. Cách hoạt động

Client ──TLS──► Nginx :443
                 1. terminate TLS
                 2. limit_req: quá rate → 503/429
                 3. location /static/ → đọc đĩa, trả luôn
                 4. location /  → chọn upstream (round-robin)
                       │ HTTP/1.1 keep-alive, thêm X-Forwarded-*
          ┌────────────┴────────────┐
          ▼                         ▼
   10.0.2.4:8080              10.0.2.5:8080
   Tomcat thread pool         Tomcat thread pool
   → DispatcherServlet → OrderController

Event loop vs thread-per-request.

Nginx: vài worker process (≈ số CPU), mỗi worker 1 event loop (epoll)
   worker ──► [conn1 chờ đọc][conn2 chờ ghi][conn3 ...][conn10k ...]
   socket nào sẵn sàng thì xử lý, không ai chiếm thread khi đang chờ

Tomcat (Spring MVC): mỗi request giữ 1 thread từ đầu tới cuối
   thread-1 ── đọc request ── gọi DB (chờ, block) ── ghi response
   thread-2 ── ...
   200 thread bận → request 201 phải xếp hàng

Vì vậy Nginx giữ hàng chục nghìn connection (kể cả client chậm) với ít RAM, nhận đủ request rồi mới chuyển nhanh cho Tomcat qua mạng nội bộ (buffering). Tomcat chỉ bận thread cho phần việc thật. Java 21 virtual threads giảm chi phí thread phía Tomcat, nhưng không thay vai trò proxy của Nginx.

6. So sánh: 502 vs 504

MãNghĩaNguyên nhân thường gặpXem ở đâu
502 Bad GatewayNginx nói chuyện với backend thất bạiApp chết / đang restart, sai port, connection refused, backend đóng connection giữa chừngconnect() failed (111: Connection refused) trong error.log
504 Gateway TimeoutBackend không trả lời kịpQuery DB chậm, deadlock, hết thread Tomcat, vượt proxy_read_timeoutupstream timed out (110)
503Không có upstream khả dụng / bị rate limitMọi server bị đánh dấu fail, limit_reqerror.log

Đừng sửa 504 bằng cách tăng proxy_read_timeout lên 300s. Tìm vì sao backend chậm; việc dài thì chuyển sang async (trả 202 + job id).

7. Khi nào nên dùng / không nên dùng

Mình thấy mỗi process tự mở một port rồi lắng nghe dữ liệu ở port đó. Vậy Java có tự làm được việc này mà không cần Tomcat không? Mình vẫn chưa rõ vai trò của Nginx và Tomcat lắm.

Đúng. Java hoàn toàn tự mở port và nhận HTTP mà không cần Tomcat. Đây chính là chìa khóa để phân biệt.

try (ServerSocket server = new ServerSocket(8080)) {
    while (true) {
        Socket client = server.accept();
        // tự đọc byte, tự parse "GET /orders/42 HTTP/1.1", header, body
        // tự lo thread cho nhiều client, tự ghi "HTTP/1.1 200 OK\r\n..."
    }
}

Chạy được, nhưng bạn phải tự viết: parse HTTP, keep-alive, chunked, thread pool, map URL → code. Tomcat là một chương trình Java đã viết sẵn phần đó.

Điểm quan trọng: với Spring Boot, Tomcat không phải process riêng. Nó là library trong cùng JVM:

Linux VM
├── Process 1: nginx (master + workers)   listen :443
│                │ HTTP
│                ▼
└── Process 2: java -jar order-service.jar
                 └── Embedded Tomcat :8080 → Spring MVC → @RestController

"Nhưng Nginx cũng nhận HTTP, Tomcat cũng nhận HTTP mà?" Đúng, cả hai đều listen port, accept connection, hiểu HTTP. Khác ở mục đích: Nginx (viết bằng C) điều phối request đi đâu; Tomcat đưa request vào Java theo chuẩn Servlet. Bài 9.9 sẽ đi sâu Servlet và DispatcherServlet bên trong Tomcat.

Sáng nay deploy bản mới xong user báo lỗi 502 vài giây. Sao vậy?

Bạn đoán đúng là do deploy. Lúc restart, port 8080 đóng → Nginx connect() bị refused → 502. Cách giảm:

# Đánh dấu tạm 1 server khi bảo trì
upstream order_backend {
    server 10.0.2.4:8080 down;
    server 10.0.2.5:8080;
    keepalive 32;
}

Phỏng vấn hỏi Nginx khác Tomcat thì trả lời sao?

Q1. Nginx khác Tomcat thế nào? Sao cần cả hai?

🇻🇳 Tiếng Việt:

Tomcat là Servlet container chạy ứng dụng Java như Spring Boot; trong Spring Boot nó được nhúng trong cùng JVM. Nginx là process riêng đứng phía trước, làm reverse proxy, load balancing, TLS termination, static file, gzip và rate limit. Không bắt buộc có cả hai: app nhỏ chỉ cần embedded Tomcat, Nginx được thêm khi cần tầng proxy.

🇬🇧 English:

Tomcat is a servlet container that runs Java web applications such as Spring Boot, where it is embedded in the same JVM. Nginx is a separate process in front that acts as a reverse proxy: load balancing, TLS termination, static files, gzip and rate limiting. Both are not mandatory; a small app can run on embedded Tomcat alone, and Nginx is added when a proxy layer is needed.

Q2. 502 và 504 khác nhau thế nào?

🇻🇳 Tiếng Việt:

502 là Nginx kết nối hoặc nhận response từ backend thất bại, thường do app chết, sai port hoặc đang restart. 504 là backend không trả lời trong proxy_read_timeout, thường do query chậm hoặc hết thread. Em xem error.log của Nginx để phân biệt rồi điều tra phía app.

🇬🇧 English:

502 means Nginx failed to connect to or got an invalid response from the backend, typically because the app is down, restarting or on the wrong port. 504 means the backend did not respond within proxy_read_timeout, usually due to slow queries or thread-pool exhaustion. I check the Nginx error log to tell them apart, then investigate the app.

Q3. Vì sao Nginx chịu được nhiều connection hơn Tomcat?

🇻🇳 Tiếng Việt:

Nginx dùng mô hình event-driven: vài worker, mỗi worker một event loop với epoll, xử lý hàng nghìn connection không cần một thread mỗi connection. Tomcat với Spring MVC dùng thread-per-request, mỗi request giữ một thread kể cả lúc chờ I/O. Đặt Nginx trước giúp hấp thụ client chậm và buffer request.

🇬🇧 English:

Nginx is event-driven: a few workers, each running an epoll event loop, handle thousands of connections without a thread per connection. Tomcat with Spring MVC uses thread-per-request, so each request holds a thread even while waiting on I/O. Putting Nginx in front absorbs slow clients and buffers requests.

Cách nhớ ngắn gọn


Tự kiểm tra

Spring Boot có embedded Tomcat thì Tomcat có phải process riêng không?

Không. Nó là library trong cùng JVM process với app.

Thiếu proxy_set_header X-Forwarded-Proto gây lỗi gì?

App tưởng request là HTTP, có thể tạo redirect/link http:// sai, gây vòng lặp redirect.

keepalive 32 trong upstream cần thêm gì để có tác dụng?

proxy_http_version 1.1 và proxy_set_header Connection "".

Log có upstream timed out (110) thì client thấy mã gì?

504 Gateway Timeout.

Vì sao đặt Nginx trước giúp Tomcat với client mạng chậm?

Nginx buffer request/response bằng event loop, Tomcat chỉ giữ thread trong thời gian xử lý thật, không bị client chậm chiếm thread.