DevOps và Mở Rộng

Triển khai Blue-Green cho cơ sở hạ tầng giải CAPTCHA

Nâng worker giải CAPTCHA lên phiên bản mới mà không cần xin lịch downtime là hoàn toàn khả thi: chạy song song hai môi trường bluegreen giống hệt nhau, đẩy traffic sang môi trường mới sau khi canary test đạt ngưỡng, rồi rollback tức thì nếu có sự cố.

Đây là cách nhiều đội scraping/QA vận hành hàng chục worker CaptchaAI 24/7 mà không cần chờ cửa sổ bảo trì.

Kiến trúc Blue-Green: hai môi trường, một traffic router

Một traffic router đứng trước hai nhóm worker gọi CaptchaAI API độc lập; chỉ một nhóm nhận traffic thật tại một thời điểm:

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

Triển khai Blue-Green trong code

Python – Bộ định tuyến Blue-Green

BlueGreenRouter gói hai CaptchaWorkerPool (blue/green) và chỉ cho switch() khi canary_test() đạt ngưỡng pass:

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")

canary_test() chỉ trả True khi tỷ lệ pass vượt ngưỡng — tránh chuyển traffic sang một bản build lỗi.

JavaScript – Chuyển đổi Blue-Green tự động

Bản Node.js tự giám sát tỷ lệ lỗi sau khi chuyển traffic và tự rollback nếu vượt ngưỡng:

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));

monitorAfterSwitch() gọi rollback() ngay khi tỷ lệ lỗi vượt 20% trong cửa sổ theo dõi — không cần ai thức đêm bấm nút.

Luồng triển khai 5 bước

Bước Hành động Ngưỡng rollback
1 Triển khai code mới vào môi trường chờ (standby) Build lỗi
2 Chạy canary test trên môi trường chờ Tỷ lệ pass < 80%
3 Chuyển traffic sang phiên bản mới
4 Giám sát tỷ lệ lỗi trong 5 phút Tỷ lệ lỗi > 20%
5 Ngừng môi trường cũ

Kinh nghiệm cắt traffic trong thực tế

  • Đưa cả hai môi trường qua cùng một bộ health check trước khi cho traffic thật chạm vào stack mới.
  • Chuyển traffic theo giai đoạn rõ ràng (ví dụ 10% → 50% → 100%), chỉ đi tiếp khi độ trễ và tỷ lệ lỗi ổn định ở mỗi bước.
  • Rollback ngay khi vượt ngưỡng đã thống nhất trước — đừng chờ "theo dõi thêm chút nữa".
  • Một đội QA outsourcing tại TP.HCM theo dõi giá Shopee/Lazada bằng ~40 worker song song thường chạy canary trên gói ADVANCE ($90/tháng, 50 thread) — dư thread cho cả hai môi trường cùng hoạt động, thay vì sát ngưỡng STANDARD ($30/tháng, 15 thread).

Khắc phục sự cố

Vấn đề Nguyên nhân Cách xử lý
Canary pass nhưng production fail Task test quá đơn giản Dùng task thực tế từ hàng đợi production
Rollback quá thường xuyên Ngưỡng giám sát quá nhạy Tăng ngưỡng lỗi; kéo dài soak time
Traffic split không rõ ràng Còn request in-flight ở môi trường cũ Chờ xử lý xong in-flight trước khi ngừng
Cả hai môi trường cùng unhealthy Lỗi dependency dùng chung (network, API) Dùng circuit breaker; không rollback vì hạ tầng chung

Câu hỏi thường gặp

Cần bao nhiêu thread để chạy song song cả hai môi trường blue và green?

Không có con số cố định — cộng thread ổn định với phần dùng cho canary. Workload cần ~40 thread nên chọn gói dư, ví dụ ADVANCE ($90/tháng, 50 thread) thay vì sát ngưỡng STANDARD ($30/tháng, 15 thread).

Blue-Green và canary deployment khác nhau ở điểm nào?

Blue-Green chuyển 100% traffic cùng lúc sau canary pass. Canary deployment tăng dần traffic (1% → 10% → 50% → 100%) — an toàn hơn cho hệ thống lớn.

Có thể triển khai Blue-Green giải CAPTCHA chỉ với một server không?

Có, chạy blue và green như hai process/container riêng trên cùng máy, dùng reverse proxy (NGINX) chuyển traffic giữa hai cổng.

Ngưỡng tỷ lệ lỗi bao nhiêu thì nên rollback thay vì tiếp tục theo dõi?

Rollback khi tỷ lệ lỗi vượt 20% trong 5 phút giám sát. Với hệ thống quan trọng, hạ ngưỡng xuống 10%.

Triển khai Blue-Green có làm gián đoạn các task CAPTCHA đang xử lý dở không?

Không, nếu làm đúng: task cũ tiếp tục polling res.php đến khi có kết quả, task mới route sang môi trường mới. Chỉ ngừng môi trường cũ khi hàng đợi in-flight đã rỗng.

Bước tiếp theo

Dựng thử router Blue-Green ở trên với dữ liệu canary thật từ hàng đợi của bạn — không cần đợi cửa sổ bảo trì để nâng cấp worker. Lấy API key CaptchaAI của bạn và bắt đầu triển khai không downtime.

Hướng dẫn liên quan:

Os comentários estão desativados para este artigo.