Як ми автоматизували тестування даних в DWH
Привіт! Мене звати Ангеліна, я Data Engineer з Uklon. Вже понад 7 років я працюю з даними, за цей час здобула значний досвід у розробці, впровадженні та автоматизації дата-інфраструктур. Оскільки бізнесу потрібно забезпечити високу якість та ефективність даних, питання автоматизації тестування займає велику частину моєї роботи.
В Uklon ми давно займаємось автоматизацією роботи з даними та тестуємо різні шляхи й інструменти. У цій статті поділюся кейсом впровадження підходу Slim CI, а також розкажу, як здешевити, спростити та автоматизувати процес тестування моделей даних та залежних від них моделей.
Трохи контексту
Тестування безумовно є важливою частиною створення програмних продуктів. І якщо про підходи до тестування програмного коду вже сказано доволі багато, то підходи до тестування моделей даних і безпосередньо самих даних, зокрема тестування наповнення DWH, висвітлюються не часто.
Якщо коротко, то DWH складається з моделей даних (таблички або view). Більшість моделей пов’язані між собою і часто залежні одна від одної. На основі цих залежностей будується Lineage Graph. За допомогою Lineage Graph можна побачити, що зміна в одній моделі даних може викликати зміни у сотнях залежних від неї моделей.
Загалом задача кожної з моделей — це або підготовлювати дані для наступної моделі, або містити дані, готові для аналітичного споживання. Логічно, що для коректної аналітики потрібні коректні дані. Їх коректність найчастіше тестується такими перевірками: наявність за певний проміжок часу, перевірка на допустимі значення, перевірка на дублікати тощо.
На практиці виходить так, що якщо вносиш зміну в одну модель даних, то тестувати треба і цю модель, і всі залежні від неї (а таких може бути й сотні). Тестувати їх бажано на даних, еквівалентних продакшн-даним. Це звучить дорого і довго, але на практиці процес можна здешевити, спростити й автоматизувати. В Uklon ми зробили це за допомогою імплементації Slim CI на базі технологій DBT, Snowflake і Gitlab.
Короткий екскурс залученими технологіями
В цьому розділі я не буду детально описувати всі функції, переваги або недоліки DBT, Snowflake і Gitlab, а сфокусуюсь на тому, як вони використовуються в Uklon як частина Business intelligence. Тож дуже коротко про залучені технології:
Snowflake — Cloud Data Platform і навіть більше. Хмарна платформа, що дозволяє ефективно виконувати велику кількість аналітичних запитів, гнучко налаштовувати доступи до об’єктів DWH і до самих даних, моніторити використання ресурсів тощо.
DBT (Data Build Tool) — це open-source фреймворк для трансформації даних. Загалом за допомогою DBT можна:
- розробляти DWH (трансформація даних);
- тестувати й документувати дані;
- використовувати систему контролю версій.
В Uklon DBT використовується для розробки DWH.
Тобто у DBT ми пишемо код, який виконується на Snowflake.
Gitlab — система контролю версій, що також надає інструмент CI/CD.
Slim CI — це
Як побудований процес внесення змін в DWH
Загалом процес змін в дата-моделі складається з наступних етапів:
- Внесення змін у відповідну дата-модель і локальне тестування.
- Створення Merge Request, під час якого відбувається не лише рев’ю кода інженером або аналітиком, а і
CI-джобою, яка перевіряє форматування, документацію тощо. - Автоматичний запуск
CI-джоби, яка тестує моделі даних (про цю джобу згодом поговоримо детальніше). - Після успішного проходження рев’ю — мердж і застосування змін на продакшн-середовищі.
Схематично це виглядає так:
Імплементація Slim CI
Ми імплементували Slim CI за допомогою СI-джоби на Gitlab. Задачі цієї джоби:
- Створення окремої бази даних для тестування.
- Клонування продакшн-даних в цю нову базу даних.
- Запуск* модифікованих дата-моделей разом із їх дочірніми моделями.
*Під запуском моделей даних я маю на увазі виконання команди dbt-build. Ця команда компілює sql код моделей (DDL і DML), виконує його і запускає тести, пов’язані з цими моделями даних.
Тепер детальніше про кожен пункт.
Створення окремої бази даних для тестування
Команду створення окремої бази даних CREATE DATABASE <DATABASE_NAME> ми огорнули у dbt macro:
{# -- This macro creates a database. #} {% macro create_database(database_name, retention=1) %} {% if database_name %} {{ log("Creating database " ~ database_name ~ "...", info=True) }} {% call statement('create_database', fetch_result=True, auto_begin=False) -%} CREATE DATABASE {{ database_name }} DATA_RETENTION_TIME_IN_DAYS = {{retention}} {%- endcall %} {%- set result = load_result('create_database') -%} {{ log(result['data'][0][0], info=True)}} {% else %} {{ exceptions.raise_compiler_error("Invalid arguments. Missing database name") }} {% endif %} {% endmacro %}
Вхідні параметри:
- database_name — ім’я новоствореної бази даних.
- retention — значення параметра DATA_RETENTION_TIME_IN_DAYS, що визначає кількість днів зберігання даних.
Тут важливо зрозуміти, що Snowflake може зберігати базу даних навіть після її явного видалення. Така поведінка зумовлена механізмом Time Travel.
За замовчуванням DATA_RETENTION_TIME_IN_DAYS дорівнює одному дню. Тому коли викликаємо macro create_database, ми явно передаємо значення 0, оскільки не хочемо платити за зберігання даних з бази, яка існує тільки протягом відпрацювання
Клонування продакшн-даних в нову базу даних
Після створення бази даних ми маємо клонувати туди продакшн-дані. Зрозуміло, що фактичне клонування десятків терабайт даних — це довге і дороге задоволення, але його можна пришвидшити й здешевити за допомогою zero copy-cloning. Суть цього механізму в тому, що безпосередньо дані фізично не копіюються.
В новій базі даних ми отримуємо посилання на вже існуючі дані й платимо лише за ті дані, які змінили чи додали вже після клонування. Тобто лише за різницю між даними на оригінальній базі даних і на клонованій.
Загалом є два способи клонування.
- Командою Snowflake:
CREATE DATABASE <object_name> CLONE <source_object_name>
В такому випадку очевидно не треба використовувати macro create_database.
- За допомогою dbt:
Спочатку ми спробували перший варіант, але він виявився занадто довгим і займав близько семи хвилин. Тож для пришвидшення клонування ми спробували використати команду dbt clone і це дало результат — процес став в середньому тривати хвилину.
Таке пришвидшення в основному можливе завдяки тому, що dbt clone можна запускати у декілька потоків. Також можна обмежити кількість об’єктів, що копіюємо. В нашому випадку це лише пов’язані зі зміненим об’єкти. Ось такий вигляд має dbt clone, що ми застосовуємо:
dbt clone —select @state:modified —state=target-base —threads=36
Запуск модифікованих дата-моделей з дочірніми моделями
Запуск модифікованих дата-моделей разом із їх дочірніми моделями виконується командою dbt build. Ми застосовуємо dbt build в такому вигляді:
dbt build —select state:modified+ —state=target-base —threads=36 ${BUILD_MODE}
Нарешті повний код
dbt-test: image: ${DBT_IMAGE} stage: test when: manual interruptible: true variables: DBT_PROFILES_DIR: ${DBT_PROJECT_DIR}/profile RUN_ELEMENTARY: "false" CI_DATABASE: db_${CI_COMMIT_SHORT_SHA} CI_WAREHOUSE: CI_WH script: - git branch - echo $CI_DATABASE - git fetch - cd ${DBT_PROJECT_DIR} - pip install -q -r requirements.txt - dbt deps # set evn variables - SNOWFLAKE_DBT_DATABASE=ANALYTICS - SNOWFLAKE_DBT_WAREHOUSE=${CI_WAREHOUSE} - echo $SNOWFLAKE_DBT_WAREHOUSE # generate mainfest.json for main branch - git checkout origin/main - dbt compile --target-path=target-base - dbt docs generate --target-path=target-base # create new db - | dbt run-operation create_database --args "{'database_name': $CI_DATABASE, 'retention': 0}" # generate manifest.json for feature branch - git checkout $CI_COMMIT_REF_NAME - dbt compile # clone db - SNOWFLAKE_DBT_DATABASE=$CI_DATABASE - dbt clone --select @state:modified --state=target-base --threads=36 # compare manifests - dbt ls --select state:modified+ --state=target-base - echo ${BUILD_MODE} - dbt build --select state:modified+ --state=target-base --threads=36 ${BUILD_MODE} - dbt docs generate --models state:modified+ --state=target-base - recce run -o recce.json artifacts: paths: - snowflake-dbt/recce.json expire_in: 1 hour after_script: - | dbt run-operation drop_database --args "{'database_name': $CI_DATABASE}"
Найпоширеніші кейси виявлення помилки
Припустимо ми видалили колонку ’CALL_STARTED_AT’ в моделі our_model, оскільки вважали її зайвою. Оскільки моделей і залежностей між ними багато, а локально ми тестували неуважно. Видалення поля може потенційно призвести до помилок у дочірніх моделях. Це і сталося, але CI джоб виявив цю помилку до мерджа змін і впав з помилкою:
Completed with 1 error and 0 warnings: Database Error in model another_our_model (models/marts/another_our_model.sql) 000904 (42000): SQL compilation error: error line 19 at position 58 invalid identifier 'CALL_STARTED_AT'
Окей, цей кейс можна покрити уважним локальним тестуванням, але автоматизація зручніша, надійніша і швидша.
Розглянемо наступний варіант. Ми вирішили, що ця умова в нашій моделі our_model зайва:
qualify ROW_NUMBER() over (partition by call_id order by __created_ts_millis desc) = 1
Відповідно ми прибрали її та прогнали змінену модель і її дочірні моделі локально. Тестовий датасет був неповний, тому локальне тестування не виявило проблем. Але наш
Completed with 1 error and 0 warnings: Database Error in model another_our_model (models/marts/another_our_model.sql) 100090 (42P18): Duplicate row detected during DML action Row Values: [redacted]
Бонус
Для красивої візуалізації потенційних змін ми вирішили використати recce. Recce — це інструмент перевірки зміни даних для проєктів dbt.
Висновки
Якісне тестування допомагає виявити проблеми вчасно і запобігти не лише помилкам «компіляції» моделей даних, а і проблемам неузгодженості даних.
Використання підходу Slim CI й механізму zero copy-cloning дозволяє тестувати зміни на продакшн-даних без надлишкового використання ресурсів.
6 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів