108 lines
5.5 KiB
Markdown
108 lines
5.5 KiB
Markdown
# Развёртывание в Docker
|
||
|
||
## Быстрый старт
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
docker compose logs -f api
|
||
```
|
||
|
||
API доступно на `http://localhost:8000` (`POST /v1/solve`).
|
||
|
||
Без compose:
|
||
|
||
```bash
|
||
docker build -t robotex-api .
|
||
docker run -d --name robotex -p 8000:8000 --shm-size=2g -v robotex-extra:/app/extra robotex-api
|
||
```
|
||
|
||
## Что внутри образа
|
||
|
||
| Компонент | Зачем |
|
||
|---|---|
|
||
| `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`, uvicorn запускается как `uvicorn main:app` (импорты в проекте вида `from api import ...`).
|
||
|
||
## Данные и volume
|
||
|
||
`/app/extra` вынесен в volume `extra`:
|
||
|
||
- `extra/user_data_dir/<session_id>` — профили браузера (cookies, local storage, fingerprint) по `session_id`;
|
||
- `extra/geoip_cache.json` — кэш геолокации прокси.
|
||
|
||
Профили со временем накапливаются — чистить вручную:
|
||
|
||
```bash
|
||
docker compose exec api sh -c 'rm -rf /app/extra/user_data_dir/*'
|
||
```
|
||
|
||
## Настройки compose
|
||
|
||
- `shm_size: 2gb` — Chrome активно использует `/dev/shm`, дефолтных 64 МБ не хватает (вкладки падают с «Aw, Snap»).
|
||
- `API_PORT` — порт на хосте (по умолчанию 8000): `API_PORT=8001 docker compose up -d`.
|
||
- `API_CPUS` / `API_MEM_LIMIT` — потолок ресурсов контейнера (по умолчанию 4 CPU / 8 ГБ, swap выключен).
|
||
При переборе памяти OOM-killer убивает процесс Chrome внутри контейнера, хост не страдает.
|
||
- `restart: unless-stopped`.
|
||
- `.env` подключается, если существует (шаблон — `env.example`).
|
||
|
||
## Переменные окружения приложения
|
||
|
||
| Переменная | По умолчанию | Описание |
|
||
|---|---|---|
|
||
| `BROWSER_SLOTS` | `10` | сколько Chrome работает параллельно; сверх — `503` |
|
||
| `NO_SLOTS_RETRY_AFTER` | `5` | значение `Retry-After` (с) в ответе `503` |
|
||
| `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 ≈ число ядер`, `API_MEM_LIMIT ≈ 0,6 ГБ × BROWSER_SLOTS + 1 ГБ`.
|
||
Пример: сервер 4 ядра / 8 ГБ → 3–4 слота. `BROWSER_SLOTS` задаётся в `.env` (по умолчанию 10 — на ~10–12 ядер).
|
||
- Сверх `BROWSER_SLOTS` запросы сразу получают `503` с `code: no_slots`, не ставятся в очередь.
|
||
|
||
Повторить замер:
|
||
|
||
```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
|
||
}
|
||
```
|
||
|
||
Таймауты прокси должны быть не меньше `timeout` запроса (по умолчанию 120 с).
|