Узкое место распределённой автоматизации почти никогда не в самих браузерах. Вы поднимаете десять узлов Selenium Grid, получаете 30–50 параллельных сессий — и обнаруживаете, что половина из них стоит в ожидании ответа CaptchaAI, потому что тариф выдаёт меньше потоков, чем Grid выдаёт слотов. Правило простое: число воркеров подбирают не по количеству узлов, а по минимуму из свободных слотов Grid и потоков в вашем плане.
Дальше — рабочая схема целиком: Grid 4 в Docker, единый клиент CaptchaAI для всех узлов, параллельный запуск задач, контроль ёмкости, автомасштабирование в Kubernetes и тот же клиент на Java. Решение CAPTCHA здесь — штатный шаг интеграционного тестирования и парсинга доступных вам страниц.
Как устроена связка Grid и CaptchaAI
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Test Script │────▶│ Grid Hub │────▶│ Node 1 │
│ (Client) │ │ (Router) │ │ Chrome x 5 │
└─────────────┘ └──────────────┘ └──────────────┘
│ ┌──────────────┐
├─────────────▶│ Node 2 │
│ │ Chrome x 5 │
│ └──────────────┘
│ ┌──────────────┐
└─────────────▶│ Node 3 │
│ Chrome x 5 │
└──────────────┘
All nodes share ──▶ CaptchaAI API (single API key)
Ключевая деталь: узлы Grid не координируют решение капчи между собой. Каждая сессия идёт в один https://ocr.captchaai.com с одним API-ключом, а очередь держит сторона API. Масштабирование здесь двухконтурное: ёмкость браузеров и ёмкость решателя — разные величины.
Как подобрать тариф под размер Grid
CaptchaAI тарифицируется по потокам — по числу одновременно решаемых задач, без лимита решений внутри потока. Считайте по слотам браузеров, а не по машинам.
| Размер стенда | Слотов Grid | Подходящий тариф |
|---|---|---|
| 2–3 узла для отладки | 10–15 | STANDARD ($30/мес, 15 потоков) |
| 10 узлов по 5 сессий | 50 | ADVANCE ($90/мес, 50 потоков) |
| Ночная регрессия на сотню браузеров | ~100 | PREMIUM ($170/мес, 100 потоков) |
Цены указаны в USD и не пересчитываются в местные валюты. Фиксированная месячная стоимость за поток предсказуемее оплаты за каждое решение.
Разверните Selenium Grid 4 в Docker
Compose-файл: хаб и три узла Chrome
version: "3"
services:
selenium-hub:
image: selenium/hub:4.21.0
container_name: selenium-hub
ports:
- "4442:4442"
- "4443:4443"
- "4444:4444"
chrome-node-1:
image: selenium/node-chrome:4.21.0
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=5
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
chrome-node-2:
image: selenium/node-chrome:4.21.0
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=5
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
chrome-node-3:
image: selenium/node-chrome:4.21.0
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=5
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
docker-compose up -d
Три узла по пять сессий дают 15 слотов — достаточно, чтобы отладить интеграцию до перехода в Kubernetes. Две тонкости этой конфигурации:
SE_NODE_MAX_SESSIONSбезSE_NODE_OVERRIDE_MAX_SESSIONS=trueигнорируется: Selenium ограничивает сессии числом ядер CPU. На слабой виртуальной машине спотыкаются именно об это.- Один Chrome под нагрузкой занимает 300–700 МБ, то есть 15 сессий — это 6–8 ГБ только под браузеры, без учёта хаба.
Подключите CaptchaAI к сессиям Grid
Клиент ниже открывает удалённую сессию на хабе, отправляет задачу в in.php и опрашивает res.php до готовности токена. Методы разделены для reCAPTCHA v2 и Cloudflare Turnstile — оба типа доступны в общем доступе.
import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from concurrent.futures import ThreadPoolExecutor, as_completed
class GridCaptchaSolver:
CAPTCHAAI_URL = "https://ocr.captchaai.com"
def __init__(self, api_key, grid_url="http://localhost:4444"):
self.api_key = api_key
self.grid_url = grid_url
def create_session(self):
"""Create a new browser session on the Grid."""
options = webdriver.ChromeOptions()
options.add_argument("--no-sandbox")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Remote(
command_executor=self.grid_url,
options=options,
)
return driver
def solve_recaptcha_v2(self, site_url, sitekey):
"""Solve reCAPTCHA v2 via CaptchaAI API."""
# Submit
resp = requests.post(f"{self.CAPTCHAAI_URL}/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": site_url,
"json": 1,
})
data = resp.json()
if data["status"] != 1:
raise Exception(f"Submit: {data['request']}")
task_id = data["request"]
# Poll
for _ in range(60):
time.sleep(5)
resp = requests.get(f"{self.CAPTCHAAI_URL}/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": 1,
})
data = resp.json()
if data["request"] == "CAPCHA_NOT_READY":
continue
if data["status"] != 1:
raise Exception(f"Solve: {data['request']}")
return data["request"]
raise Exception("Timeout")
def solve_turnstile(self, site_url, sitekey):
resp = requests.post(f"{self.CAPTCHAAI_URL}/in.php", data={
"key": self.api_key, "method": "turnstile",
"sitekey": sitekey, "pageurl": site_url, "json": 1,
})
data = resp.json()
if data["status"] != 1:
raise Exception(f"Submit: {data['request']}")
task_id = data["request"]
for _ in range(60):
time.sleep(5)
resp = requests.get(f"{self.CAPTCHAAI_URL}/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": 1,
})
data = resp.json()
if data["request"] == "CAPCHA_NOT_READY":
continue
if data["status"] != 1:
raise Exception(f"Solve: {data['request']}")
return data["request"]
raise Exception("Timeout")
def process_task(self, task):
"""Process a single CAPTCHA-protected task on a Grid node."""
driver = self.create_session()
try:
driver.get(task["url"])
time.sleep(2)
# Detect sitekey
sitekey = task.get("sitekey")
if not sitekey:
sitekey = driver.execute_script(
"return document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')"
)
if not sitekey:
return {"url": task["url"], "status": "no_captcha", "data": driver.page_source[:500]}
# Solve
token = self.solve_recaptcha_v2(task["url"], sitekey)
# Inject
driver.execute_script(f"""
document.querySelector('#g-recaptcha-response').value = '{token}';
document.querySelectorAll('[name="g-recaptcha-response"]').forEach(
el => el.value = '{token}'
);
""")
# Fill form and submit
if task.get("form_data"):
for field, value in task["form_data"].items():
driver.find_element(By.NAME, field).send_keys(value)
if task.get("submit_selector"):
driver.find_element(By.CSS_SELECTOR, task["submit_selector"]).click()
time.sleep(3)
return {
"url": task["url"],
"status": "success",
"result_url": driver.current_url,
"data": driver.page_source[:1000],
}
except Exception as e:
return {"url": task["url"], "status": "error", "error": str(e)}
finally:
driver.quit()
Что здесь важно с точки зрения эксплуатации:
- Опрос с паузой, а не цикл без задержки. Интервал 5 с и 60 итераций дают потолок ожидания в 5 мин — с запасом на пиковую нагрузку. Промежуточный ответ
CAPCHA_NOT_READY— это норма, а не ошибка. - Токен подставляется в
#g-recaptcha-responseи во все поля с тем же именем. Скрытых полей на странице может быть несколько, особенно если форма рендерится фреймворком. driver.quit()в блокеfinally. Незакрытая сессия держит слот Grid до истеченияSE_SESSION_TIMEOUT, и на длинном прогоне такие зависшие сессии съедают всю ёмкость.- Определение sitekey. Скрипт сначала берёт значение из описания задачи и только потом ищет
[data-sitekey]в DOM. Явно заданное значение надёжнее: разметку страницы могут изменить в любой момент.
Запустите задачи параллельно
def run_parallel_tasks(api_key, tasks, max_workers=10):
"""Run CAPTCHA tasks in parallel across Grid nodes."""
solver = GridCaptchaSolver(api_key)
results = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(solver.process_task, task): task
for task in tasks
}
for future in as_completed(futures):
task = futures[future]
try:
result = future.result(timeout=600)
results.append(result)
print(f"[{result['status']}] {result['url']}")
except Exception as e:
results.append({
"url": task["url"],
"status": "exception",
"error": str(e),
})
return results
# Usage
tasks = [
{
"url": "https://site-a.com/form",
"sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
"form_data": {"name": "Test User", "email": "[email protected]"},
"submit_selector": "#submit",
},
{
"url": "https://site-b.com/register",
"sitekey": "6LdKlZEpAAAAAAOQjzC2v_mJ-",
"form_data": {"username": "testuser"},
"submit_selector": "button[type='submit']",
},
# Add more tasks...
]
results = run_parallel_tasks("YOUR_API_KEY", tasks, max_workers=15)
# Summary
success = sum(1 for r in results if r["status"] == "success")
print(f"\nCompleted: {success}/{len(results)} successful")
Значение max_workers=15 не универсально. Берите минимум из трёх величин: свободные слоты Grid, потоки тарифа и нагрузка, которую выдерживает целевой стенд. Превышение даёт SessionNotCreated, очередь на стороне API или ограничение частоты запросов.
Сценарий из практики. Команда в Алматы гоняет ночную регрессию форм регистрации на staging-стенде: 120 сценариев, каждый с reCAPTCHA v2. На 6 узлах по 5 сессий получается 30 слотов, тариф ADVANCE ($90/мес, 50 потоков) закрывает решатель с запасом. Прогон упирается не в капчу, а в холодный старт Chrome, поэтому узлы поднимают за 10 мин до пайплайна. Если сценарии затрагивают реальные пользовательские данные, основания их обработки проверяются отдельно — 152-ФЗ «О персональных данных» для стендов в РФ, процедуры уровня GDPR для трансграничных команд. Это вопрос вашей внутренней политики, а не свойство сервиса.
Проверяйте ёмкость Grid перед прогоном
import requests
def check_grid_status(grid_url="http://localhost:4444"):
"""Check Selenium Grid status and available nodes."""
try:
resp = requests.get(f"{grid_url}/status")
data = resp.json()
nodes = data.get("value", {}).get("nodes", [])
total_slots = 0
available_slots = 0
print(f"Grid Status: {data['value']['ready']}")
print(f"Nodes: {len(nodes)}")
for i, node in enumerate(nodes):
slots = node.get("slots", [])
free = sum(1 for s in slots if not s.get("session"))
total_slots += len(slots)
available_slots += free
print(f" Node {i+1}: {free}/{len(slots)} slots available")
print(f"Total capacity: {available_slots}/{total_slots} available")
return available_slots
except Exception as e:
print(f"Grid check failed: {e}")
return 0
# Adjust workers based on grid capacity
available = check_grid_status()
optimal_workers = min(available, 20)
print(f"Optimal workers: {optimal_workers}")
/status показывает фактическую ёмкость на момент старта, а не проектную. Вызывайте проверку перед каждым большим прогоном: узел, отвалившийся от шины событий, исчезает из ответа молча, а задачи падают с WebDriverException без внятной причины.
Типичные ошибки и что с ними делать
| Проблема | Причина | Что сделать |
|---|---|---|
SessionNotCreated |
Свободных слотов нет | Добавить узлы или поднять SE_NODE_MAX_SESSIONS вместе с SE_NODE_OVERRIDE_MAX_SESSIONS |
| Тайм-аут на стороне Grid | Узел перегружен | Снизить число одновременных сессий на узел |
WebDriverException |
Узел отвалился от шины событий | Добавить повтор создания сессии и проверку /status |
| Нехватка памяти | Слишком много экземпляров Chrome | Выставить лимиты ресурсов и реальный максимум сессий |
| Долгое ожидание токена | Все потоки тарифа заняты | Увеличить потолок опроса, добавить повторы или перейти на тариф с большим числом потоков |
| Зависшие сессии | Драйвер не закрыт в коде | Закрывать драйвер в finally и задать SE_SESSION_TIMEOUT |
Масштабируйте узлы в Kubernetes
# selenium-grid-k8s.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: selenium-chrome-node
spec:
replicas: 5
selector:
matchLabels:
app: selenium-chrome
template:
metadata:
labels:
app: selenium-chrome
spec:
containers:
- name: chrome
image: selenium/node-chrome:4.21.0
env:
- name: SE_EVENT_BUS_HOST
value: selenium-hub
- name: SE_EVENT_BUS_PUBLISH_PORT
value: "4442"
- name: SE_EVENT_BUS_SUBSCRIBE_PORT
value: "4443"
- name: SE_NODE_MAX_SESSIONS
value: "3"
resources:
limits:
memory: "2Gi"
cpu: "1"
requests:
memory: "1Gi"
cpu: "500m"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: chrome-node-hpa
spec:
scaleRef:
apiVersion: apps/v1
kind: Deployment
name: selenium-chrome-node
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
HPA поднимает узлы по загрузке CPU, но решатель за ними не тянется автоматически: количество потоков тарифа — фиксированная величина. Если maxReplicas: 20 при трёх сессиях на под даёт 60 слотов, а тариф рассчитан на 50 потоков, часть сессий будет ждать.
Что проверить перед включением автомасштабирования
- Потолок согласован с тарифом.
maxReplicas×SE_NODE_MAX_SESSIONSне должно превышать число потоков плана. - Лимиты памяти под число сессий.
memory: 2Giиcpu: 1рассчитаны на три сессии Chrome; урезать их, не снижаяSE_NODE_MAX_SESSIONS, — прямой путь к OOM-kill. - Запас на холодный старт. Под с Chrome готов не мгновенно, поэтому HPA отстаёт от всплеска нагрузки на минуту-другую.
- Корректное завершение сессий. Задайте
terminationGracePeriodSecondsпод максимальную длительность сценария, иначе снятый под теряет результат.
Тот же клиент на Java
Если тестовый контур живёт в JVM-стеке, логика не меняется: удалённая сессия через RemoteWebDriver, пул фиксированного размера и один и тот же API-ключ CaptchaAI на все потоки.
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import java.net.URL;
import java.net.http.*;
import java.net.URI;
import java.util.concurrent.*;
public class GridCaptchaSolver {
private final String apiKey;
private final String gridUrl;
private final HttpClient httpClient;
public GridCaptchaSolver(String apiKey, String gridUrl) {
this.apiKey = apiKey;
this.gridUrl = gridUrl;
this.httpClient = HttpClient.newHttpClient();
}
public WebDriver createSession() throws Exception {
ChromeOptions options = new ChromeOptions();
options.addArguments("--no-sandbox", "--window-size=1920,1080");
return new RemoteWebDriver(new URL(gridUrl), options);
}
public List<Map<String, String>> runParallel(
List<Map<String, String>> tasks, int workers
) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(workers);
List<Future<Map<String, String>>> futures = new ArrayList<>();
for (Map<String, String> task : tasks) {
futures.add(executor.submit(() -> processTask(task)));
}
List<Map<String, String>> results = new ArrayList<>();
for (Future<Map<String, String>> future : futures) {
results.add(future.get(600, TimeUnit.SECONDS));
}
executor.shutdown();
return results;
}
}
Часто задаваемые вопросы
Сколько потоков CaptchaAI нужно на 10 узлов Grid?
Считайте по слотам, а не по узлам. Десять узлов по 5 сессий — это 50 одновременных браузеров, то есть уровень ADVANCE ($90/мес, 50 потоков). Если пик держится недолго, можно взять тариф ниже и заложить очередь на стороне опроса.
Почему узел игнорирует SE_NODE_MAX_SESSIONS?
Потому что не задан SE_NODE_OVERRIDE_MAX_SESSIONS=true. Без него Selenium ограничивает число сессий количеством доступных ядер CPU, и значение переменной просто не применяется.
Что делать, если решение возвращается дольше обычного?
Сначала проверьте, не заняты ли все потоки тарифа: при полной загрузке задача ждёт освобождения потока. Увеличьте потолок опроса, добавьте повторную попытку с экспоненциальной задержкой и только потом меняйте тариф.
Можно ли смешивать Chrome, Firefox и Edge в одном Grid?
Да. Узлы разных браузеров живут в одном хабе, а вызовы к CaptchaAI от браузера не зависят — нужны только sitekey и URL страницы. Разница проявится в потреблении памяти и во времени старта сессии.
Подходит ли эта схема для GeeTest v4 или hCaptcha?
Нет. CaptchaAI не решает hCaptcha и FunCaptcha, а поддержка GeeTest v4 заявлена как «скоро» и пока недоступна. Из семейства GeeTest доступна только v3. Типы CaptchaFox (beta), Friendly Captcha (beta) и Lemin (beta) находятся в бета-статусе, и их не стоит закладывать в критичный ночной прогон.
Что почитать дальше
- Selenium и CaptchaAI на Python: полный разбор
- Перехват запросов через Selenium Wire
- Параллельное решение капчи в worker threads на Node.js
Подключите CaptchaAI к узлам Selenium Grid и запустите первый параллельный прогон — получите API-ключ CaptchaAI и проверьте ёмкость на одном узле, прежде чем масштабировать.