控制台上那个“当前成功率”看不出问题。识别质量的退化是渐进的:成功率一周掉两个点,P95 耗时从 12 秒爬到 40 秒,等发现“任务老超时”,失败重试已经堆了几天。
把每次识别写成时间序列,看到的才是斜率——做法就是在识别函数里加几行埋点。
先选存储:Prometheus、InfluxDB 还是 TimescaleDB
| 对比项 | Prometheus | InfluxDB | TimescaleDB |
|---|---|---|---|
| 适合场景 | 运维监控告警 | 独立指标存储 | SQL 分析 |
| 查询语言 | PromQL | Flux | SQL |
都能接 Grafana,按现有技术栈选。
验证码识别该记录哪些指标
| 指标 | 类型 | 用途 |
|---|---|---|
| 识别成功率(%) | Gauge | 发现质量变化 |
| 识别耗时(毫秒) | Histogram | 看变慢趋势、定超时 |
| 按错误码的失败数 | Counter | 定位新错误模式 |
| 每次识别成本($) | Gauge | 预算跟踪 |
| 队列深度 | Gauge | 判断线程够不够 |
| 账户余额 | Gauge | 触发充值提醒 |
注意:CaptchaAI 按线程包月、次数不限,没有固定单价,代码里写死的值只是占位。
给识别函数加埋点:Python + Pushgateway
识别脚本多是短生命周期进程,Prometheus 拉不到,走 Pushgateway。
import os
import time
import requests
from prometheus_client import CollectorRegistry, Counter, Histogram, Gauge, push_to_gateway
registry = CollectorRegistry()
SOLVE_TOTAL = Counter(
"captcha_solve_total", "Total CAPTCHA solve attempts",
["type", "status"], registry=registry
)
SOLVE_LATENCY = Histogram(
"captcha_solve_latency_seconds", "CAPTCHA solve latency",
["type"], buckets=[5, 10, 15, 20, 30, 45, 60, 90, 120],
registry=registry
)
SOLVE_COST = Counter(
"captcha_solve_cost_dollars", "Total cost of CAPTCHA solves",
["type"], registry=registry
)
API_BALANCE = Gauge(
"captcha_api_balance_dollars", "CaptchaAI account balance",
registry=registry
)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
PUSHGATEWAY = os.environ.get("PUSHGATEWAY_URL", "localhost:9091")
def solve_with_metrics(sitekey, pageurl, captcha_type="recaptcha_v2"):
start = time.time()
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
SOLVE_TOTAL.labels(type=captcha_type, status="submit_error").inc()
push_metrics()
return {"error": data.get("request")}
captcha_id = data["request"]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
elapsed = time.time() - start
SOLVE_TOTAL.labels(type=captcha_type, status="solved").inc()
SOLVE_LATENCY.labels(type=captcha_type).observe(elapsed)
SOLVE_COST.labels(type=captcha_type).inc(0.00299)
push_metrics()
return {"solution": result["request"]}
if result.get("request") != "CAPCHA_NOT_READY":
SOLVE_TOTAL.labels(type=captcha_type, status="error").inc()
push_metrics()
return {"error": result.get("request")}
SOLVE_TOTAL.labels(type=captcha_type, status="timeout").inc()
push_metrics()
return {"error": "TIMEOUT"}
def push_metrics():
try:
push_to_gateway(PUSHGATEWAY, job="captcha_solver", registry=registry)
except Exception:
pass # Don't fail solving because metrics push failed
def update_balance():
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "getbalance"
})
try:
balance = float(resp.text)
API_BALANCE.set(balance)
push_metrics()
except ValueError:
pass
push_metrics() 外层的 try/except 是关键:监控挂了不能连累识别。
做面板常用的几条 PromQL
# Success rate over last hour
rate(captcha_solve_total{status="solved"}[1h])
/ rate(captcha_solve_total[1h]) * 100
# P95 solve latency
histogram_quantile(0.95, rate(captcha_solve_latency_seconds_bucket[1h]))
# Error rate by type
rate(captcha_solve_total{status="error"}[1h])
# Hourly cost
increase(captcha_solve_cost_dollars_total[1h])
四条足够撑一块面板,前两条设成告警。
把每次识别写进 InfluxDB:Python 客户端
想把明细存久一点就用 InfluxDB,每次识别写一个点。
from influxdb_client import InfluxDBClient, Point
from influxdb_client.client.write_api import SYNCHRONOUS
INFLUX_URL = os.environ.get("INFLUX_URL", "http://localhost:8086")
INFLUX_TOKEN = os.environ.get("INFLUX_TOKEN", "")
INFLUX_ORG = os.environ.get("INFLUX_ORG", "captcha")
INFLUX_BUCKET = os.environ.get("INFLUX_BUCKET", "captcha_metrics")
influx_client = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)
write_api = influx_client.write_api(write_options=SYNCHRONOUS)
def record_solve_metric(captcha_type, status, elapsed_ms, cost=0.0, error=None):
point = (
Point("captcha_solve")
.tag("type", captcha_type)
.tag("status", status)
.field("elapsed_ms", elapsed_ms)
.field("cost", cost)
.field("success", 1 if status == "solved" else 0)
)
if error:
point = point.tag("error_code", error)
write_api.write(bucket=INFLUX_BUCKET, record=point)
def record_balance(balance):
point = Point("captcha_balance").field("balance", balance)
write_api.write(bucket=INFLUX_BUCKET, record=point)
type、status、error_code 做 tag,耗时和成本做 field;sitekey 这类高基数值别做 tag。
Flux 查询:成功率、耗时与累计成本
// Success rate over last 24 hours (1-hour windows)
from(bucket: "captcha_metrics")
|> range(start: -24h)
|> filter(fn: (r) => r._measurement == "captcha_solve" and r._field == "success")
|> aggregateWindow(every: 1h, fn: mean)
|> map(fn: (r) => ({r with _value: r._value * 100.0}))
|> yield(name: "success_rate")
// Average solve time by type
from(bucket: "captcha_metrics")
|> range(start: -24h)
|> filter(fn: (r) => r._measurement == "captcha_solve" and r._field == "elapsed_ms" and r.status == "solved")
|> group(columns: ["type"])
|> aggregateWindow(every: 1h, fn: mean)
|> yield(name: "avg_latency")
// Cumulative cost
from(bucket: "captcha_metrics")
|> range(start: -24h)
|> filter(fn: (r) => r._measurement == "captcha_solve" and r._field == "cost")
|> cumulativeSum()
|> yield(name: "cumulative_cost")
三段分别算小时级成功率、按类型平均耗时和累计成本,周对比把 -24h 改成 -7d。
Node.js 侧的 Prometheus 埋点
const client = require("prom-client");
const axios = require("axios");
const register = new client.Registry();
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const solveTotal = new client.Counter({
name: "captcha_solve_total",
help: "Total CAPTCHA solve attempts",
labelNames: ["type", "status"],
registers: [register],
});
const solveLatency = new client.Histogram({
name: "captcha_solve_latency_seconds",
help: "CAPTCHA solve latency",
labelNames: ["type"],
buckets: [5, 10, 15, 20, 30, 45, 60, 90, 120],
registers: [register],
});
async function solveWithMetrics(sitekey, pageurl, type = "recaptcha_v2") {
const start = Date.now();
const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: { key: API_KEY, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
});
if (submit.data.status !== 1) {
solveTotal.inc({ type, status: "submit_error" });
return { error: submit.data.request };
}
const captchaId = submit.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (poll.data.status === 1) {
const elapsed = (Date.now() - start) / 1000;
solveTotal.inc({ type, status: "solved" });
solveLatency.observe({ type }, elapsed);
return { solution: poll.data.request };
}
if (poll.data.request !== "CAPCHA_NOT_READY") {
solveTotal.inc({ type, status: "error" });
return { error: poll.data.request };
}
}
solveTotal.inc({ type, status: "timeout" });
return { error: "TIMEOUT" };
}
// Expose metrics endpoint
const express = require("express");
const app = express();
app.get("/metrics", async (req, res) => {
res.set("Content-Type", register.contentType);
res.end(await register.metrics());
});
app.listen(9090);
Node.js 是常驻服务,直接暴露 /metrics 让 Prometheus 来拉。指标名和标签与 Python 版一致,两端才能进同一块面板。
国内环境下的验证码识别监控差异
- 类型分布不同。 国内站点多用 GeeTest(极验)、网易易盾、腾讯防水墙,海外站点以 reCAPTCHA 和 Turnstile 为主。CaptchaAI 覆盖 reCAPTCHA 全系、Turnstile、GeeTest v3 与图片验证码,GeeTest v4 为即将支持,hCaptcha 与 FunCaptcha 暂不支持;CaptchaFox、Friendly Captcha、Lemin 为测试版,别和正式支持的类型共用一条告警。
- 耗时可能双峰。 采集端在国内、站点在海外时,reCAPTCHA 的 Google 脚本加载不稳定,页面等待会混进识别耗时;按出口机房打标签才分得清。
常见排查清单
- 曲线出现断点。 Pushgateway 没收到推送,先查识别进程到 Pushgateway 的网络。
- P95 明显失真。 桶边界和真实耗时对不上,按实际分布重新划桶。
- 成功率掉到 0。 多半是余额耗尽或 API Key 失效,对着余额曲线确认。
常见问题
加埋点会拖慢识别吗?
不会,前提是推送失败被吞掉,push_metrics() 外层的 try/except 就是干这个的。与识别进程同内网,推送开销毫秒级。
每次识别的成本怎么算才准?
用月费除以当月识别次数。CaptchaAI 按线程包月、次数不限,没有官方单价:BASIC $15/月、5 线程,当月 5,000 次约合 $0.003 一次。
P95 一直偏高,先查什么?
先按 type 拆开:混在一起时图片验证码的毫秒级耗时会被 reCAPTCHA 的十几秒拉平。若某类整体抬升,再看队列深度——持续偏高多半是线程不够。
下一步
先把成功率和 P95 两条曲线跑起来,其余边用边加。获取 CaptchaAI API Key,把埋点接进自己的面板。
延伸阅读: