Timeout ở từng tầng: từ client đến database
Tiếp đi. Bài trước nói payment chậm làm hết thread, vậy timeout đặt ở đâu?
Ở bài 9.9, payment-service chậm 10 giây làm 200 Tomcat thread của order-service bị giữ. Lý do gốc: không ai đặt timeout, nên thread chờ mãi. Bài này đi qua từng tầng và đặt timeout cho đúng.
1. Bài toán: một request đi qua bao nhiêu tầng?
Mobile app ─► Azure LB ─► Nginx (api.shop.example) ─► Tomcat :8080 (order-service)
├─► RestClient ─► payment-service
└─► HikariCP ─► Postgres (VM khác)
Tầng nào cũng có thể chờ. Tầng nào cũng cần biết "chờ tối đa bao lâu".
2. Vấn đề: timeout mặc định thường là vô hạn hoặc quá lớn
HttpURLConnection/SimpleClientHttpRequestFactory: connect và read mặc định 0 = chờ mãi.- JDBC query: không có statement timeout = query khóa bảng có thể chạy hàng chục phút.
- Nginx
proxy_read_timeoutmặc định 60s: client đã bỏ đi từ lâu, backend vẫn làm. - Không timeout → thread kẹt → pool cạn → lỗi lan ra toàn service (cascading failure).
3. Giải pháp: hiểu 3 loại timeout
| Loại | Đo cái gì | Lỗi điển hình |
|---|---|---|
| Connect timeout | Thời gian dựng TCP connection (SYN → SYN-ACK) | ConnectException: connect timed out (host chết, firewall DROP) |
| Read / socket timeout | Thời gian chờ giữa 2 lần nhận byte, sau khi đã connect | SocketTimeoutException: Read timed out (server xử lý chậm) |
| Request / response timeout | Tổng thời gian cả request | WebClient TimeoutException |
| Idle / keep-alive timeout | Connection rảnh bao lâu thì đóng | Lỗi "connection reset" nếu hai bên lệch nhau |
Lưu ý: read timeout không phải tổng thời gian. Server nhỏ giọt mỗi byte 4s với read timeout 5s thì request kéo dài mãi.
Nguyên tắc vàng: timeout tầng ngoài > timeout tầng trong. Tầng trong phải fail trước, trả lỗi rõ ràng cho tầng ngoài, thay vì tầng ngoài cắt ngang khi tầng trong vẫn đang làm.
4. Code từng bước
Bước 1: Nginx (bài 9.8)
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_connect_timeout 2s;
proxy_send_timeout 10s;
proxy_read_timeout 15s; # > tổng thời gian order-service cần
}
Bước 2: Tomcat và Hikari application.yml
server:
tomcat:
connection-timeout: 5s
spring:
mvc.async.request-timeout: 12s
datasource:
url: jdbc:postgresql://10.0.2.5:5432/orders?connectTimeout=3&socketTimeout=10
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 10
connection-timeout: 2000 # ms chờ MƯỢN connection từ pool
max-lifetime: 1800000 # < idle timeout của DB/firewall
jpa.properties:
jakarta.persistence.query.timeout: 3000 # ms, statement timeout cho JPA
Bước 3: Java — RestClient gọi payment
@Bean
RestClient paymentRestClient(RestClient.Builder builder) {
// Spring Boot 3.4+
var settings = ClientHttpRequestFactorySettings.defaults()
.withConnectTimeout(Duration.ofSeconds(1))
.withReadTimeout(Duration.ofSeconds(3));
return builder.baseUrl("http://10.0.1.20:8081")
.requestFactory(ClientHttpRequestFactoryBuilder.detect().build(settings))
.build();
}
WebClient (reactive):
HttpClient http = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
.responseTimeout(Duration.ofSeconds(3));
WebClient client = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(http))
.baseUrl("http://10.0.1.20:8081").build();
client.post().uri("/payments").bodyValue(req).retrieve()
.bodyToMono(PaymentResult.class)
.timeout(Duration.ofSeconds(4)); // tổng cho cả chuỗi
OpenFeign:
spring:
cloud:
openfeign:
client:
config:
payment-service:
connect-timeout: 1000
read-timeout: 3000
JDBC thuần / JdbcTemplate và Postgres:
jdbcTemplate.setQueryTimeout(3); // giây
// hoặc từng statement
try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setQueryTimeout(3); ... }
// hoặc phía DB, cho riêng user của app
// ALTER ROLE order_app SET statement_timeout = '5s';
Bước 4: test
# payment giả lập chậm 10s → order-service phải trả lỗi sau ~3s, không phải 10s
curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" -X POST https://api.shop.example/api/orders -d '{}' -H 'Content-Type: application/json'
# 503 3.05s (order-service bắt SocketTimeoutException → trả 503 rõ ràng)
5. Cách hoạt động: timeout budget
Client app timeout 30s
└─ Azure LB idle timeout 4 phút (TCP)
└─ Nginx proxy_read_timeout 15s
└─ Tomcat / order-service (tự kết thúc trong ~8s)
├─ RestClient payment connect 1s + read 3s
├─ Hikari borrow connectionTimeout 2s
└─ SQL statement timeout 3s
Đúng: 30 > 15 > 8 > 3 → tầng trong fail trước, trả lỗi có nghĩa
Sai: Nginx 5s, payment read 30s
→ Nginx trả 504 ở giây 5, nhưng Tomcat thread vẫn chờ payment tới giây 30
→ client retry → thêm request → càng hết thread
| Mã | Ai trả | Nghĩa |
|---|---|---|
| 502 Bad Gateway | Nginx/LB | Backend trả lời "hỏng": connection refused (Tomcat chưa chạy), connection reset, response không hợp lệ |
| 504 Gateway Timeout | Nginx/LB | Backend không trả lời kịp trong proxy_read_timeout |
| 503 | App hoặc proxy | Tạm không phục vụ: app chủ động khi downstream timeout, hoặc không còn upstream healthy |
6. So sánh: timeout các client
| Client | Connect | Read/response | Mặc định nguy hiểm |
|---|---|---|---|
| RestClient/RestTemplate (JDK factory) | withConnectTimeout | withReadTimeout | 0 = vô hạn |
| WebClient (Reactor Netty) | CONNECT_TIMEOUT_MILLIS (30s) | responseTimeout, .timeout() | Không có response timeout |
| OpenFeign | connect-timeout (10s) | read-timeout (60s) | 60s quá dài |
| HikariCP | connectionTimeout = chờ mượn connection (30s) | 30s quá dài cho API | |
| JDBC | connectTimeout | setQueryTimeout/statement_timeout | Không giới hạn |
7. Khi nào dùng / lưu ý
- Luôn đặt connect + read timeout cho mọi lời gọi ra ngoài process.
- Connect timeout ngắn (0.5–2s) trong cùng VNet; read timeout theo p99 của downstream cộng biên độ.
- Retry chỉ cho thao tác idempotent, có backoff, và tính vào budget. Timeout khi charge payment không có nghĩa là thất bại: lưu trạng thái
UNKNOWN, đối soát sau. max-lifetimecủa Hikari nhỏ hơn idle timeout của firewall/NAT, để không dùng connection đã bị cắt ngầm.
Connect timeout với read timeout khác nhau sao? Lúc nào thì gặp cái nào?
Đúng là hay lẫn. Nhìn theo TCP (bài 9.2, 9.3):
connect timeout: SYN ──► ... im lặng ... (host tắt, NSG/firewall DROP, sai IP)
connection refused: SYN ──► ◄── RST (host sống, không ai listen port đó)
read timeout: đã ESTABLISHED, gửi request ──► ... server chưa trả byte nào ...
Connect timeout → nghĩ tới mạng/firewall. Read timeout → mạng ổn, nghĩ tới app/DB chậm.
Interviewer hỏi: Service A → B → C; C timeout thì sao?
Câu này hỏi về timeout budget. Ý trả lời: A cho cả chuỗi 5s, B gọi C với timeout nhỏ hơn phần còn lại (ví dụ 2s), có thể truyền deadline qua header. C timeout thì B fail nhanh: trả fallback hoặc lỗi 503 rõ ràng, circuit breaker mở nếu C lỗi liên tục, retry có backoff + jitter chỉ khi idempotent. Không để B chờ lâu hơn A, vì A bỏ cuộc rồi thì B làm tiếp cũng vô ích.
Phỏng vấn hỏi về timeout thì trả lời sao?
Q1. Connect timeout và read timeout khác nhau thế nào?
🇻🇳 Tiếng Việt:
Connect timeout là thời gian tối đa để dựng TCP connection; thường do host không tồn tại hoặc firewall drop gói. Read timeout là thời gian tối đa chờ dữ liệu sau khi đã kết nối; thường do server hoặc DB xử lý chậm. Em luôn đặt cả hai cho mọi HTTP client và DB connection.
🇬🇧 English:
Connect timeout is the maximum time to establish the TCP connection; it usually fires when the host is down or a firewall drops packets. Read timeout is the maximum time to wait for data after the connection is established; it usually means the server or database is slow. I always set both on every HTTP client and database connection.
Q2. Vì sao timeout tầng ngoài phải lớn hơn tầng trong?
🇻🇳 Tiếng Việt:
Để tầng trong fail trước và trả lỗi có ý nghĩa. Nếu Nginx timeout 5s mà app chờ payment 30s, client nhận 504 nhưng thread app vẫn bị giữ, client retry làm tải tăng gấp đôi. Em chia timeout budget: client > proxy > app > downstream call > DB query.
🇬🇧 English:
So the inner layer fails first and returns a meaningful error. If Nginx times out at 5 seconds while the app waits 30 seconds for payment, the client gets a 504 but the app thread is still busy, and client retries double the load. I split a timeout budget: client > proxy > app > downstream call > DB query.
Q3. 502 và 504 khác nhau thế nào?
🇻🇳 Tiếng Việt:
Cả hai do proxy trả. 502 là backend trả lời không hợp lệ hoặc từ chối kết nối, ví dụ app chưa chạy, connection reset. 504 là backend không trả lời kịp trong thời gian proxy chờ, ví dụ vượt
proxy_read_timeout.
🇬🇧 English:
Both come from the proxy. 502 means the backend gave an invalid response or refused the connection, for example the app is not running or the connection was reset. 504 means the backend did not answer within the proxy's wait time, for example it exceeded
proxy_read_timeout.
Q4. HikariCP connectionTimeout là gì?
🇻🇳 Tiếng Việt:
Là thời gian tối đa một thread chờ mượn connection từ pool, mặc định 30 giây. Nó không phải query timeout. Query timeout em đặt riêng bằng
setQueryTimeout, JPA query timeout hoặcstatement_timeouttrên Postgres.
🇬🇧 English:
It is the maximum time a thread waits to borrow a connection from the pool, 30 seconds by default. It is not a query timeout. I set the query timeout separately with
setQueryTimeout, the JPA query timeout or Postgresstatement_timeout.
Cách nhớ ngắn gọn
- Connect = dựng đường; Read = chờ hàng; Idle = đường rảnh bao lâu thì đóng.
- Ngoài lớn, trong nhỏ: client > LB/Nginx > app > HTTP call > SQL.
- 502 = backend hỏng/từ chối; 504 = backend chậm.
- Mặc định thường là vô hạn: tự đặt hết.
Tự kiểm tra
Firewall DROP gói SYN thì client gặp lỗi gì?
Connect timeout (không có phản hồi). Còn host sống mà không listen port thì connection refused.
Nginx proxy_read_timeout 5s, payment read timeout 30s: hậu quả?
Client nhận 504 sau 5s nhưng Tomcat thread vẫn chờ tới 30s; retry làm hết thread nhanh hơn.
Hikari connectionTimeout đo cái gì?
Thời gian chờ mượn connection từ pool, không phải thời gian chạy query.
Timeout khi gọi charge payment có nghĩa là thanh toán thất bại?
Không. Có thể đã trừ tiền; đánh dấu UNKNOWN, dùng idempotency key và đối soát/webhook.
Vì sao WebClient nên có cả responseTimeout và .timeout()?
responseTimeout giới hạn chờ response ở tầng HTTP; .timeout() giới hạn tổng cả chuỗi reactive.