29 January

[кейс Locomizer] Как за два с половиной года ускорить расчёт тепловой карты в 20 000 раз

Abnormal programmingProgrammingJavaGeoinformation servicesBig Data
Tutorial
Данная статья является продолжением серии «Кейс Locomizer», см. также


Здравствуйте.

КПДВ: TC, EMR, IDEA

Знаете, что такое «постмортем»? Это повествование о том, как мы дошли до жизни такой.

Не уверен насчёт вас, но мне чертовски нравится читать истории о процессе разработки какого-нибудь узкоспециализированного или низкоуровневого софта. У коллег может интересно работать мысль, и всегда любопытно проследить за тем, что происходило с программой от прототипа до зрелого продукта, который делает какую-то магию в малознакомой предметной области.

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

Данная статья — историческая вводная по One Ring. Кода в ней нет, и рассказ скорее популярный, чем научный. Зато только про разработку, и ни о чём другом, кроме двух с половиной лет разработки.

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

  • Big Data is big
  • Наш кейс — нестандартный
  • Прототип на C# и PostGIS
  • Первое приближение на Hadoop MapReduce
  • Появление CI и Spark
  • Третье приближение на GeoSpark
  • Японские аналитики и миграция из Azure в AWS
  • Аш назг дурбатулук, Аш назг гимбатул, Аш назг тракатулук, Аг бурзум-иши кримпатул!!
  • Оптимизация и геокатарсис с Uber H3
  • Выход во всём белом

Big Data is big


Большие данные — это не про размер.

В ежемесячном датасете по региону Greater London могут быть десятки, или даже сотни миллионов записей, но это немного. Однократное итерирование по ним с начала до конца упирается в скорость линейного чтения с диска. Если диск SSD, это займёт считанные секунды.

(Напоминаю, что датасет, о котором идёт речь, — это набор файлов в формате CSV с набором полей, специфичных для поставщика. Группировка записей с координатами анонимных пользователей в файл происходит по границам административного региона страны, префектуры, или города. Сами файлы формируются на выбранную дату, ежедневно или ежемесячно. Подробнее всё это расписано в предыдущей части, пробегите её по диагонали, если не хватает контекста.)

Наш процесс многошаговый. Начальные эвристики по обогащению сырых данных, работающие как раз в режиме однократной итерации, быстрые, и писать их можно хоть на Python, хоть на C++, хоть на PHP. Даже на слабенькой машине обработка выполнится быстро.

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

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

Возьмём что-нибудь вроде определения расстояния между парой координат. Есть крайне быстрый метод Haversine («гаверсинусов» по версии зала), который при небольших расстояниях даёт приемлемую точность, и позволяет не брать геоид WGS84, расчёт по которому работает существенно медленнее.

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

А теперь берём случай из нашего патентованного алгоритма, когда надо посчитать расстояние от каждого сигнала до каждого POI из выбранной категории, и отбросить те, которые дальше полукилометра (такое расстояние, которое легко пройти пешком).

Для региона Greater London в целевые POI категории «магазины и торговые точки» попадает примерно миллион заведений. А как я уже сказал, в ежемесячном датасете для него приходят десятки, сотни миллионов записей. И вот мы получаем…

1 000 000 POI × N 000 000 сигналов = N 000 000 000 000 расстояний.


Оп-ля, приехали. Бешаные триллионы вычислений расстояния и сравнений с пороговой константой.

Классическая ситуация с декартовым произведением. Два не особенно мощных по отдельности множества легко дают N × 1012 промежуточных результатов, и это всего лишь один месяц в одном регионе! Такое количество уже переходит в качество. Не только размер промежуточного результата уже представляет собой серьёзную проблему, потому что не влазит в память целиком, и надо сразу обрабатывать на месте получения, но и необходимое для его получения количество вычислений занимает слишком много машинного времени. И если на одну запись, с учётом всех задержек при передаче по сети, и прочих накладных расходов, тратится всего 100 наносекунд, то миллионы секунд — это дни и недели вычислений в один поток.

Или, если нам надо выкинуть из общей популяции какой-то сегмент, например, условие «не учитывать интересы пользователей, которые живут в определённом районе», то придётся device_id каждой из записей обогащённого датасета всего региона сравнить с множеством, в котором сотни тысяч записей с исключаемыми device_id жителей этого района. А это строковые сравнения по множеству, не столь быстрые как для двух интов. Снова возникает какое-то невменяемое количество нулей в оценке одной простой операции, а их у нас на полный набор эвристик для среднего проекта с десяток, а то и больше.
Big Data — это такие данные, которые из-за размера заставляют использовать специальные алгоритмические приёмы ввиду нецелесообразности или непрактичности обработки прямым путём.

…даже если финальный результат расчёта схлопывается в один экран экселевской таблицы.

