
Ngừng Quên Timeout: Cứu Ứng Dụng Khỏi Hiệu Ứng Chết Chùm
Tìm hiểu tại sao việc quên set timeout API lại gây ra hiệu ứng chết chùm trong hệ thống phân tán, và cách fix chuẩn trong Node.js, Python, Go.
Chỉ một lệnh gọi API quên set timeout cũng đủ để tạo ra hiệu ứng chết chùm kéo sập toàn bộ hệ thống microservices của bạn.
Nếu bạn đã làm backend hay kiến trúc hệ thống đủ lâu, chắc chắn bạn từng gặp một kiểu sự cố "trên trời rơi xuống": Hệ thống chính đột nhiên chết đứng, check database vẫn rảnh rang, CPU server không hề quá tải, nhưng API thì cứ quay mòng mòng không phản hồi.
Sau vài tiếng toát mồ hôi hột debug, bạn mới tìm ra thủ phạm là một cái API vớ vẩn nào đó bên thứ ba (như API bắn thông báo hay gửi tin nhắn) đang bị nghẽn mạng.
Tại sao một cái API bên ngoài phản hồi chậm lại có thể kéo sập toàn bộ service nội bộ của bạn? Câu trả lời thường cực kỳ ngớ ngẩn: một ai đó trong team đã quên set timeout.
Hôm nay, chúng ta sẽ nói về kẻ sát nhân thầm lặng trong các hệ thống phân tán—hiệu ứng chết chùm (cascading failures)—và tại sao việc nhắm mắt xài mặc định của các thư viện gọi HTTP lại là một quả bom nổ chậm trong production.
Ngày Hệ Thống Thanh Toán Sập Chỉ Vì... Gửi Email
Vài năm trước, hồi tôi mới làm một hệ thống e-commerce, mọi thứ đang chạy mượt mà thì đến ngày sale lớn, service Thanh toán (Payment) tự nhiên lăn ra chết, từ chối toàn bộ request mới.
Phản xạ đầu tiên của tôi là check database xem có bị lock không, hay cổng thanh toán của đối tác sập. Không hề. Cả hai vẫn phản hồi nhanh như chớp.
Mò mẫm trong đống log, tôi mới lôi ra được thủ phạm. Chả là sau khi thanh toán thành công, code của chúng tôi gọi một cái API đồng bộ sang dịch vụ bên thứ ba để gửi email biên lai. Khổ nỗi hôm đó dịch vụ email kia bị quá tải. Họ không sập hẳn để báo lỗi, mà họ chỉ mở kết nối rồi... để đó, không trả về gì cả.
Vì không set timeout, cái thư viện HTTP client của chúng tôi kiên nhẫn chờ đợi. Nó chờ cho đến khi đụng mốc TCP timeout của hệ điều hành, thường là mất tận 2 phút.
Trong một hệ thống có lượng traffic cao, việc "giam" một thread trong 2 phút là bản án tử hình. Cứ mỗi lần có khách thanh toán, một worker thread lại bị kẹt cứng để chờ cái API gửi email. Chỉ trong vài chục giây, toàn bộ connection pool của chúng tôi cạn sạch. Các request thanh toán mới bay tới không còn thread nào để xử lý, thế là xếp hàng chờ cho đến khi service Thanh toán hết sạch RAM và sập hoàn toàn.
Một cái tác vụ chạy nền (gửi email) không hề quan trọng lại kéo sập cả cỗ máy in tiền của công ty. Đây chính là định nghĩa sách giáo khoa của "hiệu ứng chết chùm". Và điều cay đắng là nó hoàn toàn có thể được ngăn chặn chỉ bằng một dòng code.
Cái Bẫy Của Những Cấu Hình Mặc Định

Nhiều anh em dev hay có suy nghĩ ngây thơ: "Chắc mấy thư viện chuẩn người ta phải làm timeout mặc định cẩn thận lắm rồi chứ. Ai lại để nó treo vĩnh viễn?"
Thực tế thì ngược lại. Đa số các thư viện HTTP phổ biến ưu tiên việc tải các file lớn hơn là việc "fail fast" (báo lỗi nhanh). Nếu bạn không nói gì, mặc định nó sẽ chờ đến thiên thu.
Hãy nhìn lại những công cụ bạn đang xài hằng ngày:
- Node.js
fetch: Mặc định là vô hạn. Nó chỉ đứt khi TCP connection dưới hệ điều hành chịu đầu hàng. - Axios: Thuộc tính
timeoutmặc định là0(nghĩa là không bao giờ timeout). - Python
requests: Mặc định cũng là vô hạn. - Go
http.Client: Khởi tạo zero-value thì cũng không có timeout luôn.
Đây là một cái hố bom thực sự. Khi bạn gõ const response = await fetch('https://api.example.com'), về cơ bản bạn đang nói với server của mình rằng: "Hãy khóa cứng cái luồng xử lý này lại vĩnh viễn nếu cái server bên kia dở chứng".
Cách Cài Đặt Timeout Chuẩn Chỉ

