Привет, Хабр! Я Дмитрий Смотров, тружусь бэкендером в Astra Linux в команде продукта ACM — микросервисной системе, разворачиваемой на клиентских мощностях. Мы позволяем удаленно управлять клиентской инфраструктурой. Сначала я разрабатывал функциональность снятия инвентаризации и удаленного выполнения команд установки и удаления ПО, но в один момент моя жизнь резко изменилась. На проекте возникла необходимость в функциональности красивой и настраиваемой отчетности, в чем я увидел возможность проверить себя в новой для себя области. Я вызвался разобраться и помочь продукту стать еще лучше.

Получив одобрение собственной кандидатуры, мне ничего не оставалось, кроме как спуститься в подвал своего загородного поместья, надеть костюм с маской и со словами "Этому городу нужен новый герой..." идти разбираться.

Проблематика

Стоит отметить, что отчеты в продукте уже были и представляли они собой файлики в формате .csv, что доставляло некоторые неудобства:

  1. Форматировать .csv неудобно, сам формат для этого и не был предназначен.

  2. Клиенты никак не могли выбрать данные, которые попадали в отчет: формат и наполнение были определены с нашей стороны.

  3. Никакую красивую графику добавить никак нельзя.

А развивать функциональность все же хочется: на каждой презентации продукта потенциальные клиенты спрашивают про возможность работы с отчетностью.

Постановка задачи

Из общения с потенциальными клиентами аналитики сделали выводы и сформировали следующие требования. Необходимо, чтобы:

  1. Данные охватывали все бизнес-процессы (далее БП) продукта, а не только те, что уже отражаются в текущих .csv-файликах.

  2. Была историчность. Важно видеть, как изменялись объекты системы и связи между ними.

  3. Был конструктор визуального представления. При необходимости сделать собственные графики и таблицы с нужным наполнением. Также нужна возможность подключить BI-инструмент, которым клиент привык пользоваться. 

  4. Отчеты можно было выгружать в виде файлов.

Когда я знакомился с требованиями, команда для разработки только формировалась, но уже было понятно, что реализовывать конструктор своими силами (команда продукта насчитывала 40 человек, из которых 13 разработчики) будет непростой задачей. Тем более успеть нужно до следующего релиза, который будет через 4 месяца. Забегая вперед хочу сказать, что каждое из требований было удовлетворено, хоть и не на все 100%.

Также из требований было ясно, что пользоваться операционными базами данных сервисов напрямую идея плохая: сама система хранит состояния объектов только на текущий момент, мы сразу теряем второй пункт из требований. 

Продумывая варианты реализации и способы сохранить рассудок, я пришел к пониманию, что предстоит разработать платформу для аналитики. Поэтому присмотрелся к подходам дата-инженерии: ETL-процессам, озерам данных, корпоративным хранилищам данных, Ральфу Кимбаллу и Биллу Инмону. Боевого опыта в инженерии данных не было, но напористость и отвага перемалывают такие "бытовые" трудности.

Аналитика будет строиться на данных, которые нам предстоит вытаскивать из операционной системы и хранить в отдельном хранилище. Разберемся с тем, как это хранилище организовать.

Выбор архитектуры хранилища

Важно понимать, что конечное решение будет нагружено OLAP-запросами, в отличие от привычных бэкенду OLTP-запросов. Между собой OLAP и OLTP системы отличаются по ряду параметров:

  • Типу операции: в OLTP-системе мы выполняем короткие и быстрые операции (вставка, обновление, удаление), в OLAP - мы посылаем сложные запросы на выборку и агрегацию, а потом сидим, ждем, пока фиксик пробежится по архивам и вытащит интересную нам информацию.

  • Объему данных: в OLTP — небольшие объемы данных в текущей транзакции(обновили данные пользователя), OLAP — большие объемы исторических и агрегированных данных за некоторый промежуток времени (будьте добры, мне сумму всех сделок за пос��едние 10 лет по такому контрагенту, по такому-то типу сделок, и обязательно, чтобы сделку заключал вот этот менеджер и под контролем этого юриста, что-то они у меня тут левачат, хочу разобраться).

  • Структуре данных: OLTP — нормализованные таблицы для большей целостности( всеми любимые нормальные формы), OLAP — денормализованные (звезды и снежинки, как на детском утреннике) для увеличения скорости запросов.

  • Скорости отклика: если OLTP-система зависнет на 5 минут, выполняя транзакцию, то явно что-то пошло не так, в то время как 5-минутный аналитический запрос — это еще в целом быстро, даже кофе не попить.

