Timeout ở từng tầng: từ client đến database

4 câu hỏi~25 phútTừ khóa: connect timeout, read timeout, idle timeout, proxy_read_timeout, RestClient, WebClient, Feign, HikariCP connectionTimeout, statement timeout, 502, 504, timeout budget

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

3. Giải pháp: hiểu 3 loại timeout

LoạiĐo cái gìLỗi điển hình
Connect timeoutThời gian dựng TCP connection (SYN → SYN-ACK)ConnectException: connect timed out (host chết, firewall DROP)
Read / socket timeoutThời gian chờ giữa 2 lần nhận byte, sau khi đã connectSocketTimeoutException: Read timed out (server xử lý chậm)
Request / response timeoutTổng thời gian cả requestWebClient TimeoutException
Idle / keep-alive timeoutConnection rảnh bao lâu thì đóngLỗ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 GatewayNginx/LBBackend trả lời "hỏng": connection refused (Tomcat chưa chạy), connection reset, response không hợp lệ
504 Gateway TimeoutNginx/LBBackend không trả lời kịp trong proxy_read_timeout
503App hoặc proxyTạ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

ClientConnectRead/responseMặc định nguy hiểm
RestClient/RestTemplate (JDK factory)withConnectTimeoutwithReadTimeout0 = vô hạn
WebClient (Reactor Netty)CONNECT_TIMEOUT_MILLIS (30s)responseTimeout, .timeout()Không có response timeout
OpenFeignconnect-timeout (10s)read-timeout (60s)60s quá dài
HikariCPconnectionTimeout = chờ mượn connection (30s)30s quá dài cho API
JDBCconnectTimeoutsetQueryTimeout/statement_timeoutKhông giới hạn

7. Khi nào dùng / lưu ý

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ặc statement_timeout trê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 Postgres statement_timeout.

Cách nhớ ngắn gọn


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.