Thiết kế circuit breaker cho proxy pool khi nhà cung cấp gặp sự cố
20 tháng 9, 2026
Vì sao xoay IP không phải là circuit breaker
Proxy pool không chỉ là một danh sách địa chỉ để luân phiên. Khi một nhà cung cấp gặp sự cố, việc tiếp tục chọn IP từ nhóm đó chỉ khiến lỗi được phân phối nhanh hơn. Timeout, DNS, TLS handshake hoặc lỗi gateway có thể ảnh hưởng hàng loạt request thay vì chỉ làm chậm một vài địa chỉ.
Giả sử scraper SEO chạy 12.000 lượt kiểm tra mỗi ngày. Provider A phụ trách 40% lưu lượng, nhưng sau một thay đổi mạng, p95 latency tăng từ 400 ms lên 3,8 giây và tỷ lệ 502 đạt 35%. Nếu bộ xoay IP không nhận biết tình trạng này, 40% lưu lượng vẫn được gửi vào provider A trong nhiều phút. Kết quả là dữ liệu chậm, chi phí tăng và các provider khỏe hơn bị quá tải khi nhận lưu lượng chuyển hướng.
Circuit breaker giúp hệ thống tạm ngừng sử dụng một upstream bị suy giảm, thay vì coi mọi request đều có cơ hội như nhau. Mục tiêu không phải là né mọi lỗi 4xx, mà là nhận diện nhanh các vấn đề của mạng, DNS, TLS và gateway proxy.
Phân loại lỗi trước khi mở mạch
Trước khi thay đổi trạng thái provider, hãy phân loại nguyên nhân:
- Lỗi mạng: kết nối timeout, DNS không phân giải, TCP reset hoặc TLS handshake thất bại.
- Lỗi gateway proxy: 408, 502, 503, 504 hoặc 429 đến từ chính nhà cung cấp proxy.
- Phản hồi từ trang đích: 403, 429 và 404 thường thuộc chính sách hoặc trạng thái của trang đích, không tự động chứng minh provider bị hỏng.
- Phản hồi hợp lệ nhưng chậm: request thành công nhưng vượt ngưỡng latency đã định.
Đừng mở circuit chỉ vì nhận được 403. 403 có thể là yêu cầu xác thực, giới hạn theo vùng hoặc quy tắc của trang đích. Ngược lại, 502 từ gateway và timeout lặp lại trên nhiều IP thuộc cùng provider là tín hiệu mạnh hơn. Nếu có thể, hãy ghi nhận nguồn phản hồi qua header, log của provider hoặc lớp proxy trung gian.
Ba trạng thái cần có
Một circuit breaker đáng tin cậy nên có ba trạng thái:
- Closed: Provider hoạt động bình thường và nhận lưu lượng theo trọng số.
- Open: Provider bị tạm loại. Không gửi request mới trong thời gian làm mát, trừ các probe kiểm tra.
- Half-open: Gửi một số request thử nghiệm hạn chế. Chỉ đóng circuit khi mẫu probe thành công và đáp ứng latency.
Dùng cửa sổ trượt 20 đến 30 request thay vì đếm lỗi vĩnh viễn. Như vậy, một sự cố cũ không giữ provider ở trạng thái open indefinitely. Đồng thời, đặt số mẫu tối thiểu, chẳng hạn 10 request, để tránh mở mạch do một hai lỗi ngẫu nhiên.
Nên cấu hình breaker theo provider, khu vực và giao thức. Một provider datacenter ở Mỹ có thể khỏe trong khi nhóm mobile châu Âu gặp lỗi. Nếu gộp tất cả về một trạng thái chung, hệ thống sẽ либо bỏ qua vùng lành либо loại bỏ cả provider khi chỉ một segment bị hỏng.
Đặt ngưỡng thực tế
Một cấu hình khởi đầu có thể là:
- Cửa sổ: 20 đến 30 request.
- Mẫu tối thiểu: 10 request.
- Tỷ lệ lỗi gateway: từ 25% đến 35%.
- p95 latency: lớn hơn 1 giây trong hai cửa sổ liên tiếp.
- Thời gian open: 30 đến 60 giây.
- Probe half-open: 3 đến 5 request.
Các con số này không phải hằng số phổ quát. Hãy dựa vào SLA, loại proxy và hành vi của trang đích. Residential proxy có thể có độ trễ biến động cao hơn datacenter; mobile proxy có thể cần thời gian kết nối dài hơn. Quan trọng là đo trước khi chọn ngưỡng, rồi điều chỉnh theo phần trăm lỗi thật thay vì một ngưỡng cảm tính.
Một mẫu circuit breaker bằng Python
Đoạn dưới đây minh họa trạng thái theo provider. Hàm ok đang coi các mã gateway là lỗi upstream; trong hệ thống thực tế, hãy phân biệt thêm header và metadata do provider trả về.
import random
import time
from collections import deque
from dataclasses import dataclass, field
import requests
@dataclass
class ProviderState:
window: deque = field(default_factory=lambda: deque(maxlen=30))
state: str = 'closed'
opened_at: float = 0.0
half_open_used: int = 0
min_samples: int = 10
error_ratio: float = 0.30
open_seconds: float = 30.0
latency_limit_ms: float = 1000.0
def record(self, ok, latency_ms):
self.window.append((ok, latency_ms))
if self.state == 'half_open':
self.half_open_used += 1
if not ok or latency_ms > self.latency_limit_ms:
self.state = 'open'
self.opened_at = time.time()
return
if self.half_open_used >= 3:
self.state = 'closed'
self.window.clear()
return
if len(self.window) < self.min_samples:
return
errors = sum(not item[0] for item in self.window)
if errors / len(self.window) >= self.error_ratio:
self.state = 'open'
self.opened_at = time.time()
def allow(self):
if self.state == 'closed':
return True
if self.state == 'open':
cooldown = self.open_seconds + random.uniform(0, 5)
if time.time() - self.opened_at >= cooldown:
self.state = 'half_open'
self.half_open_used = 0
return True
return False
if self.half_open_used >= 3:
return False
return True
states = {
'us-east': ProviderState(),
'eu-west': ProviderState(),
}
weights = {
'us-east': 50,
'eu-west': 30,
}
def choose_provider(names, weights):
candidates = [name for name in names if states[name].allow()]
if not candidates:
raise RuntimeError('tất cả provider đang ở trạng thái open')
total = sum(weights[name] for name in candidates)
point = random.uniform(0, total)
for name in candidates:
point -= weights[name]
if point <= 0:
return name
return candidates[-1]
def fetch(url, proxy_by_provider, proxies):
provider = choose_provider(list(proxies), weights)
state = states[provider]
started = time.perf_counter()
try:
response = requests.get(
url,
proxies={'http': proxy_by_provider[provider],
'https': proxy_by_provider[provider]},
timeout=(2, 5)
)
# 403 hoặc 429 từ trang đích được xử lý ở tầng nghiệp vụ riêng.
ok = response.status_code < 500
latency_ms = response.elapsed.total_seconds() * 1000
state.record(ok, latency_ms)
return response
except requests.RequestException:
latency_ms = (time.perf_counter() - started) * 1000
state.record(False, latency_ms)
raise
Trong dịch vụ nhiều luồng, dùng lock hoặc cơ chế single-writer cho mỗi ProviderState; nếu không, hai request có thể thay đổi trạng thái đồng thời và làm vòng lặp flapping. Backoff cũng nên có jitter, như đoạn mã, để tránh hàng nghìn worker cùng thử lại đúng một thời điểm.
Chuyển tuyến có trọng số
Khi một provider mở circuit, đừng gửi toàn bộ lưu lượng sang một provider dự phòng. Giả sử ban đầu có ba nhóm với trọng số 50, 30 và 20. Nếu nhóm 50 bị open, hãy phân bổ lại phần còn lại theo dung lượng thực tế của hai nhóm khỏe, nhưng đặt trần, chẳng hạn không cho một nhóm nhận quá 70% lưu lượng.
Việc chọn provider nên dựa trên:
- Tỷ lệ request thành công theo từng segment.
- p50 và p95 latency.
- Dung lượng còn lại và giới hạn giá.
- Khả năng giữ đúng quốc gia, giao thức và loại IP.
- Lịch sử chuyển vùng gần đây.
Chuyển hướng đột ngột có thể tạo ra lỗi mới. Provider đang khỏe có thể tự đạt đến rate limit của chính nó, hoặc trang đích nhận thấy một nguồn IP duy nhất tăng tốc độ bất thường. Hãy tăng trọng số từ từ và theo dõi phản hồi trong vài phút.
Giữ nguyên phiên và cấu hình an toàn
Circuit breaker không nên tự đổi provider giữa một phiên đang chạy. Với các tác vụ cần phiên ổn định, gán session ID qua hash tới một provider, giữ sticky TTL và chỉ di chuyển tại ranh giới phiên. Nếu provider bị open, tạm dừng hoặc hoàn tất request hiện tại trước khi chuyển.
Đừng khắc phục TLS bằng cách tắt xác thực chứng chỉ. Với HTTPS, ưu tiên CONNECT qua proxy hỗ trợ, giữ nguyên certificate validation và kiểm tra ngày hết hạn của chứng chỉ. Nếu chỉ cần truy cập TCP hoặc SOCKS5, hãy bảo đảm ứng dụng và proxy cùng hỗ trợ giao thức đó. Cấu hình timeout ngắn ở tầng kết nối cũng giúp phát hiện provider chậm mà không giữ thread hoặc worker bị chặn lâu.
Đo lường để không mở mạch nhầm
Ghi ít nhất các chỉ số sau:
- Số request theo provider, khu vực và loại proxy.
- Số lỗi theo mã HTTP và nguyên nhân network.
- p50, p95 và p99 latency.
- Số lần chuyển từ closed sang open hoặc half-open.
- Tỷ lệ request được chuyển hướng.
- Tỷ lệ lỗi của provider khỏe sau khi nhận thêm lưu lượng.
Thêm một request canary nhỏ, chẳng hạn kiểm tra một URL nội bộ hoặc endpoint không quan trọng, để phát hiện sự cố trước khi nó ảnh hưởng toàn bộ job. Dashboard nên hiển thị cả circuit state và chất lượng phản hồi; chỉ nhìn tỷ lệ lỗi tổng sẽ che giấu một provider đang suy giảm nhẹ nhưng ngày càng nghiêm trọng.
Thử nghiệm và giới hạn đạo đức
Trước khi đưa vào production, mô phỏng timeout, DNS failure, 502, 503 và latency cao. Kiểm tra rằng hệ thống không tạo retry storm, không gửi request POST nhiều lần không kiểm soát và không chọn provider không phù hợp vùng địa lý. Với các thao tác không có tính idempotent, thêm khóa phân phối hoặc cơ chế xác nhận trước khi retry.
Circuit breaker là công cụ duy trì độ bền, không phải cách né rate limit. Hãy tôn trọng robots.txt, điều khoản dịch vụ, giới hạn tần suất và cơ chế CAPTCHA; chỉ thu thập dữ liệu được phép và dùng mức tốc độ phù hợp. Nếu trang đích trả 429, hãy tăng thời gian chờ hoặc giảm tốc độ tại tầng scraper thay vì liên tục đổi IP.
Checklist triển khai
- Phân loại lỗi network, gateway và phản hồi từ trang đích.
- Tạo trạng thái closed, open và half-open cho từng provider hoặc segment.
- Dùng cửa sổ trượt, mẫu tối thiểu và backoff có jitter.
- Chuyển lưu lượng theo trọng số khỏe, có trần dung lượng.
- Giữ affinity của session và không đổi provider giữa phiên.
- Theo dõi latency, lỗi, state transition và kiểm tra bằng故障 injection.
Làm theo trình tự này giúp proxy pool phản ứng với sự cố thay vì chỉ xoay IP nhanh hơn. Hệ thống sẽ tốn ít thời gian hơn trong timeout, giảm áp lực lên provider khỏe và trả về dữ liệu ổn định hơn khi mạng thay đổi.