Đừng đổi nhà cung cấp giải CAPTCHA chỉ vì bảng giá. Cho bên hiện tại và CaptchaAI giải cùng một thử thách trong 3–5 ngày: bên cũ vẫn phục vụ production, CaptchaAI chỉ quan sát và ghi log. Hết đợt, bạn có tỷ lệ giải thành công, thời gian giải P95 và chi phí trên tải thật của mình.
Sáu số liệu cần chốt trước khi chạy song song
| Số liệu | Cách tính |
|---|---|
| Tỷ lệ giải thành công | successful_solves / total_attempts × 100 |
| Thời gian giải trung bình | Từ lúc gửi task tới lúc nhận token |
| Thời gian giải P95 | Phân vị 95 các lượt thành công |
| Tỷ lệ lỗi theo mã | Đếm riêng từng mã lỗi |
| Chi phí mỗi lượt giải | Tổng chi chia số lượt thành công |
| Token có dùng được không | Trang đích nhận hay từ chối |
Dòng cuối hay bị bỏ quên: status: 1 chỉ nói API đã trả kết quả; form phía sau có nhận token hay không phải kiểm ở tầng ứng dụng.
Benchmark của người khác không đo tải của bạn
Số liệu công bố sẵn đo trên site key khác, khung giờ khác, hạ tầng khác. Đội QA thuê ngoài ở TP.HCM chạy hồi quy hằng đêm khung 1h–5h sáng sẽ thấy bức tranh khác hẳn đội theo dõi giá Shopee, Lazada, Tiki ban ngày. Vậy nên hãy đo từ máy chủ production và rải mẫu qua nhiều khung giờ.
Loại CAPTCHA nào nên đưa vào phép đo
Chỉ so những loại cả hai bên đều giải được. CaptchaAI hỗ trợ reCAPTCHA v2/v3 (gồm Enterprise), Cloudflare Turnstile và Challenge, GeeTest v3, CAPTCHA ảnh/OCR, grid-image, BLS; CaptchaFox, Friendly Captcha và Lemin ở giai đoạn beta. hCaptcha và FunCaptcha (Arkose Labs) chưa hỗ trợ, GeeTest v4 sắp ra mắt — tách chúng khỏi phép so sánh.
Lộ trình bốn giai đoạn sang CaptchaAI
| Giai đoạn | Thời lượng | Lưu lượng | Mục tiêu |
|---|---|---|---|
| 1. Xác thực | 1 ngày | 0% production | Kiểm tra tương thích API |
| 2. Đo nền | 3 ngày | 5% (có dự phòng) | Thu số liệu trên tải thật |
| 3. Tăng dần | 1 tuần | 25% -> 50% -> 75% | Giám sát ở từng mức |
| 4. Chuyển hẳn | — | 100% CaptchaAI | Ngừng nhà cung cấp cũ |
Hai giai đoạn đầu tốn thêm tiền vì bạn trả cả hai bên. Phần của CaptchaAI cố định theo gói, không tăng theo số lượt giải: BASIC ($15/tháng, 5 thread) đủ cho đợt đo nhỏ, ADVANCE ($90/tháng, 50 thread) cho khối lượng lớn — giá tính theo thread (luồng giải đồng thời).
Sơ đồ bộ đo song song
Ứng dụng gọi một router; router nhân đôi request sang hai bên rồi đẩy vào bộ thu số liệu.
┌──────────────┐
│ Your App │
└──────┬───────┘
│
┌──────▼───────┐
│ CAPTCHA │
│ Router │
└──┬───────┬───┘
│ │
┌────────▼──┐ ┌──▼────────┐
│ Current │ │ CaptchaAI │
│ Provider │ │ │
└────────┬──┘ └──┬────────┘
│ │
┌──▼───────▼──┐
│ Metrics │
│ Collector │
└─────────────┘
Dựng bộ đo bằng Python
Class dùng chung cho hai bên
Cả hai đều dùng API kiểu in.php / res.php, nên một class là đủ: gửi task, polling, trả về SolveResult kèm thời gian.
import os
import time
import requests
from dataclasses import dataclass, field
from typing import Optional
from concurrent.futures import ThreadPoolExecutor
@dataclass
class SolveResult:
provider: str
success: bool
solution: Optional[str] = None
error: Optional[str] = None
elapsed: float = 0.0
cost: float = 0.0
class CaptchaProvider:
def __init__(self, name, submit_url, result_url, api_key):
self.name = name
self.submit_url = submit_url
self.result_url = result_url
self.api_key = api_key
self.session = requests.Session()
def solve_recaptcha(self, sitekey, pageurl):
start = time.time()
resp = self.session.post(self.submit_url, data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
return SolveResult(
provider=self.name, success=False,
error=data.get("request"), elapsed=time.time() - start
)
captcha_id = data["request"]
for _ in range(60):
time.sleep(5)
result = self.session.get(self.result_url, params={
"key": self.api_key, "action": "get",
"id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
return SolveResult(
provider=self.name, success=True,
solution=result["request"], elapsed=time.time() - start
)
if result.get("request") != "CAPCHA_NOT_READY":
return SolveResult(
provider=self.name, success=False,
error=result.get("request"), elapsed=time.time() - start
)
return SolveResult(
provider=self.name, success=False,
error="TIMEOUT", elapsed=time.time() - start
)
Chạy song song và in báo cáo
Cùng một sitekey gửi tới hai bên trong hai thread, chờ cả hai xong rồi mới tổng hợp.
class ParallelTestRunner:
def __init__(self, primary, challenger):
self.primary = primary
self.challenger = challenger
self.results = {"primary": [], "challenger": []}
def run_test(self, sitekey, pageurl, num_runs=20):
print(f"Running {num_runs} parallel solves...")
for i in range(num_runs):
with ThreadPoolExecutor(max_workers=2) as executor:
primary_future = executor.submit(
self.primary.solve_recaptcha, sitekey, pageurl
)
challenger_future = executor.submit(
self.challenger.solve_recaptcha, sitekey, pageurl
)
primary_result = primary_future.result()
challenger_result = challenger_future.result()
self.results["primary"].append(primary_result)
self.results["challenger"].append(challenger_result)
print(f" Run {i+1}/{num_runs}: "
f"{self.primary.name}={'OK' if primary_result.success else 'FAIL'} "
f"({primary_result.elapsed:.1f}s) | "
f"{self.challenger.name}={'OK' if challenger_result.success else 'FAIL'} "
f"({challenger_result.elapsed:.1f}s)")
return self.generate_report()
def generate_report(self):
report = {}
for label, results in self.results.items():
total = len(results)
successes = sum(1 for r in results if r.success)
times = [r.elapsed for r in results if r.success]
errors = [r.error for r in results if not r.success]
report[label] = {
"provider": results[0].provider if results else "unknown",
"total": total,
"successes": successes,
"success_rate": (successes / total * 100) if total else 0,
"avg_time": sum(times) / len(times) if times else 0,
"min_time": min(times) if times else 0,
"max_time": max(times) if times else 0,
"errors": errors
}
return report
# Usage
current = CaptchaProvider(
name="CurrentProvider",
submit_url="https://current-provider.com/in.php",
result_url="https://current-provider.com/res.php",
api_key="current_key"
)
captchaai = CaptchaProvider(
name="CaptchaAI",
submit_url="https://ocr.captchaai.com/in.php",
result_url="https://ocr.captchaai.com/res.php",
api_key=os.environ["CAPTCHAAI_API_KEY"]
)
runner = ParallelTestRunner(primary=current, challenger=captchaai)
report = runner.run_test(
sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl="https://example.com/form",
num_runs=20
)
for label, stats in report.items():
print(f"\n{stats['provider']}:")
print(f" Success rate: {stats['success_rate']:.1f}%")
print(f" Avg time: {stats['avg_time']:.1f}s")
print(f" Min/Max: {stats['min_time']:.1f}s / {stats['max_time']:.1f}s")
if stats['errors']:
print(f" Errors: {stats['errors']}")
Chia một phần lưu lượng, có dự phòng
Giai đoạn 2 cho một phần lưu lượng thật đi qua CaptchaAI; lượt nào lỗi thì rơi về bên cũ.
import random
class TrafficSplitter:
def __init__(self, primary, challenger, challenger_pct=10):
self.primary = primary
self.challenger = challenger
self.challenger_pct = challenger_pct
def solve(self, sitekey, pageurl):
if random.randint(1, 100) <= self.challenger_pct:
result = self.challenger.solve_recaptcha(sitekey, pageurl)
if not result.success:
# Fall back to primary on failure
return self.primary.solve_recaptcha(sitekey, pageurl)
return result
return self.primary.solve_recaptcha(sitekey, pageurl)
# Start with 10%, increase as confidence builds
splitter = TrafficSplitter(current, captchaai, challenger_pct=10)
result = splitter.solve(sitekey="...", pageurl="...")
Chỉ tăng tỷ lệ khi số lượt rơi về bên cũ ổn định suốt 24 giờ.
Bản Node.js tương đương
Logic giống hệt bản Python, chỉ đổi runtime.
const axios = require("axios");
class CaptchaProvider {
constructor(name, submitUrl, resultUrl, apiKey) {
this.name = name;
this.submitUrl = submitUrl;
this.resultUrl = resultUrl;
this.apiKey = apiKey;
}
async solveRecaptcha(sitekey, pageurl) {
const start = Date.now();
try {
const submit = await axios.post(this.submitUrl, null, {
params: { key: this.apiKey, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
});
if (submit.data.status !== 1) {
return { provider: this.name, success: false, error: submit.data.request, elapsed: (Date.now() - start) / 1000 };
}
const captchaId = submit.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get(this.resultUrl, {
params: { key: this.apiKey, action: "get", id: captchaId, json: 1 },
});
if (poll.data.status === 1) {
return { provider: this.name, success: true, solution: poll.data.request, elapsed: (Date.now() - start) / 1000 };
}
if (poll.data.request !== "CAPCHA_NOT_READY") {
return { provider: this.name, success: false, error: poll.data.request, elapsed: (Date.now() - start) / 1000 };
}
}
return { provider: this.name, success: false, error: "TIMEOUT", elapsed: (Date.now() - start) / 1000 };
} catch (err) {
return { provider: this.name, success: false, error: err.message, elapsed: (Date.now() - start) / 1000 };
}
}
}
async function parallelTest(current, captchaai, sitekey, pageurl, runs = 20) {
const results = { current: [], captchaai: [] };
for (let i = 0; i < runs; i++) {
const [currentResult, captchaaiResult] = await Promise.all([
current.solveRecaptcha(sitekey, pageurl),
captchaai.solveRecaptcha(sitekey, pageurl),
]);
results.current.push(currentResult);
results.captchaai.push(captchaaiResult);
console.log(`Run ${i + 1}/${runs}: ${current.name}=${currentResult.success ? "OK" : "FAIL"} ` +
`(${currentResult.elapsed.toFixed(1)}s) | ${captchaai.name}=${captchaaiResult.success ? "OK" : "FAIL"} ` +
`(${captchaaiResult.elapsed.toFixed(1)}s)`);
}
for (const [label, data] of Object.entries(results)) {
const successes = data.filter((r) => r.success).length;
const times = data.filter((r) => r.success).map((r) => r.elapsed);
const avgTime = times.length ? times.reduce((a, b) => a + b, 0) / times.length : 0;
console.log(`\n${label}: ${successes}/${runs} success (${((successes / runs) * 100).toFixed(1)}%), avg ${avgTime.toFixed(1)}s`);
}
}
// Run
const currentProvider = new CaptchaProvider("CurrentProvider", "https://current-provider.com/in.php", "https://current-provider.com/res.php", "current_key");
const captchaai = new CaptchaProvider("CaptchaAI", "https://ocr.captchaai.com/in.php", "https://ocr.captchaai.com/res.php", process.env.CAPTCHAAI_API_KEY);
parallelTest(currentProvider, captchaai, "SITE_KEY", "https://example.com", 20);
Bốn kết luận sai hay gặp khi đọc số liệu
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
| CaptchaAI chậm hơn | Độ trễ mạng từ máy chạy đo | Đo lại từ máy chủ production |
| Tỷ lệ thành công lệch nhiều | Mẫu quá nhỏ | Chạy hơn 50 lượt, rải khung giờ |
| Token bị từ chối | Token đã hết hạn | Dùng token ngay sau khi nhận |
| Chi phí tăng gấp đôi | Giải cùng CAPTCHA hai lần | Giới hạn đợt đo vài ngày |
Nếu log đo chứa dữ liệu người dùng thật, hãy tối giản trường lưu và đặt hạn xóa (Nghị định 13/2023/NĐ-CP).
Câu hỏi thường gặp
Chạy song song có ảnh hưởng production không?
Không, nếu ở giai đoạn 1 ứng dụng chỉ dùng kết quả của bên cũ, kết quả CaptchaAI chỉ ghi log. Rủi ro bắt đầu từ giai đoạn 2, nên bắt buộc có dự phòng.
Đợt đo nên dài bao lâu?
Ít nhất ba ngày liên tục và từ 50 lượt mỗi bên, phủ cả ngày thường, cuối tuần và giờ thấp điểm.
Cần bao nhiêu thread cho một đợt đo?
Mỗi thread là một lượt giải đang chạy. Đo ở 5 luồng thì BASIC ($15/tháng, 5 thread) là đủ; 30–50 luồng thì dùng ADVANCE ($90/tháng, 50 thread).
Hai bên ngang nhau thì chọn theo gì?
So mô hình giá và độ ổn định của mã lỗi: giá theo thread cho hóa đơn cố định hằng tháng, tính theo lần giải thì chi phí biến động.
Chuyển hẳn rồi có cần đo lại không?
Có. Giữ script đo, chạy một đợt ngắn mỗi quý hoặc khi trang đích đổi cấu hình CAPTCHA.
Bài viết liên quan
- Pipeline kiểm thử tự động với CaptchaAI
- CAPTCHA trong CI/CD với GitHub Actions
- Kiểm thử E2E bằng Cypress
Các bước tiếp theo
Tạo tài khoản CaptchaAI, lấy API key và chạy 50 lượt giải song song với bên hiện tại — vài ngày sau bạn có số liệu của chính mình.
Hướng dẫn liên quan: