В одной из заметок нашего блога мы освещали подход к хранению и обработке данных, о котором получили несколько вполне ожидаемых вопросов такого плана: «В общем, ждём реализации, вот уж оно залетает...». По результатам нескольких реализаций я расскажу о плюсах и минусах этого подхода на примере одной из наших разработок.


Своим заказчикам мы обычно предлагаем достаточно мощный и гибкий инструмент BI, способный решить все их задачи, однако это — зарубежный коммерческий продукт, а клиентов всё чаще интересует тема импортозамещения. В рамках изучения наших перспектив в этом плане мы начали тестирование собственного инструментария BI, используя open-source решения и платформу разработки, построенную на квинтетах.




В качестве эталона мы взяли наш существующий коммерческий продукт, так что сравнение, по возможности, будем проводить с ним.


Задача заключалась в создании следующих компонентов:


  • база данных для хранения витрин данных и служебной информации (пользователи, настройки и т.п.);
  • веб-интерфейс системы и её ORM;
  • функционал BI — загрузка данных, произвольные отчеты, графики, сводные таблицы.

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


Тематика заказа — сверка банковской отчетности и анализ её данных, накопленных за последние 7 лет. Исходные данные хранятся в HDFS, сами отчеты рассчитываются там же, далее их результаты попадают в витрину реляционной базы данных. Витрины содержат около 300ГБ данных для нескольких десятков различных отчетных форм и нескольких сотен связанных отчетов. Система должна обслуживать 20 пользователей в пике нагрузки, время отклика интерфейса должно быть в пределах 1 секунды.


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


Вот так создаётся структура данных — мы определяем нужные термины, а из них формируем объекты нашей предметной области:



После нескольких минут активности в формате copy/paste мы получили требуемую структуру данных для одной формы и нескольких вспомогательных справочников для неё. Структура данных формы (после декомпозиции аналитиком) выглядит так:




Аналитик, знакомый с бизнес-смыслом представленных здесь данных, читает здесь следующее: Каждый экземпляр Ф110 (Форма 110 в терминологии ЦБ) имеет заданную точность (она бывает «точная» или «округленная») и включает в себя набор кодов, для каждого из которых указаны суммы в рублях и валюте.


В навигаторе данных, где данные представлены согласно заданной структуре, мы видим такую таблицу о существующих в системе экземплярах формы:




Идентифицирующее значение формы — это её отчетная дата, отчетные коды хранятся в виде подчиненного массива, размер которого мы видим в скобках.


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


Таблица с данными в эталонной системе:


Кстати, именно поэтому мы позиционируем платформу (далее будем называть её Интеграл) как средство разработки для аналитика, а не программиста.


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



(структура приведена не целиком)


Когда структура данных готова, можно загружать в неё данные. Самый простой способ здесь — загрузить файл, подготовленный в формате Интеграла (аналог .csv, но с разметкой типов). Этот формат содержит описание данных и сами данные.


Посмотреть файл

В примере ниже первые 3 строки файла описывают структуру формы (если её нет в системе, то она будет создана), а далее идут сами данные — параметры формы и параметры подчиненных ей объектов отчета.


268:Ф110:DATE;277;270;
277:Точность:SHORT;
270:Код обозначения расшифровки:SHORT;Сумма в рублях:SIGNED;Сумма в иностранной валюте:SIGNED;
268::20121231;281;;
270::A/5.2;1233682389.47;;
277:281:Точная;
270::A/5.3;622836720.22;;
270::A/6.4;19800;;
270::A/9.2;27125165.14;;
270::S16203/1.2;608607846.309999;;
270::S16305/4;2727510994.84;;
270::S16305/4.1;32049069.51;;
270::S16305/14;2737711.65;;
270::S25302/4;2725748122.98999;;
270::S25302/4.1;40952511.36;;
270::IL/2;87429694.5699999;62717458.21;
270::IL/4;33517212.95;;
270::IL/9;1423281.69;8278.24;
270::IL/11;86433534.5699999;519956.63;
270::IA/1;147792224.509999;4517060.94;
270::IA/2;737704.92;;
270::IA/3;27099836.07;2637.79;
270::IA/6;5607868.86;408410.4;
270::IA/8;103837028.49;48841202.69;
270::IA/10;112302573.56;;
268::20121231;280;;
270::A/5.2;1233682;;
277:280:Округленная;
270::A/5.3;622837;;
270::A/6.4;20;;
270::A/9.2;27125;;
270::S16203/1.2;608608;;

Для этой формы в базе данных 4470 отчетных дат, которые, будучи выгружены в плоский файл, занимают чуть больше 1 МБ. В исходной базе данных (Oracle) они занимают 3.1 МБ (без индексов) в нормализованном виде и 4.2 МБ в денормализованной витрине, которую мы и пытаемся повторить в виде квинтетов. Квинтеты проиндексированы и нормализованы, и в их формате эти данные занимают уже 10МБ.

Объемы данных для сравнения сведены в таблицу (в мегабайтах):
Текст РСУБД Квинтеты
Данные 1.1 3.1 5.1
Денормализованные 4.2
Индексы 6.2 5.1
Данные + Индексы 9.3 10.2

В этом месте следует обратить внимание на размер, занимаемый базами при сравниваемых подходах. За счет дополнительной нормализации квинтетов и расходов на составные индексы в эталонной базе итоговый занимаемый базами размер практически одинаков.


Эталонная база:


Квинтеты:


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


Для справки: в базе данных полный размер одной этой формы, включая вспомогательные отчеты и настройки, составляет около 400 МБ (она сравнительно невелика).


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


Список пользователей с их ролями выглядит так:




Если кликнуть название роли (отмечено красным овалом на рисунке выше), то можно посмотреть на её содержимое:




Объектам роли может быть задано 3 уровня доступа, возможно применение маски:




Редактирование данных также сделано средствами базового интерфейса. Вот, например, форма редактирования пользователя:




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




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


Например, у нас есть так называемый расшифровочный отчет к форме 110, он содержит неагрегированные данные, по которым она построена. Вот так выглядит этот отчет:




В целях проверки правильности формы пользователь должен иметь возможность делать любые выборки, сортировку, фильтрацию, группировку, транспонирование, а также создавать собственные вычисляемые поля. Наш плагин вызывается кнопкой «Действия» вверху таблицы.




Плагин повторяет функционал построителя запросов Интеграла, но помимо выборки данных может рисовать графики и сводные таблицы. Например, нам нужно наложить фильтр по Разделу, добавить пару вычисляемых колонок и сделать выборку сумм с группировкой по ним. Задаём всё это:




Новые колонки добавляются в список одноименной ссылкой. По кнопке «Вычисления» задаем формулы для них с помощью простого конструктора:




Задаем новый порядок колонок и нажимаем «Применить» и наш отчет видоизменяется, как требовалось — вместо 7 базовых колонок мы видим три, две из которых мы только что создали:




Как это работает под капотом?

Плагин общается с веб-сервисом приложения по api, он выполнил такой запрос:


api/neo/report/1392573?FR_date=20181231&FR_%D0%A0%D0%B0%D0%B7%D0%B4%D0%B5%D0%BB=&FR_%D0%9A%D0%BE%D0%B4_%D0%BE%D0%B1%D0%BE%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F=&SELECT=LEFT(\:1392578\:\,5),SUBSTRING(\:1392578\:\,6\,3),1392617:SUM&ORDER=1392617&FR_%D0%A0%D0%B0%D0%B7%D0%B4%D0%B5%D0%BB=1&TOTALS=1392617:SUM&LIMIT=10

И получил такой ответ:


