TL;DR: Cách khắc phục nhanh
Hầu hết các lỗi ReadTimeout xảy ra do thư viện requests mặc định sẽ chờ vô thời hạn. Để khắc phục, hãy luôn truyền một giá trị timeout cụ thể tính bằng giây. Nếu script của bạn đã bị timeout ở một giá trị nhất định, hãy thử tăng nó lên 60 hoặc 90 giây để máy chủ có thêm thời gian xử lý.
import requests
try:
# Sử dụng timeout 60 giây cho các endpoint API chậm
response = requests.get('https://api.example.com/data', timeout=60)
response.raise_for_status()
except requests.exceptions.ReadTimeout:
print("The server is taking too long to respond. Try again later.")
Tại sao máy chủ lại "ngó lơ" bạn?
Hãy tưởng tượng ReadTimeout giống như việc bạn đã ngồi vào bàn trong nhà hàng, gọi món, và sau đó chờ đợi cả tiếng đồng hồ mà nhà bếp vẫn không mang món khai vị ra. Bạn đã kết nối thành công với máy chủ, nhưng nó không gửi lại bất kỳ dữ liệu nào trong khoảng thời gian giới hạn của bạn.
Điều này khác với ConnectTimeout, nơi bạn thậm chí không thể bước qua cửa chính. Nếu bạn gặp lỗi read timeout, có khả năng máy chủ đã nhận được yêu cầu nhưng bị kẹt. Điều này thường xảy ra vì một vài lý do cụ thể sau:
- **Nghẽn cổ chai cơ sở dữ liệu:** Máy chủ đang thực thi một truy vấn SQL khổng lồ mất hơn 45 giây để hoàn tất.
- **Cạn kiệt tài nguyên:** API đang bị quá tải (ví dụ: trong đợt flash sale) và yêu cầu đang nằm trong một hàng đợi dài.
- **Payload quá lớn:** Bạn đã yêu cầu 50.000 bản ghi cùng lúc và máy chủ đang chật vật để tuần tự hóa (serialize) lượng JSON lớn đó.
- **Chất lượng mạng kém:** Tỷ lệ mất gói tin cao khiến kết nối bị đình trệ giữa chừng.
Các giải pháp từng bước
1. Sử dụng Timeout chi tiết
Thay vì một con số duy nhất, hãy truyền một tuple vào timeout. Điều này cho phép bạn thiết lập một khoảng thời gian ngắn cho kết nối ban đầu và một khoảng thời gian dài hơn cho việc truyền dữ liệu thực tế. Mẹo nhỏ: Sử dụng 3,05 giây cho connection timeout, vì nó phù hợp với chu kỳ truyền lại gói tin TCP.
# (timeout kết nối, timeout đọc)
# Chờ 3.05s để kết nối, và 60s để dữ liệu thực tế được gửi đến
response = requests.get('https://api.example.com', timeout=(3.05, 60))
2. Xây dựng chiến lược thử lại (Retry) mạnh mẽ
Các sự cố mạng thường chỉ là tạm thời. Thay vì để script bị lỗi, hãy sử dụng HTTPAdapter để tự động thử lại yêu cầu với cơ chế exponential backoff (thử lại sau khoảng thời gian tăng dần). Điều này có nghĩa là script sẽ chờ lâu hơn giữa mỗi lần thử (ví dụ: 0,6s, 1,2s, 2,4s) để tránh gây thêm áp lực cho máy chủ đang gặp khó khăn.
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def get_resilient_session(retries=3, backoff=0.3):
session = requests.Session()
retry_strategy = Retry(
total=retries,
read=retries,
connect=retries,
backoff_factor=backoff,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
# Mã này sẽ thử lại tối đa 3 lần trước khi bỏ cuộc
client = get_resilient_session()
response = client.get('https://api.example.com', timeout=30)
3. Giảm bớt kích thước Payload
Nếu bạn kiểm soát API hoặc API đó hỗ trợ lọc, hãy ngừng yêu cầu mọi thứ cùng một lúc. Yêu cầu 1.000 mục cùng lúc là nguyên nhân dẫn đến timeout. Hãy chia nhỏ yêu cầu để giữ thời gian phản hồi dưới 10 giây.
- Triển khai **phân trang (pagination)** (ví dụ: `?limit=50&offset=0`).
- Sử dụng **lọc trường (field filtering)** để chỉ tải xuống các khóa (keys) mà bạn thực sự cần.
- Kiểm tra xem máy chủ có hỗ trợ `Accept-Encoding: gzip` để nén dữ liệu truyền tải hay không.
4. Stream các phản hồi lớn
Khi tải xuống các tệp trên 100MB, đừng đợi toàn bộ tệp tải vào bộ nhớ. Sử dụng stream=True để xử lý dữ liệu ngay khi nó được gửi đến. Điều này ngăn kết nối bị timeout trong khi RAM của bạn đang bị lấp đầy.
with requests.get('https://api.example.com/big-export.csv', stream=True, timeout=60) as r:
r.raise_for_status()
for chunk in r.iter_content(chunk_size=1024 * 8):
# Xử lý 8KB mỗi lần
save_to_disk(chunk)
Cách kiểm tra logic Timeout của bạn
Bạn không cần phải đợi máy chủ thật bị lỗi để kiểm tra mã của mình. Sử dụng httpbin.org để mô phỏng phản hồi chậm theo yêu cầu.
- **Kích hoạt lỗi:** Yêu cầu máy chủ chờ 10 giây, nhưng đặt timeout của bạn là 5 giây.
```
import requests try: requests.get('https://httpbin.org/delay/10', timeout=5) except requests.exceptions.ReadTimeout: print("Success: The timeout was caught!")
- **Xác minh thành công:** Đặt timeout của bạn thành 15 giây cho cùng mức trì hoãn 10 giây đó. Yêu cầu bây giờ sẽ hoàn tất thành công với mã trạng thái 200.
## Tài liệu tham khảo thêm
- [Hướng dẫn chính thức của Requests: Timeouts](https://requests.readthedocs.io/en/latest/user/advanced/#timeouts)
- [Tài liệu về Retry của urllib3](https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html#urllib3.util.Retry)

