Sửa lỗi 'context deadline exceeded' trong Go: Timeout HTTP và Cơ sở dữ liệu

intermediate🔷 Go2026-04-23| Go (Golang) 1.13+, Linux, macOS, Windows

Error Message

context deadline exceeded
#go#golang#context#timeout#http#cơ sở dữ liệu

Thông báo lỗi

Có lẽ bạn ở đây vì nhật ký hệ thống (logs) môi trường production vừa tăng vọt với một thông báo khó hiểu. Trong Go, lỗi này là cách runtime thông báo: "Bạn đã bảo tôi đợi X thời gian, nhưng thao tác này đã mất X + 1." Nó thường xuất hiện trong logs của bạn như thế này:

Get "https://api.payments.com/v1/charge": context deadline exceeded

Hoặc, nếu một truy vấn cơ sở dữ liệu bị treo trong quá trình migration nặng:

panic: context deadline exceeded

Nguyên nhân gốc rễ: Tại sao điều này xảy ra?

Lỗi context deadline exceeded kích hoạt khi một context.Context chạm giới hạn thời gian trước khi tác vụ hoàn thành. Go sử dụng Context để ngăn chặn các tiến trình "zombie" tiêu tốn CPU và bộ nhớ khi một dịch vụ upstream bị đình trệ.

Hãy coi đó như một công tắc ngắt điện bảo vệ. Các nguyên nhân phổ biến bao gồm:

  • Độ trễ API bên ngoài: Một dịch vụ bên thứ ba đang gặp sự cố, khiến thời gian phản hồi p99 tăng từ 200ms lên 10 giây.
  • Truy vấn DB không được lập chỉ mục: Một câu lệnh SELECT trên một bảng có 5 triệu hàng đang thực hiện quét toàn bộ bảng (full table scan).
  • Timeout quá ngắn: Thiết lập timeout 50ms cho một quá trình bắt tay TLS (TLS handshake) phức tạp vốn dĩ cần 150ms.
  • Cold Starts: Các serverless function (như AWS Lambda) mất 3 giây để khởi động trong khi phía gọi chỉ đợi 1 giây.

Cách khắc phục 1: Cấu hình HTTP Client Timeout thực tế

Các giá trị mặc định của thư viện tiêu chuẩn thường rất nguy hiểm. Ví dụ, http.DefaultClient không có timeout. Nếu một máy chủ từ xa chấp nhận kết nối nhưng không bao giờ gửi dữ liệu, goroutine của bạn sẽ treo mãi mãi, cuối cùng dẫn đến rò rỉ bộ nhớ (memory leak).

Rủi ro: Quá khắt khe

// 10ms hiếm khi đủ cho một vòng khứ hồi (round-trip) qua internet công cộng
ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Millisecond)
defer cancel()

req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.github.com", nil)
res, err := http.DefaultClient.Do(req) // Chắc chắn thất bại trong hầu hết các môi trường

Cách tiếp cận bền vững

Định nghĩa một client tùy chỉnh với một rào chắn toàn cục, sau đó sử dụng context để kiểm soát chi tiết cho từng request.

client := &http.Client{
    Timeout: time.Second * 30, // Giới hạn tối đa tuyệt đối
}

// Cấp cho request cụ thể này 5 giây để hoàn thành
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
defer cancel()

req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
if err != nil {
    log.Fatal(err)
}

resp, err := client.Do(req)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        fmt.Println("The upstream API failed to respond within 5 seconds.")
    }
    return
}

Cách khắc phục 2: Tối ưu hóa cửa sổ truy vấn cơ sở dữ liệu

Các database driver như pgx hoặc database/sql đều tuân thủ context. Nếu bạn thấy lỗi này ở lớp DB, truy vấn của bạn có khả năng đang tranh chấp khóa (lock) hoặc xử lý quá nhiều dữ liệu.

Ví dụ với SQL Context

ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
defer cancel()

// QueryContext đảm bảo chúng ta không đợi mãi mãi cho một hàng bị khóa
query := "SELECT email FROM users WHERE last_login < $1"
rows, err := db.QueryContext(ctx, query, "2023-01-01")
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        log.Println("Query aborted: Execution exceeded 3-second limit. Check indexes on 'last_login'.")
    }
    return err
}

Mẹo nhỏ: Trong các web handler, hãy sử dụng r.Context(). Nếu người dùng tải lại trình duyệt, context sẽ tự động hủy. Điều này giúp cơ sở dữ liệu không lãng phí tài nguyên cho một kết quả mà người dùng sẽ không bao giờ thấy.

Cách khắc phục 3: Xác thực môi trường

Nếu mã của bạn trông có vẻ đúng nhưng lỗi vẫn tiếp diễn, điểm nghẽn nằm ở bên ngoài. Sử dụng các bước sau để cô lập độ trễ:

  • Tăng giới hạn: Tạm thời tăng timeout lên 60 giây. Nếu tác vụ hoàn thành trong 45 giây, mã của bạn ổn — dịch vụ downstream của bạn đơn giản là chậm.
  • Đo độ trễ: Chạy curl -o /dev/null -s -w "Connect: %{time_connect} TTFB: %{time_starttransfer} Total: %{time_total}\n" https://api.url.
  • Phân tích SQL: Chạy EXPLAIN ANALYZE trên truy vấn bị lỗi để tìm các chỉ mục còn thiếu hoặc quét tuần tự (sequential scans).

Các bước xác minh

  • Mô phỏng độ trễ: Sử dụng một công cụ như Toxiproxy để thêm 5 giây độ trễ vào môi trường local và xác minh cơ chế xử lý lỗi của bạn được kích hoạt.`
  • Kiểm tra Error Wrapping: Luôn sử dụng errors.Is(err, context.DeadlineExceeded) thay vì so khớp chuỗi.
  • Kiểm tra Logs: Đảm bảo nhật ký của bạn ghi lại URL nào hoặc truy vấn nào đã bị timeout. Một thông báo "deadline exceeded" chung chung sẽ vô dụng nếu thiếu ngữ cảnh.

Thực hành tốt nhất

  1. Tránh Hardcoding: Lưu trữ thời gian trong tệp cấu hình (ví dụ: API_TIMEOUT=5s) để bạn có thể điều chỉnh hiệu suất mà không cần triển khai lại.

  2. Rào chắn Middleware: Sử dụng một timeout middleware trong Gin hoặc Echo để đặt giới hạn toàn cục 10 giây cho tất cả các request đến. Điều này ngăn một endpoint chậm chạp duy nhất làm sập toàn bộ dịch vụ của bạn.

// Ví dụ: Thiết lập giới hạn toàn cục 5 giây cho tất cả các route
router.Use(timeout.New(
    timeout.WithTimeout(5 * time.Second),
    timeout.WithHandler(func(c *gin.Context) {
        c.Next()
    }),
))

Related Error Notes