Самые популярные концепции для организации хранилища данных — Data Warehouse и Data Lake. Они давно (с 2000-х и 2010-х соответственно) и активно применяются, и достаточно просты в реализации. Обе направлены на работу с OLAP-запросами, так как создавались для решения аналитических задач.

Использование Data Lake (далее DL) хранилища для наших целей было бы самым простым в плане технической реализации, но с продуктовой точки зрения получалось плохо: 

  1. Целевые пользователи озер — это ML-модели, аналитики данных и data-scientist'ы, в то время как наши пользователи больше соответствуют профилю бизнес-аналитика.

  2. Данные в DL сырые и не структурируются, либо структурируются слабо. При подключении собственного BI-инструмента клиенту придется сильно погружаться в то, что эти данные из себя представляют, в то время как он хочет просто подключить BI и быстро получить данные, нужные для бизнеса. Плюс мы не собираемся хранить графики, логи, изображения (явные примеры неструктурированных данных).

  3. Предоставляет большую гибкость в работе с данными за счет отсутствия предварительной обработки данных и возможности сгружать все, что душе угодно.

Data Warehouse (далее DWH) же, наоборот, реализовать сложнее, но подходит лучше:

  1. Целевые пользователи — бизнес-аналитики.

  2. Хранит строго структурированные данные, которые проходят предварительную очистку и трансформацию.

  3. Оптимизировано для быстрого выполнения сложных аналитических запросов и построения отчетности.

Поэтому я выбрал DWH как целевую картину, к которой мы придем в конечном итоге.

Да, на текущий момент появились и другие концепции, такие как Modern Data Warehouse (Data Lake и Data Warehouse используются вместе), Data Lakehouse (Data Lake �� программным слоем, делающим работу Data Lake похожей на реляционную БД) и Data Mesh (концепция для децентрализованного хранения очень больших объемов данных), появившиеся как решения проблем первых двух концепций. Но все, что связано с DL, нам не подходит, а Data Mesh предназначен для куда бОльшего объема данных, нежели у нас будет.


Для внутреннего устройства DWH есть несколько архитектур. Схематично покажу их на примере того, как будут преобразовываться данные компьютера при перемещении из операционной базы данных в хранилище.

Архитектура Кимбалла

Схема архитектуры Кимбалла для хранилища данных
Схема архитектуры Кимбалла для хранилища данных

Как видно на схеме, архитектура Кимбалла подразумевает загрузку данных из операционных баз данных и преобразование в размерный вид без промежуточного сохранения операционных данных в хранилище. 
Для пользователей доступна область представления — конечные таблицы размерных моделей хранилища, с ориентацией на подробное описание бизнес-процессов предприятия.

Архитектура Инмона

Схема архитектуры Инмона для хранилища данных
Схема архитектуры Инмона для хранилища данных

Инмон предлагает сохранять (обязательно в третьей нормальной форме) операционные данные предприятия в специальное хранилище(в так называемое хранилище данных предприятия или EDW), после чего отдельно, а не в составе основного процесса выгрузки данных, формировать витрины данных. Витрина данных — это область хранилища, данные которой представляют собой агрегацию интересующих департамент метрик. По большей степени витрины ориентируются на запросы отдельных департаментов предприятия, а не на общее описание бизнес-процессов.

Бизнес-клиентам открыт доступ к хранилищу данных предприятия, так как эти данные имеют высокий уровень детализации и появляются раньше, чем данные витрин. Стоит учесть, что данные в этом хранилище — это зачастую данные из OLTP-баз данных, которые по своей структуре плохо подходят для OLAP-запросов, что приведет к проблемам с производительностью.

Гибридная архитектура Кимбалла и Инмона

Схема гибридной архитектуры для хранилища данных
Схема гибридной архитектуры для хранилища данных

Совместили половинки обеих архитектур. EDW используется как источник для заполнения области представления в кимбалловском стиле, с которой уже работают бизнес-клиенты. Данные в области представления описывают бизнес-процессы организации и лучше подготовлены для OLAP запросов за счет размерной структуры. 

И что из этого нам подойдет?
Казалось бы, берем гибридную, она ведь должна совмещать в себе плюсы от кимбалловской и инмановской архитектур! И да, так и есть, но:

  • Реализовывать EDW в моем случае прям неприятно: придется дважды писать и отлаживать ETL, а бессонных ночей и без того хватает.

  • Главный плюс от этого самого EDW — возможность легко и просто перестраивать область представления тогда, когда нам это будет удобно, — не факт, что потребуется. Предпосылок к частому изменению бизнес-правил, из-за которых необходимо будет перестраивать витрины, на текущий момент нет, а вероятность их возникновения мала.

