В этой статье я расскажу, как мы разрабатывали облачный сервис Scorocode, с какими проблемами столкнулись, и, что самое важное, поделюсь планами развития.
Небольшой опрос в конце статьи позволит читателям отдать голоса за планируемые в будущем функции, тем самым повлияв на стратегию развития сервиса.
Предпосылки
Уже с 2011 года я активно использовал Parse для проведения быстрых экспериментов с разработкой мобильных и web-приложений. Полезность сервиса не вызывала сомнений, лишь некоторые огрехи периодически вызывали желание найти что-то более удобное.
Со временем, попробовав несколько похожих сервисов, я пришел к следующим выводам:
- Backend как сервис нужен многим разработчикам систем в архитектуре «клиент-сервер», так как ускоряет работу в разы, особенно на начальном этапе, когда структура данных и серверная логика уже нужны, а ресурс для её разработки ограничен.
- Основной функционал такого сервиса: структурированное хранение и доступ к данным с помощью SDK для разных платформ и возможность разработки серверной логики. Дополнительный функционал, как сообщения sms/push/email, готовые объекты типа пользователи/роли, с одной стороны, может реализовываться разработчиками самостоятельно, с другой стороны, еще больше ускоряет работу и позволяет сосредоточиться на frontend.
- Нет ни одного сервиса с полноценной документацией на русском языке. Да, есть переводы кусков и небольшое количество примеров, но они не дают полного понимания возможностей и подводных камней сервиса.
В связи с этим возникла идея разработки подобного сервиса для русскоязычной аудитории, в котором можно было бы реализовать собственные пожелания и пожелания пользователей.
Историю о том, как мы в 2015 году проходили организационный путь от идеи до инвестпроекта опущу, об этом лучше расскажут мои коллеги. А вот на технической части остановлюсь подробнее.
Средства разработки
После определения минимума функций, которые нужно было реализовать, стали определяться со средствами разработки.
Вариант использования платного проприетарного ПО сразу было отметен в связи с его несостоятельностью. Основной причиной явилось то, что в течение последних 5-6 лет отрасль разработки ПО претерпела сильные качественные изменения в положительную сторону. Те задачи, которые раньше могли решаться исключительно с помощью платформ и средств «монстров» IT, сегодня могут быть решены быстро и эффективно с помощью современных средств разработки, языков программирования и платформ, большинство из которых распространяется под лицензией MIT.
Итак, мы сформировали перечень функций, и стали выбирать платформу, на которой будет работать основная часть сервиса – сервер API. По нашему мнению, один сервер должен был держать не менее 10 тысяч запросов в секунду, чтобы из таких серверов можно было собирать кластеры, выдерживающие нагрузку до 50 тысяч запросов в секунду. Это число появилось не случайно. В одной из разрабатываемых нами промышленных систем есть именно такие требования к нагрузке, и мы приняли его за отправную точку, с прицелом на перевод backend этой системы в облако (кстати, использование требований к этой же системе нам позволило рассчитать экономические выгоды от использования облачного backend).
В итоге было протестировано 3 варианта реализации API с форматом обмена JSON. Тестирование проводилось с помощью Яндекс.Танк. Результаты:
- Node.js + Express.js – 4 000 запросов в секунду
- Node.js + Total.js – 1 500 запросов в секунду
- Собственный сервер, написанный на Golang – 20 000 запросов в секунду
Добавлю, что в качестве СУБД была единогласно выбрана mongoDB, как современная, масштабируемая СУБД, выдерживающая большие нагрузки, с подробной и качественной документацией и большим числом примеров и драйверов под популярные языки программирования.
Выбор был сделан в пользу собственной разработки, и мы начали прорабатывать архитектуру.
Архитектура
Основной задачей при построении архитектуры сервиса была реализация масштабируемой кластерной системы. После экспериментов мы пришли к следующей конфигурации:
- Точка входа запросов к API – DNS Round Robin, распределяет вызовы между балансировщиками;
- Балансирировщик – Nginx, распределяет запросы между серверами API;
- Сервер API – собственная разработка на Golang, реализована в архитектуре MC (Model-Controller), каждый сервер получает данные о приложении их из основной базы данных (mongoDB), в том числе адрес кластера данных, в котором хранятся данные приложения, кэшируя эти данные (кэш в Redis – 10 минут, со сбросом при внесении изменений в приложение);
- Кластер данных – кластер mongoDB и инстанс Redis;
- Файловое хранилище – OpenStack Swift;
- Сервер очередей – RabbitMQ, используется для постановки в очередь заданий на запуск серверных скриптов, отправки сообщений и т.п.
- Микросервисы – собственные разработки на Golang: миграция с Parse, отправка сообщений (email, PUSH, SMS), выполнение серверного кода (модуль, использующий движок Google V8)
В процессе разработки и тестирования возникало много мелких задач, но в целом архитектура оказалась жизнеспособной и комплекс успешно прошел испытания.
Функции
Как я уже писал выше, на начальном этапе мы реализовали базовый функционал. Границы минимального набора базировались на необходимости миграции пользователей Parse и минимальными требованиями к функциям backend для разработки не очень сложных приложений.
В ходе реализации функциональности возникали серьёзные и не очень проблемы. Пару характерных привожу ниже.
Проблема 1. Скорость парсинга BSON.
Как вы знаете, mongoDB отдает данные в формате BSON, который достаточно просто разбирается и конвертируется в JSON. Тем не менее на больших объемах парсинг BSON занимает довольно приличное время. К примеру, на выборке 1000 документов среднего размера парсинг BSON в JSON занимает более 1,5 секунд. Для нас такая скорость была неприемлемой.
Попробовали полностью переписать парсер драйвера mgo.v2. Не помогло. Пришли к выводу, что уменьшить время можно было либо увеличением частоты и количества ядер на сервере, либо перекладыванием этой задачи на клиента.
В итоге было принято решение все выборки возвращать в формате BSON с последующим разбором в SDK на клиенте. Так это и работает по сей день.
Проблема 2. Скорость работы триггеров JavaScript.
Изначально движком, который будет выполнять серверные скрипты, был выбран Google V8, и он прекрасно справился со своей задачей для асинхронных скриптов. А вот с триггерами на операции с данными возникли проблемы.
Сам движок V8 очень шустрый, но стартует он относительно медленно – 150-300 миллисекунд. А у нас было ограничение на время работы триггера – 500 миллисекунд. Отдавать половину этого времени на старт движка было неразумно. Создавать пул заранее запущенных «воркеров» – нажить кучу проблем с переключением контекстов.
Поэтому для триггеров нами был выбран самый быстрый вариант для выполнения кода JavaScript в Golang – библиотека Otto Роберта Кримена. Да, у нее есть определенные ограничения, но для задачи выполнения триггеров она подошла идеально. На основе этой библиотеки нами был реализован «терминатор» стека вызовов для прерывания бесконечного цикла вызовов триггеров (например, когда в триггере beforeInsert
вызывается операция insert
).
О проблемах и задачах, возникающих в процессе реализации, можно писать бесконечно. Надеюсь, что аудитория сама укажет технические темы, о которых интересно было бы почитать, и я с удовольствием о них расскажу.
Что дальше?
Сейчас мы запланировали и начали работы над новыми функциями системы. Учитывая стабильно высокий уровень интереса к сервису Scorocode, мы бы хотели узнать мнение сообщества о необходимости реализации таких функций. Готовы ответить на все ваши вопросы в комментариях к статье.
Автор: Scorocode