Як ми будували data-платформу: workaround’и, KISS і операційна стійкість
Вітаю! Мене звати Андрій, я Data Engineering Lead у продуктовій компанії HOLYWATER з екосистеми Genesis. У попередній статті я розповів про фундамент data-платформи: архітектуру, неймінг, ELT, типи даних і роботу з legacy. Ця ж буде про речі, які зазвичай не потрапляють у high-level діаграми, але саме вони визначають, чи живе система роками, чи поступово розсипається.
Тут йтиметься про workaround’и, принцип простоти, data-продукти як окрему сутність, а також про операційну стійкість команди.

Planning і resource allocation
Зі зростанням платформи основним викликом стає не реалізація окремих технічних рішень, а керування потоком запитів. Джерела даних, вимоги бізнесу та пріоритети змінюються постійно, й без явної моделі алокації команда швидко переходить у режим реактивної роботи.

Ми зафіксували для себе приблизну структуру розподілу зусиль:
- ~20% — інтеграція нових джерел;
- ~30% — задачі маркетингової аналітики;
- ~20% — продуктова аналітика;
- ~10% — автоматизації;
- ~20% — технічний борг і розвиток платформи.
Всередині кожного блоку задачі ранжуються за пріоритетом, і в роботу береться стільки, скільки дозволяє поточний capacity команди. Мета — зберігати передбачуваність і не «проїдати» весь ресурс на термінових запитах.
Чергування та операційна стійкість

Щоб платформа не трималася на героїзмі окремих людей, ми ввели тижневі чергування. Черговий відповідає за:
- реакцію на алерти;
- первинну обробку запитів від стейкхолдерів;
- контроль аномалій у костах.
Важливий момент: реагувати ≠ вирішити все самостійно. Часто це означає:
- швидко оцінити критичність;
- виправити дрібну проблему;
- зафіксувати задачу в беклозі з контекстом і пріоритетом;
- залучити інших інженерів, якщо це інцидент.
Подібний підхід знімає постійне навантаження з ліда й дозволяє команді працювати більш фокусовано.
Bus factor і передача контексту

Раніше знання й ключовий контекст щодо кількох доменів були сконцентровані в однієї людини. Її відпустка або хвороба могли паралізувати розвиток усього домену (наприклад, маркетингової аналітики).
Ми поступово відійшли від жорсткого персонального ownership’у доменів і перейшли до моделі, де будь-хто з команди міг працювати з будь-яким напрямом. Відповідно, тепер контекст передається не через усні домовленості, а через:
- неймінг і структуру шарів;
- модульність dbt;
- code review;
- публічні канали запитів.
Що це дало:
- за відсутності людини зменшується capacity, але домен працює;
- можна швидко залучити кількох інженерів до складної проблеми;
- зменшується кількість «унікальних» рішень;
- code review стає інструментом уніфікації підходів;
- часто інженери прагнуть поєднувати роботу з операційними завданнями, як-от платіжні таблиці з можливістю розробки повноцінних сервісів — і ми можемо надати їм таку можливість.
Компроміси:
- запити мають проходити через публічний канал, а не в особисті;
- публічний запит бачать ліди напрямів, що вимагає виваженішої комунікації;
- передача знань між інженерами займає час.
Ми свідомо пішли на ці компроміси, адже довгострокова керованість платформи для нас важливіша за локальну швидкість.
Коли workaround — це інструмент, а не «милиця»

Якщо ви працювали з маркетинговими або продуктовими даними, то точно бачили в SQL щось подібне:
IF campaign_name LIKE 'AAA%' THEN 'ios' ELSE platform AS platform
Подібні конструкції зʼявляються не випадково. Зазвичай це наслідок:
- human error або помилки у джерелі;
- одиничного або обмеженого набору винятків;
- ситуації, яку вже виправили «на майбутнє», але історичні дані змінити неможливо.
Проблема не в самому workaround’i, а в тому, де і як він реалізований.
Коли workaround має сенс
Ми для себе сформулювали чіткі передумови, за яких workaround допустимий:
- існує канонічний спосіб визначення поля;
- помилка більше не повторюється;
- дані в джерелі неможливо або недоцільно виправляти ретроспективно.
Якщо всі три умови виконані — workaround має право на існування.
Як ми реалізуємо workaround’и
Замість того щоб розмазувати IF / ELSE по всім датамартам та ускладнювати бізнес-логіку регулярними виразами, ми виносимо винятки в окрему таблицю.
Приклад:
- campaign_id — ключ;
- platform — поле, яке потрібно виправити (може бути і декілька колонок).
І застосовуємо це так:
COALESCE(workaround.platform, marketing.platform) AS platform FROM marketing LEFT JOIN workaround USING (campaign_id)
Що це дає:
- всі винятки локалізовані в одному місці;
- основна логіка залишається читабельною;
- таблиця workaround може рости, але код — ні.
Збільшення таблиці workaround — керованіша складність, ніж розростання IF/ELSE і регулярних виразів у кожному датамарті.
KISS як щоденне інженерне рішення