{
«columns»: [
«LEFT(:1392578:,5)»,
«SUBSTRING(:1392578:,6,3)»,
«1392617»
],
«formats»: [
«SHORT»,
«SHORT»,
«SIGNED»
],
«data»: {
«LEFT(:1392578:,5)»: [
«60324»,
«40817»,
«47425»,
«47404»,
«60302»,
«47404»
],
«SUBSTRING(:1392578:,6,3)»: [
«810»,
«810»,
«810»,
«840»,
«810»,
«978»
],
«Сумма в рублях»: [
«153 825.71»,
«527 901.11»,
«2 415 189.23»,
«3 000 000.02»,
«5 588 330.88»,
«58 000 000.00»
]
},
«totals»: [
«„,
“»,
«69 685 246.95»
]
}

Если кому интересно, какой реальный SQL-запрос был выполнен в базе квинтетов, то вот он:


Посмотреть SQL

Аналитик не видит этот SQL, он пользуется построителем запросов, описанным ниже.


SELECT LEFT(a182088.val, 5) v13,
SUBSTRING(a182088.val, 6, 3) v14,
SUM(round(a182090.val, 2)) ’Сумма в рублях’
FROM neo a182081
LEFT JOIN neo a182083 ON a182083.up=a182081.id AND a182083.t=182083
LEFT JOIN neo a182088 ON a182088.up=a182083.id AND a182088.t=182088
LEFT JOIN neo a182090 ON a182090.up=a182083.id AND a182090.t=182090
LEFT JOIN neo a182091 ON a182091.up=a182083.id AND a182091.t=182091
LEFT JOIN neo a182092 ON a182092.up=a182083.id AND a182092.t=182092
LEFT JOIN neo a299 ON a299.t=299 AND a182083.val=a299.val
LEFT JOIN neo a328 ON a328.up=a299.id AND a328.t=328
LEFT JOIN neo a303 ON a303.up=a299.id AND a303.t=303
LEFT JOIN neo a304 ON a304.up=a299.id AND a304.t=304
LEFT JOIN neo a182089 ON a182089.up=a182083.id AND a182089.t=182089
WHERE a182081.up!=0 AND length(a182081.val)!=0
AND a182081.t=182081 AND a182081.val=’20181231?
AND a328.val =’1?
AND a303.val>=’19000101? AND a303.val<=’20181231?
AND a304.val>=’20181231? AND a304.val<=’20991231?
GROUP BY v13, v14
ORDER BY CAST(SUM(round(a182090.val, 2)) AS SIGNED)
LIMIT 10

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




Интеграл сам способен сгенерировать условия объединения таблиц, так как все они определяются связями квинтетов, однако в случае проверки версионности нам пришлось объединить таблицы вручную и явно указать условие для JOIN. Коды в условии ON — это идентификаторы объектов колонок и запросов.


Запрос объединяет три таблицы, выбирая из них следующие поля данных (реквизит «Колонки запроса»):



(полноразмерная картинка)

Здесь перечислены колонки отчета, вычисляемые поля, фильтры и прочее, из чего состоит SQL-запрос. Построитель отчета позволяет реализовать почти любую конструкцию языка SQL, в том числе объединение запросов и вложенные запросы.



Помимо выборок с группировками пользователю полезен механизм сводных таблиц. Мы добавили в наш плагин популярный инструмент для работы с таблицами pivottable.js.org.
Выберем интересующие нас колонки и переключимся в режим сводных таблиц:




Здесь, используя drag’n’drop, мы можем анализировать данные, полученные сконфигурированной нами выборкой, включая наши произвольные поля. Кроме того, здесь можно дополнительно фильтровать данные по любому полю.




Для рисования графиков мы использовали бесплатный продукт www.amcharts.com. С ним, как и с pivottable, все достаточно просто: мы выбираем тип графика и инициализируем компонент нашим массивом данных, полученным из Интеграла:




На этом мы, фактически, выполнили поставленную задачу в той мере, которая удовлетворяет пользователей существующего продукта. Теперь у нас есть система, проходящая по требованиям импортозамещения: все продукты свободные и заменяемые. Да, мы реализовали далеко не все возможности, что предоставляет существующая система, а только те, что необходимы этому заказчику. Но ведь мы только начали!


