Cách sửa lỗi timeout khi làm việc qua proxy
Yêu cầu bị treo, script gặp lỗi TimeoutError, dữ liệu không được nhận. Tình huống quen thuộc? Lỗi timeout qua proxy — một trong những vấn đề phổ biến nhất khi scraping và tự động hóa. Hãy cùng phân tích nguyên nhân và cung cấp các giải pháp cụ thể.
Tại sao lỗi timeout xảy ra
Timeout — không phải một vấn đề duy nhất, mà là triệu chứng. Trước khi điều trị, bạn cần hiểu nguyên nhân:
Máy chủ proxy chậm. Máy chủ bị quá tải hoặc proxy ở xa địa lý sẽ thêm độ trễ cho mỗi yêu cầu. Nếu timeout của bạn là 10 giây, nhưng proxy phản hồi trong 12 giây — lỗi xảy ra.
Bị chặn từ phía trang web đích. Trang web có thể cố ý "treo" các yêu cầu đáng ngờ thay vì từ chối rõ ràng. Đây là chiến lược chống bot — giữ kết nối mở vô thời hạn.
Vấn đề DNS. Proxy phải phân giải tên miền. Nếu máy chủ DNS của proxy chậm hoặc không khả dụng — yêu cầu sẽ treo ở giai đoạn kết nối.
Cấu hình timeout không chính xác. Một timeout chung cho tất cả — lỗi phổ biến. Connect timeout và read timeout — những thứ khác nhau, và cần cấu hình riêng biệt.
Vấn đề mạng. Mất gói tin, kết nối proxy không ổn định, vấn đề định tuyến — tất cả đều dẫn đến timeout.
Các loại timeout và cách cấu hình
Hầu hết các thư viện HTTP hỗ trợ nhiều loại timeout. Hiểu rõ sự khác biệt giữa chúng — chìa khóa để cấu hình đúng.
Connect timeout
Thời gian thiết lập kết nối TCP với proxy và máy chủ đích. Nếu proxy không khả dụng hoặc máy chủ không phản hồi — timeout này sẽ kích hoạt. Giá trị được khuyến nghị: 5-10 giây.
Read timeout
Thời gian chờ dữ liệu sau khi thiết lập kết nối. Máy chủ đã kết nối, nhưng im lặng — read timeout sẽ kích hoạt. Đối với các trang thông thường: 15-30 giây. Đối với API nặng: 60+ giây.
Total timeout
Tổng thời gian cho toàn bộ yêu cầu từ đầu đến cuối. Bảo hiểm chống lại kết nối bị treo. Thường: connect + read + dự phòng.
Ví dụ cấu hình trong Python với thư viện requests:
import requests
proxies = {
"http": "http://user:pass@proxy.example.com:8080",
"https": "http://user:pass@proxy.example.com:8080"
}
# Bộ dữ liệu: (connect_timeout, read_timeout)
timeout = (10, 30)
try:
response = requests.get(
"https://target-site.com/api/data",
proxies=proxies,
timeout=timeout
)
except requests.exceptions.ConnectTimeout:
print("Không thể kết nối đến proxy hoặc máy chủ")
except requests.exceptions.ReadTimeout:
print("Máy chủ không gửi dữ liệu kịp thời")
Đối với aiohttp (Python không đồng bộ):
import aiohttp
import asyncio
async def fetch_with_timeout():
timeout = aiohttp.ClientTimeout(
total=60, # Timeout chung
connect=10, # Để kết nối
sock_read=30 # Để đọc dữ liệu
)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(
"https://target-site.com/api/data",
proxy="http://user:pass@proxy.example.com:8080"
) as response:
return await response.text()
Retry-logic: cách tiếp cận đúng
Timeout — không phải lúc nào cũng là lỗi chết người. Thường thì yêu cầu lặp lại sẽ thành công. Nhưng retry cần được thực hiện một cách thông minh.
Độ trễ hàm mũ
Đừng gửi yêu cầu lặp lại liên tục mà không có tạm dừng. Sử dụng exponential backoff: mỗi lần thử tiếp theo — với độ trễ tăng dần.
import requests
import time
import random
def fetch_with_retry(url, proxies, max_retries=3):
"""Yêu cầu với retry và độ trễ hàm mũ"""
for attempt in range(max_retries):
try:
response = requests.get(
url,
proxies=proxies,
timeout=(10, 30)
)
response.raise_for_status()
return response
except (requests.exceptions.Timeout,
requests.exceptions.ConnectionError) as e:
if attempt == max_retries - 1:
raise # Lần thử cuối cùng — ném lỗi
# Độ trễ hàm mũ: 1s, 2s, 4s...
# + jitter ngẫu nhiên để không tạo sóng yêu cầu
delay = (2 ** attempt) + random.uniform(0, 1)
print(f"Lần thử {attempt + 1} không thành công: {e}")
print(f"Thử lại trong {delay:.1f} giây...")
time.sleep(delay)
Thư viện tenacity
Đối với code production, sử dụng các giải pháp sẵn có sẽ tiện hơn:
from tenacity import retry, stop_after_attempt, wait_exponential
from tenacity import retry_if_exception_type
import requests
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
retry=retry_if_exception_type((
requests.exceptions.Timeout,
requests.exceptions.ConnectionError
))
)
def fetch_data(url, proxies):
response = requests.get(url, proxies=proxies, timeout=(10, 30))
response.raise_for_status()
return response.json()
Xoay vòng proxy khi timeout
Nếu một proxy liên tục gây ra timeout — vấn đề nằm ở nó. Giải pháp hợp lý: chuyển sang proxy khác.
import requests
from collections import deque
from dataclasses import dataclass, field
from typing import Optional
import time
@dataclass
class ProxyManager:
"""Trình quản lý proxy với theo dõi các lần thất bại"""
proxies: list
max_failures: int = 3
cooldown_seconds: int = 300
_failures: dict = field(default_factory=dict)
_cooldown_until: dict = field(default_factory=dict)
def get_proxy(self) -> Optional[str]:
"""Lấy proxy hoạt động"""
current_time = time.time()
for proxy in self.proxies:
# Bỏ qua proxy đang trong cooldown
if self._cooldown_until.get(proxy, 0) > current_time:
continue
return proxy
return None # Tất cả proxy đang trong cooldown
def report_failure(self, proxy: str):
"""Báo cáo yêu cầu không thành công"""
self._failures[proxy] = self._failures.get(proxy, 0) + 1
if self._failures[proxy] >= self.max_failures:
# Đưa proxy vào cooldown
self._cooldown_until[proxy] = time.time() + self.cooldown_seconds
self._failures[proxy] = 0
print(f"Proxy {proxy} được đưa vào cooldown")
def report_success(self, proxy: str):
"""Đặt lại bộ đếm lỗi khi thành công"""
self._failures[proxy] = 0
def fetch_with_rotation(url, proxy_manager, max_attempts=5):
"""Yêu cầu với tự động chuyển proxy khi lỗi"""
for attempt in range(max_attempts):
proxy = proxy_manager.get_proxy()
if not proxy:
raise Exception("Không có proxy khả dụng")
proxies = {"http": proxy, "https": proxy}
try:
response = requests.get(url, proxies=proxies, timeout=(10, 30))
response.raise_for_status()
proxy_manager.report_success(proxy)
return response
except (requests.exceptions.Timeout,
requests.exceptions.ConnectionError):
proxy_manager.report_failure(proxy)
print(f"Timeout qua {proxy}, thử proxy khác...")
continue
raise Exception(f"Không thể lấy dữ liệu sau {max_attempts} lần thử")
Khi sử dụng residential proxies với xoay vòng tự động, logic này được đơn giản hóa — nhà cung cấp tự động chuyển IP ở mỗi yêu cầu hoặc theo khoảng thời gian được chỉ định.
Yêu cầu không đồng bộ với kiểm soát timeout
Khi scraping hàng loạt, yêu cầu đồng bộ không hiệu quả. Cách tiếp cận không đồng bộ cho phép xử lý hàng trăm URL song song, nhưng cần xử lý cẩn thận timeout.
import aiohttp
import asyncio
from typing import List, Tuple
async def fetch_one(
session: aiohttp.ClientSession,
url: str,
semaphore: asyncio.Semaphore
) -> Tuple[str, str | None, str | None]:
"""Tải một URL với xử lý timeout"""
async with semaphore: # Giới hạn song song
try:
async with session.get(url) as response:
content = await response.text()
return (url, content, None)
except asyncio.TimeoutError:
return (url, None, "timeout")
except aiohttp.ClientError as e:
return (url, None, str(e))
async def fetch_all(
urls: List[str],
proxy: str,
max_concurrent: int = 10
) -> List[Tuple[str, str | None, str | None]]:
"""Tải hàng loạt với kiểm soát timeout và song song"""
timeout = aiohttp.ClientTimeout(total=45, connect=10, sock_read=30)
semaphore = asyncio.Semaphore(max_concurrent)
connector = aiohttp.TCPConnector(
limit=max_concurrent,
limit_per_host=5 # Không quá 5 kết nối trên một host
)
async with aiohttp.ClientSession(
timeout=timeout,
connector=connector
) as session:
# Đặt proxy cho tất cả yêu cầu
tasks = [
fetch_one(session, url, semaphore)
for url in urls
]
results = await asyncio.gather(*tasks)
# Thống kê
success = sum(1 for _, content, _ in results if content)
timeouts = sum(1 for _, _, error in results if error == "timeout")
print(f"Thành công: {success}, Timeout: {timeouts}")
return results
# Cách sử dụng
async def main():
urls = [f"https://example.com/page/{i}" for i in range(100)]
results = await fetch_all(
urls,
proxy="http://user:pass@proxy.example.com:8080",
max_concurrent=10
)
asyncio.run(main())
Quan trọng: Đừng đặt song song quá cao. 50-100 yêu cầu đồng thời qua một proxy — đã là nhiều. Tốt hơn là 10-20 với nhiều proxy.
Chẩn đoán: cách tìm nguyên nhân
Trước khi thay đổi cài đặt, hãy xác định nguồn của vấn đề.
Bước 1: Kiểm tra proxy trực tiếp
# Kiểm tra đơn giản qua curl với đo thời gian
curl -x http://user:pass@proxy:8080 \
-w "Connect: %{time_connect}s\nTotal: %{time_total}s\n" \
-o /dev/null -s \
https://httpbin.org/get
Nếu time_connect lớn hơn 5 giây — vấn đề nằm ở proxy hoặc mạng đến nó.
Bước 2: So sánh với yêu cầu trực tiếp
import requests
import time
def measure_request(url, proxies=None):
start = time.time()
try:
r = requests.get(url, proxies=proxies, timeout=30)
elapsed = time.time() - start
return f"OK: {elapsed:.2f}s, status: {r.status_code}"
except Exception as e:
elapsed = time.time() - start
return f"FAIL: {elapsed:.2f}s, error: {type(e).__name__}"
url = "https://target-site.com"
proxy = {"http": "http://proxy:8080", "https": "http://proxy:8080"}
print("Trực tiếp:", measure_request(url))
print("Qua proxy:", measure_request(url, proxy))
Bước 3: Kiểm tra các loại proxy khác nhau
Timeout có thể phụ thuộc vào loại proxy:
| Loại proxy | Độ trễ điển hình | Timeout được khuyến nghị |
|---|---|---|
| Datacenter | 50-200 ms | Connect: 5s, Read: 15s |
| Residential | 200-800 ms | Connect: 10s, Read: 30s |
| Mobile | 300-1500 ms | Connect: 15s, Read: 45s |
Bước 4: Ghi nhật ký chi tiết
import logging
import requests
from requests.adapters import HTTPAdapter
# Bật ghi nhật ký debug
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("urllib3").setLevel(logging.DEBUG)
# Bây giờ bạn sẽ thấy tất cả các giai đoạn của yêu cầu:
# - Phân giải DNS
# - Thiết lập kết nối
# - Gửi yêu cầu
# - Nhận phản hồi
Danh sách kiểm tra giải pháp lỗi timeout
Thuật toán ngắn gọn để hành động khi gặp timeout:
- Xác định loại timeout — connect hay read? Đây là những vấn đề khác nhau.
- Kiểm tra proxy riêng biệt — nó có hoạt động không? Độ trễ là bao nhiêu?
- Tăng timeout — có thể các giá trị quá tích cực cho loại proxy của bạn.
- Thêm retry với backoff — timeout đơn lẻ là bình thường, quan trọng là tính ổn định.
- Cấu hình xoay vòng — tự động chuyển sang proxy khác khi có vấn đề.
- Giới hạn song song — quá nhiều yêu cầu đồng thời sẽ quá tải proxy.
- Kiểm tra trang web đích — có thể nó đang chặn hoặc throttle yêu cầu của bạn.
Kết luận
Lỗi timeout qua proxy — vấn đề có thể giải quyết được. Trong hầu hết các trường hợp, chỉ cần cấu hình đúng timeout cho loại proxy, thêm retry-logic và triển khai xoay vòng khi sự cố xảy ra. Đối với các tác vụ có yêu cầu cao về độ ổn định, hãy sử dụng residential proxies với xoay vòng tự động — tìm hiểu thêm tại proxycove.com.