Files
robotex/docs/Deployment.md
T
protokeyandClaude Opus 5.5 1a4bd8d672 feature: API-ключи клиентов и журнал использования в Postgres (#4, #17)
- Postgres в compose; пакет db (SQLAlchemy async): clients, windows, requests.
- Все эндпоинты, кроме /health, — только с ключом в X-API-Key, иначе 401; без
  выпущенных ключей доступа нет. В базе — только sha256 ключа.
- Задача и окно сессии привязаны к клиенту: чужой ключ их не видит.
- Воркер пишет журнал: окно (открыто, последнее обращение, закрыто) и задачу
  (в очереди, начало, конец, итог, данные без секретов). Сбой записи не роняет задачу.
- python -m db.cli create|list|revoke|stats; make key-create/key-list/key-revoke/stats.
- smoke и loadtest с ключом.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:48:48 +04:00

12 KiB
Raw Blame History

Развёртывание в 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 воркеров
postgres postgres:17-alpine клиенты и API-ключи, журнал окон браузера и запросов

Наружу открыт только порт api. Прокси и ключ 2captcha из запроса передаются только в сообщении очереди и в Redis не сохраняются.

Воркеров можно запустить несколько (docker compose up -d --scale worker=2): каждый берёт задачи из общей очереди, а задачи сессии с открытым окном api направляет в персональную очередь воркера, где это окно. Профили сессий лежат в volume воркера — воркеры на одном хосте делят его.

Клиенты и ключи

Без ключей API никого не пускает. Ключ показывается один раз при выпуске, в базе хранится только sha256.

make key-create name=acme   # выпустить ключ клиенту
make key-list               # клиенты: начало ключа, активен или отозван, последнее обращение
make key-revoke id=3        # отозвать

На сервере без make: docker compose exec api python -m db.cli create acme (и list, revoke <id>, stats).

Статистика использования

Воркер пишет в Postgres:

  • windows — окно браузера: клиент, задача, открывшая окно, время открытия, последнего обращения и закрытия;
  • requests — задача: клиент, v1/v2, время постановки в очередь, начала и конца, итог (status, code, duration_ms), данные задачи без секретов (без логина и пароля прокси, ключа 2captcha, вводимых значений и значений cookies);
  • clients — время создания клиента и последнего обращения.

Сводка по клиентам — число запросов, успешные, среднее и p95 время, окна и часы работы браузера, ошибки по code:

make stats days=7

Ошибка записи в журнал не роняет задачу — только предупреждение в логе воркера. Схема создаётся при старте api и воркера.

Что внутри образа 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 — кэш геолокации прокси.

Воркер удаляет профили, которыми не пользовались PROFILE_TTL_DAYS дней (по умолчанию 7): при старте и затем раз в час. Профиль с открытым окном не трогается. Клиент, вернувшийся позже, получает новый профиль и заново проходит антибот. Удалить все профили вручную:

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; порт наружу не открыт).
  • POSTGRES_PASSWORD — пароль пользователя robotex в Postgres (по умолчанию robotex; порт наружу не открыт). RabbitMQ запускается с consumer_timeout 4 ч: задача может выполняться дольше 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 сколько хранится результат задачи
PROFILE_TTL_DAYS 7 worker: через сколько дней без использования удаляется профиль браузера
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 задаётся сам
DATABASE_URL postgresql+asyncpg://robotex:robotex@localhost:5432/robotex 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 короткие.