Cách fix thì quá dễ, nhưng bạn phải tạo cho mình thói quen khắt khe với mọi network call. Dưới đây là cách xử lý trên vài tech stack phổ biến.
JavaScript / TypeScript (Node.js & Browser)
Ngày xưa, muốn gắn timeout cho fetch thì phải viết một cục AbortController bọc trong setTimeout nhìn rất rườm rà. Giờ đây, JavaScript hiện đại đã cung cấp một utility sạch sẽ hơn rất nhiều: AbortSignal.timeout().
// ĐỪNG LÀM THẾ NÀY
const data = await fetch('https://api.example.com/notify');
// HÃY LÀM THẾ NÀY
const response = await fetch('https://api.example.com/notify', {
// Bắn ra AbortError nếu request chạy lâu hơn 3000ms
signal: AbortSignal.timeout(3000)
});
const data = await response.json();
Nếu dùng Axios, nguyên tắc bất di bất dịch là luôn set timeout ở config global:
import axios from 'axios';
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000 // 5 giây. Không bao giờ được để số 0!
});
Python
Thư viện requests quốc dân là trùm gây treo script nếu dùng không cẩn thận. Luôn luôn truyền tham số timeout. Với các hệ thống production, bạn thậm chí nên truyền một tuple để quy định riêng rẽ thời gian chờ kết nối (connect timeout) và thời gian chờ đọc dữ liệu (read timeout).
import requests
# ĐỪNG LÀM THẾ NÀY
# response = requests.get('https://api.example.com/data')
# HÃY LÀM THẾ NÀY: (connect_timeout, read_timeout)
try:
response = requests.get('https://api.example.com/data', timeout=(3.05, 5))
data = response.json()
except requests.exceptions.Timeout:
print("API phản hồi quá chậm. Cắt kết nối ngay!")
(Mẹo nhỏ: Nên set connect timeout là một số lớn hơn bội số của 3 một chút — ví dụ 3.05 giây — để khớp với cơ chế truyền lại gói tin TCP chuẩn).
Go (Golang)
Trong Go, gọi HTTP mà không có timeout sẽ sinh ra lỗi rò rỉ (leak) goroutines. Tích tụ đủ lâu, app của bạn sẽ văng lỗi Out of Memory.
// ĐỪNG LÀM THẾ NÀY
// client := &http.Client{}
// HÃY LÀM THẾ NÀY
client := &http.Client{
Timeout: 5 * time.Second,
}
// Hoặc xịn hơn, dùng Context để kiểm soát linh hoạt:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
resp, err := client.Do(req)
Chuyện Gì Xảy Ra Sau Khi Timeout?
Gắn timeout mới chỉ là bước cầm máu. Bạn đã ngăn được việc treo hệ thống, nhưng request đó vẫn thất bại. Cách ứng dụng của bạn phản ứng với thất bại đó mới quyết định độ "trâu bò" (resilient) của hệ thống.
- Fail Fast (Chết nhanh còn hơn hấp hối): Trả ngay lỗi 503 hoặc một thông báo thân thiện cho user. Thà báo cho user biết "Hệ thống đang bận, thử lại sau" ngay lập tức còn hơn bắt họ nhìn cái spinner quay mòng mòng 2 phút để rồi cuối cùng vẫn văng lỗi.
- Graceful Degradation (Suy thoái có duyên): Có bắt buộc phải có dữ liệu từ bên thứ ba không? Nếu API gợi ý sản phẩm bị timeout, đừng kéo sập luôn trang chủ. Hãy catch cái lỗi timeout đó và trả về một danh sách sản phẩm phổ biến được hardcode sẵn. User sẽ không hề hay biết hệ thống đang gặp sự cố.
- Asynchronous Processing (Xử lý bất đồng bộ): Trở lại câu chuyện hệ thống thanh toán của tôi, đúng ra chúng tôi không bao giờ được phép gọi API gửi email đồng bộ ngay trong luồng thanh toán. Những tác vụ không sinh ra tiền trực tiếp cần được ném vào một Message Queue (như RabbitMQ hay Kafka), rồi cho một con worker chạy ngầm xử lý sau với cơ chế retry của riêng nó.
- Circuit Breaker (Cầu dao tự động): Nếu một service cứ gọi là timeout liên tục 5 lần, thì cớ gì bạn phải cố đấm ăn xôi gọi lần thứ 6? Hãy dùng pattern Circuit Breaker. Khi tỷ lệ lỗi vượt quá mức cho phép, cầu dao sẽ "ngắt" và từ chối mọi request mới trong vài chục giây. Việc này vừa giúp bạn fail fast, vừa cho cái service đang hấp hối kia chút thời gian để thở và hồi phục.
Nguyên Tắc Vàng Trong Hệ Thống Phân Tán
Môi trường mạng là một nơi đầy rẫy hiểm nguy. Chuyện một dịch vụ bên thứ ba chậm lại hay sập nguồn không phải là câu hỏi có xảy ra hay không, mà là khi nào xảy ra.
Nếu có một điều đọng lại sau bài viết này, tôi hy vọng bạn nhớ kỹ: Không bao giờ tin tưởng mạng lưới, và không bao giờ tin tưởng tuyệt đối vào một service khác. Bất kỳ một luồng gọi nào ra ngoài hệ thống đều phải bị gông cùm bởi một giới hạn thời gian (time limit) khắt khe.
Bây giờ, hãy mở codebase của bạn lên. Search thử các từ khóa fetch(, axios., requests.get hay http.Client. Tôi cá là bạn sẽ tìm thấy ít nhất một cái network call "trần trụi" đang chực chờ gây họa. Thêm timeout vào đó, và đêm nay bạn sẽ ngủ ngon hơn một chút.
viết bởi
Nguyên Tech
Phản hồi
Đang tải bình luận…