Архитектура Инмона, кроме того, предоставляет прямой доступ к хранилищу сырых данных предприятия, что лично я отношу к минусу, так как это по сути прямое нарушение инкапсуляции. Также нет необходимости в большом количестве разных витрин для разных отделов и разных показателей.

Остается архитектура Кимбалла, которую проще и быстрее можно реализовать и которая имеет единственную открытую область для пользователя. Ее минус в виде меньшей устойчивости к изменениям бизнес-процессам и не является толком минусом в моем случае.

Поэтому по соотношению плюсов и минусов я выбрал архитектуру Кимбалла.

Разработка размерной модели бизнес-процессов продукта через согласованные измерения  

Область представления в кимбалловской архитектуре хранилища представляет собой размерную модель бизнес-процессов. Перейдем к проектированию этой модели. 

Тут начинается один из самых трудных этапов. Из требований к постановке задачи нужно предусмотреть историчность данных и охватить бизнес-процессы продукта. 

Описать БП необходимо в терминологии измерений и фактов, где измерения — это описательный контекст, отвечающий на вопросы "кто, что, где, когда и как", по атрибутам которого происходит фильтрация и группировка фактов, а факты — это показатели результата произошедшего процесса. Например магазин продает товар, акт купли-продажи — это завершенный процесс, количество купленного товара и сумма покупки - это факты, товар — это измерение. Измерения и факты представляются в виде таблиц в базе данных.

В историчности данных нас интересует последовательность состояний сущностей системы (устройства, инвентаризации, каталоги и т.д.) во времени, а также последовательность состояний связей между сущностями. С измерениями на помощь к нам приходят понятия медленно меняющихся измерений (SCD — slowly changing dimensions), которые описывают работу с измерениями, меняющимися во времени. Всего есть 7 способов (типов), но я решил использовать второй тип: добавлять новую строку и помечать старую строку как потерявшую актуальность с добавлением дат вступления в силу и потери актуальности строки. А как быть со связями?

Связи сущностей можно представить как процесс, у которого есть стадия начала (связь установлена) и конечная стадия (связь разорвана). И вот тут на сцену выходит таблица фактов накопительного снимка. Допустим, в системе переместили устройство в каталог. Мы обнаружили это и занесли факт перемещения в каталог с пометкой текущей даты. После устройство переместили в другой каталог. Для предыдущего факта перемещения помечаем дату потери актуальности, а факт новой связи устройства с каталогом создаем.

Вроде бы удовлетворили требования историчности, но в процессе появились еще несколько трудностей. 

Первая заключалась в том, что связь факта и SCD строится по состоянию SCD на текущий момент, по последней активной записи. В процессе жизни SCD меняется, но факт будет ссылаться на ту старую запись, а у нас много сценариев, когда нужна самая актуальная запись измерения. Поэтому тут я схитрил и указал в качестве ключей общий и неизменяемый для всех записей измерения идентификатор. Совсем не "по учебнику", но львиную часть запросов нам это упростило, а негативных последствий не принесло.

Суть второй трудности была в том, что у меня возникала связь многие ко многим в измерениях факта. Если конкретнее, то в измерении с информацией о ЦПУ (ГПУ, ОЗУ и так почти по всем аппаратным комплектующим) я хочу явно указать, какие процессоры обнаружены на устройстве. Ассоциативная таблица как бы не под запретом, но при ее использовании структура модели из "звезды" превращается в тыкву "снежинку", что не есть хорошо. По итогу, я просто решил засунуть список идентификаторов нужных мне процессоров списком в JSON-столбец. Нет, это не  джуновское мышление, а частичка подхода Big One Table. До сих пор не до конца уверен, стоило ли так делать, ведь на BI-части добавились функции для работы с JSON и подзапрос, в то время как можно было ограничиться простой операцией JOIN. 