Так залетало или нет?


Отдельно стоит обсудить быстродействие получившейся системы. В этой задаче, при достаточно большом объеме данных (сотни гигабайт), выборки затрагивают небольшие фрагменты, квинтеты которых всегда собираются с использованием индексов. Это приводит к тому, что при любом размере базы данных запросы отрабатывают с приемлемой скоростью, не приводя к лавинообразной деградации производительности.


Мы записали 20 действий пользователей в тест-скрипт и прогнали его на сервисе loadimpact.com. Получилось 27 различных запросов, потому что некоторые действия выполняются за 2 запроса к серверу (для построения постраничного отображения, например).


Тестовый скрипт получился такой
import { group, sleep } from ’k6?;
import http from ’k6/http’;

// Version: 1.3
// Creator: Load Impact URL test analyzer

export let options = {
stages: [
{
«duration»: «3m0s»,
«target»: 25
}
],
maxRedirects: 0,
discardResponseBodies: true,
};

export default function() {

	group("page_1 — https://*****.ru/neo/dict«, function() {
		let req, res;
		req = [{
			«method»: «get»,
			«url»: «https://*****.ru/neo/info»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«upgrade-insecure-requests»: «1»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8»
				}
			}
		}];
		res = http.batch(req);
		sleep(0.62);
		req = [{
			«method»: «get»,
			«url»: «https://*****.ru/neo/info»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«upgrade-insecure-requests»: «1»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392573?FR_date=20181231&LIMIT=10»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «image/webp,image/apng,image/*,*/*;q=0.8»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392573?FR_date=20181130&ORDER=1392617&LIMIT=10&RECORD_COUNT»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392573?FR_date=20181031»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392573?FR_date=20180930&RECORD_COUNT»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «*/*»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1387723?&LIMIT=10»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «*/*»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1387723?&LIMIT=10&RECORD_COUNT»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «*/*»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392741?»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «image/webp,image/apng,image/*,*/*;q=0.8»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392757?&LIMIT=10»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «image/webp,image/apng,image/*,*/*;q=0.8»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392768?&LIMIT=10»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/download/neo/img/nav_dropdown_arrow.svg»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «image/webp,image/apng,image/*,*/*;q=0.8»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/download/neo/img/nav_detailed_report.svg»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «image/webp,image/apng,image/*,*/*;q=0.8»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/download/neo/img/nav_classifiers.svg»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «image/webp,image/apng,image/*,*/*;q=0.8»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/download/neo/img/nav_launch_report.svg»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «image/webp,image/apng,image/*,*/*;q=0.8»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/download/neo/img/nav_manage_form_status.svg»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «image/webp,image/apng,image/*,*/*;q=0.8»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/download/neo/img/nav_quality_management.svg»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «image/webp,image/apng,image/*,*/*;q=0.8»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/download/neo/img/nav_download.svg»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «image/webp,image/apng,image/*,*/*;q=0.8»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/download/neo/css/variables.css»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392779?&LIMIT=10»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392538?&LIMIT=10&RECORD_COUNT»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/edit_obj/1392129»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/edit_obj/1390552»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/object/18»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1387723?&LIMIT=140,10»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1387723?&LIMIT=140,10&RECORD_COUNT»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392573?FR_date=20180731&FR_%D0%A0%D0%B0%D0%B7%D0%B4%D0%B5%D0%BB=&FR_%D0%9A%D0%BE%D0%B4_%D0%BE%D0%B1%D0%BE%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F=&SELECT=1392576,1392617:SUM,1392589&LIMIT=100»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392678?FR_date=20180831&FR_section=1&SELECT=1392698,1392685,1392690&LIMIT=10»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392678?FR_date=20180630&FR_section=1&SELECT=1392698,1392685,1392690&LIMIT=10&RECORD_COUNT»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392678?FR_date=20180531&FR_section=1&SELECT=1392698,1392685,1392690&LIMIT=500»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392678?FR_date=20181231&FR_section=2&SELECT=1392698,1392685,1392690&LIMIT=500»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392678?FR_date=20181231&FR_section=4&SELECT=1392698,1392685,1392690,1392694&LIMIT=20»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/report/1392678?FR_date=20181231&FR_section=4&SELECT=1392698,1392685,1392690,1392694&LIMIT=20&RECORD_COUNT»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/neo/info»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/api/neo/report/1392678?FR_date=20190131&FR_section=1&FR_precision=280&SELECT=1392698,1392685,1392715&LIMIT=50»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/api/neo/report/1392678?FR_date=20190131&FR_section=1&FR_precision=280&SELECT=1392698,1392685,1392715&LIMIT=50&RECORD_COUNT»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/api/neo/report/1392573?FR_date=20190131&FR_section=1&FR_precision=280&FR_%D0%9A%D0%BE%D0%B4_%D0%BE%D0%B1%D0%BE%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F=A60302/9&LIMIT=10»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://*****.ru/api/neo/report/1392573?FR_date=20190131&FR_section=1&FR_precision=280&ORDER=1392617&FR_%D0%9A%D0%BE%D0%B4_%D0%BE%D0%B1%D0%BE%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F=A60302/9&LIMIT=10»,
			«params»: {
				«headers»: {
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «text/css,*/*;q=0.1»,
					«referer»: «https://*****.ru/neo/dict»
				}
			}
		},{
			«method»: «get»,
			«url»: «https://fonts.gstatic.com/s/roboto/v19/KFOmCnqEu92Fr1Mu4mxP.ttf»,
			«params»: {
				«headers»: {
					«origin»: «https://*****.ru»,
					«accept-encoding»: «gzip, deflate»,
					«accept-language»: «en-US»,
					«user-agent»: «Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3183.0 Safari/537.36»,
					«accept»: «*/*»,
					«referer»: «https://fonts.googleapis.com/css?family=Roboto»
				}
			}
		}];
		res = http.batch(req);
		// Random sleep between 5s and 10s
		sleep(Math.floor(Math.random()*5+5));
	});

}