Можно попробовать распараллелить «наивный» обработчик по количеству имеющихся виртуальных процессоров на машине, где запускаем расчёт. Можно поделить датасет на куски и запустить расчёт стразу на десятке виртуальных машин в облаке. Но это всё не даст качественно отличного результата. Масштабирование «в ширину» даёт diminishing returns начиная с некоторой ширины. А ещё обязательно вылезет проблема синхронизации и партиционирования, да и управление целым парком виртуальных машин будет стоить конских времени и денег. Держать их всё время включёнными дорого, а запускать и останавливать по требованию трудозатратно.

Поэтому для больших данных используются специальные программные комплексы из экосистемы Hadoop, уже имеющей средства управления на масштабе, а также специальный набор алгоритмов, который позволяет есть мамонта мелкими порциями без риска подавиться астрономическим количеством промежуточных данных, и здорово упрощает жизнь big data разработчика. Но нельзя так просто взять и начать использовать Hadoop. Сначала нужно составить план.

Особенно, если…

Наш кейс — нестандартный


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

Подход №1. Data Lake


Для данных, которые накапливаются с течением времени, и остаются актуальными навсегда, проектируются хранилища специального вида, так называемые «озёра данных».

Архитектура таких хранилищ оптимизируются для быстрого случайного доступа. Множество собираемых датасетов переводится в специализированный формат, позволяющий быстро выполнять многокритериальные выборки и срезы по наборам колонок. В отличие от традиционных реляционных и документо-ориентированных баз, в озёрах данных используются колоночные хранилища. Обычно они финальные, то есть, формат контейнеров с данными такой, что после заливки и индексации данные в одном датасете больше никогда не изменяются. Например, файлы parquet, не предусматривающие модификации.

После этого набегает толпа дата-сатанистов или дата-аналитиков, и в специализированном софте («ноутбуках» типа Jupyter) занимается сбором статистики, показателей и т.п. онлайн. Эта статистика выгружается из озера куда-то наружу, или же просто складывается рядом в виде таких же финальных файлов для последующей агрегации.

Подход №2. Data Streaming


Для данных, которые поступают в реальном времени, и должны быть быстро обработаны (то есть, потоковые данные), проектируются шины данных, или очереди сообщений.

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

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

Здесь рулит Apache Kafka и быстрые хранилища типа Aerospike.

Наш случай


А вот наш случай в эти два подхода не вписывается.

Во-первых, нам нет смысла держать озеро данных, потому что актуальность датасета редко превышает год (треки пользаков за 2016 год в 2019 уже никому не нужны), и заказчикам каждый раз нужна совершенно непредсказуемая часть всех накопленных данных. Также, из-за того, что для каждого сегмента популяции и категории делается свой собственный шаблон, мы всё равно вынуждены брать только требуемый кусок, и сливать их в общее озеро особого смысла нет. Проще держать каждый ежемесячный датасет в исходном виде — файлами CSV в своей отдельной директории. Путь к файлу получается …/поставщик/страна/регион/подрегион/год/месяц/файлы_датасета, и выбор какого-то подмножества выполняется просто по маске имени файла, например, …/Tamoco/UK/Greater_London/*/2019/{6,7,8}/*.csv.

Во-вторых, сама природа датасетов дискретная, а не потоковая. Можно было бы, конечно, прямо в процессе загрузки в сетевое хранилище предрасчитывать какие-то показатели, но готовые тепловые карты для региона «Москва» и соседнего региона «Московская область» никак не соотносятся с готовой тепловой картой объединённого региона «Москва и область» (из-за того что слишком многие живут в области, а работают в Москве), и мы всё равно заранее не знаем, какой регион нам понадобится. Может быть, ни «Москва», ни «Московская область», а только какой-нибудь «City 17». Очень дорого гонять эвристики и предрасчитывать показатели для всех датасетов.

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

Не думаю, что мы такие уж уникальные. В разговорах с коллегами регулярно всплывает необходимость инструментовки похожих burst-кейсов, но тут каждый выстраивает процесс по-своему. Обычно к имеющемуся конвейеру из подходов №1 или №2 как-то сбоку присобачиваются решения для нестандартных случаев; наш же процесс полностью состоит из частных проектов, у нас все задачи типа «burst».

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

Прототип на C# и PostGIS


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

Сработало и на людях.

И тогда возник проект Locomizer. Я говорю «проект», потому что он как бы стартап с LLC для заключения договоров, но не совсем. Члены нашего коллектива раскиданы по всему миру, работают в разных местах и конторах как фрилансеры или аутсорсеры (и не все full-time), и мы применяем наши алгоритмы для очень разных заказчиков с разными моделями взаимодействия по мере поступления или нахождения заказов. Есть подписки, но больше частных одноразовых задач.

Но это сейчас так. А несколько лет назад всё было ещё хаотичнее. Кто написал первую программную реализацию для расчёта скора, мне вообще неведомо. (Если вы вдруг знаете этих неизвестных героев, передайте им привет.) В конце своей прошлой статьи про карьеру программиста в отдельно взятом городе я написал буквально следующее: «Пришёл я побеседовать в место, где работаю сейчас, а ПМ прямо с порога заявил, что проект — адский. Ничоси. Опять же, ГИС, только расчёты все на MapReduce (а хочется, чтобы на Spark), карты на ArcGIS, и всё это крутится в облаках, которые никто не умеет девопсить. По-моему, прекрасный вариант!» — в тот момент оно было уже так, и восстановить самый первый этап становления проекта в коде я могу только по воспоминаниям mitra_kun, который сам появился на проекте всего годом раньше.

Зачаточные эвристики по обработке сырых датасетов были написаны на PHP, Python и C++, а основной расчёт скора для тепловой карты выполнялся программулькой на C#.

Весь проект на C#

Работала она так:

  1. Сначала напрямую читаем из файла датасета строки в массив.
  2. Прогоняем его foreach’ем, строим хеш-таблицу по пользакам.
  3. База POI — это буквальная таблица в базе PostgreSQL с PostGIS-овскими полями типа GEOMETRY, и для вычисления расстояния между каждым сигналом пользака и каждой POI через небольшую хранимку дёргается функция ST_DISTANCE, результат складывается в хеш-таблицу с ключом для каждого пользака.
  4. После чего делаем foreach по таблице с накоплением результата interest score для каждого ключа в массиве.
  5. Ещё раз группируем, уже по каждой категории.
  6. После окончания расчёта, который целиком занимает от пары часов до недели времени, результат складывается в файл CSV…
  7. …и потом ещё вручную обрабатывается, накладывается на карту, и визуализируется в ArcGIS.

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

Первое приближение на Hadoop MapReduce


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

Как я уже сказал, стандартная платформа для обработки больших данных — это экосистема Hadoop. Большой набор разнородных библиотек, включающий в себя распределённую файловую систему, шедулеры для распараллеливания задач, относительно удобные абстракции над map-reduce, движки для выполнения запросов, и ещё огромную кучу всякой всячины для анализа данных. А ещё вся эта программная инфраструктура доступна в облаках от разных вендоров в виде интегрированных пакетов, и автоматизируется, но об этом чуть позже.

Окей гугл, искать Hadoop. Мои предшественники взяли прототип, и переписали основной расчёт с C# на Java, буквально заменив все foreach на соответствующие хадуповские Mapper и Reducer, а все шаги по подготовке и обогащению датасетов вынесли в отдельные утилиты на скриптовых языках, чтобы быстрее разрабатывать, потому что с появлением разных заказчиков алгоритмы начали активно эволюционировать. Отдельно начали писать бэкэнд для Web UI на Spring (не лучшее решение, если нет предыдущего опыта разработки на Java, лучше бы написали на PHP), с фронтом на Node.js с интеграцией карт из ArcGIS.

Маленькая часть проекта на Java

Подняли «большой кластер» Hadoop на пяти виртуалках в Microsoft Azure под это дело. Почему Azure? Во-первых, для стартапов там большая скидка первые несколько лет. Во-вторых, в этом облаке уже был задеплоен ArcGIS Desktop для Windows для визуализации карт.

Кластер Hadoop развернули вручную, а не из соответствующего сервиса Azure HDInsight, с настройкой которого возникли затруднения. На каждой из машин кластера подняли Postgre+PostGIS (довольно-таки сомнительное решение, потому что MR и база начинают конкурировать за процессор), чтобы не ходить за расстояниями на отдельный сервер. Сделали небольшой скрипт, который раскидывал реплики базы POI по узлам кластера.

Проект оставался всё ещё прототипом, просто чуть более продвинутым. По-прежнему использовался PostGIS, потому что появился геофенсинг, и ребята пока не знали, каким ещё образом его можно реализовать с минимальными трудозатратами. По ощущениям, всё ужасно тормозило, а количество шагов, которые надо было проделать вручную, перевалило за полтора десятка.

Именно в этот момент меня и заинтересовало предложение от малоизвестной в нашем небольшом, но сильно ИТ-шном городке (в Ижевске больше семи десятков контор со штатами разработчиков, где трудятся около трёх тысяч программистов) конторы с абсолютно generic названием «Русские Информационные Технологии», которым вдруг ни с того ни с сего потребовался Senior Java Developer с большим опытом деплойментов и автоматизации, и хотя бы краем уха слышавший о Big Data и облаках. Ну, об облаках и больших данных я к тому времени чуть-чуть чего-то да слышал.

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

Ну, если код и процессы понятного качества, то в них точно есть где заняться оптимизацией. Для начала можно хотя бы запросы к PostGIS не по одному кидать, а пакетно, штук по 5000 точек за раз. Базы данных, как правило, неплохо оптимизированы для разруливания декартовых произведений. Сказано — сделано, хранимка с вызовом ST_DISTANCE переписана таким образом, чтобы возвращать сразу большой массив для пакета точек, и на пустом месте расчёт ускорился сразу в 40 раз, потому что не надо было теперь устанавливать соединение до базы так часто и так много, и индексы по геометрии в таблице с POI стали работать с большим смыслом.

Правда, в расчёт тут же закралась противная эзотерическая ошибка, связанная с тем, что прототип был не совсем корректно перенесён с C# на Java. Ребята упустили смысл одной важной переменной, а формальное ТЗ по прототипу до них вообще не дошло, потерявшись где-то по дороге. Мы потом восстанавливали все алгоритмы по обрывочным описаниям, но это было уже очень сильно позже. Впрочем, результат расчёта эта ошибка в целом не портила, просто снизила контрастность тепловой карты.

Но из MapReduce много производительности не выжмешь, потому что маппер читает данные из HDFS и записывает их обратно, а следущий в цепочке редюсер делает то же самое, и так пока все шаги не закончатся. А ещё весьма неудобно управлять многошаговым процессом, особенно если в алгоритме есть ветвления из-за настроек. Весь алгоритм — это хардкод, и если хочется как-то переупорядочить шаги, приходится выносить их в отдельные модули со своей запускалкой, и обёртывать какой-то логикой снаружи.

Ну и дёргать PostGIS изнутри расчёта, даже если продублировать базу на каждую ноду кластера — это всё равно весьма больная идея.

Появление CI и Spark


— Автоматизируй это! — мой второй основной профессиональный интерес, после энтерпрайзного программирования на жабе… А нет. Второй — это пицца, паста и пудинги, значит, пусть будет третий, — это постановка процессов и их автоматизация. (Я, как шеф-повар, люблю чтобы всё было на п. Хэштег #печеньки.)

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

Хождение по граблям как раз составляло самую серьёзную проблему, которую надо было решать в первую очередь. Сначала я развернул на отдельной небольшой виртуалке TeamCity, и настроил сборку с прогоном всех тестов, чтобы проверенный артефакт всегда был под рукой, и его не нужно было закидывать на кластер вручную. Вторым шагом стало написание обёртки для запуска одной задачи MR с указанным датасетом и набором параметров на кластере прямо из того же TC, с автоматическим же копированием исходных датасетов в кластер и результатов расчёта в хранилище результатов.

А третьим шагом, который с непривычки занял очень много времени, стала автоматизация развёртывания самого кластера, тюнинг его параметров, и запуск расчёта на датасете, сложенном в Azure Blob Storage. Просто вдруг появились проекты, для которых стало не хватать статического кластера из пяти виртуалок и/или датасеты которых не следовало перемешивать со свалкой старых файлов на HDFS.

Azure HDInsight — это на самом деле Hortonworks HDP (земля ему пухом), и какая-то часть его настроек вынесена в API, а какая-то может быть прописана только через Ambari. Развёртывание кластера в зависимости от загрузки облака может занимать до часу времени, и цикл тюнинга, — то есть, проверки влияния какого-нибудь набора настроек на производительность именно нашего кода, — может занимать целый день. Локальная версия HDP Sandbox в виртуалке жрёт 11 гигов оперативки, и чудовищно требовательная к дисковой подсистеме, так что даже локальная отладка процесс крайне неприятный, да и настройки у неё немного отличаются от облачной версии. Я угрохал кучу времени на эксперименты, но хотя бы разобрался, как это всё работает, и что делать, если расчёт вдруг вешается на середине с очередным OOM, потому что разбирать логи вручную тоже довольно неприятно.

Пока я разбирался с HDP, другой программист начал унифицировать разрозненные этапы подготовки датасетов на Apache Spark. В Spark решена проблема постоянной записи/чтения промежуточных данных, возникающих между шагами одного расчёта, и вообще, он спроектирован с учётом всех плохих мест MR, и умеет из коробки в разы больше. И спарковские ленивые RDD — это очень удобная штука.

Параллельно я скриптовал Azure Templates на PowerShell для настройки edge node под PostGIS — отдельный толстый-претолстый инстанс в составе кластера, с кучей ядер и памяти для ускорения запросов, а также прогона предварительных шагов по подготовке датасетов, которые складывались сначала на его локальный диск, а затем уже загружались в HDFS на кластере.

Так что скриптовая обвязка, которая изначально задумывалась, что будет работать как интерактивно, так и в пакетном режиме на TC отдельным билдом, постепенно научилась запускать произвольное сочетание шагов на MR, Spark, и прочих, нами не используемых программных пакетов из комплекта HDInsight, — но пока ещё с рудиментарной параметризацией. Впрочем, вынос параметров билда в соседний репозиторий с набором .ini файлов (для каждого компонента платформы и для каждого шага процесса), и ведение шаблонов процессов в ветках этого репозитория оказалось настолько удобной практикой, что мы пользуемся ею и по сей день.

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

Третье приближение на GeoSpark


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

Второй программист, не имевший ранее опыта работы ни в команде, ни с энтерпрайзом, поступал со своими модулями довольно прямолинейно — завершив перенос одной какой-нибудь эвристики на Spark, он просто копировал весь проект целиком, и начинал в нём заменять старый алгоритм на новый. В итоге, когда таких параллельных модулей скопилось штук восемь, каждый с похожим, но немного отличным набором параметров, чуточку отличной семантикой вызова, — а также большим количеством дублирующегося служебного кода, — они стали представлять очередную проблему. Чем больше кода, тем больше времени уходит на его поддержку, особенно если он не прекращает эволюционировать всё это время. А из-за постоянного copy-paste в них начали копиться неиспользуемые параметры и прочий мусор.

Закончив с горящей проблемой автоматизации и разобравшись с настройкой кластеров, теперь я уже мог взяться за модули подготовки данных и эвристик. Для начала вынес весь повторяющийся код в отдельный проект Commons, подключаемый как git submodule, и в расчётных модулях стало в разы меньше бардака. Собрал шаблон для типичной эвристики, и новый проект форкался уже из него, без необходимости заменять куски кода и без ненужной грязи в истории коммитов. Разработка стала вестись быстрее.

Следующая большая проблема, которую следовало победить, вытекала из самой логики расчёта с декартовым произведением сигналы × POI.

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

Ответ: партиционировать и сигналы, и POI по геометрической сетке.

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

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

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

И если 99% партиций с малонасыщенными полигонами отработают быстро, то 1% спарковских джобов с ячейками высокой плотности продолжают висеть до победного, жрать память как не в себя, и портить всю малину. Spark всё старается держать в памяти, и если в RDD сильный разброс в размере партиций, то весь тюнинг по потреблению памяти летит коту под хвост, потому что его приходится делать под самые большие.

Получилось, что 99% расчёта ускорились с геометрическим партиционированием в сотни раз, а 1% длинного хвоста свёл всю оптимизацию почти на нет.

В целом переход на GeoSpark дал выигрыш раз в пять, но только на очень неэкономных по памяти размерах экзекуторов, — и, соответственно, кластерах с дорогими виртуалками. Короче, геометрическое партиционирование для высокоплотных геоданных оказалось тупиковым путём.

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

Японские аналитики и миграция из Azure в AWS


У японцев интересный менталитет. Сами они никуда не торопятся, но стоит только гайдзину дать им укусить палец, оттяпают обе руки. Никогда не называйте японцам конкретных сроков! А если называете, то берите как минимум трёхкратный запас. Согласовывать техническое задание придётся чудовищно долго и трудно, и мешать будет не только знаменитая японская дотошность, но и разница в мышлении. На реализацию финального варианта ТЗ может просто не остаться времени.

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

Во-первых, никакого Azure. Только AWS, только хардкор.

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

Извиняюсь за качество, скриншот из баг-репорта, других не осталось

В какой-то момент я немножечко психанул, и сделал набор «элементарных операций» — штук 15 примитивных действий над RDD с вызовом базовых методов типа джойнов, маппингов, проставления умолчальных значений, суммирования значений колонок, — и прочих таких мелких операций, — чтобы быстренько менять логику цепочки расчётов, как будто это набор операторов на SQL.

(Штатный Spark SQL в нашем случае неприменим из-за того, что нет ни строгой типизации, ни сторогого набора полей. В датасете может в любой момент добавиться сколько угодно дополнительных полей, в которых лежит что попало, да и меняется по ходу process flow он сильно. Прописывать метаданные под постоянно меняющиеся условия слишком мартышкин труд.)

Высокоуровневая задача была такая: выбрать произвольный регион Японии, и построить тепловую карту за произвольный период времени по произвольному набору категорий с туевой хучей показателей на полигон. Что за показатели, как их считать — этого толком не понимал сам заказчик.

Тестовый (то есть, маленький) датасет с сигналами пользаков за 2016–2017 годы, на котором нам предстояло отработать технологию, — 5 терабайтов данных, 14 000 000 000 записей. В одном только Токио несколько миллионов POI, а в сетке по региону Хоккайдо 1 600 000 ячеек.

И карты по всем двум тысячам категорий для каждой из 47 японских перфектур надо считать «на лету», потому что продаваться оно должно было как облачный сервис.

Прекрасная задача, чтобы сломать мозг. Где-то на три-четыре порядка выше наших тогдашних возможностей по показателям «скорость расчёта» и «объём данных».

Взгрустнув, решили всё-таки сделать предрасчёт по каждому региону (слава синтоистским богам, объединять регионы японцам было не нужно) и месяцу, чтобы тепловая карта строилась по заранее подготовленным скорам. Пусть не в реальном времени, но несколько минут или десятков (для центрального Токио) минут. Предрасчёт занял несколько месяцев с кластерами по 25 самых мощных виртуалок, доступных в токийском регионе AWS.

Но чтобы запуститься в AWS, надо было сначала переписать автоматизацию под API AWS. А разные облака, хоть и предлагают внешне аналогичные сервисы, внутренне устроены совершенно иначе. Хорошо, что к этому моменту PowerShell уже добралось до релиз-кандидата версии 6, и азуровские обвязочные скрипты для разворота кластера и запуска расчёта можно было портировать, и смело запускать на линуксовом TeamCity (потому что разворачивать сервера на Windows в AWS — это так себе идея). Точнее, не портировать, а открыть на одном мониторе имеющийся скрипт, а на втором писать параллельную имплементацию для другого облака.

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

Но сама экосистема Hadoop в амазоновской инкарнации — EMR — штука, более близкая к ванильной, и работать с ней легче, чем с HDInsight. Ну, хотя бы с чем-то оказалось проще.

Но не с S3. Здесь беда вылезла откуда не ждали. У S3 есть недокументированные лимиты. Например, в одном бакете не может быть больше ~11 000 000 объектов, потому что их API где-то в глубоких недрах делает сортировку ключей в лексикографическом порядке на каждый (каждый!) запрос, и буфер, выделенный под неё, просто не позволяет отсортировать большее количество строк, особенно если они длинные. Для ускорения предрасчёта мы не делали слияний партиций в конце, и в какой-то момент упёрлись в этот лимит, после которого процесс просто остановился.

По уму слияние обязательно нужно делать, и даже есть инструмент — утилита s3-dist-cp, но её использование отдельная головная боль. Утилиту точно писали хищники для чужих, настолько она контринтуитивно себя ведёт. И у неё есть фатальный недостаток — под сливаемый файл нужно столько же места на HDFS, сколько занимают все исходные. И сливать десятки тысяч файлов партиций размером от сотен байт до десятков мегабайт, размазанных по кластеру из 25 машин, она будет ну очень долго.

Впрочем, уже при миллионе объектов в бакете S3 начинает незаметно тротлить запросы к нему. А в условиях eventual consistency это вообще беда — Spark, не дождавшись следующего парта условленное количество раз, может и упасть. Решение есть — использовать проприетарную амазоновскую надстройку EMRFS, но она работает поверх DynamoDB, а это очень дорогая штука. И с собственными лимитами по количеству запросов в секунду.

Короче, в условиях тотального отсутствия времени мы приняли решение откатиться на статическую схему — развернуть перманентный кластер на инстансах немаленького размера (пусть дорого, но дешевле, чем DynamoDB), слить все терабайты исходного и предрасчитанных датасетов в HDFS на нём, и считать карты локально.

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

Недостаток — сетка Japan Mesh применима только к Японии и островным территориям, на которые она исторически претендует, но не для остального мира. Зато хотя бы для японцев стало возможно отказаться от медленного GeoSpark, и партиционировать сигналы равномерно без привязки к внешней геометрии. А с уходом «длинного хвоста» расчёт сразу ускорился ещё раз так в 10.

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

Да и в любом случае где-то посередине работы японцы всё равно попросили перенести всю инфраструктуру с одного аккаунта AWS на другой. И как бы пофиг на всю проведённую работу по настройке. Ну, критическую часть процесса развёртывания я успел к моменту перехода заскриптовать в шаблон CloudFormation, так что миграция прошла более-менее ровно.

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

Бррр. Вспоминаю этот проект с ужасом и содроганием.

Аш назг дурбатулук, Аш назг гимбатул, Аш назг тракатулук, Аг бурзум-иши кримпатул!!


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

Мы выучили студента на Java Junior, и он провёл исследование кучи географических библиотек, в результате которого наконец получилось выбрать правильную, и выбросить из окружения PostGIS.

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

Пока мы не додумались, что нужна либа с нормальным геоидом (желательно, чтобы такой же, как в PostGIS, WGS84), результаты не сходились с ожидаемыми. Но после перехода на GeographicLib бутылочное горлышко в виде коннектов к Postgre было устранено, и финальный этап расчёта скора ускорился в 40 раз. Ушёл головняк с дополнительной настройкой отдельного инстанса RDS под базу и заливкой в неё дампа с POI, который переехал к обычным датасетам в S3. Унификация!

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

Другой важный момент. Однажды, в сотый раз глядя на скрипты обвязки, вызывающие один модуль Spark за другим, я подумал, а какого, собственно, лешего мы не замыкаем их накоротко?

Зачем каждый раз сохранять в S3 или HDFS промежуточные результаты, если итоговую RDD предыдущего модуля можно просто перенаправить на вход следующего в цепочке. Сказано — сделано, MetaRunner написался за пару часов. Наличие commons в этом сильно помогло, модули достаточно унифицировались к тому времени, тем более что параметры каждого из модулей уже лежали в одном и том же tasks.ini, с префиксами ключей, соответствующих их именам.
Вашему вниманию представляется блок-схема построения карты (финальный шаг перед выдачей на фронт, но не финальный вариант), написанная на элементарных операциях:

Блок-схема процесса подготовки тепловой карты

Если избавиться от 24 промежуточных обращений к HDFS, конкретно этот расчёт ускоряется приблизительно в 50 раз.

А что, если в шаблон процесса добавить поддержку переменных, чтобы не перегенерировать каждый раз tasks.ini при любом изменении параметров в Property Store?

— Аш назг! — заорал я. Коллеги недоумённо переглянулись. У чувака крыша из-за этих японцев съехала, ну да ладно, бывает.
— Аш назг… бурзум-иши кримпатул, — прорычал я гроулом (не очень получилось), и пошёл к ПМу обсуждать слияние всех 15 (количество эвристик и вспомогательных утилит постепенно росло) расчётных модулей в монорепозиторий.

Если уж закорачивать модули между собой, то не маяться больше с подкладываем всех отдельных JAR в classpath спарка, а пускай весь пакет патентованной логики Locomizer (и наших вспомогательных операций) собирается в один fat JAR. Заодно и локально можно будет теперь запускать, без кластера. И что важно, логику по разбору tasks.ini можно будет перенести из обвязки на PowerShell в код на Java, где подстановку переменных сделать существенно проще.

Коллеги поржали над предложением назвать проект «Кольцом всевластья», — One Ring, — но чуточка здорового пафоса никогда не помешает.

Улучив момент очередного раунда бесконечных согласований ТЗ по фронту, я собрал все модули в кучу. Maven — инструмент достаточно продвинутый, чтобы разрулить зависимости в многомодульном проекте, так что получилось вычистить последние куски дублирующегося кода, унифицировать версии всех библиотек, и сделать варианты билдов для локального и облачного окружений. Притом каждый модуль остаётся в своём собственном подпроекте, и его автор может работать над ним достаточно независимо, не мешая остальным.

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

С единой логикой обработки параметров оказалось возможным сделать внятную единую объектную модель для конфигурации модуля, и сделать нормальные проверки на валидность и консистентность конфигураций модулей между собой в рамках одного процесса. Особенно это важно с датасетами в формате CSV — контроль количества и порядка полей в каждой записи RDD, а также правильность передачи самого датасета с выхода одного модуля на вход нескольких последующих ложатся целиком на вызывающую сторону. И если таковая точка контроля одна, то её уже можно сделать хорошо.
Почему мы не поднимаемся уровнем выше и работаем с RDD, а не датафреймами? По той же причине, что и не используем Spark SQL. Но кроме того, реализация на Spark — это конечный, финальный этап кода, который начинается с white paper, полностью отлаживается на Python, и только потом в несколько шагов оптимизируется до наиболее производительного варианта. А чем ближе к примитивам базовой библиотеки, тем обычно быстрее работает код.

… если у разработчика руки растут из плеч и голова светлая. Теоретически.

Получается, что в наших условиях гораздо проще строку исходного CSV гонять в виде компактного хадуповского нативного Text (под капотом это просто массив байтов), и описывать только те колонки, которые знает текущая операция, и только для неё. Также, по результатам экспериментов датафреймы дают больший оверхед по потреблению памяти, чем необходимость на входе каждой операции парсить CSV, и на выходе сжимать обратно в Text. Ну и ещё — нам важно сохранять возможность вручную партиционировать промежуточные RDD после каждого шага, потому что к ним могут примешиваться новые датасеты из хранилища (на схеме это отлично видно), так что всё равно приходится спускаться уровнем ниже, как бы ни хотелось остаться на уровне логики white paper.

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

Сам файл конфигурации tasks.ini из разнородного набора параметров для каждого модуля сразу же превратился в программу на своеобразном декларативном языке программирования. Не особо красивом, но внутренне логичном и относительно человеко-понятном. Добить его до настоящего DSL с собственным синтаксисом — не проблема, но я этого не стал делать за ненадобностью. Но чуть позже всё же добавил представление в JSON для будущего фронта с визуальным редактором.

Короткозамкнутый процесс в среднем получил очередное трёх-пятикратное ускорение по сравнению с цепочкой отдельных вызовов Spark jobs.

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

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

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

А вот чего не получилось, так это фронта. Старый локомайзеровский Web UI безнадёжно устарел, новый японский мы так и не сумели допилить до вменяемого состояния до того, как они от него полностью отказались. Да и сам код бэкэнда этого UI, написанный задней левой ногой в тёмную октябрьскую ночь, я так и не смог до конца причесать просто из-за большого объёма.

Оптимизация и геокатарсис с Uber H3


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

Но я наконец-то избавился от необходимости поддерживать бэк у фронта с его богомерзссским, голм, голм, спрингом. (Это моё персональное мнение. EE я не люблю лишь чуточку меньше, потому что в ней меньше автомагии и неявных умолчаний; так-то оно без разницы, поверх какой страшной энтерпрайзной гадости писать RESTы).

Появилось время заглянуть внутрь каждого модуля с пристрастием.
Роль техлида на проекте — в том числе и надзор за практикой программистов, а не только выбор и одобрение технологий. Но во время ревью обычно смотришь только на самый верхний уровень кода, с точки зрения того, понятно ли он написан, и лёгок ли будет в дальнейшей поддержке. Заворачиваешь только какие-то явные глупости или очевидные логические ошибки. Закапываться в уровень прагматики с оптимизацией — это значит полностью повторить ход мыслей разработчика и сделать его работу повторно. Заставлять переписывать легаси в ущерб нуждам бизнеса, потому что оно ужасное с технической точки зрения — тоже не задача техлида.
Не то что бы я невнимательно смотрел на код коллег раньше. Просто каждый занимается данной ему задачей, и пока она выполняется им с желаемым результатом, не стоит мешать разработчику делать его работу. Если алгоритм работает правильно, и это подтверждено тестами, то всё хорошо. По скорости работы, — приемлемая она, или иначе, — решение принимает ПМ.

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

Но иногда технический долг надо разгрести, чтобы он не похоронил проект под своим весом.

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

Языки высокого уровня — они такие, заставляют мыслить абстрактно.

Но в то же время разработчику стоит помнить и о низком уровне, сколь бы в высокие абстракции он ни воспарил. Например, что любая лямбда, переданная в метод .map(), внутри которой происходит аллокация памяти под некоторый жирный объект, вызывается заново для каждой записи, и заново аллоцирует этот самый объект, а никакая из имеющихся JVM не любит жирных повторных аллокаций.

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

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

Когда я туда нырнул, в составе One Ring было 29 операций (некоторые модули содержат больше одной). Когда вынырнул — 43, причём каждая быстрее, чем исходные, от нескольких процентов до десятков раз. Но что более ценно, те операции, которые раньше давились данными на партициях в 10 000 элементов, теперь запросто пережёвывали куски в миллион записей. Кое-где для этого пришлось пожертвовать гибкостью и читаемостью кода, кое-где обошлось простой заменой .map() на .mapPartition(), но зато код перестал падать.

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

Такой вариант нашёлся — Uber H3.

Как я понимаю, в названии H3 зашифровано «hexagonal tree» — и это географическая сетка с замечательными свойствами. Она стабильная на всём диапазоне координат, чудовищно быстрая (вызывается нативный код), даёт ячейки равномерного размера без разрывов на всей суше, и позволяет делать кучу разных вариантов покрытия полигонов, точек и путей. Также у ячейки шестиугольной сетки минимальное количество соседей, и следующий уровень покрывает семь ячеек предыдущего строго над центром нижележащей ячейки, что удобно при построении агрегирующих карт.

С переходом на H3, кажется, пазл полностью сложился.

Если сравнивать с тем, что было в начале, — 2.5 года назад, — то с недель, которые тратились на одну несчастную тепловую карту по датасету до пары миллионов сигналов, мы пришли к минутам, которые тратятся на десятки карт с датасетами, на размер которых дата-аналитик уже и не особенно обращает внимание (приходится ворчать, когда он выставляет слишком высокий пресет для размера кластера, если запись результата в S3 занимает больше времени, чем сам расчёт). Да и в сам TC он уже не смотрит, а просто забивает матрицу параметров куда-то там у себя, и питоном дёргает нужное количество нужных билдов.

Добавить новую операцию — нужно просто правильно имплементировать класс Operation (можно и на Scala, если хочется), обвесить её метаданными, включить в свой конфиг, а дальше One Ring само разберётся, правильно ли вы вызываете новую эвристику или обработку в составе цепочки.

Ну и работает это всё как локально, так и в AWS. В каком-нибудь другом облаке тоже будет, если оно поддерживает S3, а Spark там можно дёрнуть через Livy — а от всех других внешних зависимостей мы избавились.

Выход во всём белом


— Гэндальф?!

Но фронта для запуска гибких процессов у нас по-прежнему нет. И сами шаблоны таких процессов приходится писать дедовским методом — руками в VSCode, а хотелось быть набрасывать мышкой в редакторе, похожем на Visio. Примерно таком:

Мокап интерфейса для редактирования процесса

Я даже сделал небольшой REST сервис в составе One Ring, который имеет всё необходимое, чтобы такой редактор написать, но последний раз фронтом я занимался лет 10 назад, и не в курсах нынешних трендов. Не на JSF же мне его склепать, эт будет даже не ретро, а уже какое-то некро. Неплохо бы сделать его статическим SPA на чём-нибудь современном. Только я понятия не имею, на чём.

Мой шкурныйличный интерес к раскрытию кода One Ring (репозиторий я ещё добью контентом, но смотреть можно уже сейчас), надеюсь, ясен. И если найдётся кто-нибудь достаточно смелый, чтобы заняться этой задачей, я напишу вменяемое техническое задание со спецификациями.
Ну а в целом нам, команде data engineers, совершенно не хочется держать готовый инструмент у себя в чулане. Мы уверены: он пригодится не только нам. И не только для нужд GIS, а вообще любого burst-процессинга датасетов с параметризуемыми шагами обработки.
В заключительной статье (или паре статей, опять что-то слишком длинно получается) я расскажу, как собирать, запускать, расширять и использовать One Ring для ваших исследовательских задач.

* Исходный код One Ring OSS не включает несвободные патентованные алгоритмы эвристик Locomizer. Но его репозиторий будет содержать интерфейсы и описания, по которым могут быть воссозданы свободные имплементации этих эвристик методом clean room, то есть, без подсказок с моей стороны по коду.

Благодарности


… коллегам Григорию pomadchin за существенные замечания по сути предмета, и sshikov за независимую оценку читаемости текста, а также Антону dartov Задорожному за неожиданный фидбэк по предыдущей статье серии.
Tags:javaapache sparkawsaws rdsaws s3aws emrmavenалгоритмыалгоритмы обработки данныхэвристические алгоритмыпостмортем
Hubs: Abnormal programming Programming Java Geoinformation services Big Data
+6
2.6k 24
Comments 38
Popular right now