Во время решения третьей возникшей проблемы я казался себе как тот ослик из притчи о рациональном осле. Многие поля измерений в оригинальных записях операционных БД могут иметь значения null. Казалось бы, нет значения — нет проблем, но в измерениях null-значения не приветствуются: конечному пользователю (напомню, у нас это бизнес-аналитик) не хочется думать, что это за null такой. Можем в BI заменить все null-значения на нужное значение, но вроде бы мы должны именно в зоне представления, то есть в базе данных хранилища, указывать максимально понятные конечному пользователю данные. Окей, если поле строковое ставим — заглушку в виде константы, а если числовое? Делать числовое поле строковым? Еще же может быть такое, что по инвентаризации захотят что-нибудь посчитать (например, сколько всего используемых жестких дисков в каком-нибудь Ижевске), а у нас вместо числа жестких дисков указана строковая константа. С null-зна��ениями агрегатные функции SQL работать умеют, и ладно, что null вносит ошибку в подсчет, главное, что расчет вообще происходит. А вот со строками агрегация отработает не так, как с числами, если вообще отработает, и таким образом мы усложняем логику казалось бы простейшего запроса, так как придется использовать CASE-операторы с приведением типов на стороне BI, если мы захотим что-нибудь посчитать. Вопросов больше, чем ответов. В конце концов, я все же склонился в сторону замены числового типа поля на строковый. Припоминают ли мне это решение? Негативной связи не поступало, но я все равно начал чаще оглядываться на улице.

Альтернативы последним двум решениям с удовольствием почитаю в комментариях.

Я предполагал, что какое-то количество палок таки залетят мне в колеса, но времени на их решение затратил больше запланированного, из-за чего смоделировать и реализовать в коде все БП системы я не успевал, поэтому для начала смоделировали БП, относящиеся к уже существующей отчетности. Так что требование о полном моделировании БП выполнить не удалось. Приведу схему смоделированных бизнес-процессов, сократив описание измерений в целях соблюдения NDA.

Схема бизнес-процессов продукта через согласованные измерения
Схема бизнес-процессов продукта через согласованные измерения

Обратите внимание, что кроме таблицы фактов накопительных снимков, есть также таблицы фактов транзакций и фактов периодических снимков. Их использовали для моделирования БП снятия инвентаризаций с устройств.

Теперь предстоит выбрать технологии для реализации.

Выбор инструментов для реализации  

На данном этапе предстояло решить три вопроса:

  • В чем мы будем хранить данные?

  • Чем обеспечить доставку данных до хранилища?

  • Чем визуализировать данные?

Разрабатывать все самостоятельно — дело затратное и долгое, поэтому рассматривали open-source инструменты с открытой лицензией. Языки программирования исходных текстов рассматриваемых инструментов тоже играют роль, так как, используя инструмент, мы берем его на поддержку и закрываем найденные уязвимости по принципам разработки безопасного программного обеспечен��я (РБПО), которым в компании следуют. Команда бэкенд-разработки на ежедневной основе использует Python, что я и учитывал при выборе.

Пойдем по порядку.

Хранение

