В одной из заметок нашего блога мы освещали подход к хранению и обработке данных, о котором получили несколько вполне ожидаемых вопросов такого плана: «В общем, ждём реализации, вот уж оно залетает...». По результатам нескольких реализаций я расскажу о плюсах и минусах этого подхода на примере одной из наших разработок. Своим заказчикам мы обычно предлагаем достаточно мощный и гибкий инструмент BI, способный решить все их задачи, однако это — зарубежный коммерческий продукт, а клиентов всё чаще интересует тема импортозамещения. В рамках изучения наших перспектив в этом плане мы начали тестирование собственного инструментария BI, используя open-source решения и платформу разработки, построенную на квинтетах. В качестве эталона мы взяли наш существующий коммерческий продукт, так что сравнение, по возможности, будем проводить с ним. Задача заключалась в создании следующих компонентов: За основу мы взяли платформу разработки, которая включает конструктор модели данных, запросов, шаблонизатор и поддерживает ролевую модель управления доступами пользователей. Тематика заказа — сверка банковской отчетности и анализ её данных, накопленных за последние 7 лет. Исходные данные хранятся в HDFS, сами отчеты рассчитываются там же, далее их результаты попадают в витрину реляционной базы данных. Витрины содержат около 300ГБ данных для нескольких десятков различных отчетных форм и нескольких сотен связанных отчетов. Система должна обслуживать 20 пользователей в пике нагрузки, время отклика интерфейса должно быть в пределах 1 секунды. Создание структуры данных не заняло у нас много времени: редактор типов платформы позволяет создавать и описывать объекты бизнеса как есть, без необходимости адаптировать требования пользователя к специфике окружения (типы и размерность данных, ключи, ограничения, имена полей данных, идентификаторы и проч.). Это первый плюс квинтетов, который мы можем зафиксировать. Вот так создаётся структура данных — мы определяем нужные термины, а из них формируем объекты нашей предметной области: После нескольких минут активности в формате copy/paste мы получили требуемую структуру данных для одной формы и нескольких вспомогательных справочников для неё. Структура данных формы (после декомпозиции аналитиком) выглядит так: Аналитик, знакомый с бизнес-смыслом представленных здесь данных, читает здесь следующее: Каждый экземпляр Ф110 (Форма 110 в терминологии ЦБ) имеет заданную точность (она бывает «точная» или «округленная») и включает в себя набор кодов, для каждого из которых указаны суммы в рублях и валюте. В навигаторе данных, где данные представлены согласно заданной структуре, мы видим такую таблицу о существующих в системе экземплярах формы: Идентифицирующее значение формы — это её отчетная дата, отчетные коды хранятся в виде подчиненного массива, размер которого мы видим в скобках. Как было упомянуто, мы немного декомпозировали данные, если их сравнивать с исходной структурой, чтобы не хранить в базе длинные ряды повторяющихся значений, вроде того, что видит программист или администратор базы данных: Кстати, именно поэтому мы позиционируем платформу (далее будем называть её Интеграл) как средство разработки для аналитика, а не программиста. Структура вспомогательных данных несоизмеримо больше, потому что в ней хранятся все исходные данные, настройки отчета, правила для проверки целостности данных внутри отчета и в связанных с ним отчетах, история расчетов и сверок, а также некоторые правила бизнес-трансформации данных при построении отчета: (структура приведена не целиком) Когда структура данных готова, можно загружать в неё данные. Самый простой способ здесь — загрузить файл, подготовленный в формате Интеграла (аналог .csv, но с разметкой типов). Этот формат содержит описание данных и сами данные. В примере ниже первые 3 строки файла описывают структуру формы (если её нет в системе, то она будет создана), а далее идут сами данные — параметры формы и параметры подчиненных ей объектов отчета. В этом месте следует обратить внимание на размер, занимаемый базами при сравниваемых подходах. За счет дополнительной нормализации квинтетов и расходов на составные индексы в эталонной базе итоговый занимаемый базами размер практически одинаков. При этом в Интеграле у нас проиндексированы сразу все поля таблицы, а в эталонной системе — только дата, код и сумма в рублях, что потребует дополнительных затрат при возникновении новых потребностей. Для справки: в базе данных полный размер одной этой формы, включая вспомогательные отчеты и настройки, составляет около 400 МБ (она сравнительно невелика). Итак, данные загружены и предстоит самая сложная часть проекта — создание интерфейса. Интерфейс эталонной системы позволяет просматривать данные форм, вспомогательных отчетов, настроек и жизненного цикла форм. Для управления доступами и общения с базой данных мы использовали базовые возможности нашей платформы — ролевую модель и конструктор отчетов. Список пользователей с их ролями выглядит так: Если кликнуть название роли (отмечено красным овалом на рисунке выше), то можно посмотреть на её содержимое: Объектам роли может быть задано 3 уровня доступа, возможно применение маски: Редактирование данных также сделано средствами базового интерфейса. Вот, например, форма редактирования пользователя: Специфическое меню нашего приложения и его рабочие места мы сверстали в одном компактном файле, поскольку все они однотипны: форма запроса из 2-3 элементов и таблица с результатами запросов. Например, у нас есть так называемый расшифровочный отчет к форме 110, он содержит неагрегированные данные, по которым она построена. Вот так выглядит этот отчет: В целях проверки правильности формы пользователь должен иметь возможность делать любые выборки, сортировку, фильтрацию, группировку, транспонирование, а также создавать собственные вычисляемые поля. Наш плагин вызывается кнопкой «Действия» вверху таблицы. Плагин повторяет функционал построителя запросов Интеграла, но помимо выборки данных может рисовать графики и сводные таблицы. Например, нам нужно наложить фильтр по Разделу, добавить пару вычисляемых колонок и сделать выборку сумм с группировкой по ним. Задаём всё это: Новые колонки добавляются в список одноименной ссылкой. По кнопке «Вычисления» задаем формулы для них с помощью простого конструктора: Задаем новый порядок колонок и нажимаем «Применить» и наш отчет видоизменяется, как требовалось — вместо 7 базовых колонок мы видим три, две из которых мы только что создали: Плагин общается с веб-сервисом приложения по api, он выполнил такой запрос: И получил такой ответ: Если кому интересно, какой реальный SQL-запрос был выполнен в базе квинтетов, то вот он: Запрос может показаться несколько сложнее, чем ожидалось, потому что он выбирает актуальные данные отчетных кодов с учетом их срока действия. В построителе отчетов Интеграла этот запрос состоит из колонок отчета, параметров и условий объединения таблиц, если это необходимо. Интеграл сам способен сгенерировать условия объединения таблиц, так как все они определяются связями квинтетов, однако в случае проверки версионности нам пришлось объединить таблицы вручную и явно указать условие для JOIN. Коды в условии ON — это идентификаторы объектов колонок и запросов. Запрос объединяет три таблицы, выбирая из них следующие поля данных (реквизит «Колонки запроса»): Здесь перечислены колонки отчета, вычисляемые поля, фильтры и прочее, из чего состоит SQL-запрос. Построитель отчета позволяет реализовать почти любую конструкцию языка SQL, в том числе объединение запросов и вложенные запросы. Помимо выборок с группировками пользователю полезен механизм сводных таблиц. Мы добавили в наш плагин популярный инструмент для работы с таблицами pivottable.js.org.
Выберем интересующие нас колонки и переключимся в режим сводных таблиц: Здесь, используя drag’n’drop, мы можем анализировать данные, полученные сконфигурированной нами выборкой, включая наши произвольные поля. Кроме того, здесь можно дополнительно фильтровать данные по любому полю. Для рисования графиков мы использовали бесплатный продукт www.amcharts.com. С ним, как и с pivottable, все достаточно просто: мы выбираем тип графика и инициализируем компонент нашим массивом данных, полученным из Интеграла: На этом мы, фактически, выполнили поставленную задачу в той мере, которая удовлетворяет пользователей существующего продукта. Теперь у нас есть система, проходящая по требованиям импортозамещения: все продукты свободные и заменяемые. Да, мы реализовали далеко не все возможности, что предоставляет существующая система, а только те, что необходимы этому заказчику. Но ведь мы только начали! Отдельно стоит обсудить быстродействие получившейся системы. В этой задаче, при достаточно большом объеме данных (сотни гигабайт), выборки затрагивают небольшие фрагменты, квинтеты которых всегда собираются с использованием индексов. Это приводит к тому, что при любом размере базы данных запросы отрабатывают с приемлемой скоростью, не приводя к лавинообразной деградации производительности. Мы записали 20 действий пользователей в тест-скрипт и прогнали его на сервисе loadimpact.com. Получилось 27 различных запросов, потому что некоторые действия выполняются за 2 запроса к серверу (для построения постраничного отображения, например). Вот что мы увидели, запустив скрипт — генератор нагрузки отправлял нашему приложению всё возрастающее число запросов в секунду, получилась классическая лесенка для поиска максимальной/пиковой производительности: Этот генератор отправляет запросы пачками, а не равномерно, как это было бы при одновременной работе пользователей. Поэтому график показывает скачки нагрузки, и общие показатели также выглядят хуже в таком режиме. С точки зрения нашего сервера всё это происходило вот так: Поскольку база данных организована с использованием метаданных, каждый запрос тест-скрипта к приложению преобразуется в набор SQL-запросов. В данном тесте, судя по логам, требовалось от 8 до 33 SQL-запросов для выполнения каждого такого запроса: необходимо проверить сессию пользователя и его права, выбрать мета-данные, сконструировать SQL-запрос (включая вложенные и присоединенные, если есть) и только после этого выполнить его. Тем не менее, при 20-25 запросах в секунду (пиковая нагрузка при работе 20 пользователей), сервер укладывается в требования по производительности — 1 секунда на отклик. В целом ресурсы его недогружены, хотя это самый бюджетный вариант: 1 ядро 2.4 ГГц при 1 ГБ оперативной памяти. На ненагруженной системе (до 10 запросов в секунду) запросы к серверу выполняются за 0.1-0.3 секунды, в зависимости от сложности выборки. Аналитик