Есть такой популярный класс задач, в которых требуется проводить достаточно глубокий анализ всего объема цепочек работ, регистрируемых какой-либо информационной системой (ИС). В качестве ИС может быть документооборот, сервис деск, багтрекер, электронный журнал, складской учет и пр. Нюансы проявляются в моделях данных, API, объемах данных и иных аспектах, но принципы решения таких задач примерно одинаковы. И грабли, на которые можно наступить, тоже во многом похожи.
Для решения подобного класса задач R подходит как нельзя лучше. Но, чтобы не разводить разочарованно руками, что R может и хорош, но о-о-очень медленный, важно обращать внимание на производительность выбираемых методов обработки данных.
Является продолжением предыдущих публикаций.
Обычно, поверхностный подход «в лоб» не является самым эффективным. 99% задач, связанных с анализом и обработкой данных начинаются с их импорта. В этом кратком очерке рассмотрим проблемы, возникающие на базовом этапе импорта данных, на примере типовой задачи «глубокого» анализа данных инсталляции Jira.
Постановка задачи
Дано:
- jira внедрена и используется в процессе разработки ПО как система управления задачами и багтрекер.
- Прямого доступа к БД jira нет, взаимодействие осуществляется через REST API (гальваническая развязка).
- Забираемые json файлы имеют весьма сложную древовидную структуру с вложенными кортежами, требуемые для выгрузки всей истории действий. Для расчета же метрик требуется относительно небольшое количество параметров, разбросанных по разным уровням иерархии.
Пример штатного jira json на рисунке.
Требуется:
- На основании данных jira необходимо найти узкие места и точки возможного роста эффективности процессов разработки и повышения качества получаемого продукта на основе анализа всех зарегистрированных действий.
Решение
Теоретически в R есть несколько различных пакетов по загрузке json и преобразованию их в data.frame
. Наиболее удобным выглядит пакет jsonlite
. Однако, прямое преобразование иерархии json в data.frame
затруднительно в силу многоуровневого вложения и сильной параметризированности структуры записей. Выцепление конкретных параметров, связанных, например, с историей действий, может потребовать различных доп. проверок и циклов. Т.е. задачу можно решить, но для json файла размером в 32 задачи (включает все артефакты и всю историю по задачам) такой нелинейный разбор средствами jsonlite и tidyverse занимает ~10 секунд на ноутбуке средней производительности.
Сами по себе 10 секунд — это немного. Но ровно до момента, пока этих файлов не становится слишком много. Оценка на сэмпле разбора и загрузки подобным «прямым» методом ~4000 файлов (~4 Гб) дала 8-9 часов работы.
Такое большое количество файлов появилось неспроста. Во-первых, jira имеет временные ограничения на REST сессию, вытащить все балком невозможно. Во-вторых, будучи встроенным в продуктивный контур, ожидается ежедневная выгрузка данных по обновленным задачам. В-третих, и это будет упомянуто дальше, задача очень хороша для линейного масштабирования и думать о параллелизации надо с самого первого шага.
Даже 10-15 итераций на этапе анализа данных, выявления необходимого минимального набора параметров, обнаружения исключительных или ошибочных ситуаций и выработки алгоритмов постпроцессинга дают затраты в размере 2-3 недели (только счетное время).
Естественно, что подобная «производительность» не подходит для операционной аналитики, встроенной в продуктивный контур, и очень неффективно на этапе первичного анализа данных и разработки прототипа.
Пропуская все промежуточные детали, сразу перехожу к ответу. Вспоминаем Дональда Кнута, засучиваем рукава и начинаем заниматься микробенчмаркингом всех ключевых операций безжалостно срезая все, что только можно.
Результирующее решение сводится к следующим 10 строчками (это сутевой скелет, без последующего нефункционального обвеса):
library(tidyverse)
library(jsonlite)
library(readtext)
fnames <- fs::dir_ls(here::here("input_data"), glob = "*.txt")
ff <- function(fname){
json_vec <- readtext(fname, text_field = "texts", encoding = "UTF-8") %>%
.$text %>%
jqr::jq('[. | {issues: .issues}[] | .[]',
'{id: .id, key: .key, created: .fields.created,
type: .fields.issuetype.name, summary: .fields.summary,
descr: .fields.description}]')
jsonlite::fromJSON(json_vec, flatten = TRUE)
}
tictoc::tic("Loading with jqr-jsonlite single-threaded technique")
issues_df <- fnames %>%
purrr::map(ff) %>%
data.table::rbindlist(use.names = FALSE)
tictoc::toc()
system.time({fst::write_fst(issues_df, here::here("data", "issues.fst"))})
Что здесь интересного?
- Для ускорения процесса загрузки хорошо использовать специализированные профилированные пакеты, такие как
readtext
. - Применение потокового парсера
jq
позволяет перевести все выцепление нужных атрибутов на функциональный язык, опустить его на CPP уровень и минимизировать ручные манипуляции над вложенными списками или списками вdata.frame
. - Появился очень перспективный пакет
bench
для микробенчамарков. Он позволяет изучать не только время исполнения операций, но и манипуляции с памятью. Не секрет, что на копировании данных в памяти можно терять очень много. - Для больших объемов данных и простой обработки часто приходится в финальном решении отказываться от
tidyverse
и переводить трудоемкие части наdata.table
, в частности здесь идет слияние таблиц средствами именноdata.table
. А также все преобразования на этапе постпроцессинга (которые включены в цикл посредством функцииff
также сделаны средствамиdata.table
с подходом изменения данных по ссылке, либо пакетами, построенными с применениемRcpp
, например, пакетanytime
для работы с датами и временем. - Для сброса данных в файл и последующего чтения очень хорош пакет
fst
. В частности, всего доли секунды уходят на сохранение всей аналитики jira истории за 4 года, а данные сохраняются именно как типы данных R, что хорошо для последующего их переиспользования.
В ходе решения был рассмотрен подход с применением пакета rjson
. Вариант jsonlite::fromJSON
примерно в 2 раза медленнее, чем rjson = rjson::fromJSON(json_vec)
, но пришлось оставить именно его, потому как в даных бывают NULL значения, а на этапе преобразования NULL
в NA
в списках, выдаваемых rjson
мы теряем преимущество, а код утяжеляется.
Заключение
- Подобный рефакторинг привел к изменению времени процессинга всех json файлов в однопоточном режиме на этом же ноутбуке с 8-9 часов до 10 минут.
- Добавление параллелизации задачи средствами
foreach
практически не утяжелило код (+ 5 строчек) но снизило время исполнения до 5 минут. - Перевод решения на слабенький linux сервер (всего 4 ядра), но работающего на SSD в многопоточном режиме свело время исполнения до 40 секунд.
- Публикация на продуктивный контур (20 ядер, 3 ГГц, SSD) дало снижение времени исполнения до 6-8 секунд, что является более чем приемлемым для задач операционной аналитики.
Итого, оставаясь в рамках платформы R, простым рефакторингом кода удалось добитьcя уменьшения времени исполнения с ~9 часов до ~9 секунд.
Решения на R могут быть вполне быстрыми. Если у вас что-то не получается, попробуйте взглянуть на это под другим углом и с применением свежих методик.
Предыдущая публикация — «Аналитический паRашют для менеджера».
Автор: i_shutov