DevOps & Scaling

验证码解决基础设施的蓝绿部署

验证码 worker 升级时,最怕流量没切完成功率就跌了,你却来不及回滚。蓝绿部署把风险降到最低:blue、green 两套环境同时在线,新版本先在待机侧跑通金丝雀测试,确认无误再切流量,出错可秒级回滚。

什么时候该用蓝绿部署

换 API 版本、调整线程数、升级依赖时,都值得用蓝绿而不是原地重启——出问题只需切一次流量,不用回滚代码。

蓝绿架构长什么样

                    ┌─────────────────────┐
[Scraper Clients] → │   Traffic Router    │
                    └──────┬──────┬───────┘
                           │      │
                     Active│      │Standby
                           ▼      ▼
                    ┌───────┐  ┌───────┐
                    │ BLUE  │  │ GREEN │
                    │Workers│  │Workers│
                    └───┬───┘  └───┬───┘
                        │          │
                        └────┬─────┘
                             ▼
                    [CaptchaAI API]

路由器只把请求转发给当前激活的一侧,另一侧待机。

用 Python 和 JavaScript 实现路由器

两份实现逻辑一致:提交任务、轮询结果、切换时把流量转到待机侧。

Python 实现:蓝绿路由器

import os
import time
import threading
import requests

API_KEY = os.environ["CAPTCHAAI_API_KEY"]


class CaptchaWorkerPool:
    """Represents one environment (blue or green)."""

    def __init__(self, name, config):
        self.name = name
        self.config = config
        self.session = requests.Session()
        self.tasks_solved = 0
        self.errors = 0
        self.healthy = True

    def solve(self, task):
        resp = self.session.post("https://ocr.captchaai.com/in.php", data={
            "key": API_KEY,
            "method": task.get("method", "userrecaptcha"),
            "googlekey": task["sitekey"],
            "pageurl": task["pageurl"],
            "json": 1
        })
        data = resp.json()
        if data.get("status") != 1:
            self.errors += 1
            return {"error": data.get("request")}

        captcha_id = data["request"]
        for _ in range(60):
            time.sleep(5)
            result = self.session.get(
                "https://ocr.captchaai.com/res.php",
                params={
                    "key": API_KEY,
                    "action": "get",
                    "id": captcha_id,
                    "json": 1
                }
            ).json()
            if result.get("status") == 1:
                self.tasks_solved += 1
                return {"solution": result["request"]}
            if result.get("request") != "CAPCHA_NOT_READY":
                self.errors += 1
                return {"error": result.get("request")}

        self.errors += 1
        return {"error": "TIMEOUT"}

    @property
    def error_rate(self):
        total = self.tasks_solved + self.errors
        return self.errors / total if total > 0 else 0.0

    @property
    def stats(self):
        return {
            "name": self.name,
            "solved": self.tasks_solved,
            "errors": self.errors,
            "error_rate": round(self.error_rate, 4),
            "healthy": self.healthy
        }


class BlueGreenRouter:
    def __init__(self, blue_config, green_config):
        self.blue = CaptchaWorkerPool("blue", blue_config)
        self.green = CaptchaWorkerPool("green", green_config)
        self.active = self.blue
        self.standby = self.green
        self.lock = threading.Lock()

    def solve(self, task):
        """Route task to the active environment."""
        with self.lock:
            pool = self.active
        return pool.solve(task)

    def switch(self):
        """Swap active and standby environments."""
        with self.lock:
            self.active, self.standby = self.standby, self.active
            print(f"Switched: {self.active.name} is now ACTIVE")
        return self.active.name

    def rollback(self):
        """Switch back to the previous environment."""
        return self.switch()

    def canary_test(self, test_tasks, threshold=0.9):
        """Run test tasks on standby before switching."""
        successes = 0
        for task in test_tasks:
            result = self.standby.solve(task)
            if "solution" in result:
                successes += 1

        success_rate = successes / len(test_tasks) if test_tasks else 0
        passed = success_rate >= threshold
        print(
            f"Canary test: {successes}/{len(test_tasks)} "
            f"({success_rate:.0%}) — {'PASS' if passed else 'FAIL'}"
        )
        return passed

    @property
    def status(self):
        return {
            "active": self.active.stats,
            "standby": self.standby.stats
        }


# Usage
router = BlueGreenRouter(
    blue_config={"version": "1.2.0", "workers": 4},
    green_config={"version": "1.3.0", "workers": 4}
)

# Canary test before switching
test_tasks = [
    {"sitekey": "6Le-wvkS...", "pageurl": "https://example.com/test"}
]

if router.canary_test(test_tasks, threshold=0.8):
    router.switch()
    print(f"Now active: {router.status['active']['name']}")
else:
    print("Canary failed — staying on current environment")

JavaScript 实现:自动切换器

const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;

class BlueGreenDeployment {
  constructor() {
    this.environments = {
      blue: { name: "blue", version: null, solved: 0, errors: 0 },
      green: { name: "green", version: null, solved: 0, errors: 0 },
    };
    this.activeEnv = "blue";
  }

  get active() {
    return this.environments[this.activeEnv];
  }
  get standby() {
    return this.environments[this.activeEnv === "blue" ? "green" : "blue"];
  }

  async deploy(version, config = {}) {
    const target = this.standby;
    target.version = version;
    target.solved = 0;
    target.errors = 0;

    console.log(`Deployed v${version} to ${target.name} (standby)`);

    // Run canary checks
    const canaryPassed = await this.canaryCheck(config.canaryTasks || []);
    if (!canaryPassed && config.canaryTasks?.length > 0) {
      console.log("Canary check failed — aborting deployment");
      return { success: false, reason: "canary_failed" };
    }

    // Switch traffic
    this.activeEnv = target.name;
    console.log(`Switched traffic to ${target.name} (v${version})`);

    // Monitor for rollback
    if (config.monitorDuration) {
      const stable = await this.monitorAfterSwitch(config.monitorDuration);
      if (!stable) {
        this.rollback();
        return { success: false, reason: "post_deploy_errors" };
      }
    }

    return { success: true, active: this.activeEnv };
  }

  async canaryCheck(tasks) {
    if (tasks.length === 0) return true;

    let successes = 0;
    for (const task of tasks) {
      try {
        await this.solveCaptcha(task);
        successes++;
      } catch (err) {
        console.log(`Canary task failed: ${err.message}`);
      }
    }

    const rate = successes / tasks.length;
    console.log(`Canary: ${successes}/${tasks.length} (${(rate * 100).toFixed(0)}%)`);
    return rate >= 0.8;
  }

  async monitorAfterSwitch(durationMs) {
    const start = Date.now();
    const checkInterval = 10000;

    while (Date.now() - start < durationMs) {
      await new Promise((r) => setTimeout(r, checkInterval));
      const errorRate = this.active.errors /
        Math.max(1, this.active.solved + this.active.errors);

      if (errorRate > 0.2) {
        console.log(`Error rate ${(errorRate * 100).toFixed(1)}% — triggering rollback`);
        return false;
      }
    }
    return true;
  }

  rollback() {
    const previous = this.activeEnv === "blue" ? "green" : "blue";
    console.log(`Rolling back: ${this.activeEnv} → ${previous}`);
    this.activeEnv = previous === "blue" ? "blue" : "green";
  }

  async solveCaptcha(task) {
    const submitResp = await axios.post("https://ocr.captchaai.com/in.php", null, {
      params: {
        key: API_KEY,
        method: "userrecaptcha",
        googlekey: task.sitekey,
        pageurl: task.pageurl,
        json: 1,
      },
    });

    if (submitResp.data.status !== 1) {
      this.active.errors++;
      throw new Error(submitResp.data.request);
    }

    const captchaId = submitResp.data.request;
    for (let i = 0; i < 60; i++) {
      await new Promise((r) => setTimeout(r, 5000));
      const pollResp = await axios.get("https://ocr.captchaai.com/res.php", {
        params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
      });

      if (pollResp.data.status === 1) {
        this.active.solved++;
        return pollResp.data.request;
      }
      if (pollResp.data.request !== "CAPCHA_NOT_READY") {
        this.active.errors++;
        throw new Error(pollResp.data.request);
      }
    }
    this.active.errors++;
    throw new Error("TIMEOUT");
  }
}

// Deploy new version with canary and monitoring
const deployer = new BlueGreenDeployment();

deployer
  .deploy("1.3.0", {
    canaryTasks: [
      { sitekey: "6Le-wvkS...", pageurl: "https://example.com/test" },
    ],
    monitorDuration: 60000, // Monitor for 1 minute after switch
  })
  .then((result) => console.log("Deploy result:", result));

流量切换实战要点

  1. 导流前,先让蓝、绿两侧跑同一套健康检查探针,确认延迟和拒绝率一致。
  2. 分阶段转移流量,每步之间留出观察窗口。
  3. 识别耗时、拒绝率或队列深度突破阈值时,立即回滚。

部署上线:5 步走完整流程

步骤 操作 触发回滚的条件
1 把新版本部署到待机环境 构建失败
2 在待机环境跑金丝雀测试 成功率低于 80%
3 把流量切到新版本 ——
4 持续监控错误率(5 分钟) 错误率超过 20%
5 下线旧环境 ——

国内站点多用 GeeTest(极验),海外常见 reCAPTCHA、Turnstile——两侧可按类型调优参数。

提示:金丝雀测试优先用生产队列的真实任务,而不是随手写的测试数据。

常见问题

金丝雀测试要跑多久才能放心切换?

至少跑通 10 个真实验证码任务;关键系统建议让 5%–10% 生产流量稳定运行 10 分钟以上。

国内网络访问 reCAPTCHA 不稳定,会影响测试吗?

会,reCAPTCHA 依赖 Google 托管脚本,国内网络下常加载不稳定,用 GeeTest(极验)任务测试更贴近真实情况。

回滚之后,处理中的任务会不会丢?

不会丢,但可能超时——已提交的任务会继续轮询,建议单独设超时和重试。

蓝绿部署和金丝雀发布该怎么选?

  • 蓝绿:一次切换 100% 流量,简单、回滚快。
  • 金丝雀:分批放量,更稳但更复杂,大系统更值得用。

故障排查

问题 原因 处理方式
金丝雀通过但生产失败 测试任务过简单 用生产队列的真实任务
频繁触发回滚 监控阈值太激进 放宽阈值,延长观察时间
切换时流量不干净 旧环境还有处理中请求 等任务跑完再下线
两个环境同时不健康 共享依赖出问题 加断路器,别因基础设施回滚

下一步

充满信心地上线——获取你的 CaptchaAI API 密钥,接入路由器。

相关指南:

该文章已禁用评论。