Files
robotex/docs/Deployment.md
T
protokeyandClaude Opus 5.5 7b511d5d38 feature: архитектура v2 — api + очередь + worker, удержание окна браузера (#78)
- Сервис 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>
2026-10-13 19:13:52 +04:00

136 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Развёртывание в Docker
## Быстрый старт
```bash
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` — кэш геолокации прокси.
Профили со временем накапливаются — чистить вручную:
```bash
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_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` | сколько хранится результат задачи |
| `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 с очередью та же.
Повторить замер:
```bash
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 короткие.