Що більше логіки ми реалізовували, то частіше зіштовхувалися з тим, що складні концепції важко підтримувати. Тому більшість архітектурних рішень ми почали перевіряти одним питанням: чи зможе інший інженер зрозуміти це через пів року без мого пояснення?
Якщо відповідь «ні», ми зазвичай переглядаємо рішення.
Кілька прикладів із практики
- Підіймати сервер і керувати паралелізмом чи написати лінійну Cloud Function, яка скейлиться автоматично? Обрали друге.
- Робити складні перетворення чи два шари простих? Вибір на користь того, що простіше пояснити.
- Створювати dbt-моделі з параметрами, які розкатуються в різні проєкти чи один проєкт і колонка «назва застосунку»? Друге.
Колись dbt-моделі здавались логічним рішенням, адже можуть розкатуватися в різні проєкти залежно від вхідних параметрів. Але на практиці:
- частина логіки ставала зрозумілою лише в runtime;
- зміни в одному проєкті могли ламати інший;
- дебаг ставав непрогнозованим.
Значно простіше й масштабованіше рішення — зберігати дані всіх застосунків у спільній таблиці та позначати їх колонкою приналежності (app/source).
Важливий нюанс: для ідентифікації застосунку краще використовувати стабільний ідентифікатор, а не назву. Назви та канали закупівлі змінюються. Ідентифікатор залишається стабільним і їх краще прокласти через Link.
Data-продукти, незалежні від застосунків

У певний момент ми стикнулися з типовою системною проблемою: одна й та ж бізнес-логіка почала по-різному реалізовуватися в різних застосунках. Наприклад, маркетингова атрибуція. В одному продукті — одна модель, в іншому — інша, десь ще третя. Найпростіший шлях у такій ситуації — прив’язати аналітику до конкретного застосунку: attribution_app_a, attribution_app_b і так далі.
Але замість того, щоб прив’язувати дата-продукти до застосунків, ми пішли іншим шляхом і відв’язали логіку від продуктів. У нас з’явилися окремі дата-продукти:
- first_touch_attribution
- last_touch_reattribution
У кожному з них можуть одночасно бути дані з кількох наших застосунків. Один дата-продукт може використовуватися різними застосунками. Застосунки, своєю чергою, можуть паралельно використовувати два різних типи атрибуції, поки ми вирішуємо, який підхід працює краще.
Одразу виникає очевидне запитання: а як потім (за потреби) це розділяти (в окремі проєкти)? Відповідь виявилась простішою, ніж здається. Якщо застосунку потрібен якийсь дата-продукт, ми копіюємо цю модель даних (на рівні dbt-моделі). На практиці це значно простіше, ніж підтримувати десятки майже однакових пайплайнів.
Цікаво, що з моменту першого обговорення цього підходу я постійно чув аргумент: «У нас на кожному застосунку все по-іншому». Минуло три роки — і ми досі в основному об’єднуємо, а не розділяємо.
Плюси й мінуси підходу
Плюси:
- готовий дата-продукт легко підключається до нового застосунку;
- додаткові доопрацювання потрібні лише для справді специфічних кейсів, а таких небагато;
- суттєво зменшується ентропія в системі;
- інженери не губляться в контексті, бо логіка зосереджена в одному місці;
- фікс у продукті автоматично працює для всіх застосунків;
- з’являється дисципліна неймінгу й чітке розуміння, що саме робить дата-продукт;
- обрахунок метрики на рівні всієї компанії стає тривіальним без ручного «зшивання» даних.
Мінуси:
- з’являється одна точка відмови;
- зміни потребують узгодження між командами;
- інколи доводиться використовувати «не ідеальний, але спільний» підхід замість локально оптимального.
Утім, для нас ці мінуси виявилися прийнятною платою за передбачуваність, масштабованість і контроль складності.
Як ми поєднуємо несумісне