Вот что мы увидели, запустив скрипт — генератор нагрузки отправлял нашему приложению всё возрастающее число запросов в секунду, получилась классическая лесенка для поиска максимальной/пиковой производительности:




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


С точки зрения нашего сервера всё это происходило вот так:




Поскольку база данных организована с использованием метаданных, каждый запрос тест-скрипта к приложению преобразуется в набор SQL-запросов. В данном тесте, судя по логам, требовалось от 8 до 33 SQL-запросов для выполнения каждого такого запроса: необходимо проверить сессию пользователя и его права, выбрать мета-данные, сконструировать SQL-запрос (включая вложенные и присоединенные, если есть) и только после этого выполнить его.


Тем не менее, при 20-25 запросах в секунду (пиковая нагрузка при работе 20 пользователей), сервер укладывается в требования по производительности — 1 секунда на отклик. В целом ресурсы его недогружены, хотя это самый бюджетный вариант: 1 ядро 2.4 ГГц при 1 ГБ оперативной памяти.


На ненагруженной системе (до 10 запросов в секунду) запросы к серверу выполняются за 0.1-0.3 секунды, в зависимости от сложности выборки.


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

Комментарии (2)


  1. vaboretti
    16.05.2019 13:39

    Очень сложная реализация BI тула.

    Посмотрите, как сделан github.com/statsbotco/cube.js (а уж тем более, уже есть несколько других опенсурсных BI). Ваша штука не взлетит


    1. NikZanyat Автор
      16.05.2019 14:06

      Спасибо за полезную ссылку, есть на что посмотреть и подумать.

      Ваша штука не взлетит
      .
      Немного голословно и непонятно, куда не взлетит.
      Тут важно понимать цель — наша штука рассчитана на конкретные потребности, и, при всем разнообразии доступных компонентов, в нашем случае исследование buy vs build не имеет готового buy решения.