Tutorials

验证码识别性能趋势:用时间序列数据持续监控

控制台上那个“当前成功率”看不出问题。识别质量的退化是渐进的:成功率一周掉两个点,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)

typestatuserror_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,把埋点接进自己的面板。

延伸阅读:

该文章已禁用评论。