Наскільки R швидкий для продуктива?

Є такий популярний клас задач, в яких потрібно проводити досить глибокий аналіз всього обсягу ланцюжків робіт, зареєстрованих якої-небудь інформаційної системи (ІС). В якості ІС може бути документообіг, сервіс деск, багтрекер, електронний журнал, складський облік і пр. Нюанси виявляються в моделях даних, 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 секунд на ноутбуці середньої продуктивності.

Читайте також  Поради щодо запуску мобільної гри: Частина 1, Soft launch

 

Самі по собі 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"))})

 

Що тут цікавого?

 

  1. Для прискорення процесу завантаження добре використовувати спеціалізовані профільовані пакети, такі як readtext.
  2. Застосування потокового парсера jq дозволяє перевести всі выцепление потрібних атрибутів на функціональний мову, опустити його на CPP рівень і мінімізувати ручні маніпуляції над вкладеними списками або списками data.frame.
  3. З’явився дуже перспективний пакет bench для микробенчамарков. Він дозволяє вивчати не тільки час виконання операцій, але і маніпуляції з пам’яттю. Не секрет, що на копіювання даних в пам’яті можна втрачати дуже багато.
  4. Для великих обсягів даних і простий обробки часто доводиться у фінальному вирішенні відмовлятися від tidyverse і переводити трудомісткі частини на data.table, зокрема тут йде злиття таблиць засобами саме data.table. А також всі перетворення на етапі постпроцесингу (які включені в цикл допомогою функції ff також зроблені засобами data.table з підходом зміни даних за посиланням, або пакетами, побудованими з застосуванням Rcpp, наприклад, пакет anytime для роботи з датами і часом.
  5. Для скидання даних у файл і подальшого читання дуже хороший пакет fst. Зокрема, лише частки секунди йдуть на збереження всієї аналітики jira історії за 4 роки, а дані зберігаються саме як типи даних R, що добре для подальшого їх перевикористання.
Читайте також  Генератор коду для Laravel — на введення RAML, на висновок JSON-API

 

В ході рішення був розглянутий підхід із застосуванням пакета rjson. Варіант jsonlite::fromJSON приблизно в 2 рази повільніше, ніж rjson = rjson::fromJSON(json_vec), але довелося залишити саме його, бо в даних бувають NULL значення, а на етапі перетворення NULL в NA в списках, що видаються rjson ми втрачаємо перевагу, а код збільшується.

Висновок

  1. Подібний рефакторинг привів до зміни часу процесингу всіх json файлів в однопоточному режимі на цьому ж ноутбуці за 8-9 годин до 10 хвилин.
  2. Додавання паралелізації задачі засобами foreach практично не утяжелило код (+ 5 рядків), але знизило час виконання до 5 хвилин.
  3. Переклад рішення на слабенький linux сервер (всього 4 ядра), але працює на SSD в багатопотоковому режимі звело час виконання до 40 секунд.
  4. Публікація на продуктивний контур (20 ядер, 3 ГГц, SSD) дало зниження часу виконання до 6-8 секунд, що є більш ніж прийнятним для задач операційної аналітики.

Отже, залишаючись в рамках платформи R, простим рефакторінгом коду вдалося домогтися зменшення часу виконання з ~9 годин до ~9 секунд.

Рішення на R можуть бути цілком швидкими. Якщо у вас щось не виходить, спробуйте поглянути на це під іншим кутом і з застосуванням свіжих методик.

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *