Trong bài viết Xây dựng Proxy Rotator với Python & Gunicorn trên VPS, chúng ta đã tạo ra một hệ thống mạnh mẽ. Nó quản lý và xoay vòng proxy hiệu quả từ file proxies.json. Tuy nhiên, khi hệ thống phát triển, mô hình Proxy xoay tự động này sẽ bộc lộ hai hạn chế lớn.
Thứ nhất, trạng thái proxy (điểm số, trạng thái sống/chết) được lưu trong bộ nhớ. Mỗi khi khởi động lại ứng dụng, toàn bộ dữ liệu này sẽ mất. Thứ hai, hệ thống không thể mở rộng theo chiều ngang. Chạy nhiều Gunicorn worker sẽ tạo ra nhiều trạng thái riêng biệt, gây ra sự không nhất quán.
Bài viết này là bản nâng cấp toàn diện. Chúng ta sẽ giải quyết triệt để những vấn đề trên bằng cách đưa Redis vào làm “bộ não” trung tâm cho máy chủ Proxy. Redis sẽ là nơi lưu trữ trạng thái bền bỉ và chia sẻ, biến Proxy Rotator của bạn thành một dịch vụ thực sự sẵn sàng cho production.
Toàn bộ giải pháp dùng Sorted Set + Lua script + TTL với các lệnh Redis phổ biến (ZADD, ZRANGEBYSCORE, EVAL/EVALSHA, SETEX…), tương thích với hầu hết mọi phiên bản Redis hiện hành (từ phiên bản 2.6 trở lên), đảm bảo bạn có thể áp dụng ngay mà không cần lo lắng về vấn đề tương thích.
Những gì bạn sẽ nhận được từ bài viết này:
- Tại sao cần nâng cấp: Giải quyết vấn đề mất trạng thái khi khởi động lại và không thể mở rộng (scale) của hệ thống cũ.
- Giải pháp cốt lõi: Chuyển toàn bộ trạng thái (proxy pool, điểm số) sang Redis, sử dụng cấu trúc Sorted Set để quản lý thông minh.
- Kỹ thuật chính: Dùng Lua script để đảm bảo tính nguyên tử (atomicity), giải quyết triệt để vấn đề tương tranh (race condition).
- Kết quả: Một hệ thống Proxy Rotator hoàn chỉnh, hiệu năng cao, sẵn sàng cho production với API (FastAPI) và hướng dẫn triển khai bằng Docker.
Kiến trúc hệ thống mới
Để giải quyết các hạn chế, chúng ta sẽ thay đổi kiến trúc. Thay vì mỗi tiến trình tự quản lý trạng thái, tất cả sẽ giao tiếp với một nguồn chân lý duy nhất: Redis. Điều này đảm bảo dữ liệu luôn nhất quán và bền bỉ, dù bạn chạy một hay một trăm worker.
Sơ đồ kiến trúc
Mô hình mới của chúng ta rất đơn giản nhưng cực kỳ mạnh mẽ. Mọi yêu cầu từ client sẽ đi qua API Server (FastAPI). Thay vì đọc file hay truy cập bộ nhớ cục bộ, API Server sẽ thực hiện mọi logic lấy, cập nhật, và quản lý proxy trực tiếp trên Redis.
Mô hình dữ liệu thông minh trên Redis
Chúng ta sẽ không dùng Redis như một kho key-value đơn giản. Thay vào đó, chúng ta tận dụng cấu trúc dữ liệu chuyên dụng của nó. Cốt lõi của hệ thống mới là một Sorted Set duy nhất, đóng vai trò là “bể proxy” (proxy pool).
- Key:
proxies:pool
- Member: Địa chỉ proxy (ví dụ:
1.2.3.4:8080)
- Score: Đây là điểm thông minh nhất. Thay vì điểm số 1-100,
score sẽ là Unix timestamp của thời điểm proxy sẵn sàng để sử dụng lại.
Cách tiếp cận này giải quyết bài toán cooldown một cách tự nhiên. Một proxy được coi là “khả dụng” nếu score (timestamp) của nó nhỏ hơn hoặc bằng thời gian hiện tại. Khi một proxy được sử dụng, score của nó sẽ được cập nhật thành thời gian hiện tại + thời gian cooldown.
Ghi chú về Replication và High Availability
Một câu hỏi thường gặp trong môi trường production là: “Liệu script Lua có hoạt động tốt với cơ chế nhân bản Master-Replica không?”. Câu trả lời là có, hoàn toàn hoạt động tốt. Redis được thiết kế thông minh để xử lý việc này. Thay vì nhân bản các lệnh bên trong script, Redis sẽ nhân bản chính lệnh EVALSHA cùng với mã script và các tham số. Điều này đảm bảo rằng mọi replica (bản sao) sẽ thực thi chính xác cùng một logic, giữ cho dữ liệu của bạn luôn nhất quán trên toàn bộ hệ thống.
Xây dựng lõi – Lớp quản lý ProxyManager
Bây giờ, hãy bắt tay vào viết code. Chúng ta sẽ xây dựng một lớp ProxyManager để đóng gói tất cả logic tương tác với Redis. Cấu trúc này giúp code sạch sẽ, dễ bảo trì và tái sử dụng.
Định nghĩa đối tượng Proxy
Để code rõ ràng và hiện đại, chúng ta sẽ sử dụng @dataclass để định nghĩa cấu trúc cho một đối tượng Proxy. Điều này tốt hơn nhiều so với việc dùng dictionary thông thường.
# app/models.py
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class Proxy:
"""Đại diện cho một đối tượng proxy trong hệ thống."""
id: str
host: str
port: int
username: Optional[str] = None
password: Optional[str] = None
# Sử dụng field để cung cấp giá trị mặc định cho list
protocols: list[str] = field(default_factory=lambda: ["https" (hoặc thêm "socks5" nếu có hỗ trợ)])
@property
def url(self) -> str:
"""Tạo URL proxy hoàn chỉnh từ các thành phần."""
if self.username and self.password:
return f"http://{self.username}:{self.password}@{self.host}:{self.port}"
return f"http://{self.host}:{self.port}"
Logic nguyên tử: Trái tim bất tử của hệ thống
Khi nhiều worker cùng lúc yêu cầu proxy, một vấn đề nghiêm trọng có thể xảy ra: Race Condition (Tình trạng tranh chấp). Worker A và Worker B có thể cùng lúc thấy proxy X là tốt nhất và cùng lấy nó ra sử dụng. Đây là lúc tính nguyên tử (atomicity) phát huy tác dụng.
Chúng ta cần một thao tác duy nhất, không thể bị xen ngang, để “Tìm proxy khả dụng và khóa nó lại”. Redis cung cấp một giải pháp hoàn hảo cho việc này: Lua script. Đoạn script này sẽ chạy trực tiếp trên server Redis, đảm bảo không một tiến trình nào khác có thể can thiệp vào giữa chừng.
Đoạn mã Lua “Pop-and-Lease”:
-- KEYS[1]: Tên của Sorted Set (ví dụ: "proxies:pool")
-- ARGV[1]: Thời gian hiện tại (current Unix timestamp)
-- ARGV[2]: Thời gian "thuê" (lease time, ví dụ: 30 giây)
-- Lấy ra proxy đầu tiên có score <= thời gian hiện tại (đã sẵn sàng)
local proxies = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, 1)
-- Nếu không tìm thấy proxy nào, trả về nil
if #proxies == 0 then
return nil
end
local proxy = proxies[1]
local lease_until = tonumber(ARGV[1]) + tonumber(ARGV[2])
-- Cập nhật lại score của proxy đó thành thời gian hết hạn thuê
-- Đây là hành động "khóa" proxy lại
redis.call('ZADD', KEYS[1], lease_until, proxy)
-- Trả về proxy đã được chọn
return proxy
Triển khai ProxyManager trong Python
Giờ hãy tích hợp đoạn script Lua trên vào lớp ProxyManager của chúng ta. Lớp này sẽ quản lý kết nối, tải script, và cung cấp các phương thức để tương tác với proxy pool, là một cách tiếp cận nâng cao hơn so với việc sử dụng Proxy với Python Requests thông thường.
# app/manager.py
import time
import redis
from typing import Optional
# Mã Lua script
LUA_POP_AND_LEASE_SCRIPT = """
local proxies = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, 1)
if #proxies == 0 then
return nil
end
local proxy = proxies[1]
local lease_until = tonumber(ARGV[1]) + tonumber(ARGV[2])
redis.call('ZADD', KEYS[1], lease_until, proxy)
return proxy
"""
class ProxyManager:
def __init__(self, redis_url: str, pool_key: str = "proxies:pool"):
# Sử dụng ConnectionPool để tái sử dụng kết nối, rất quan trọng cho performance
pool = redis.ConnectionPool.from_url(redis_url, decode_responses=True)
self.db = redis.Redis(connection_pool=pool)
self.pool_key = pool_key
# Tải script vào Redis và cache lại mã SHA, giúp gọi lại nhanh hơn
self.pop_script_sha = self.db.script_load(LUA_POP_AND_LEASE_SCRIPT)
def add_proxy(self, proxy_url: str):
"""Thêm một proxy mới vào pool, sẵn sàng để sử dụng ngay."""
# Score là timestamp hiện tại, nghĩa là nó có thể được lấy ra ngay lập tức
self.db.zadd(self.pool_key, {proxy_url: time.time()})
def reserve_proxy(self, lease_seconds: int = 30) -> Optional[str]:
"""
Lấy và "thuê" một proxy khả dụng. Đây là hàm cốt lõi.
Nó sẽ trả về URL của proxy hoặc None nếu không có proxy nào sẵn sàng.
"""
now = time.time()
try:
proxy = self.db.evalsha(self.pop_script_sha, 1, self.pool_key, now, lease_seconds)
return proxy
except redis.exceptions.NoScriptError:
# Nếu Redis bị restart và mất cache, tải lại script và thử lại
self.pop_script_sha = self.db.script_load(LUA_POP_AND_LEASE_SCRIPT)
proxy = self.db.evalsha(self.pop_script_sha, 1, self.pool_key, now, lease_seconds)
return proxy
def release_proxy(self, proxy_url: str, cooldown_seconds: int = 1):
"""
"Trả" proxy về pool sau khi dùng thành công.
Proxy sẽ sẵn sàng sau một khoảng cooldown ngắn.
"""
new_score = time.time() + cooldown_seconds
self.db.zadd(self.pool_key, {proxy_url: new_score})
def penalize_proxy(self, proxy_url: str, penalty_seconds: int = 300):
"""
"Phạt" proxy nếu dùng thất bại.
Proxy sẽ bị khóa trong một khoảng thời gian dài hơn.
"""
new_score = time.time() + penalty_seconds
self.db.zadd(self.pool_key, {proxy_url: new_score})
def remove_proxy(self, proxy_url: str):
"""Xóa vĩnh viễn một proxy khỏi pool."""
self.db.zrem(self.pool_key, proxy_url)
Nâng cấp API Server với FastAPI
Với lớp ProxyManager đã sẵn sàng, việc nâng cấp API Server từ bài viết trước trở nên rất đơn giản. Chúng ta chỉ cần thay thế đối tượng ProxyRotator cũ bằng ProxyManager và cập nhật lại logic của các endpoint.
# app/main.py
import os
from fastapi import FastAPI, HTTPException, Security
from fastapi.security import APIKeyHeader
from contextlib import asynccontextmanager
from .manager import ProxyManager
# Đọc URL Redis từ biến môi trường để linh hoạt hơn
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
# Khởi tạo đối tượng manager toàn cục
manager = ProxyManager(redis_url=REDIS_URL)
# Cấu hình bảo mật API Key
API_KEY_NAME = "X-API-KEY"
VALID_API_KEY = os.getenv("API_KEY", "YOUR_SECRET_API_KEY") # Đọc key từ môi trường
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=True)
async def get_api_key(api_key: str = Security(api_key_header)):
if api_key == VALID_API_KEY:
return api_key
raise HTTPException(status_code=403, detail="Invalid API Key")
app = FastAPI(dependencies=[Security(get_api_key)])
@app.get("/proxy/get")
def get_proxy(lease_seconds: int = 60):
"""Lấy một proxy khả dụng và thuê nó trong một khoảng thời gian."""
proxy_url = manager.reserve_proxy(lease_seconds)
if not proxy_url:
raise HTTPException(status_code=503, detail="No healthy proxies available.")
return {"proxy": proxy_url}
@app.post("/proxy/report")
def report_result(proxy_url: str, success: bool):
"""Client báo cáo kết quả sử dụng proxy."""
if success:
manager.release_proxy(proxy_url)
message = f"Proxy {proxy_url} released successfully."
else:
manager.penalize_proxy(proxy_url)
message = f"Proxy {proxy_url} penalized."
return {"status": "ok", "message": message}
# Tái triển khai Sticky Sessions bằng Redis
@app.get("/proxy/sticky")
def get_sticky_proxy(client_key: str, sticky_seconds: int = 300):
"""Lấy proxy theo session, sử dụng Redis TTL."""
sticky_key = f"sticky:{client_key}"
proxy_url = manager.db.get(sticky_key)
if not proxy_url:
proxy_url = manager.reserve_proxy(lease_seconds=sticky_seconds)
if not proxy_url:
raise HTTPException(status_code=503, detail="No healthy proxies available.")
# Dùng SETEX để set key với thời gian hết hạn (TTL)
manager.db.setex(sticky_key, sticky_seconds, proxy_url)
return {"proxy": proxy_url, "client_key": client_key}
Di chuyển dữ liệu & triển khai
Hệ thống đã sẵn sàng, bước cuối cùng là di chuyển dữ liệu từ file proxies.json cũ và đóng gói ứng dụng để triển khai.
Script di chuyển dữ liệu
Chúng ta cần một script chạy một lần để đọc tất cả proxy từ file proxies.json và thêm chúng vào Redis.
# migrate_json_to_redis.py
import json
import os
import redis
def migrate():
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
JSON_FILE = "proxies.json"
POOL_KEY = "proxies:pool"
print(f"Connecting to Redis at {REDIS_URL}...")
r = redis.from_url(REDIS_URL, decode_responses=True)
if not os.path.exists(JSON_FILE):
print(f"Error: {JSON_FILE} not found.")
return
with open(JSON_FILE, "r") as f:
proxies_from_json = json.load(f)
migrated_count = 0
for proxy_info in proxies_from_json:
proxy_url = proxy_info.get("url")
if proxy_url:
# zadd sẽ tự động cập nhật nếu proxy đã tồn tại
r.zadd(POOL_KEY, {proxy_url: time.time()})
migrated_count += 1
print(f"Migration complete. Migrated {migrated_count} proxies to Redis set '{POOL_KEY}'.")
if __name__ == "__main__":
migrate()
Triển khai với Docker Compose
Docker Compose là cách tốt nhất để chạy ứng dụng của chúng ta cùng với Redis trong môi trường production. Nó đảm bảo cả hai dịch vụ khởi động cùng nhau và có thể giao tiếp qua mạng nội bộ trên VPS của bạn.
requirements.txt
fastapi
uvicorn
gunicorn
redis
Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY ./app /app/app
# CMD để chạy Gunicorn với 4 Uvicorn worker
CMD ["gunicorn", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "app.main:app", "--bind", "0.0.0.0:8000"]
docker-compose.yml
version: '3.8'
services:
redis:
image: redis:7-alpine
restart: always
volumes:
- redis_data:/data
command: redis-server --appendonly yes
proxy-rotator:
build: .
restart: always
ports:
- "8000:8000"
environment:
# Kết nối tới Redis service tên là 'redis'
- REDIS_URL=redis://redis:6379
# Đặt API Key của bạn ở đây
- API_KEY=YOUR_SECRET_KEY
depends_on:
- redis
volumes:
redis_data:
Để triển khai, bạn chỉ cần chạy lệnh docker-compose up -d. Hệ thống Proxy Rotator hiệu năng cao của bạn giờ đã sẵn sàng hoạt động!
Câu hỏi thường gặp (FAQ)
1. Tại sao Redis lại tốt hơn file JSON để quản lý Proxy Rotator?
Redis vượt trội hơn hẳn vì:
- Tốc độ cực nhanh do hoạt động trên RAM.
- Xử lý được hàng ngàn yêu cầu đồng thời mà không bị khóa file.
- Dữ liệu (trạng thái, điểm số proxy) được giữ lại ngay cả khi ứng dụng khởi động lại.
2. Tại sao lại dùng Sorted Set mà không phải Hash hay List trong Redis?
Đây là một lựa chọn kỹ thuật quan trọng. Mỗi cấu trúc dữ liệu có một thế mạnh riêng:
- List: Chỉ là hàng đợi đơn giản. Bạn không thể truy vấn hiệu quả các proxy dựa trên “điểm số” hay “thời gian khả dụng”.
- Hash: Rất tốt để lưu thông tin chi tiết của một proxy, nhưng không cung cấp cơ chế sắp xếp cho nhiều proxy.
- Sorted Set (Lựa chọn hoàn hảo): Nó vừa đảm bảo mỗi proxy là duy nhất, vừa gắn liền mỗi proxy với một “điểm số” (score). Quan trọng nhất, Redis tự động duy trì sự sắp xếp này, cho phép chúng ta thực hiện các truy vấn cực nhanh như “lấy proxy có score thấp nhất” – đây chính là chìa khóa cho hiệu năng của hệ thống.
3. “Race Condition” trong Proxy Rotator là gì?
Đó là tình trạng khi nhiều tiến trình/worker cùng lúc thấy một proxy là “tốt nhất” và đồng thời lấy nó ra sử dụng. Điều này dẫn đến việc một proxy bị dùng nhiều lần trong cùng một thời điểm, gây lãng phí tài nguyên và làm sai lệch trạng thái của proxy pool.
4. Sử dụng Lua script có thực sự cần thiết không?
Có, nó cực kỳ quan trọng. Các lệnh Redis đơn lẻ không thể giải quyết được “race condition”. Lua script cho phép chúng ta gộp nhiều lệnh (tìm proxy tốt nhất và cập nhật trạng thái “đang sử dụng” của nó) thành một thao tác nguyên tử duy nhất, không thể bị xen ngang, đảm bảo mỗi proxy chỉ được cấp cho một worker tại một thời điểm.
5. Hệ thống này có thể quản lý bao nhiêu proxy?
Hệ thống này có thể dễ dàng quản lý hàng trăm nghìn proxy. Redis và cấu trúc dữ liệu Sorted Set được thiết kế để duy trì hiệu năng cao ngay cả với tập dữ liệu rất lớn, với độ phức tạp của các thao tác cốt lõi chỉ là O(log N).
6. Giải pháp này có hoạt động với Proxy dân cư hoặc các loại proxy khác không?
Có. Hệ thống này quản lý proxy thông qua URL của chúng. Nó hoàn toàn không quan tâm đó là proxy dân cư, proxy datacenter hay proxy di động. Miễn là bạn có URL kết nối, hệ thống đều có thể quản lý được.
Kết luận
Bằng cách chuyển từ file JSON và trạng thái trong bộ nhớ sang Redis, chúng ta đã nâng cấp Proxy Rotator từ một công cụ hữu ích thành một dịch vụ mạnh mẽ, đáng tin cậy.
Những lợi ích chính đạt được:
- Bền bỉ: Trạng thái proxy được lưu trữ an toàn, không bị mất khi khởi động lại.
- Khả năng mở rộng: Bạn có thể chạy nhiều worker hoặc nhiều server cùng lúc, tất cả đều chia sẻ một trạng thái chung duy nhất.
- Hiệu năng cao: Tốc độ của Redis và logic nguyên tử của Lua script đảm bảo hệ thống phản hồi nhanh và xử lý tương tranh một cách hoàn hảo.
Hệ thống mới này tạo ra một nền tảng vững chắc để bạn tiếp tục phát triển các tính năng nâng cao hơn, như xây dựng một giao diện dashboard để theo dõi trạng thái proxy, hoặc tích hợp các logic phạt/thưởng phức tạp hơn, phục vụ cho các tác vụ như thu thập dữ liệu theo lịch (scheduled data collection) cho hệ thống giám sát. Chúc bạn thành công!
Tài liệu tham khảo