#system_design
Недавно проводил лекцию по DWH на курсе System Design от nevzorov.courses.
На лекции разбирали довольно частый практический кейс:
- есть ряд поддерживаемых источников данных (Sources);
- есть множество клиентов (Customers);
- для каждого клиента необходимо сохранять и обрабатывать данные из его источников (Customer Sources);
- вопрос: как лучше спроектировать Data Lake под эту задачу?
Вариант 1:
customers/<customer_name>/source=<source_name>Вариант 2:
sources/<source_name>/customer=<customer_name>Интуитивно рука тянется к 1 варианту... Однако для Data Lake и дальнейшей DWH-инфраструктуры часто лучше именно 2 вариант:
raw/sources/<source_name>/customer=<customer_name>/...
cleaned/sources/<source_name>/customer=<customer_name>/...
...
Например:
...
raw/sources/google_play/customer=rammstein/dt=2026-05-24/*.parquet
raw/sources/google_play/customer=sabaton/dt=2026-05-24/*.parquet
raw/sources/google_play/customer=megadeth/dt=2026-05-24/*.parquet
...
raw/sources/trustpilot/customer=rammstein/dt=2026-05-24/*.parquet
raw/sources/trustpilot/customer=led_zeppelin/dt=2026-05-24/*.parquet
raw/sources/trustpilot/customer=lordi/dt=2026-05-24/*.parquet
...
Почему?
1. Source естественным образом превращается в таблицу.
Для
AWS Athena, Apache Trino или Apache Spark - google-play, trustpilot и т.д. - это отдельные логические таблицы, разложенные по Parquet-файлам и партициям в виде Customer'ов:SELECT
*
FROM
"raw"."google_play"
WHERE
("customer" = 'rammstein') AND ("dt" >= DATE '2026-05-01');
У
google-play даже в сыром виде (и уж тем более в очищенном) есть какая-то своя схема данных, ключи, timestamp'ы, правила дедупликации, SLA, логика инкрементальной загрузки и т.д.У
trustpilot и любого другого Source'а - свои.Если же сделать наоборот:
...
raw/customers/rammstein/source=google_play/dt=2026-05-24/*.parquet
raw/customers/rammstein/source=trustpilot/dt=2026-05-24/*.parquet
...
raw/customers/sabaton/source=google_play/dt=2026-05-24/*.parquet
...
то один логический источник
google-play размазывается по разным корням. А дальше начинается адъ 👹:- отдельные таблицы на каждый Source каждого Customer'а;
-
UNION ALL запросы и VIEW-шки;- бардак с
Data Governance.В общем, Data Lake, а следом за ним и DWH медленно, но неотвратимо превращаются в DataSwamp 😄
2. Data Mesh проще делать именно по Source'ам.
Естественная единица владения - это не «папка клиента» (Customer), а доменный источник (Source).
У каждого такого Source'а есть отдельная команда-владелец, контракты, документация, SLA, data quality checks и правила эволюции схемы.
Команда, отвечающая за
google_play, должна владеть одной папкой sources/google_play/customer=<customer_name>/*, а не тысячами подпапок customers/*/source=google_play/*.- Добавили нового клиента? Добавили новую партицию.
- Поменяли контракт источника? Обновили один data product.
- Поймали баг в ingestion? Чиним одний единственный ETL-pipeline.
3. Pipeline'ы обычно тоже мыслят именно Source'ами:
...
google_play
trustpilot
...
А не:
...
ingest_rammstein_everything
ingest_sabaton_everything
ingest_led_zeppelin_everything
...
Иначе очень быстро появляются «особые клиенты»:
- у этого legacy CSV;
- у этого timezone в строке;
- у этого timestamp иногда
null;- у этого producer шлёт дубликаты;
- у этого «ну вы там руками поправьте, пожалуйста».
Поздравляю, у вас не DWH, а зоопарк с
Airflow DAG-ами 🦓4. Наконец, Source-First Layout упрощает сложную аналитику:
SELECT
"customer", count(*)
FROM
"raw"."google_play"
WHERE
"dt" = DATE '2026-05-24'
GROUP BY
"customer";
Можно с лёгкостью строить Usage-Based Billing по конкретным Source'ам, позволять даже менеджменту без труда копаться в данных и т.д.
Таким образом, проектируя DWH-систему лучше думать не о том, какие у вас будут клиенты, а о том, какие источники данных вы будете для них поддерживать.
С уважением,
Михаил Масягин
