DevOps والتوسع

النشر باللونين الأزرق والأخضر للبنية الأساسية لحل اختبار CAPTCHA

عندما يعتمد سير عملك على معدّل حلٍّ ثابت لاختبارات CAPTCHA، تتحوّل كل دقيقة توقّف أثناء تحديث الخدمة إلى طلبات فاشلة وصفحات لم تُجمع بياناتها. النشر باللونين الأزرق والأخضر يعالج هذا مباشرة: تُشغّل نسختين متطابقتين من بنيتك — واحدة نشطة تستقبل الحركة، وأخرى في وضع الاستعداد — ثم تنقل الحركة إلى الإصدار الجديد لحظة التأكد من جاهزيته، وتعيدها فورًا إلى الإصدار السابق إن ظهر أي خلل.

يستحقّ هذا النمط عناءه حين تكون خدمة الحل جزءًا من مسارٍ إنتاجي لا يحتمل الانقطاع: عمّال scraping طويلة التشغيل، أو مهام معالجة دُفعية تعتمد على معدّل نجاح مستقر. أما لو كنت تُشغّل عاملًا واحدًا أو بيئة صغيرة نادرة الاستخدام، فقد يكون النشر المتدرّج أو نافذة صيانة قصيرة أبسط من إدارة بيئتين كاملتين طوال الوقت.

ملاحظة حول الأرقام: العتبات الواردة هنا — مثل معدّل الخطأ ونسبة نجاح الكناري — قيمٌ إرشادية تتأثّر بنوع CAPTCHA وحجم الحركة وبيئة التشغيل. اضبطها على قياساتك الخاصة قبل اعتمادها في الإنتاج.

كيف يعمل النمط الأزرق-الأخضر خطوة بخطوة

تقوم الفكرة على أربعة أدوار واضحة: بيئة نشطة تحمل كامل الحركة، وبيئة استعداد متطابقة تنتظر جاهزة، ومُوجِّه حركة (Traffic Router) يقرّر إلى أين تذهب الطلبات، وطبقة مراقبة تتابع معدّل الخطأ بعد كل تبديل. تنشر الإصدار الجديد على بيئة الاستعداد أولًا، فتبقى الحركة الحقيقية بعيدة عنه حتى تتأكد من سلامته.

عند التأكد، يحوّل المُوجِّه الحركة دفعة واحدة من الأزرق إلى الأخضر. وإن ظهرت أخطاء بعد التبديل، فالتراجع ليس عملية إعادة نشر بل مجرد تبديل عكسي يعيد الحركة إلى البيئة السابقة التي ما زالت تعمل. هذا ما يجعل زمن التعافي يُقاس بالثواني لا بالدقائق. المخطط التالي يوضّح مسار الطلب من عملاء الـ scraping حتى واجهة CaptchaAI:

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

متى يكون الأزرق-الأخضر خيارًا مناسبًا؟

ليس كل نشر يستحق بيئتين كاملتين. استخدم الجدول التالي لتقرير ما إذا كان النمط يخدم حالتك أم يضيف كلفة بلا فائدة:

الحالة هل يناسبها النمط؟ السبب
عمّال إنتاج متعددون خلف موازِن حِمل نعم يمكنك تحويل الحركة بسرعة والتراجع فورًا
فريق يحتاج اختبار كناري قبل التحويل الكامل نعم يسهّل التحقق من الإصدار الجديد على بيئة الاستعداد
خدمة داخلية صغيرة قليلة الاستخدام غالبًا لا كلفة بيئتين قد تفوق الفائدة المرجوّة
نشرة تتطلب تغييرًا غير متوافق في قاعدة البيانات نعم، مع خطة ترحيل منفصلة التحويل وحده لا يحلّ مشكلة توافق البيانات

مثال من السوق: منصّة مقارنة أسعار في الخليج تجمع بيانات المتاجر الكبرى خلال ذروة الجمعة البيضاء. في تلك الأيام تتضاعف الحركة، وأي تحديث لخدمة الحل عبر نافذة صيانة يعني فقدان بيانات تسعير لا تُعوَّض. مع النمط الأزرق-الأخضر يُنشر التحديث على بيئة الاستعداد ويُختبر بحمل حقيقي مصغّر، ثم تُحوَّل الحركة في ثوانٍ دون أن يشعر خط جمع البيانات بأي انقطاع.

التنفيذ العملي

مُوجِّه الحركة الأزرق-الأخضر في Python

يمثّل الصنف CaptchaWorkerPool بيئة واحدة (زرقاء أو خضراء) ويتتبّع عدد المهام المحلولة والأخطاء ومعدّل الخطأ. أما BlueGreenRouter فيحتفظ بمؤشّرين — نشط واستعداد — ويوفّر تبديلًا آمنًا عبر قفل، إضافة إلى اختبار كناري يشغّل مهام حقيقية على بيئة الاستعداد قبل أي تحويل:

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 تشغيل اختبارات الكناري على الاستعداد معدّل النجاح < 80%
3 تحويل الحركة إلى الإصدار الجديد
4 مراقبة معدّل الخطأ لمدة 5 دقائق معدّل الخطأ > 20%
5 الاستغناء عن البيئة القديمة

استكشاف الأعطال الشائعة

معظم مشكلات النشر الأزرق-الأخضر تعود إلى فروق خفيّة بين البيئتين أو إلى اختبار كناري غير ممثِّل للحمل الحقيقي:

المشكلة السبب المحتمل الإجراء
البيئة الجديدة تجتاز الاختبار ثم تفشل بعد التحويل إعدادات أو أسرار مختلفة بين البيئتين قارن المتغيرات والاتصالات الخارجية قبل تحويل الحركة
نجاح الكناري ثم ارتفاع الأخطاء لاحقًا عيّنة الكناري صغيرة ولا تمثّل الحمل الفعلي استخدم مهامّ أكثر تنوّعًا وواصل المراقبة دقائق بعد التحويل
التراجع يعيد الحركة لكن الأعطال تستمر عطل في بنية مشتركة كمفتاح API أو الوكيل افصل بين عيوب الإصدار وعيوب البنية المشتركة
كلفة البنية ارتفعت كثيرًا ترك بيئة الاستعداد كاملة العمل طوال الوقت صغّر الاستعداد خارج أوقات النشر ووسّعه قبل التحويل فقط

أسئلة شائعة

هل تتضاعف تكلفة الـ threads في CaptchaAI عند تشغيل بيئتين؟

تُحاسب CaptchaAI على أساس عدد الـ threads المتزامنة قيد التنفيذ، لا على عدد عمليات الحل ولا على عدد البيئات، مع عدد حلول غير محدود لكل thread ضمن الباقة. بيئة الاستعداد الخاملة لا ترسل طلبات، فلا تستهلك threads تُذكر. الاستهلاك الفعلي يرتبط بالطلبات المتزامنة التي تعالجها البيئة النشطة، ويظهر حِمل إضافي قصير لثوانٍ فقط أثناء اختبار الكناري على بيئة الاستعداد قبل التبديل.

كم مهمة ينبغي أن يشمل اختبار الكناري قبل التبديل؟

لا تكتفِ بمهمة واحدة. شغّل عددًا من عمليات الحل الحقيقية على بيئة الاستعداد يكفي لتمثيل تنوّع حركتك: أنواع CAPTCHA المختلفة وصفحات فعلية من قائمة إنتاجك، لا عناوين اختبار مبسّطة. للأنظمة الحسّاسة، مرّر نسبة صغيرة من حركة الإنتاج (نحو 5–10%) عبر بيئة الاستعداد لبضع دقائق قبل التحويل الكامل، ليكشف الاختبار المشكلات قبل أن يراها المستخدمون.

كيف أتجنّب فقدان الطلبات قيد المعالجة لحظة التبديل؟

قبل الاستغناء عن البيئة القديمة، اترك الطلبات الجارية عليها تُكمل دورتها (drain) بدل قطعها فجأة. وجّه الطلبات الجديدة إلى البيئة الجديدة فقط، وانتظر حتى تفرغ قائمة المهام قيد المعالجة في القديمة. وبما أن كل thread يمثّل عملية حل واحدة قيد التنفيذ، فإن الانتظار بضع ثوانٍ حتى تُغلق آخر مهمة يمنع فقدان نتائج نصف مكتملة.

ما مدى صرامة عتبات المراقبة التي تُطلق التراجع؟

العتبات الصارمة جدًا تُطلق تراجعات متكررة على تذبذبات عابرة، والمتساهلة تترك خطأً حقيقيًا يتفاقم. ابدأ من نطاق معقول — كتراجع تلقائي عند تجاوز معدّل الخطأ 20% خلال نافذة مراقبة قصيرة — ثم اضبطه حسب طبيعة حركتك. وميّز دائمًا بين خطأ ناتج عن الإصدار الجديد يستدعي التراجع، وخطأ في بنية مشتركة كمفتاح API أو الوكيل لا يُحلّ بالتراجع.


الخطوات التالية

أدلة ذات صلة

التعليقات غير مفعّلة لهذا المقال.