Сквозной проект аналитической платформы: схема, загрузка, измерения, отказ, восстановление и инженерная защита
Соберите систему с нуля, измерьте её поведение и проведите учебное восстановление
Команда развивает B2B SaaS. Приложение отправляет просмотры, действия и покупки клиентов из разных часовых поясов. Дашборд должен отвечать за секунду на рабочих объёмах, данные хранятся год, а каждый арендатор видит только свои строки.
Вам нужно подготовить техническое решение и воспроизводимый протокол испытаний. Одного SQL-файла недостаточно: приложите DDL, запросы, планы, метрики, инструкции для дежурного и отчёт о восстановлении.
Один узел не позволяет честно испытать сетевое разделение или потерю кворума. Для этапов репликации используйте отдельный учебный кластер либо представьте проект конфигурации и явно пометьте непроверенные допущения.
Создайте каталог отчёта со следующими артефактами:
clickhouse-capstone/
├── README.md
├── ddl.sql
├── load.sql
├── queries.sql
├── security.sql
├── backup_restore.sql
├── evidence/
│ ├── baseline.tsv
│ ├── optimized.tsv
│ └── plans.txt
└── instructions/
├── slow-query.md
├── replica-lag.md
└── restore.mdВ README.md укажите:
SELECT version(), timezone();Исходная таблица намеренно неидеальна. Она нужна как контрольная точка:
CREATE DATABASE IF NOT EXISTS saas;
CREATE TABLE saas.events_raw
(
event_time DateTime64(3, 'UTC'),
tenant_id UInt32,
user_id UInt64,
event_id UUID,
event_type String,
country String,
plan String,
duration_ms UInt64,
revenue Float64,
properties String
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(event_time)
ORDER BY event_id;Сгенерируйте не менее 10 миллионов строк. Для короткой проверки уменьшайте число в одном месте:
INSERT INTO saas.events_raw
SELECT
toDateTime64('2026-01-01 00:00:00', 3, 'UTC')
+ toIntervalMillisecond(number * 250) AS event_time,
1 + number % 1000 AS tenant_id,
cityHash64(number) % 5000000 AS user_id,
generateUUIDv4() AS event_id,
['view', 'click', 'search', 'purchase'][1 + number % 4] AS event_type,
['RU', 'DE', 'US', 'BR', 'IN'][1 + number % 5] AS country,
['free', 'team', 'business'][1 + number % 3] AS plan,
1 + number % 30000 AS duration_ms,
if(number % 4 = 3, (number % 100000) / 100.0, 0.0) AS revenue,
concat('{"source":"', ['web', 'mobile'][1 + number % 2], '"}') AS properties
FROM numbers(10000000);Проверьте количество активных частей, размер на диске и сжатие:
SELECT
table,
count() AS active_parts,
sum(rows) AS rows,
formatReadableSize(sum(bytes_on_disk)) AS disk,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio
FROM system.parts
WHERE database = 'saas' AND active
GROUP BY table;Выберите три запроса:
Перед каждым запуском задайте осмысленный query_id. Сохраните план и фактические показатели:
EXPLAIN indexes = 1
SELECT event_type, count()
FROM saas.events_raw
WHERE tenant_id = 42
AND event_time >= '2026-01-10 10:00:00'
AND event_time < '2026-01-10 11:00:00'
GROUP BY event_type;
SYSTEM FLUSH LOGS;
SELECT
query_id,
query_duration_ms,
read_rows,
read_bytes,
memory_usage,
result_rows
FROM system.query_log
WHERE type = 'QueryFinish'
AND query_id LIKE 'capstone-baseline-%'
ORDER BY event_time;Не сравнивайте только wall-clock time. На локальной машине он меняется из-за кэша и соседних процессов. Главные показатели этого этапа — прочитанные строки и байты, память и устойчивость результата на нескольких запусках.
Создайте saas.events. Обоснуйте каждое отличие от исходной таблицы:
ORDER BY должен следовать реальным фильтрам;LowCardinality только там, где это подтверждено кардинальностью;Nullable;JSON, если набор путей действительно динамический.Пример — отправная точка, а не эталон для любого проекта:
CREATE TABLE saas.events
(
event_time DateTime64(3, 'UTC'),
tenant_id UInt32,
user_id UInt64,
event_id UUID,
event_type LowCardinality(String),
country LowCardinality(FixedString(2)),
plan Enum8('free' = 1, 'team' = 2, 'business' = 3),
duration_ms UInt32,
revenue Decimal64(2),
properties JSON
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (tenant_id, event_time, event_type, user_id)
TTL toDateTime(event_time) + INTERVAL 1 YEAR DELETE;Перенесите данные через INSERT INTO ... SELECT. Повторите измерения без изменения самих аналитических запросов. Отчёт должен отвечать на два вопроса: сколько данных удалось не прочитать и какой ценой это достигнуто.
Опишите контракт доставки:
Для синхронной загрузки сравните пакеты 1, 1 000 и 10 000 строк. В ClickHouse 26.3 асинхронные вставки включены по умолчанию, но это не отменяет проверку подтверждения записи и наблюдаемость:
SELECT
event_time,
query,
status,
flush_time,
rows,
bytes,
exception
FROM system.asynchronous_insert_log
ORDER BY event_time DESC
LIMIT 20;Покажите, как клиент повторит запрос после таймаута и не создаст логический дубль события. ReplacingMergeTree сам по себе не гарантирует обработку «ровно один раз»: версии сходятся во время слияния, а обычный запрос может временно увидеть их все.
Выберите ровно один механизм:
До изменения сформулируйте гипотезу и критерий успеха. После изменения измерьте также цену: размер на диске, время вставки, число частей и сложность загрузки истории. Один удачный запуск ещё не доказывает, что оптимизация сработала.
Создайте роли ingest, analyst и ops. Минимальные требования:
ingest пишет только в исходные таблицы;analyst читает витрины без доступа к административным таблицам;ops может диагностировать и прерывать запросы;tenant_id;system.query_log.Проверяйте запреты негативными тестами: недостаточно показать, что разрешённый запрос работает.
Для кластера опишите число шардов и реплик, ключ шардирования и размещение Keeper. Затем проведите один безопасный эксперимент:
До эксперимента запишите ожидаемые симптомы, сигнал оповещения и действие дежурного. После — сравните ожидание с фактами в system.replicas, system.replication_queue, system.errors, system.query_log и серверном журнале.
Не имитируйте потерю данных на окружении, которое нельзя пересоздать.
Создайте отдельную резервную базу и выполните встроенный BACKUP. Конкретный диск резервных копий должен быть настроен администратором заранее:
BACKUP DATABASE saas
TO Disk('backups', 'saas-full.zip')
SETTINGS id = 'capstone-full';
SELECT id, status, error
FROM system.backups
WHERE id = 'capstone-full';Удалять исходную базу для проверки не нужно. Восстановите копию под другим именем:
RESTORE DATABASE saas AS saas_restore
FROM Disk('backups', 'saas-full.zip')
SETTINGS id = 'capstone-restore';Сравните число строк, контрольные агрегаты и права доступа. Зафиксируйте фактические:
Репликация не заменяет резервную копию: ошибочный DROP, неверная мутация или повреждённые данные могут распространиться на все реплики.
Подготовьте инженерную записку на 2–4 страницы:
| Критерий | 0 баллов | 1 балл | 2 балла |
|---|---|---|---|
| Воспроизводимость | Шаги неполны | Запуск требует ручных догадок | Чистое окружение собирается по инструкции |
| Схема | Решения не объяснены | Есть общие аргументы | Решения связаны с запросами и объёмом |
| Измерения | Только субъективное «быстрее» | Есть время одного запуска | Есть план, I/O, память и повторные прогоны |
| Загрузка | Нет модели повторов | Пакеты описаны | Есть идемпотентность, регулирование потока и наблюдаемость |
| Безопасность | Один администратор | Роли созданы | Есть минимальные права и негативные тесты |
| Отказоустойчивость | Сбои не рассмотрены | Есть схема кластера | Проведён эксперимент и обновлена инструкция дежурного |
| Восстановление | Бэкап не восстановлен | RESTORE выполнен | Проверены данные, RPO и RTO |
| Инженерная защита | Решения перечислены | Компромиссы частичны | Ограничения и риски сформулированы честно |
Зачёт — не менее 12 из 16 баллов, при этом по восстановлению и измерениям нельзя получить ноль.
Сильный проект не обязан показывать минимальную задержку. Он должен показывать управляемую систему: измерения воспроизводятся, настройки имеют причины, сбои обнаруживаются, восстановление проверено, а границы решения названы прямо.