Первые сутки сервис падал каждый час, но сейчас система выдерживает пиковые запросы без даунтайма.

Исходная задача

Мне нужно было автоматизировать процесс сбора спортивных данных (NFL, NBA, UFC) с dingerodds для дальнейшего анализа и обучения моделей. Источник выбран из-за:

  • доступного REST API (пример запроса ниже)

  • свежих коэффициентов и статистики

  • наличия исторических данных

GET /api/v1/events/upcoming?market=moneyline&sport=baseball
Authorization: Bearer <token>

Но оказалось, что API отваливается под минимальной нагрузкой и плохо обрабатывает батчи (особенно GET /events/history).


Проблемы

  1. Rate-limits не задокументированы, но явно действуют: после ~60 запросов в минуту — 429.

  2. API отдает 502/504 на пиковых батчах.

  3. Нет webhook или pub/sub, всё надо опрашивать вручную.

  4. Нестабильный ответ JSON — иногда odds приходят как null, иногда как {}.


Архитектура решения

Обёртка над API (Python, asyncio, httpx)

import httpx
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1))
async def fetch(url: str, headers: dict):
    async with httpx.AsyncClient(timeout=10) as client:
        response = await client.get(url, headers=headers)
        response.raise_for_status()
        return response.json()
  • Circuit Breaker через кастомную реализацию с fallback

  • Поддержка конкурентных запросов (asyncio.gather)

  • Встроенные прокси-пулы с ротацией IP

  • В случае fail → log в Loki, push alert в Telegram

CI/CD pipeline (GitLab)

stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - docker build -t registry/app:$CI_COMMIT_SHA .
    - docker push registry/app:$CI_COMMIT_SHA

deploy:
  stage: deploy
  script:
    - helm upgrade --install dingerodds-chart ./chart \
        --set image.tag=$CI_COMMIT_SHA
  • Helm + custom values для окружений (dev, prod)

  • Secrets через sealed-secrets

  • Логика деплоя: если main — обновляем prod, иначе dev

Kubernetes (Yandex Cloud)

  • 1 Deployment: api-fetcher — воркер, который забирает данные с dingerodds

  • 1 CronJob: retrain-models — переобучение моделей каждые 6 часов

  • MinIO: хранилище parquet-файлов (s3://sports-data/{sport}/{date}.parquet)

  • Horizontal Pod Autoscaler: масштабируем api-fetcher по CPU > 70%

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
  scaleTargetRef:
    kind: Deployment
    name: api-fetcher
  minReplicas: 1
  maxReplicas: 5
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

ETL: Dask + Parquet

  • После каждого батча сохраняем данные в parquet с разделением по sport и timestamp

  • Используем fastparquet как backend

  • Dask позволяет агрегировать большие объемы без перегрузки памяти

import dask.dataframe as dd

df = dd.from_pandas(pandas_df, npartitions=4)
df.to_parquet("s3://sports-data/nfl/2025-07-24/", engine="fastparquet")

Мониторинг

  • Prometheus: метрики по количеству успешных и проваленных запросов

  • Loki + Grafana: логи по статус-кодам, времени ответа

  • Alertmanager: шлёт уведомления в Telegram, если API не отвечает > 2 минут

Результаты

  • Система в проде уже 14 дней — ни одного критического отказа

  • Обновление данных каждые 5 минут

  • ML-модели обучаются с актуальными и полными датасетами

  • Аптайм > 99.95% (до внедрения был ~85%)

  • Время от запроса до появления данных в parquet — < 2 мин

Планы на будущее

  • Перейти на pub/sub модель (если dingerodds даст такую возможность)

  • Интегрировать Redis для кеширования популярных выборок

  • Сделать delta-lake поверх MinIO + добавить версионирование данных

  • Использовать Kafka для push-событий, если появится мульти-источник

Выводы

Работа с нестабильным API — это почти всегда про защиту от самого источника.

Если бы я просто подключил dingerodds без буферов, ретраев и лимитов — прод упал бы в первые часы.

Обёртка + CI/CD + отказоустойчивая архитектура на Kubernetes решают большую часть боли.

Комментарии (0)