Привет, меня зовут Максим Дьяков, я основатель сообщества Russian Hackers - рассказываем о хакатонам, помогаем разработчикам идти вверх по карьеной лестнице, а стартапам покорять луну. А еще мы пилим свой SaaS - HackeR, платформа для организации хакатонов. В этой статье я расскажу, как для неё реализовывался новый функционал, поделюсь подходами и опытом, которые могут быть полезны при разработке и других продуктов.

"Откуда растут ноги"

Совсем недавно мы поводили хакатон BIGTARGET для компаний Лента и Microsoft, где как раз использовалась наша платформа, но так как для нас это был первый именно полноценный DS хакатон, нам было необходимо доработать нашу платформу - сделать LeaderBoard и расчет Score.

Если тебе интересно больше узнать о Data Science хакатонах, как организовать подобный ивент и как мы работали над BIGTARGET, то рекомендую сначала прочитать нашу статью - "Организация Data Science онлайн-хакатона: личный опыт, на чем сделать акценты и как организовать хакатон именно вам".

При разработке любого функционала мы стараемся соблюсти оптимальный баланс по стоимости, времени и качеству. Так как наш стек это Angular с кастомизированной библиотекой всех необходимых элементов и Node.JS в связке с MongoDB, то мы буквально как из конструктора смогли собрать LeaderBord и форму всего за 1 рабочий день. Главным нашим опасением была непосредственно обработка логики расчета метрики – мы имеем дело с датасетом на 270к+ строк, в хакатоне участвует 20+ команд, а хостимся мы на самом обычном одноядерном сервере-картошке за 400р/месяц.

Вот так выглядела форма для отправки решений у участников - все минималистично и просто, а главное понятно и работает.

Оценив ситуацию, мы решили действовать итеративно:

Этап 1 - завести “хоть как-то”

Node.JS позволяет достаточно просто запускать python скрипты и считывать результаты, поэтому в первой реализации мы просто сохраняли полученный файл с результатами на сервер, вызывали скрипт, забирали результат, записывали его в БД и возвращали Score на клиент.

metric.calculate = function(filename, callback) {
    const python = spawn('python3', ['metrics/metrics.py', filename]);
    let result;
    python.stdout.on('data', function (data) {
        console.log('Recieving data ...');
        result = data.toString();
    });
    python.on('close', (code) => {
        console.log(`Child process is finished with code: ${code}`);
        callback(JSON.parse(result))
    });
}
Пара строк кода и вы можете использовать скрипты на python в своем JS-проекте

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

Протестировав эту реализацию на нашем сервере, мы увидели, что хотя все обрабатывается в довольно приемлемое время – около 20 секунд на запрос, мы можем «упасть» в последние минуты перед дедлайном: переполнение кэша (файлы с решениями весят по 10мб), непредвиденные ошибки и т.д.

Этап 2 - “лучше одной картошки - ведро картошки”

Следующим этапом было поднятия кластера – одновременно на каждом доступном ядре запускается независимая копия приложения. Мы используем процессный менеджер PM2 от keymetrics.io (максимально рекомендую решение всем старатаперам), который позволяет достаточно просто управлять продакшеном и автоматически развертывать и настраивать кластер.

Когда я говорю, что поднять кластер просто - это значит, что это действительно просто!

Это могло бы быть решением, но у нашего облачного провайдера Vscale.io (дочка Selectel) можно было арендовать сервер только с 4 ядрами, а автоматической миграции на сам Selectel не предусмотрено. Поэтому, хоть мы и апскейнули сервер, 100% уверенности у нас не было.

Кстати, мы не немного пропустили момент обновления записей в нашей БД – так как у нас документо-ориентированная база, нужно максимально аккуратно относиться к обновлению документов: одна команда одновременно отправила с нескольких компьютеров свое решение, попала на разные ядра кластера, что привело к сохранению в базу только последнего обработанного результата (результаты хранились как вложенный массив в документе, поэтому для добавления нужно было сначала считать документ, обработать его локально и заново сохранить). Но мы быстро узнали об этом и пофиксили буквально за пару минут, а дальше пристально мониторили ситуацию по логам.
Пока один старатап разворачивал архитектуру с SQL, второй вышел на IPO и стал единорогом - выбирайте тот стек, который позволит вам быть гибкими и быстрыми. 

Этап 3 - “python в сделку не входил...”

И так, у нас есть python скрипт с логикой расчета метрики, который запускается локально командой из Node.js. Как нам сделать этот процесс максимально отказоустойчивым и безопасным для продакшена в целом. Идеи, предложения?

Cloud Functions! Это просто идеальное решение для подобных задач. Вы берете и с минимальными доработками заливаете свой скрипт и получаете готовое решение, над которым можно издеваться как угодно.

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

Итак, наш итоговый сетап:

  • Загрузка решения – фронтенд Angular
  • Отправка решения на кластер: на 4 ядра, Node.js (Express), PM2, Nginx
  • Проверка пользователя и отправка на решения Azure Cloud Function
  • Сохранение результатов в DB и возвращение Score на клиент

По итогу все прошло без каких-либо проблем и только с 1 багом, описанным ранее: система спокойно переварила 281 решения общим объемом 1,9 ГБ. Скорее всего мы где-то слишком сильно перестраховались, где-то архитектуру можно было сделать более оптимальной, и т.д. Но! Вся реализация заняла всего 1 день работы full-stack разработчика и 3 часа специалиста из Microsoft. И я считают, если ты стартап, время - это самый главный ресурс, который нужно утилизировать с максимальной эффективностью.

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

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