Nginx reverse proxy đứng trước order-service
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
- Port 80/443 cần quyền root; chạy JVM bằng root là rủi ro.
- TLS, cert renew, redirect HTTP→HTTPS phải làm trong app.
- Giờ có 2 instance (bài 9.7) → ai chia tải?
- Client mạng chậm giữ thread Tomcat lâu (slow client), mỗi connection chậm chiếm một thread trong pool 200.
- File tĩnh (ảnh, JS) cũng đi qua JVM, phí.
- Không có chỗ tập trung để rate limit, gzip, chặn request quá lớn.
3. Giải pháp: Nginx làm reverse proxy
┌──────────────┐
Internet ──────────►│ Nginx :443 │ process riêng
└──────┬───────┘
▼
┌──────────────────────┐
│ JVM: Tomcat :8080 │
│ Spring Boot │
└──────────────────────┘
| Nginx | Tomcat |
|---|---|
| Reverse proxy, routing request | Chạy Java web application |
| Load balancing | Servlet container |
| SSL/TLS termination | Chạy Servlet / Spring MVC |
| Serve static files, gzip | Xử lý business logic qua app |
| Rate limiting, giới hạn body | Kế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ĩa | Nguyên nhân thường gặp | Xem ở đâu |
|---|---|---|---|
| 502 Bad Gateway | Nginx nói chuyện với backend thất bại | App chết / đang restart, sai port, connection refused, backend đóng connection giữa chừng | connect() failed (111: Connection refused) trong error.log |
| 504 Gateway Timeout | Backend không trả lời kịp | Query DB chậm, deadlock, hết thread Tomcat, vượt proxy_read_timeout | upstream timed out (110) |
| 503 | Không có upstream khả dụng / bị rate limit | Mọi server bị đánh dấu fail, limit_req | error.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
- Nên: một VM tự quản, cần TLS, nhiều instance, static file, rate limit đơn giản.
- Có thể bỏ: đã có Azure Application Gateway/ALB làm TLS + LB, hoặc chạy trong Kubernetes với Ingress (thường chính là Nginx Ingress).
- Không dùng Nginx thay API Gateway khi cần logic theo service (JWT, route theo service discovery): đó là việc của Spring Cloud Gateway.
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:
- Có 2 upstream: Nginx gặp lỗi kết nối sẽ thử server kế tiếp (
proxy_next_upstream error timeout;là mặc định cho lỗi/timeout). Deploy lần lượt từng instance. server.shutdown: gracefulđể request đang chạy hoàn tất trước khi tắt.- Không retry POST tự động:
proxy_next_upstreammặc định không retry request non-idempotent (POST) sau khi đã gửi đi. Giữ như vậy để tránh tạo trùng đơn.
# Đá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
- Nginx: "gửi request đi đâu?". Tomcat: "chạy request trong Java thế nào?".
- upstream + proxy_pass + proxy_set_header X-Forwarded-*.
- 502 = không nói chuyện được với backend; 504 = backend quá chậm.
- Nginx event loop; Tomcat thread-per-request.
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.