improve docker compose setup

This commit is contained in:
qananasikq
2026-04-09 20:34:34 +03:00
parent 042ffdcfbb
commit eb9fb48ea0
5 changed files with 223 additions and 81 deletions

View File

@@ -32,6 +32,8 @@ IAAI_AFTER_PAGE_CHANGE_MAX_S=6.0
# Sync settings # Sync settings
IAAI_SYNC_ONLY_NEW=true IAAI_SYNC_ONLY_NEW=true
IAAI_TOKENS_FILE=/data/tokens.json
IAAI_RUNTIME_CONFIG_FILE=/app/runtime_config.json
# Retry / backoff # Retry / backoff
IAAI_RETRY_DELAY_SECONDS=2.5 IAAI_RETRY_DELAY_SECONDS=2.5

View File

@@ -1,4 +1,4 @@
"""Alembic env.py — подключение к БД через Settings.""" # Alembic env.py — подключение к БД через Settings.
import os import os
import sys import sys
@@ -27,7 +27,7 @@ target_metadata = Base.metadata
def run_migrations_offline() -> None: def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.""" # Run migrations in 'offline' mode.
url = config.get_main_option("sqlalchemy.url") url = config.get_main_option("sqlalchemy.url")
context.configure( context.configure(
url=url, url=url,
@@ -40,7 +40,7 @@ def run_migrations_offline() -> None:
def run_migrations_online() -> None: def run_migrations_online() -> None:
"""Run migrations in 'online' mode.""" # Run migrations in 'online' mode.
connectable = engine_from_config( connectable = engine_from_config(
config.get_section(config.config_ini_section, {}), config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.", prefix="sqlalchemy.",

View File

@@ -0,0 +1,47 @@
"""Add performance indexes for growing database
Revision ID: 002_add_indexes
Revises: 001_initial
Create Date: 2026-04-09
"""
from typing import Sequence, Union
from alembic import op
revision: str = "002_add_indexes"
down_revision: Union[str, None] = "001_initial"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# cars: ускорение фильтрации по бренду в API и статистике
op.create_index("ix_cars_brand", "cars", ["brand"])
# cars: составной индекс бренд+модель для комбинированных фильтров
op.create_index("ix_cars_brand_model", "cars", ["brand", "model"])
# cars: ускорение фильтрации по году (year_min/year_max)
op.create_index("ix_cars_year", "cars", ["year"])
# cars: ускорение фильтрации по статусу продажи
op.create_index("ix_cars_is_sold", "cars", ["is_sold"])
# cars: ускорение сортировки ORDER BY last_seen_at DESC (пагинация)
op.create_index("ix_cars_last_seen_at", "cars", ["last_seen_at"])
# images: ускорение JOIN/DELETE по car_id (критично при upsert)
op.create_index("ix_images_car_id", "images", ["car_id"])
# sync_runs: ускорение поиска stale runs по статусу
op.create_index("ix_sync_runs_status", "sync_runs", ["status"])
def downgrade() -> None:
op.drop_index("ix_sync_runs_status", table_name="sync_runs")
op.drop_index("ix_images_car_id", table_name="images")
op.drop_index("ix_cars_last_seen_at", table_name="cars")
op.drop_index("ix_cars_is_sold", table_name="cars")
op.drop_index("ix_cars_year", table_name="cars")
op.drop_index("ix_cars_brand_model", table_name="cars")
op.drop_index("ix_cars_brand", table_name="cars")

View File

@@ -1,5 +1,36 @@
x-app-env: &app-env
IAAI_DATABASE_URL: ${IAAI_DATABASE_URL:-postgresql+psycopg2://iaai:iaai@postgres:5432/iaai_scraper}
IAAI_REDIS_URL: ${IAAI_REDIS_URL:-redis://redis:6379/0}
CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis:6379/0}
CELERY_RESULT_BACKEND: ${CELERY_RESULT_BACKEND:-redis://redis:6379/0}
IAAI_DATABASE_POOL_RECYCLE_SECONDS: ${IAAI_DATABASE_POOL_RECYCLE_SECONDS:-1800}
CELERY_TASK_SOFT_TIME_LIMIT: ${CELERY_TASK_SOFT_TIME_LIMIT:-900}
CELERY_TASK_TIME_LIMIT: ${CELERY_TASK_TIME_LIMIT:-1200}
CELERY_BROKER_VISIBILITY_TIMEOUT: ${CELERY_BROKER_VISIBILITY_TIMEOUT:-7200}
CELERY_WORKER_MAX_TASKS_PER_CHILD: ${CELERY_WORKER_MAX_TASKS_PER_CHILD:-20}
IAAI_TOKENS_FILE: ${IAAI_TOKENS_FILE:-/data/tokens.json}
IAAI_RUNTIME_CONFIG_FILE: ${IAAI_RUNTIME_CONFIG_FILE:-/app/runtime_config.json}
TZ: ${TZ:-UTC}
x-env-file: &env-file
- path: .env
required: false
x-app-service: &app-service
build: .
env_file: *env-file
environment: *app-env
volumes:
- ./runtime_config.json:/app/runtime_config.json:ro
x-worker-service: &worker-service
<<: *app-service
volumes:
- ./runtime_config.json:/app/runtime_config.json:ro
- tokens_data:/data
services: services:
# ─── PostgreSQL ─────────────────────────────────────────── # PostgreSQL
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
container_name: iaai-postgres container_name: iaai-postgres
@@ -17,8 +48,13 @@ services:
interval: 5s interval: 5s
timeout: 3s timeout: 3s
retries: 5 retries: 5
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
# ─── Redis (Celery broker) ──────────────────────────────── # Redis (Celery broker)
redis: redis:
image: redis:7-alpine image: redis:7-alpine
container_name: iaai-redis container_name: iaai-redis
@@ -30,76 +66,109 @@ services:
interval: 5s interval: 5s
timeout: 3s timeout: 3s
retries: 5 retries: 5
command: >
redis-server
--appendonly yes
--save 60 1000
volumes:
- redisdata:/data
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
# ─── FastAPI ────────────────────────────────────────────── # DB migrations
api: migrate:
build: . <<: *app-service
container_name: iaai-api container_name: iaai-migrate
restart: unless-stopped restart: "no"
env_file:
- path: .env
required: false
environment:
IAAI_DATABASE_URL: postgresql+psycopg2://iaai:iaai@postgres:5432/iaai_scraper
IAAI_REDIS_URL: redis://redis:6379/0
CELERY_BROKER_URL: redis://redis:6379/0
CELERY_RESULT_BACKEND: redis://redis:6379/0
ports:
- "8000:8000"
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
command: alembic upgrade head
# FastAPI
api:
<<: *app-service
container_name: iaai-api
restart: unless-stopped
ports:
- "8000:8000"
depends_on:
migrate:
condition: service_completed_successfully
redis: redis:
condition: service_healthy condition: service_healthy
command: > command: >
uvicorn iaai_scraper.api.app:app uvicorn iaai_scraper.api.app:app
--host 0.0.0.0 --port 8000 --workers 2 --host 0.0.0.0 --port 8000 --workers 1
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5)"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
stop_grace_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
# ─── Celery Worker ──────────────────────────────────────── # Celery Worker
worker: worker:
build: . <<: *worker-service
container_name: iaai-worker container_name: iaai-worker
restart: unless-stopped restart: unless-stopped
env_file:
- path: .env
required: false
environment:
IAAI_DATABASE_URL: postgresql+psycopg2://iaai:iaai@postgres:5432/iaai_scraper
IAAI_REDIS_URL: redis://redis:6379/0
CELERY_BROKER_URL: redis://redis:6379/0
CELERY_RESULT_BACKEND: redis://redis:6379/0
depends_on: depends_on:
postgres: migrate:
condition: service_healthy condition: service_completed_successfully
redis: redis:
condition: service_healthy condition: service_healthy
stop_grace_period: 60s stop_grace_period: 60s
command: > command: >
celery -A iaai_scraper.worker.celery_app worker celery -A iaai_scraper.worker.celery_app worker
--loglevel=info --concurrency=1 --pool=prefork --loglevel=info --concurrency=1 --pool=prefork
-Q scraping --without-heartbeat -Q scraping --max-tasks-per-child=20
healthcheck:
test: ["CMD", "celery", "-A", "iaai_scraper.worker.celery_app", "inspect", "ping", "-d", "celery@$$HOSTNAME"]
interval: 60s
timeout: 20s
retries: 3
start_period: 40s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
# ─── Celery Beat (периодический планировщик) ────────────── # Celery Beat (периодический планировщик)
beat: beat:
build: . <<: *worker-service
container_name: iaai-beat container_name: iaai-beat
restart: unless-stopped restart: unless-stopped
env_file:
- path: .env
required: false
environment:
IAAI_DATABASE_URL: postgresql+psycopg2://iaai:iaai@postgres:5432/iaai_scraper
IAAI_REDIS_URL: redis://redis:6379/0
CELERY_BROKER_URL: redis://redis:6379/0
CELERY_RESULT_BACKEND: redis://redis:6379/0
depends_on: depends_on:
postgres: migrate:
condition: service_healthy condition: service_completed_successfully
redis: redis:
condition: service_healthy condition: service_healthy
command: > command: >
celery -A iaai_scraper.worker.celery_app beat celery -A iaai_scraper.worker.celery_app beat
--loglevel=info --loglevel=info
healthcheck:
test: ["CMD", "python", "-c", "import pathlib,sys; p=pathlib.Path('/tmp/celerybeat-schedule'); sys.exit(0 if p.exists() else 1)"]
interval: 60s
timeout: 20s
retries: 3
start_period: 60s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
volumes: volumes:
pgdata: pgdata:
redisdata:
tokens_data:

View File

@@ -1,57 +1,81 @@
#!/bin/bash #!/bin/bash
set -e set -euo pipefail
# Start HTTP→SOCKS5 proxy bridge if SOCKS5 upstream is configured is_worker_command() {
if [ -n "$SOCKS5_PROXY_HOST" ]; then local joined="$*"
echo "[entrypoint] Starting proxy bridge (HTTP :8899 → SOCKS5 $SOCKS5_PROXY_HOST:${SOCKS5_PROXY_PORT:-1002})..." case "$joined" in
if [ -z "$IAAI_PROXY_SERVER" ]; then *"celery -A iaai_scraper.worker.celery_app worker"*|*" celery -A iaai_scraper.worker.celery_app worker"*)
return 0
;;
esac
return 1
}
is_scrape_cli_command() {
local joined="$*"
case "$joined" in
*"collect-listing"*|*"scrape-vehicle"*|*"sync-vehicle"*|*"sync-listing"*)
return 0
;;
esac
return 1
}
needs_browser_runtime() {
is_worker_command "$@" || is_scrape_cli_command "$@"
}
start_proxy_bridge_if_needed() {
if ! needs_browser_runtime "$@"; then
return 0
fi
if [ -z "${SOCKS5_PROXY_HOST:-}" ]; then
return 0
fi
echo "[entrypoint] Starting proxy bridge (HTTP :8899 → SOCKS5 ${SOCKS5_PROXY_HOST}:${SOCKS5_PROXY_PORT:-1002})..."
if [ -z "${IAAI_PROXY_SERVER:-}" ]; then
export IAAI_PROXY_SERVER="http://127.0.0.1:8899" export IAAI_PROXY_SERVER="http://127.0.0.1:8899"
elif [ "$IAAI_PROXY_SERVER" = "http://localhost:8899" ]; then elif [ "${IAAI_PROXY_SERVER}" = "http://localhost:8899" ]; then
export IAAI_PROXY_SERVER="http://127.0.0.1:8899" export IAAI_PROXY_SERVER="http://127.0.0.1:8899"
fi fi
echo "[entrypoint] Using browser proxy: $IAAI_PROXY_SERVER"
echo "[entrypoint] Using browser proxy: ${IAAI_PROXY_SERVER}"
python -m iaai_scraper.proxy_bridge & python -m iaai_scraper.proxy_bridge &
BRIDGE_PID=$! BRIDGE_PID=$!
sleep 1 sleep 1
if ! kill -0 $BRIDGE_PID 2>/dev/null; then
if ! kill -0 "${BRIDGE_PID}" 2>/dev/null; then
echo "[entrypoint] ERROR: iaai_scraper.proxy_bridge failed to start" echo "[entrypoint] ERROR: iaai_scraper.proxy_bridge failed to start"
exit 1 exit 1
fi fi
echo "[entrypoint] Proxy bridge started (PID $BRIDGE_PID)"
fi
# Xvfb needed only for worker (Playwright) — not for API or beat echo "[entrypoint] Proxy bridge started (PID ${BRIDGE_PID})"
NEEDS_XVFB=false }
case "$1" in
celery*|python*main*) start_xvfb_if_needed() {
NEEDS_XVFB=true if ! needs_browser_runtime "$@"; then
;; return 0
*) fi
# Check if any arg contains "worker"
for arg in "$@"; do
case "$arg" in
*worker*) NEEDS_XVFB=true; break ;;
esac
done
;;
esac
if [ "$NEEDS_XVFB" = "true" ]; then
# IAAI blocks headless Chromium on Linux; run headed via Xvfb virtual display # IAAI blocks headless Chromium on Linux; run headed via Xvfb virtual display
export DISPLAY=:99 export DISPLAY=:99
export IAAI_HEADLESS=false export IAAI_HEADLESS=false
if [ -S /tmp/.X11-unix/X99 ] || [ -f /tmp/.X99-lock ]; then
echo "[entrypoint] Reusing existing Xvfb on DISPLAY=${DISPLAY}"
return 0
fi
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp & Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
XVFB_PID=$! XVFB_PID=$!
sleep 0.5 sleep 0.5
echo "[entrypoint] Xvfb started (PID $XVFB_PID, DISPLAY=$DISPLAY)" echo "[entrypoint] Xvfb started (PID ${XVFB_PID}, DISPLAY=${DISPLAY})"
fi }
# Run Alembic migrations (only for API service, skip for worker/beat) start_proxy_bridge_if_needed "$@"
case "$1" in start_xvfb_if_needed "$@"
uvicorn*)
echo "[entrypoint] Running Alembic migrations..."
alembic upgrade head || echo "[entrypoint] WARNING: Alembic migration failed, continuing..."
;;
esac
exec "$@" exec "$@"