В планах — пользоваться методами Кимбалла для моделирования бизнес-процессов продукта, и итоговые модели хорошо ложатся на реляционный лад. Поэтому любимый PostgreSQL, который используется у нас в сервисах, который знают большое число администраторов, с которым не будет проблем по части РБПО (пакет postgres'a проверен за нас) подмигивает из-за угла своими большими глазами.

Ребята из BigData-тусовки ратовали за ClickHouse за его производительность на больших объемах и хорошее сжатие данных при хранении, но тут нет уверенности, что мы столкнемся с таким объемом данных, а расходы на поддержку СУБД падают на плечи прямо сейчас.

Как итог — остановились на PostgreSQL.

Доставка

"Моментальная" доставка посредством CDC (Debezium) не подходила по ряду пунктов:

  1. У нас нет требований в realtime аналитике.

  2. Выбранная архитектура хранилища не позволит красиво и удобно реплицировать состояния операционных БД, так как архитектура без EDW (см. предыдущий раздел). Пошли смотреть на инструменты планирования задач. Airflow — пожалуй, самый популярный из них. Сам себя именует "платформой оркестрации данных", и спорить тут сложно, так как функциональности и интеграций со сторонними инструментами у него реализовано прилично.  

В качестве альтернативы рассматривали Luigi. Более легковесный фреймворк, но без механизма запуска задач по расписанию. Для этого предлагается использовать cron или его аналог. Также у Luigi меньшая поддержка, такой вывод я сделал по статистике коммитов в репозитории (вот статистика Airflow для сравнения, срез за последний год). Эти два факта стали решающими для выбора Airflow, даже если не вся его мощь нам не нужна на текущий момент.

Визуализация

Как я уже говорил, свой конструктор реализовывать возможности нет, а требование есть. Так что я отправился на поиски BI-тулов.

На ум сразу пришли два кандидата: многим знакомая Apache Grafana и Apache SuperSet. Оба под открытой лицензией, оба позволяют создавать и настраивать дашборды (тот самый необходимый конструктор), а также выгружать настроенные дашборды в файлы.
Есть принципиальные различия в том, что Grafana больше ориентирована на мониторинги и написана на Go + Typescript (репозиторий на гитхабе), когда Superset ориентирован на бизнес-аналитику, что нам и нужно, а реализован на Python + Typescript (ссылка на репозиторий) плюс имеется экспертиза в организации отчетности именно в Superset'е. Могли ли звезды сложиться еще более удачно?  

Дело осталось за малым — реализовать все в коде.

Реализация  

Опишу интересные моменты.

Я втянул SQLAlchemy в ETL-процессы в качестве основного способа обращения к БД. Да, в культуре инженеров-данных принято пользоваться чистым SQL, но моя неформальная натура пошла против системы. На самом деле логика следующая: в случае моего отпуска или болезни кто угодно может подхватить за мной работу, так почему бы не облегчить этому счастливчику жизнь привычным для него инструментом? Для миграций я использовал alembic по тем же соображениям. В AF уже используется алхимия версии 1.4 (в остальных сервисах у нас версия 2.0), из-за чего пришлось малость переписать определение моделей. Индексы для postgresql-диалекта автоматически не определяются алембиком при генерации миграции, поэтому их приходится писать руками и следить за тем, чтобы случайно их не удалить при следующей миграции, так как alembic определяет эти индексы как враждебные и удаляет. В целом, эксперимент оказался удачным.

Вместо коннекторов Airflow к БД, которые нужно отдельно настраивать в админке AF, в DAG-скрипты прокидываются креды для подключения к БД. Да-да, опять какие-то бэкендерские штуки, но для этого было несколько причин:

  • Желание сделать установку максимально похожей на установку уже существующих сервисов.

  • При необходимости от Airflow можно отказаться и перейти на другую платформу оркестрации, в том числе и самописную (требования РБПО беспощадны). 

Часто повторяющиеся между устройствами данные (установленные пакеты ПО, установленные жесткие диски, мониторы, плашки оперативной памяти и т.д.) вынесли в отдельные справочные измерения. Частично из-за этого и была проблема с появлением нежелательных ассоциативных таблиц, но зато можно легко посмотреть техническое оснащение устройств компании в отдельных таблицах.

Все числовые коды, перечисления типа "computer", "laptop", "pc" и тому подобные технические значения, которые присутствуют в операционных БД, переводятся в человекочитаемый вид. Null-значения записываются как "Не определено"(та самая строковая константа, о которой говорилось выше). Напомню, что конечный клиент — не технический специалист, ему нужно выдать понятные для него данные.

Я сразу накинул индексы на те таблицы измерений и фактов, в которые пишется больше всего данных. Простые btree по дате, чтобы быстрее доставать самые свежие изменения, сделали запросы быстрее, а отклик BI — короче.

Сделанные нами дашборды, чарты и датасеты запаковали в архив и ставим пользователю с поправками на URL хранилища. Чтобы пользователь не мучался с ручным импортом дашбордов, мы автоматизировали этот процесс.

Результат


Сделали почти все, что запланировали, и то, что не планировали:

  1. Рассмотрели популярные концепции построения аналитических систем.

  2. Выбрали подходящую нашей ситуации архитектуру.

  3. Разобрались с размерным моделированием и разработали модель бизнес-процессов продукта. Отдельное внимание уделили работе с историчными данными.

  4. Подобрали инструментарий для реализации ETL-подсистемы и отображения красивых графиков. Подбор ос��ществлялся с учетом возможного взятия инструментария на поддержку для соблюдения требований РБПО.

  5. Собрали все воедино, дождались разряда молнии и оживили наше творение! Оно работает, и работает хорошо.

По цифрам имеем, что загрузка данных длится в среднем 10 минут на 100000 устройств в операционных БД, отображение графиков без фризов в пределах одной секунды. Уже есть клиенты, которые делают собственные дашборды под свои нужды и просят нас включить их в состав предустановленных. Не обошлось без казусов: кому-то все же удалось установить подсистему не так, как планировалось. Помогли исправить и предотвратили подобную возможность ошибки с нашей стороны. Клиенты довольны.

По итогу получилось создать мощную и гибкую систему отчетности. В планах — расширение охватываемых бизнес-процессов, улучшение ETL-системы, эксперименты с ClickHouse для поддержания еще бОльших объемов данных. 

А на этом все. Благодарю за внимание и повторно приглашаю обсудить в комментариях возникшие у вас вопросы. Отдельно хочу выразить благодарность команде за помощь и предоставленную возможность, а также коллегам с HL 2024 за то, что поделились своим опытом проектирования хранилищ.