- Сервис api (FastAPI, без Chrome): POST /v2/tasks, GET /v2/tasks/{id} (доступ по cookie
сессии), DELETE /v2/sessions/current; /v1/solve — прежний контракт поверх очереди.
- Сервис worker (dramatiq + RabbitMQ): слоты браузера, окна сессий между задачами
(browser_idle_timeout 0–120 мин, простой — TTL ключа в Redis), heartbeat.
- Маршрутизация задач сессии в персональную очередь воркера с открытым окном.
- Redis: задачи, результаты (RESULT_TTL_HOURS), окна, воркеры. Прокси и ключ 2captcha
в Redis не сохраняются.
- Движок: AsyncStealthySession.run() — действия на текущей странице без навигации.
- Два образа (robotex-api, robotex-worker), compose с rabbitmq и redis,
make up/down/logs/smoke, релиз собирает все образы.
- Тесты: api + worker в одном процессе (fakeredis), настоящий воркер dramatiq на
StubBroker, браузерные тесты на новой схеме, smoke для запущенного стенда.
- Документация: README, API (v2), Deployment, Usage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.8 KiB
Развёртывание в Docker
Быстрый старт
make up # собрать образы и запустить api, worker, rabbitmq, redis
make smoke # проверить: /health, /v1/solve, /v2/tasks, удержание окна
make logs # логи api и worker
make down
API доступно на http://localhost:8000.
Сервисы
клиент ─▶ api ──▶ RabbitMQ ──▶ worker (Chrome + Xvfb) ──▶ Redis ◀── api
| Сервис | Образ | Что делает |
|---|---|---|
api |
robotex-api (Dockerfile.api, без Chrome) |
принимает задачи, ставит в очередь, отдаёт результаты из Redis; /v1/solve ждёт результат сам |
worker |
robotex-worker (Dockerfile) |
dramatiq worker.actors: выполняет задачи в Chrome, держит окна сессий (browser_idle_timeout) |
rabbitmq |
rabbitmq:4.1-alpine |
очереди: общая robotex и персональная robotex.worker.<id> у каждого воркера |
redis |
redis:8.2-alpine |
задачи и результаты (TTL RESULT_TTL_HOURS), открытые окна (TTL = простой), heartbeat воркеров |
Наружу открыт только порт api. Прокси и ключ 2captcha из запроса передаются только в сообщении очереди и в Redis
не сохраняются.
Воркеров можно запустить несколько (docker compose up -d --scale worker=2): каждый берёт задачи из общей очереди,
а задачи сессии с открытым окном api направляет в персональную очередь воркера, где это окно. Профили сессий лежат
в volume воркера — воркеры на одном хосте делят его.
Что внутри образа worker
| Компонент | Зачем |
|---|---|
python:3.12-slim-bookworm |
базовый образ, Python той же версии, что и в .venv |
requirements.txt |
ставится из PyPI (папка wheels/ в образ не попадает — она под Ubuntu Noble и неполная) |
Google Chrome stable (patchright install --with-deps chrome) |
сессия запускается с real_chrome=True → channel chrome |
Xvfb (xvfb-run) |
браузер работает в headful-режиме (headless=False), нужен виртуальный дисплей 1920x1080x24 |
tini (ENTRYPOINT, PID 1) |
пробрасывает сигналы и подбирает осиротевшие процессы Chrome/Xvfb — без него копятся зомби-процессы |
пользователь app (uid 1000) |
Chrome не запускается от root без --no-sandbox |
Рабочая директория — /app/src. Воркер — один процесс dramatiq (--processes 1: открытые окна живут в его памяти),
потоков WORKER_THREADS (по умолчанию 2 × BROWSER_SLOTS).
Данные и volume
/app/extra воркера вынесен в volume (extra локально, api-data в релизе):
extra/user_data_dir/<session_id>— профили браузера (cookies, local storage, fingerprint) поsession_id;extra/geoip_cache.json— кэш геолокации прокси.
Профили со временем накапливаются — чистить вручную:
docker compose exec worker sh -c 'rm -rf /app/extra/user_data_dir/*'
Настройки compose
shm_size: 2gbу worker — Chrome активно использует/dev/shm, дефолтных 64 МБ не хватает (вкладки падают с «Aw, Snap»).API_PORT— порт api на хосте (по умолчанию 8000):API_PORT=8001 docker compose up -d.WORKER_CPUS/WORKER_MEM_LIMIT— потолок ресурсов воркера (по умолчанию 4 CPU / 8 ГБ, swap выключен). При переборе памяти OOM-killer убивает процесс Chrome внутри контейнера, хост не страдает.RABBITMQ_PASSWORD— пароль пользователяrobotexв RabbitMQ (по умолчаниюrobotex; порт наружу не открыт). RabbitMQ запускается сconsumer_timeout4 ч: задача может выполняться дольше 30 минут по умолчанию.stop_grace_period: 60sу worker — при остановке он доделывает текущие задачи и закрывает окна.restart: unless-stopped..envподключается, если существует (шаблон —env.example).
Переменные окружения приложения
| Переменная | По умолчанию | Описание |
|---|---|---|
BROWSER_SLOTS |
4 |
worker: сколько окон Chrome открыто одновременно — и выполняющих задачу, и удерживаемых. Задача без свободного слота ждёт в очереди |
WORKER_THREADS |
2 × BROWSER_SLOTS |
worker: потоков dramatiq |
WORKER_ID |
hostname | worker: имя воркера и его персональной очереди |
RESULT_TTL_HOURS |
24 |
сколько хранится результат задачи |
V1_QUEUE_WAIT |
30 |
api: сколько /v1/solve ждёт, пока воркер возьмёт задачу; дальше 503 |
NO_SLOTS_RETRY_AFTER |
5 |
api: значение Retry-After (с) в ответе 503 |
RABBITMQ_URL |
amqp://guest:guest@localhost:5672/ |
api, worker; в compose задаётся сам |
REDIS_URL |
redis://localhost:6379/0 |
api, worker; в compose задаётся сам |
LOG_LEVEL |
INFO |
DEBUG — полный дамп запроса/ответа (секреты клиента маскируются) |
Нагрузка
Замер: freelancehunt.com/projects за Cloudflare Turnstile, captcha_type: cloudflare, ожидание списка проектов,
каждый запрос — новый профиль. Прокси — gost из профиля loadtest. Хост: 16 ядер, 31 ГБ.
| Лимит | Параллельно | Успешно | Медиана, с | Пик CPU | Пик RAM |
|---|---|---|---|---|---|
| 4 CPU | 1 | 1/1 | 5 | 3,5 ядра | 0,5 ГБ |
| 4 CPU | 5 | 3/5 | 68 | 4,1 | 2,8 ГБ |
| 4 CPU | 10 | 4/10 | 229 | 4,2 | 6,0 ГБ |
| 4 CPU | 15 | 3/15 | 261 | 4,2 | 6,1 ГБ |
| 12 CPU | 3 | 3/3 | 8 | 7,2 | 1,4 ГБ |
| 12 CPU | 5 | 4/5 | 8 | 12,1 | 2,4 ГБ |
| 12 CPU | 10 | 9/10 | 23 | 12,3 | 4,7 ГБ |
Выводы:
- Узкое место — CPU. Один Chrome на прохождении Turnstile съедает 2–3,5 ядра. Когда процессора не хватает, время растёт в десятки раз, а Cloudflare перестаёт пропускать (на 4 CPU и 10 параллельных — 6 из 10 застряли на челлендже).
- Память — около 0,5–0,6 ГБ на браузер.
- Правило подбора:
BROWSER_SLOTS ≈ число ядер,WORKER_MEM_LIMIT ≈ 0,6 ГБ × BROWSER_SLOTS + 1 ГБ. Пример: сервер 4 ядра / 8 ГБ → 3–4 слота.BROWSER_SLOTSзадаётся в.env. - Удерживаемые окна (
browser_idle_timeout) занимают слоты, но почти не тратят CPU. - Сверх
BROWSER_SLOTSзадачи v2 ждут в очереди, а/v1/solveсразу получает503сcode: no_slots.
Замеры выше сделаны на v1 (один сервис, Chrome в api); схема нагрузки на Chrome с очередью та же.
Повторить замер:
API_PORT=8001 docker compose --profile loadtest up -d
python tests/loadtest.py --api http://127.0.0.1:8001 --levels 1 5 10
Параметры скрипта: --url, --selector, --captcha-type, --proxy, --levels. Каждый запрос создаёт новый профиль
в extra/user_data_dir — после прогона их стоит почистить.
Reverse proxy
В проде контейнер ставится за Caddy (TLS). Пример Caddyfile:
api.example.com {
reverse_proxy localhost:8000
}
Таймауты прокси для /v1/solve должны быть не меньше timeout запроса (по умолчанию 120 с); запросы v2 короткие.