ADR-0019: ClickHouse для истории цен/остатков и аналитики
Status: accepted Date: 2026-04-17 Deciders: команда проекта
Контекст
В системе есть две существенно разные нагрузки:
- OLTP на canonical/offers/credentials — обслуживается PostgreSQL (ADR-0004).
- 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 матч-решения с features | Visibility 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 table —
ReplicatedMergeTree/ReplicatedReplacingMergeTreeper use case. - DLQ: при ошибке парсинга в MV — запись попадает в
_errorstable с 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 (сроки хранения)
| Table | Hot retention | Cold (S3 backup tier) | Drop |
|---|---|---|---|
price_history | 90 дней | 2 года | 2 года |
stock_history | 30 дней | 1 год | 1 год |
pricing_calculations | 30 дней | 1 год | 1 год |
match_decisions_audit | 1 год | 3 года | 3 года |
connector_metrics | 14 дней | — | 14 дней (агрегаты в Prometheus / Grafana) |
search_queries_log | 90 дней | 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 userapp_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).
Ссылки
- ADR-0002 (Kafka).
- ADR-0004 (PostgreSQL as primary store).
- ADR-0006 (Elasticsearch over OpenSearch).
../data-storage-decisions.md../schemas/clickhouse/README.md