ADR-0019: ClickHouse для истории цен/остатков и аналитики

Status: accepted Date: 2026-04-17 Deciders: команда проекта

Контекст

В системе есть две существенно разные нагрузки:

  1. OLTP на canonical/offers/credentials — обслуживается PostgreSQL (ADR-0004).
  2. OLAP: история цен, история остатков, аналитика по поведению поставщиков, данные для ML (eval harness, golden set, retraining).

Объёмы для OLAP по оценкам Phase 1–3:

  • offer.observation.v1 → ~100M записей/мес при 10 поставщиках с ETM-подобным размером каталога;
  • offer.price_changed.v1 / offer.stock_changed.v1 → дополнительные 10–50M/мес.

PostgreSQL для такого объёма аналитики неэффективен. Elasticsearch — не для агрегатов по time series. Нужно отдельное хранилище.

В high-level-architecture.md и data-storage-decisions.md ClickHouse уже фигурирует как часть стека, но без ADR — выбор не зафиксирован, retention/ingestion path/schema эволюция не определены.

Решение

ClickHouse — основное хранилище time-series истории и аналитики. Не source of truth.

Роль и границы

Что хранится в CHЧто не хранится в CH
price_history — все наблюдения цен по (offer_id, credential_id, observed_at)Текущая цена / остаток (PG read model)
stock_history — то же по остаткамСами SupplierOffer, Customer, Canonical Product
pricing_calculations — сэмплы breakdown’ов (для аудита и ML)Event store (PG)
match_decisions_audit — sampled матч-решения с featuresVisibility policy state (PG, event-sourced)
connector_metrics — детальные метрики ingestion (per credential)Operational state (sessions, jobs)
search_queries_log — анонимизированные клиентские запросы (для retrain)Customer PII (хранится только в PG с шифрованием)

CH — production-ready проекция: пересоздаётся replay’ем из Kafka topics (см. ADR-0002, event-sourcing.md). Если CH упал и потерян — не блокируем основной user-facing трафик, только аналитику.

Путь ingestion

Kafka topic                           ClickHouse
  ──────────────                       ────────
  offer.price_changed.v1   ──┐
  offer.stock_changed.v1   ──┼──→  Kafka Engine table
  offer.observation.v1     ──┤        │
  matching.decided.v1      ──┘        ▼
                                   Materialized view
                                        │
                                        ▼
                                   target table (MergeTree variants)
  • Kafka Engine table в CH потребляет топики — нет отдельного сервиса-синка для базового пути.
  • Materialized view делает преобразование (parsing JSON, типизация, partitioning).
  • Target tableReplicatedMergeTree / ReplicatedReplacingMergeTree per use case.
  • DLQ: при ошибке парсинга в MV — запись попадает в _errors table с raw payload, метрика ch_ingestion_errors_total{topic}.

Раскладка схемы

clickhouse/
├── databases/
│   ├── tracium_history.sql      # price_history, stock_history
│   ├── tracium_audit.sql        # pricing_calculations, match_decisions_audit
│   └── tracium_metrics.sql      # connector_metrics, search_queries_log
└── migrations/                  # versioned DDL via go-migrate (clickhouse driver)

Naming: snake_case, partitioning по toYYYYMM(observed_at) для time-series, TTL и storage policy явно указаны в DDL.

Retention (сроки хранения)

TableHot retentionCold (S3 backup tier)Drop
price_history90 дней2 года2 года
stock_history30 дней1 год1 год
pricing_calculations30 дней1 год1 год
match_decisions_audit1 год3 года3 года
connector_metrics14 дней14 дней (агрегаты в Prometheus / Grafana)
search_queries_log90 дней1 год1 год

Реализация: TTL observed_at + INTERVAL 90 DAY TO DISK 'cold', TTL observed_at + INTERVAL 2 YEAR DELETE. Cold tier — S3-backed disk (MinIO в self-hosted setup). Backup — отдельный процесс clickhouse-backup в S3.

Эволюция схемы

  • ADR-process для breaking-изменения схемы (drop column, type change).
  • Optional column add — без ADR, через миграцию.
  • Двойная запись при breaking-изменении: новый MV пишет в новую таблицу, старая работает 7 дней, переключение чтения, drop старой через 30 дней.

Доступ

  • Чтение: только сервисы analytics, meta-search (для recommendations), admin BI dashboards.
  • Запись: только Kafka Engine + ad-hoc backfill (через документированный runbook, audit-trail).
  • Прямые INSERT из application code — запрещены (защита через grants на CH user app_writer = NONE).

NFR

  • p95 query latency на price_history за последние 90 дней по одному canonical_id: < 200 мс.
  • Ingestion lag (Kafka offset → видно в CH SELECT) — p95 < 30 секунд.
  • Reindex/backfill из Kafka — допустимо, но операция planned (документируется в runbook).

Последствия

Плюсы

  • Time-series аналитика на правильном движке: дешёвые агрегаты, partitioning, TTL out-of-the-box.
  • Kafka Engine + MV — нет отдельного сервиса для базового sink (меньше кода, меньше операций).
  • Чёткая граница: CH — проекция, не source of truth. Падение CH не блокирует продукт.
  • Schema, retention, наименования — формализованы.

Минусы

  • ClickHouse — operational overhead (ZooKeeper / Keeper, replica management, backup).
  • Kafka Engine path замыкает CH на доступность Kafka — но это уже фундаментальная зависимость (ADR-0002).
  • Восстановление CH из Kafka возможно только в пределах retention топиков (raw 30 дней, normalized 14 дней) — для более глубокого восстановления нужен clickhouse-backup из S3.

Нейтральные последствия

  • На начальных этапах допустимо запустить single-node CH (без репликации) с periodic backup в S3. Переход на cluster — без миграции данных, через clickhouse-copier или INSERT INTO ... SELECT FROM remote(...).

Рассмотренные альтернативы

TimescaleDB (PG extension)

Удобно (один сторадж). Но: на 100M+/мес записей PG-based решение быстро упирается в IO, аналитические агрегаты медленные. Партиционирование Timescale не даёт того сжатия, что MergeTree.

Druid / Pinot

Ближе по нагрузке, но более сложная operational модель и меньшая зрелость SQL-интерфейса. Команда уже знакома с CH из предыдущих проектов.

S3 + Athena/Trino (data lake)

Подходит для batch analytics, но latency для интерактивных дашбордов и ML feature store — неприемлемая. Можно использовать как дополнительный cold tier (что мы и делаем через S3 disk).

”Просто Elasticsearch”

ES для time-series делает это хуже CH (storage overhead, дороже агрегаты). ES остаётся для full-text + faceted поиска (ADR-0006).

Ссылки