В аналітиці нам часто доводиться забезпечувати свіжість (near-realtime) і одночасно повноту історичних даних. Ці вимоги частково суперечать одна одній. Однак замість вибору «або-або» ми реалізували гібридний підхід, близький до Lambda-архітектури.
Скажімо, у нас є дані в .json (тут розглянемо операцію парсингу, але може бути й щось на кшталт агрегації). Тоді сценарій роботи виглядає так:
- закриті періоди обробляємо batch-пайплайном;
- поточний період (наприклад, дані за сьогодні) парсимо у realtime_view;
- обидва джерела об’єднуємо у фінальній view.
Ключова ідея — не дублювати логіку. Ми можемо не реалізовувати парсинг двічі, а звертатись до realtime_view із різними фільтрами залежно від періоду.
Це дає:
- стабільну історію;
- актуальні дані для оперативних рішень;
- простий інтерфейс для споживачів даних.
Тестування даних
Не хочу сказати, що тут в нас все ідеально. Є два практичні рівні: пошук аномалій та тестування змін. На першому ми:
- перевіряємо, чи існують дані за вчора;
- контролюємо звʼязки між таблицями;
- звіряємо деякі контрольні суми.
Щодо тестування змін — окремого тестового середовища в dbt ми поки що не зробили. Натомість:
- нові моделі або зміни запускаємо з _dev-суфіксами;
- перевіряємо результати;
- після цього прибираємо суфікс і викочуємо в продакшн.
Це рішення не універсальне, але працює за нашого темпу змін.
Observability, алерти й кост-контроль

Observability. Наші потреби на 90% закриваємо через чіткий неймінг шарів та lineage в dbt.
Alerting. Якщо падають Airflow-джоби (серед них завантаження й тести даних) або сервіси realtime-завантаження (Cloud Functions, що ловлять webhook), ми отримуємо алерти в Slack — і черговий на них реагує.
Cost Control. Ми моніторимо вартість використання ресурсів GCP, а також інших сервісів, що використовуємо для аналітики та інженерії даних. Окрім того, маємо метаметрику — частку витрат на аналітику в ревеню від користувачів.
Багато це чи мало — витрачати 0.1% на аналітичну інфраструктуру? Не знаю. Але якщо частка зменшується, наша оптимізація успішна.
Додатково:
- налаштовані квоти для захисту від випадкових дорогих запитів;
- регулярний перегляд аномалій.
Доступи й контроль змін:
- Сирі дані та PII закриті від аналітиків.
- Змінювати вручну можна тільки у власній пісочниці, все інше — через dbt і code review, під наглядом старших аналітиків та інженерів.
- Доступи видаються автоматизовано через Terraform. Це досить зручно автоматизує видачу ролей в різних проєктах, створення особистого датасету тощо.
Документація. Ми не намагаємось описати словами усе, що маємо. Натомість документуємо шари та їхню відповідальність, фіксуємо ключові схеми та покладаємось на неймінг і декомпозицію в коді.

Логіка така, що якщо я почну описувати роботу сервісу, імовірно я перекладатиму з мови Terraform на «людську» англійську. Якщо ж у коді використаний адекватний неймінг та декомпозиція, то текст буде менш зрозумілий за сам код сервісу.
Відповідно, якщо вибір стоїть між добре спроєктованим кодом і слабко деталізованою документацією або гарно описаним поганим дизайном, ми обираємо перше.
Висновки

Філософія нашого підходу зводиться до трьох речей:
- інженерна дисципліна;
- мінімізація ентропії;
- баланс між короткостроковою вигодою та довгостроковою підтримкою.
Не бійтеся інвестувати час у «нудні» речі: суворий неймінг, модульність, єдине джерело правди. Пам’ятайте, що інженерна дисципліна — це не тягар, а ваш захист від хаосу й неминуче принесе зростання. Кожен архітектурний компроміс сьогодні має бути свідомим рішенням із чітким планом, як і коли ви його «погасите» завтра.
Будуйте надійний фундамент, обирайте простоту замість складності (KISS). Так ви зможете перетворити Data Engineering із постійної боротьби за виживання на стратегічну перевагу, яка забезпечить масштабованість та успіх вашої компанії на роки вперед. Успіхів вам у цій боротьбі за мінімізацію ентропії!
P. S. Якщо вважаєте ownership інженерів над доменами невідворотнім — напишіть в коментарях!
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

2 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів