Як ми будували data-платформу: архітектура, неймінг, ELT, модульність і робота з legacy
Привіт усім! Мене звати Андрій, я — Data Engineering Lead у продуктовій компанії HOLYWATER з екосистеми Genesis. Уже понад 8 років я працюю в ІТ, останні чотири будував data-платформу в HOLYWATER. За цей час ми кілька разів змінювали підходи, ламали власні рішення й поверталися до простіших.
Один з висновків, до якого ми дійшли: якщо архітектуру та неймінг зробити правильно, потреба в «повноцінній» документації різко зменшується. Але далі починаються питання стійкості пайплайнів, типів даних, backfill’ів, «джерела правди», модульності й того, як не перенести legacy-складність у нову систему.

З чого все почалося
Перше, з чим я зіштовхнувся — потреба каталогізації даних. На старті усі дані зберігались у DWH, джерел було багато. Це було для нас плюсом, адже зазвичай дані розкидані різними базами даних. З мінусів — регулярно виникали питання на кшталт «Звідки беруться дані?», «Як зберігаються?», «Де лежать?» та «Для чого використовуються?».
Ми спробували описати ці дані й досить швидко зрозуміли: описаний хаос не перестає бути хаосом. Документація застаріває, каталоги потребують постійної підтримки, а відповіді на базові питання все одно залишаються неочевидними.
Тоді ми вирішили сфокусуватися на іншому й зробити так, щоб сама структура даних відповідала на ці питання без додаткових пояснень.
Неймінг як частина архітектури

Ключове рішення — винести сенс у назву. Назва таблиці або view* має одразу відповідати на три питання:
- звідки дані;
- на якому етапі трансформації;
- для чого вони призначені.
Так з’явилась layered data architecture з self-documenting неймінгом.
*У нашому випадку під назвою таблиці йдеться також про назву датасету (схеми).
Шари даних:

import_*
Сирі дані з зовнішніх джерел:
- import_stripe
- import_facebook_ads
Жодних трансформацій — лише збереження «як є» (raw файл або навіть JSON-рядком).
Якщо дані віддаємо назовні для якогось сервісу — використовуємо export_*.
stage_*
Проміжний шар, де відбувається:
- дедублікація;
- перейменування колонок;
- маскування;
- об’єднання різних endpoint’ів одного джерела.
Приклад: stage_stripe.
core_*
На цьому рівні це вже не просто «дані зі Stripe чи з Facebook», а показники про бізнес, тобто це revenue або marketing_spend. Для дебагу буде не зайвим додати колонку із назвою таблиці джерела.
dm_* (data marts)
Шар для репортингу.
Назва завжди явно містить гранулярність, наприклад dm_user_date_revenue (доходи розбиті на користувача та дату).
Гранулярність — ключова характеристика датамарту після самих метрик.
sandbox_*

Єдине місце, де дозволено руками створювати й видаляти таблиці та view — це персональні sandbox аналітиків.
service_* — технічні таблиці, логи, службові дані.
dictionary_* — довідники (країни, валюти тощо).
Цей підхід спростив навігацію, зменшив кількість питань «куди класти/де шукати» і дав структурну основу для подальшої модульності, зводячи документацію до опису неймінгу (шарів).
Правила неймінгу, які ми для себе зафіксували

Це дрібниці, але саме вони зменшують когнітивне навантаження в щоденних задачах.
- Не використовуємо абревіатури, якщо це не загальноприйняті поняття. Не економимо літери коштом чужих мізків.
- Не використовуємо множину: user, а не users.
- Не використовуємо all:
revenue_ad — це дохід з реклами,
revenue — це увесь дохід. - Не додаємо слова, що не несуть сенсу: table/data/value та інші.
- Назва описує отриману сутність, а не спосіб її отримання: success_transaction, а не transactions_filtered. Це дає змогу абстрагуватися від upstream-логіки й значно спрощує рефакторинг.
Далі постало не менш критичне питання: як зробити пайплайни стійкими до змін, збоїв і зростання. Тут і з’являються ELT, типи даних, idempotency та рішення, які дали нам змогу перестати боятися backfill’ів.
Чому ми перейшли на ELT
Одна з проблем — падіння конвеєрів даних.
Серед причин:
- у джерелі з’явилась нова колонка*;
- змінився тип поля;
- API повернуло неочікувану структуру.
*Важливо не використовувати всюди «*», аби перелік колонок був детермінований, а поява нової колонки не руйнувала пайплайни.
Якщо пайплайн падає на етапі трансформації — дані втрачаються. А втрачати дані через те, що не вдалося їх розпарсити через нову колонку чи тип, доволі неприємно. Аби цього уникнути й не робити ручний backfill у випадку падіння, ми перейшли на ELT.
- Load — зберігаємо дані «як є» (raw файл або JSON рядком).
- Transform — парсимо, нормалізуємо й агрегуємо вже всередині DWH.
Таким чином, навіть якщо схема даних змінилася — load відпрацював коректно, а transform-крок можна змінити під нову схему й запустити із потрібного нам місця, не будуючи два окремі пайплайни для штатного та аварійного завантаження даних.

Трохи особливостей використання різних типів даних
Типи даних — ще одне джерело прихованих проблем.
- string — для ідентифікаторів. Часто вони прилітають з різних джерел в одну колонку та можуть бути схожими на integer. Але з іншого джерела це може бути uuid чи base64. Автовизначення типів — зло. Адже ідентифікатор виду «36478890184» у BigQuery легко приводиться до дати «Monday, December 21, 3125 9:03:04 AM».
- integer — конкретна кількість/лічильники.
- float — обʼєм і суми з дробами.
- datetime — для дат, завжди в UTC, щоб не думати про часові зони.

Діапазони замість «за вчора»
Пайплайн, що обробляє батчі, має залежати від діапазону дат у вхідних параметрах, а не обробляти дані «за вчора». Це:
- спрощує backfill даних при падінні, зміні схеми;
- дозволяє більш гнучко працювати із великими періодами часу, щоб пробігтися по всьому попередньому року, простіше передати діапазон, аніж 365.2425 раз викликати джобу;
- дає гнучкість при переході на частіші батчі та дозволяє розбити день на менші частини — до прикладу, по 8 годин.
Discrepancy, SSOT і модульність

На певному етапі до нас почали регулярно приходити з питанням: «Чому одна й та ж метрика в різних таблицях має різні значення?»
Типові поради:
- навісити більше тестів;
- додати семантичний шар.
Ми почали з іншого — дослідження причини розбіжностей. Результат — виявили, що проблема майже завжди виникала через дублювання логіки розрахунків у різних місцях.
Розрахунки повторюються, але з певними відмінностями, що викликані хаотичністю розробки через відсутність пріоритетів, поспіхом, плаваючими відповідальностями... Та ми тут про технічні виклики. Отож, ми пішли шляхом модульності:
- метрика рахується в єдиному місці;
- використовується в кількох датамартах;
- зміни вносяться централізовано.
Розрахунки декомпозуються згідно з naming convention і відповідальністю шарів.
Результат: зменшення discrepancy, швидші правки та вища довіра до даних.
Швидкість vs якість: як ми домовились із бізнесом

Запит «зробіть якось, аби швидше» — неминучий. І, як ми знаємо, nothing`s more permanent than a temporary solution. Ще на початку своєї кар’єри я зауважив: менеджери часто просять реалізувати завдання швидко і водночас оцінюють його виконання за якістю. Сліпе слідування вимогам дуже швидко призводить до скриптів-милиць, крихкості системи, відсутності довіри до даних, неможливості підтримки й розвитку.
Ми спробували розглянути бізнес-замовників як наших партнерів, а не ворогів інженерії, та почали шукати спосіб працювати і швидко, і якісно. Рідко це можна зробити одночасно, тому для себе зафіксували процесний контракт:
- MVP (aka «на милицях») можна робити швидко за умови, що ми знаємо правильний спосіб (який довший);
- для MVP є дата/спринт, коли рішення допрацьовується;
- обмеження MVP чітко проговорені з бізнесом, щоб на його базі не ухвалювали ризиковані рішення.
Наприклад, для обробки S2S-подій ми швидко реалізували Cloud Function, але одразу зафіксували, що це тимчасове рішення до рефактору основного сервісу.
Legacy: як стикувати старе і нове

Тут все просто: створили нове, у 10 разів краще — переїхали, старе вимкнули — й насолоджуйтеся.
Звісно, я жартую. Деякі переїзди займають роки й часто постає питання: «Як стикувати частини нової інфраструктури зі старою?». В цьому випадку ми намагаємось адаптувати legacy під нове, бо legacy ми зрештою вимкнемо, а перенос складності в нову «чисту» інфраструктуру — це створення нового legacy.
Принцип: складність має залишатися в legacy, а не мігрувати в нову систему.
Для нас «нове» майже завжди означає «правильне». Тому базове правило звучить так: нові компоненти не повинні підлаштовуватися під старі обмеження, якщо ми плануємо від них відмовлятися. Якщо ж адаптувати нову архітектуру під legacy, ми фактично створюємо новий legacy, тільки з кращою документацією.
Практичний кейс: уніфікація аналітики кількох продуктів

Компанія розвиває багато застосунків. Колись їхня аналітика розвивалася паралельно в різних проєктах зі своїми особливостями. У новій архітектурі ми пішли шляхом уніфікації й створили спільне джерело для всіх. Умовно: є спільна таблиця маркетингових витрат із колонкою «назва застосунку». Далі частина legacy-розрахунків почала використовувати нове спільне джерело, але фільтрувати дані тільки для «свого» застосунку.
Таким чином:
- нова система розвивається незалежно;
- старі частини поступово «підʼїжджають» до неї;
- ми не дублюємо логіку у двох місцях.
Звісно, виключення можуть бути й без compatibility layer не обійтися. У таких випадках ми, до прикладу:
- додаємо окремі поля для зворотної сумісності з legacy (legacy_app_name);
- не використовуємо ці поля в нових розрахунках.
Ключова ідея — можливість легко відʼєднати legacy, коли настане час. Якщо для цього достатньо видалити одну колонку або джойн — значить рішення було правильним.
Підсумок

У цій частині ми:
- заклали фундамент data-платформи через layered architecture і self-documenting неймінг;
- перейшли на ELT, щоб зробити пайплайни стійкими до змін і backfill’ів;
- зменшили discrepancy між метриками завдяки модульності й SSOT;
- виробили підхід до компромісів між швидкістю, якістю та legacy.
У наступній частині я розповім про:
- те, чим workaround відрізняється від «милиць»;
- «не ускладнювати» як головний принцип розробки;
- data-продукти, незалежні від застосунків;
- observability, алерти й контроль костів.
Якщо знаєте краще, як з’їжджати із legacy, поділіться інструкцією в коментарях :)
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.
17 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів