Модуль 15. Data Products & API: как безопасно отдавать витрины наружу
Это подробная инструкция «как превратить витрины в продукт»: спроектировать стабильные эндпоинты, обернуть их квотами/кэшем/версионированием, обеспечить RLS/маскирование, задать контракты и SLO, и подключить наблюдаемость и аудит. В конце — чек-лист, артефакты и готовые конфиги.
Зачем и что считаем успехом
Цель: отдаём метрики с предсказуемой латентностью и одинаковыми цифрами для всех потребителей.
Критерии приёмки (SLO):
- Доступность API ≥ 99.9%.
- p95 задержка: KPI-эндпоинты ≤ 500–1500 мс (зависит от окна).
- Свежесть: не хуже SLA витрин (например, NRT ≤ 5 минут).
- Контракты: стабильные схемы и версии (/v1, /v2).
- Безопасность: RLS/маскирование, кэш без утечек между арендаторами, аудит запросов.
Архитектура Data Product слоя
[ Клиенты / Партнёры / Сервисы ]
│ HTTPS (JWT/API Key)
▼
+--------------------+
| API Gateway | ─ кеш 30–300s, rate-limit, auth, валидация параметров
| (Nginx / FastAPI) |
+--------------------+
│ HTTP (параметризованные запросы)
▼
+--------------------+ только vw_* (семантический слой)
| ClickHouse (HTTP) | ─ RLS/маскирование, профили, квоты
+--------------------+
│
▼
Наблюдаемость: Nginx access/error logs + CH query_log/part_log/replication + APM
Опора: никаких «сырых» таблиц наружу; только vw_*. Любые тяжёлые метрики (Distinct/квантили/Top-K) — предагрегаты-состояния (см. Модули 2/8/11/14).
Контракты и версия API
Формат контракта (фрагмент OpenAPI-идеи)
- Базовый путь: /api/v1/kpi
- Параметры: from: date, to: date, shop: int?, granularity: enum(hour|day).
- Ответ: application/json или application/octet-stream (Parquet).
- Идемпотентность: только GET/HEAD.
- Версионирование: /v1 — стабильная схема; изменения логики → /v2.
SLO на эндпоинт
- GET /api/v1/kpi: p95 ≤ 800 мс на окне ≤ 31 день; тело ≤ 1e6 строк.
- GET /api/v1/top_sku: p95 ≤ 400 мс, не более 10k строк.
Семантика и безопасность на стороне ClickHouse
Вьюха-источник (пример)
CREATE OR REPLACE VIEW db_sem.vw_kpi_day AS
SELECT day, shop_id,
sumMerge(view_state) AS views,
sumMerge(buy_state) AS buys,
sumMerge(rev_state) AS revenue,
revenue / NULLIF(buys,0) AS aov,
buys / NULLIF(views,0) AS cr
FROM db_marts.agg_events_day_state
GROUP BY day, shop_id;
RLS и маскирование
-- Политика арендности: показываем только записи своего tenant_id
CREATE ROW POLICY rp_tenant ON db_sem.vw_kpi_day
FOR SELECT USING tenant_id = currentSetting('tenant_id');
-- Пример маскировки PII
CREATE OR REPLACE VIEW db_sem.vw_orders_masked AS
SELECT day, shop_id,
substring(sha256Hex(toString(customer_id)),1,12) AS customer_hash,
net_sales
FROM db_sem.vw_orders;
Роли, профили, квоты
CREATE ROLE api_reader;
CREATE USER api_ro IDENTIFIED BY '***';
GRANT api_reader TO api_ro;
GRANT SELECT ON db_sem.vw_* TO api_reader;
-- Профиль ограничений для API
CREATE SETTINGS PROFILE api_limits
SET max_execution_time=2, max_memory_usage='4G', max_rows_to_read=50000000, max_threads=8,
result_overflow_mode='break';
ALTER USER api_ro SETTINGS PROFILE api_limits, tenant_id=123; -- RLS параметр
Золотое правило: BI/клиенты читают только db_sem.vw_*. Любые «медленные» конструкции (FINAL, JOIN больших на большие, DISTINCT/квантили «на лету») — запрещаем линтерами/ревью.
Параметризация запросов и «ограничитель по партиции»
ClickHouse HTTP понимает параметры: в SQL пишем {name:Type}, в запросе передаём param_name=value.
SQL-шаблон:
SELECT day, shop_id, views, buys, revenue, aov, cr
FROM db_sem.vw_kpi_day
WHERE day BETWEEN {from:Date} AND {to:Date}
AND ( {shop:UInt32} = 0 OR shop_id = {shop:UInt32} )
ORDER BY day, shop_id
FORMAT JSON
HTTP-вызов (пример):
curl -u api_ro:*** \ -G 'https://api.company.com/clickhouse' \ --data-urlencode 'query=/*kpi_v1*/ SELECT ... (как выше) ...' \ --data-urlencode 'param_from=2025-07-01' \ --data-urlencode 'param_to=2025-07-31' \ --data-urlencode 'param_shop=0'
Ограничитель по партиции: в шлюзе валидируем, что окно не больше N дней (например, 31), и обязательно присутствует from/to. Без них — 400 Bad Request. Так исключаем FULL SCAN.
Пагинация: «seek-based», не OFFSET
Для длинных списков используем «пагинацию по ключу» (идемпотентно и дёшево).
Шаблон:
-- page_token = last_day,last_shop,last_id
SELECT day, shop_id, sku_id, net_sales
FROM db_sem.vw_top_sku
WHERE day BETWEEN {from:Date} AND {to:Date}
AND ( (day, shop_id, sku_id) > ({last_day:Date}, {last_shop:UInt32}, {last_sku:UInt32}) )
ORDER BY day, shop_id, net_sales DESC, sku_id
LIMIT {limit:UInt32};
Клиент передаёт page_token из последней строки предыдущего ответа. OFFSET избегаем: он читает «всё до».
Кеш и троттлинг на шлюзе (Nginx)
Кеш 30–300 секунд (вариант)
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:100m inactive=10m max_size=20g;
map $http_authorization $auth_hash { default "anon"; "~^Bearer\s+(.+)$" $1; }
map "$request_method|$request_uri|$auth_hash" $api_cache_key {
default $request_method$request_uri$auth_hash;
}
server {
listen 443 ssl;
server_name api.company.com;
# Rate limit: 60 req/min per token
limit_req_zone $auth_hash zone=api_ratelimit:10m rate=60r/m;
location /api/v1/kpi {
limit_req zone=api_ratelimit burst=30 nodelay;
proxy_set_header X-Request-Id $request_id;
proxy_set_header Authorization $http_authorization;
proxy_set_header Accept-Encoding ""; # CH сам сожмёт
proxy_cache api_cache;
proxy_cache_key $api_cache_key;
proxy_cache_valid 200 30s;
add_header X-Cache $upstream_cache_status;
proxy_pass http://clickhouse-http/; # внутренний upstream
}
}
Важно: кэш-ключ включает токен ($auth_hash) — это не даёт «протечь» данные между арендаторами при RLS. На публичных общих витринах можно выносить auth из ключа.
Когда 30–300 секунд?
- NRT-плитки/мини-KPI — 15–60 с;
- Дневные/часовые — 60–300 с;
- По-умолчанию — 30 с, если SLA свежести 5 мин.
Форматы и компрессия
- JSON / JSONCompact — удобно для фронтов.
- Parquet — для партнёров/батчей (передача «куска данных»).
- В CH можно задать FORMAT Parquet и получить бинарный поток. Ограничьте строки/байты ответов.
Примеры:
-- JSON SELECT ... FORMAT JSON -- Parquet (стрим) SELECT ... FORMAT Parquet
Nginx не меняет тело; по Accept можете маршрутизировать на разные SQL-шаблоны (через map).
Журналирование и аудит
Корреляция запросов
Пробрасываем X-Request-Id в CH: используем query_id и/или log_comment.
--data-urlencode 'query_id=api-req-12345' \ --data-urlencode "log_comment=api.kpi_v1 shop=$shop window=$from..$to"
В ClickHouse смотрим system.query_log:
SELECT event_time, user, query_id, log_comment, read_rows, read_bytes, query_duration_ms FROM system.query_log WHERE type='QueryFinish' AND query_id = 'api-req-12345';
Журналы доступа
- В Nginx включите подробный access_log с request_id, cache_status, upstream_response_time.
- Периодический отчёт «дорогих эндпоинтов» по read_bytes/duration (см. М11).
Мини-API поверх CH: примеры эндпоинтов
KPI
GET /api/v1/kpi?from=2025-07-01&to=2025-07-31&shop=0&granularity=day Headers: Authorization: Bearer <token>
SQL: как в §4. Ответ: [{day, shop_id, views, buys, revenue, aov, cr}, ...]
Top-SKU (seek-пагинация)
GET /api/v1/top_sku?from=2025-07-01&to=2025-07-31&shop=101&limit=100&page_token=2025-07-10,101,500123
Экспорт Parquet
GET /api/v1/kpi.parquet?from=2025-07-01&to=2025-07-31&shop=0
Интеграционные (contract)-тесты: «golden» ответы
Что тестируем
- Схема ответа (поля/типы).
- Бизнес-инварианты (например, cr ∈ [0,1]).
- Стабильность чисел на «золотом» окне данных.
- Ограничитель по окну: запрос без from/to → 400; окно > 31 дня → 400.
- RLS: пользователь «A» не видит данные «B».
Пример «golden» проверки (псевдо-bash)
resp=$(curl -s -H "Authorization: Bearer $TOKEN" \ "https://api.company.com/api/v1/kpi?from=2025-07-01&to=2025-07-07&shop=0") # Проверка схемы и инвариантов (jq) echo "$resp" | jq -e 'all(.[]; (.cr >= 0 and .cr <= 1))' >/dev/null # Сравнение с эталонным хэшем [ "$(echo "$resp" | sha256sum | cut -d' ' -f1)" = "$(cat tests/golden/kpi_2025-07-01_07.sha256)" ]
«Устав запросов» для внешних продуктов
- Обязательные from/to (ограничение окна ≤ 31 день).
- Никакого SELECT *, список полей фиксирован.
- Запрет FINAL, запрет JOIN больших на большие.
- Только параметризация через {param:Type}; никакой подстановки строк.
- LIMIT обязателен; для списков — только seek-пагинация.
- Тяжёлые метрики — только из rollup-слоя (…State/…Merge).
- Любой новый эндпоинт — сначала VIEW и DQ-тест, затем API.
Кейсы и приёмы
Кейс A: «Пики трафика и шторма кэша»
- Симптом: p95 растёт в 3–5 раз, CH без ошибок.
- Диагностика: access_log — MISS-бурсты; query_log — одинаковые запросы в секунду.
- Фикс: увеличить кеш-TTL с 30 до 120 с на горячих тайлах, включить stale-while-revalidate (через njs или side-кеш), агрессивный limit_req для редких «дорогих» параметров.
Кейс B: «Партнёр скачивает 2ГБ Parquet каждые 10 сек»
- Риск: SLO остальных падает.
- Фикс: отдельный профиль/квоты, ограничение размера ответа (result_rows, result_bytes), перенос на «экспортные джобы» (подписка на файл в object storage), «грубый» rate-limit по токену.
Кейс C: «Пользователь видит чужие данные»
- Причина: общий кэш без ключа авторизации.
- Фикс: включить $auth_hash в proxy_cache_key; прогнать контрактный тест RLS; рассмотреть кэш на уровне CH (query-cache) только для публичных витрин.
Риски и митигации
|
Риск |
Как проявляется |
Митигация |
|---|---|---|
|
SQL-инъекция |
Странные ошибки/таймауты при неожиданных параметрах |
Только {param:Type}, запрет string-concat, whitelisting параметров на шлюзе |
|
FULL SCAN |
read_bytes ~ размеру таблицы, p95 взлетает |
Обязательные from/to, лимит окна, фильтр по партиции в SQL, линтер «enforce_where_time» |
|
Утечка PII |
В ответе «лишние» поля/данные |
BI/клиенты → только vw_*, маскирование, RLS, кэш-ключ включает auth |
|
Cache stampede |
Одновременные MISS на горячих тайлах |
TTL 30–300 s, прогрев, rate-limit, «stale on error» |
|
Кросс-арендный кэш |
A видит данные B |
$auth_hash в ключе кэша; раздельные upstream для публичных/приватных |
|
Медленные «дорогие» запросы |
p95/пики CPU |
Перевести в rollup-слой, запретить DISTINCT/quantile «на лету», LIMIT BY |
|
Нестабильная схема ответа |
Клиенты падают после релиза |
Версии /v1→/v2, семантика в VIEW, contract-тесты и changelog |
|
Длинные ответы |
Таймауты/разрывы |
Ограничить result_rows/result_bytes, пагинация «по ключу», Parquet как экспорт |
Чек-лист публикации API
- Эндпоинт читает только db_sem.vw_*.
- Есть SQL-шаблон с параметрами {from:Date}/{to:Date} и ограничителем окна.
- Профиль/квоты api_limits применены к пользователю/ролям.
- RLS/маскирование включены; кэш-ключ содержит auth-компонент.
- Nginx: proxy_cache, limit_req, access_log с request_id.
- Contract-тесты (golden) и интеграционные прошли.
- Наблюдаемость: графики p95, cache hit, read_bytes, top queries.
- Документация: спека параметров, пример запроса/ответа, версия и SLO.
- Процедура деплоя /v2 + план sunset /v1.
Артефакты модуля (что отдаём команде)
- Спека эндпоинтов (/api/v1/kpi, /api/v1/top_sku, /api/v1/kpi.parquet) с параметрами, SLO и ошибками.
- Шаблоны SQL (параметризованные) и семантические VIEW (vw_*).
- Конфиги Nginx/API-гейта: proxy_cache, limit_req, proxy_cache_key с auth, проброс X-Request-Id.
- Скрипты тестов контрактов (golden), примеры curl/jq.
- Устав запросов и линтер-правила (запрет FINAL, SELECT *, enforce time filter).
- Гайд по RLS/маскированию/профилям/квотам для CH.
- Дашборд наблюдаемости: latency/throughput, cache hit, read_bytes, top queries, ошибки.
Вывод
Data Product поверх ClickHouse — это три слоя дисциплины:
- Семантика и физика: vw_*, предагрегаты-состояния, запрет «дорогих» конструкций.
- Шлюз: параметризация, ограничитель окна, кэш 30–300 s, rate-limit, версия /v1.
- Безопасность и наблюдаемость: RLS/маскирование, профили/квоты, аудит и контракт-тесты.
С такой схемой вы отдаёте витрины как продукт — быстро, стабильно и без сюрпризов для клиентов и аудиторов.
Arenadata QuickMarts (ADQM) — корпоративная платформа на базе ClickHouse для быстрого слоя витрин и near-real-time аналитики. Решает задачи «быстрых» дашбордов и API с низкой латентностью и высокой конкуррентностью, работает поверх вашего DWH/лейкхауса как serving-уровень. Даёт предсказуемую производительность на терабайтно-петабайтных объёмах за счёт колоночного хранения, компрессии и предагрегатов (Materialized Views, AggregatingMergeTree), подключается к Kafka/S3 и стандартным BI-инструментам по SQL/HTTP. Для корпоративных ИТ ADQM предлагает поддержку и SLA, отказоустойчивые кластеры (HA/DR), безопасность (RBAC, LDAP/OIDC, шифрование трафика и данных), мониторинг и резервное копирование. Платформа хорошо ложится на методологию курса: семантика vw_*, роллап-слои, NRT-ингест, SLO/наблюдаемость и «гвардейки» для BI/API. Итог — быстрый запуск витрин за недели, снижённые риски в проде и предсказуемая стоимость владения.



