Một request đi từ client tới Spring Boot như thế nào?
Bắt đầu phần Networking nhé. Mình viết Spring Boot quen rồi nhưng không hình dung được request đi qua những gì trước khi tới controller.
Chương này dùng một project chạy xuyên suốt: order-service (Spring Boot, port 8080) deploy trên một Azure VM Ubuntu, sau này đặt sau Nginx. Database Postgres nằm trên một VM khác, port 5432. Một developer ngồi ở laptop (hoặc một VM khác) gọi vào order-service. Bài này vẽ bản đồ tổng thể; các bài sau đi sâu từng chặng.
1. Bài toán: curl một dòng, nhưng bên dưới là cả chục bước
curl https://api.example.com/orders/123
Developer chỉ thấy: gõ lệnh, nhận JSON. Nhưng khi nó chậm 5 giây, hoặc báo Connection timed out, bạn phải biết nó hỏng ở chặng nào.
2. Vấn đề: không có bản đồ thì debug bằng cách đoán
- "API chậm" có thể do DNS, do TLS handshake, do Nginx, do Tomcat hết thread, hay do query DB.
- "Không gọi được" có thể do sai domain, port chưa mở, app chỉ listen
127.0.0.1. - Không biết chặng nào thì sẽ sửa code Java trong khi lỗi nằm ở firewall.
3. Giải pháp: chia request thành các chặng
| # | Chặng | Câu hỏi nó trả lời | Bài |
|---|---|---|---|
| 1 | DNS | api.example.com là IP nào? | 9.4 |
| 2 | IP + TCP | Tới máy đó, mở kết nối vào port nào? | 9.2, 9.3 |
| 3 | TLS | Có đúng server thật không? Mã hóa ra sao? | 9.5 |
| 4 | HTTP | Gửi method, path, header, body gì? | 9.6 |
| 5 | Load balancer / NAT | Request vào VM nào? IP public đổi thành private ra sao? | 9.7 |
| 6 | Nginx | Reverse proxy chuyển request sang 127.0.0.1:8080 | 9.8 |
| 7 | Tomcat | Thread nào xử lý? Hết thread thì sao? | 9.9 |
| 8 | Controller → DB | Lấy connection từ HikariCP, gọi Postgres :5432 | 9.10 |
| * | Timeout ở mọi chặng | Chờ bao lâu thì bỏ cuộc? | 9.10 |
| * | Debug | Dùng công cụ nào cho chặng nào? | 9.11 |
4. Từng bước trên order-service
Bước 1: App đơn giản nhất:
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderRepository repo;
public OrderController(OrderRepository repo) {
this.repo = repo;
}
@GetMapping("/{id}")
public ResponseEntity<Order> get(@PathVariable Long id) {
return repo.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
Bước 2: Config (application.yml), password lấy từ biến môi trường:
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://db-vm.internal:5432/orders
username: ${DB_USER}
password: ${DB_PASSWORD}
Bước 3: Nhìn thời gian từng chặng bằng curl -w:
curl -o /dev/null -s -w '
dns: %{time_namelookup}s
tcp: %{time_connect}s
tls: %{time_appconnect}s
ttfb: %{time_starttransfer}s
total: %{time_total}s
' https://api.example.com/orders/123
dns: 0.012s
tcp: 0.045s
tls: 0.110s
ttfb: 0.380s <-- server xử lý ~270ms (Tomcat + DB)
total: 0.381s
Các số là cộng dồn. ttfb - tls ≈ thời gian server xử lý. Nếu dns mất 2 giây thì đừng tối ưu SQL.
5. Cách hoạt động: bản đồ toàn cảnh
Laptop developer
|
| (1) DNS: api.example.com -> 203.0.113.10
v
Internet
|
| (2) TCP SYN tới 203.0.113.10:443 (3-way handshake)
| (3) TLS handshake (certificate, key)
| (4) HTTP: GET /orders/123
v
Azure: Public IP / Load Balancer (NAT -> private IP 10.0.1.4)
| NSG cho phép inbound 443?
v
Azure VM (Ubuntu) -- firewall ufw cho phép 443?
|
v
Nginx :443 (TLS kết thúc ở đây)
| proxy_pass http://127.0.0.1:8080
v
Tomcat :8080 (embedded trong Spring Boot)
| acceptor nhận connection, worker thread "http-nio-8080-exec-7"
v
DispatcherServlet -> OrderController.get(123)
| HikariCP lấy connection
v
Postgres VM 10.0.2.5:5432 (chỉ cho subnet app vào)
|
v
Response đi ngược lại: JSON -> Tomcat -> Nginx -> TLS -> TCP -> Laptop
6. So sánh: lỗi thường gặp theo chặng
| Triệu chứng | Chặng nghi ngờ |
|---|---|
Could not resolve host | DNS |
Connection refused | TCP: không có process listen port đó |
Connection timed out | TCP: firewall/NSG chặn |
SSL certificate problem | TLS |
502 Bad Gateway | Nginx không gọi được Tomcat |
504 Gateway Timeout | Tomcat/DB trả lời quá chậm |
| Chậm dần khi tải cao | Tomcat thread pool / Hikari pool cạn |
7. Khi nào cần bản đồ này
- Cần: debug lỗi kết nối, latency cao, deploy lên VM/cloud, phỏng vấn system design.
- Không cần đào sâu: khi chạy local mọi thứ trên
localhost, hầu hết chặng 1, 3, 5 biến mất. Đó là lý do "chạy local được, lên server thì không".
Chạy local thì gọi localhost:8080 là xong, sao lên VM lại thêm nhiều thứ vậy?
Đúng rồi, local đơn giản vì client và server cùng một máy:
Local: curl -> loopback (127.0.0.1) -> Tomcat :8080
VM: curl -> DNS -> Internet -> NSG -> ufw -> Nginx -> Tomcat :8080
Loopback không đi qua card mạng, không qua firewall cloud, thường không có TLS. Lên VM, mỗi lớp thêm vào là một chỗ có thể chặn. Bài 9.3 sẽ đi từng lớp đó.
Trong sơ đồ có cả Nginx lẫn Tomcat, mỗi cái lo phần nào?
Cả hai đều là HTTP server, nhưng vai trò khác:
- Nginx: đứng ngoài cùng, giữ TLS certificate, phục vụ static file, giới hạn request, chuyển tiếp tới một hoặc nhiều backend. Rất giỏi giữ hàng chục nghìn connection.
- Tomcat: servlet container chạy code Java của bạn. Mỗi request chiếm một worker thread (mặc định tối đa 200).
Nhờ Nginx đứng trước, Tomcat chỉ cần listen 127.0.0.1:8080, không lộ ra Internet. Chi tiết ở bài 9.8 và 9.9.
Phỏng vấn hỏi "chuyện gì xảy ra khi gõ URL vào trình duyệt" thì trả lời sao?
Q1. Chuyện gì xảy ra khi client gọi https://api.example.com/orders/123?
🇻🇳 Tiếng Việt:
Đầu tiên client resolve DNS để lấy IP. Sau đó mở TCP connection tới port 443 bằng 3-way handshake, rồi làm TLS handshake để xác thực server và thống nhất key. Tiếp theo gửi HTTP request. Request đi qua load balancer, tới Nginx; Nginx kết thúc TLS và proxy sang Tomcat ở 8080. Tomcat giao cho một worker thread, DispatcherServlet gọi controller, controller lấy connection từ HikariCP để query Postgres. Response đi ngược lại cùng đường.
🇬🇧 English:
First the client resolves the domain via DNS. Then it opens a TCP connection to port 443 with a three-way handshake, followed by a TLS handshake to verify the server and agree on keys. Then it sends the HTTP request. The request goes through the load balancer to Nginx, which terminates TLS and proxies to Tomcat on port 8080. Tomcat assigns a worker thread, the DispatcherServlet calls the controller, and the controller borrows a connection from HikariCP to query Postgres. The response travels back the same way.
Q2. API chậm, bạn khoanh vùng thế nào?
🇻🇳 Tiếng Việt:
Em tách theo chặng: dùng
curl -wđể xem thời gian DNS, TCP connect, TLS và time-to-first-byte. Nếu TTFB lớn thì vấn đề ở server: xem access log Nginx, metric Tomcat thread, Hikari pending connection và slow query của DB. Nếu DNS hoặc connect lớn thì là vấn đề mạng.
🇬🇧 English:
I split it by stage:
curl -wshows DNS, TCP connect, TLS and time to first byte. If TTFB is high, the problem is on the server side, so I check the Nginx access log, Tomcat thread metrics, Hikari pending connections and slow queries. If DNS or connect time is high, it is a network problem.
Cách nhớ ngắn gọn
- DNS → TCP → TLS → HTTP → LB → Nginx → Tomcat thread → Controller → DB.
- Mỗi chặng có lỗi đặc trưng: resolve, refused, timeout, certificate, 502, 504.
curl -wcho biết thời gian từng chặng.- Local "chạy được" vì loopback bỏ qua gần hết các chặng.
Tự kiểm tra
Thứ tự các chặng từ client tới controller?
DNS, TCP handshake, TLS handshake, HTTP request, load balancer/NAT, Nginx, Tomcat worker thread, DispatcherServlet, controller, (DB qua connection pool).
502 và 504 từ Nginx nói lên điều gì?
502: Nginx không kết nối được/nhận phản hồi lỗi từ backend. 504: backend trả lời quá chậm, vượt timeout của Nginx.
Trong curl -w, làm sao ước lượng thời gian server xử lý?
Lấy time_starttransfer - time_appconnect (hoặc trừ time_connect nếu không có TLS).
Vì sao để Tomcat listen 127.0.0.1 khi có Nginx?
Chỉ Nginx trên cùng máy gọi được Tomcat; Tomcat không lộ trực tiếp ra Internet.