12 мая 2016 в 10:50

Эволюция сервера приложений на NodeJS

В нашей системе мирно сосуществуют 2 сервера. Основной сервер(ядро), написанный на JAVA и сервер приложений — NodeJS, именно ему и посвящена данная статья.
Изначально у сервера приложений существовало 2 фундаментальные задачи:

1) проксирование запросов к основному серверу для того, чтобы уменьшить неспецифичную нагрузку и сэкономить ресурсы для решения более важных задач;
2) реализация client-specific функционала для того, чтобы не пришлось вносить изменения в код ядра при появлении клиентских “хотелок”.

Строго говоря, наличие сервера приложений вовсе не обязательно для функционирования системы, т.к. ядро имеет полноценное REST API, реализующее весь основной функционал системы. Несколько слов о протоколе. RTLSCP (real track location system communication protocol) – протокол, работающий поверх HTTP и позволяющий получать данные и выполнять базовые операции с системой RealTrac с использованием запросов и ответов в формате JSON/KML/PNG.
Базовая часть RTLSCP делится на 3 подмножества:

● RTLSCP REST API (синхронные ресурсы)
● RTLSCP WebSocket API (потоковые данные)
● RTLSCP Asynchronous API (асинхронные команды)

Таким образом, любой желающий может запилить свой сервер приложений или подключаться к ядру напрямую и пользоваться базовыми возможностями системы. Но таких энтузиастов оказалось мало. Поэтому, в качестве четвертого подмножества выступает RTLSCP Ext – расширение протокола, реализуемое сервером приложений со специфичным для клиента функционалом.
Большую часть клиентов интересовала не только информация, поступающая в режиме реального времени, ведь для использования в бизнес процессах довольно часто используются массив данных за определенный промежуток времени, или проще говоря, история. В связи с этим значительную часть функционала RTLSCP Ext составляла именно работа с сохранением и пред/постобработкой данных под нужды конкретного заказчика. Необходимость хранить историю привела к появлению БД на стороне сервера приложений. Время шло, внутренние структуры данных менялись, появлялись новые. Изменялся формат хранения данных, клиентские потребности росли. Ведь всем хочется, чтобы отчеты собирались быстрее и при этом занимали меньше места. Все это приводило к усложнению структуры базы данных, архитектуры сервера и усложнению ее поддержки. Повышение объемов обрабатываемых данных приводило к увеличению нагрузки на сервер, а однопоточность NodeJS приводила к замедлению работы сервера, и никакая асинхронность тут уже не спасала.
На первых порах проблемы производительности решались путем запуска дополнительных воркеров под определенные задачи. Для этого использовался стандартный модуль cluster. Поначалу это помогало и какое-то время все были довольны.
Позже пришло понимание того, что этого нам мало и появилось желание распараллеливать выполнение ряда задач. Но существующая архитектура не позволяла сделать подобное нахаляву. Также, частенько стали возникать ситуации, в которых приходилось заботиться о синхронизации данных между запускаемыми воркерами, да и обмен большими объемами данных проходил не очень гладко… Поддерживать все это разношерстное и весьма громоздкое “добро” было очень лениво накладно.
В связи с этими и некоторыми другими фундаментальными проблемами, не так давно было принято решение изменить архитектуру системы в целом и сервера приложений в частности.
Исходя из сложившейся ситуации к новой архитектуре были предъявлены следующие требования:

1. простота распараллеливания выполняемых задач,
2. модульная архитектура системы.

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

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

Философия или модель акторов предполагает, что все вокруг является актором, что частично схоже в моделью ООП, но предполагается, что последовательность создания акторов или обмена сообщениями между ними не определена.
В процессе своей жизнедеятельности актор, на основании поступивших данных, производит некоторые вычисления и продуцирует новую порцию данных, в этом они больше похожи на “чистые функции”, чем на объекты. Основное достоинство акторов ˗ это конечность действий и параллелизм. В идеале актор является сущностью, которая ни от чего не зависит и ничего не знает об окружении, в котором работает.
Обмен данными между акторами происходит посредством обмена сообщениями через общую шину. Сообщения могут быть адресными, для этого актор должен знать реквизиты адресата, или широковещательными.
Архитектура, управляемая событиями, экстремально слабо связана и хорошо распределена. При этом имеет асинхронную природу, поскольку порядок обработки или доставки сообщений никак не регламентируется.

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

PS: Как по мне, микромодульная архитектура является перспективным подходом для реализации проектов на Node.js и не только. Данный подход позволяет писать более простой и прозрачный код. Не стоит также забывать, что использование небольших модулей позволяет упростить процедуру написания тестов и ускорить локализацию ошибок.
Автор: @RTL-Service
ГК «РТЛ Сервис»
рейтинг 52,09
Компания прекратила активность на сайте

Комментарии (23)

  • 0
    А чего просто Akka не вкрутили?
    • 0
      Используем Vert.x. Он лучше подходит для решения наших задач.
      • 0
        Интересно чем Akka не подошла.
        • 0
          Не рискну за автора отвечать, но предположу:
          отсутствием нормальной библиотеки для http/web? Мы ведь понимаем, что spray — это классный прототип и блестящая идея, а akka-http до сих пор в каком-то не очень понятном состоянии?

          Вообще, надо сказать, мне как выходцу и Scala-фанатику это не очень приятно писать, но для того, что описал автор (быстрая реализация хотелок заказчика), связка Akka + Scala + Play 2 (или любой другой фреймворк на этом месте) очень дорогая связка. Мало того, что технологии не совсем мейнстримовые, и разработчиков на Scala надо не только найти, но еще и заставить придерживаться более-менее одного код-стайла, так еще и сам стек куда-то в непонятную сторону поворачивает — Typesafe переименовался в LightBend, Scala 2.12 где-то еще в разработке, Akka Http в каком-то невменяемом состоянии, в Play2 впилили !!runtime Dependency Injection.

          Оно конечно всё работает, но вот именно Скальность всего происходящего вызывает вопросы.
          • 0
            1. А что не так с runtime DI?
            2. Я в одном из проектов вообще использую Akka.Net + Asp.Net WebApi2 + SignalR + C# :)))) очень хорошо себя показывает. И русурсы найти не сложно. Главное за пул реквестами смотреть.
            • 0

              А Akka.Net уже вышел из беты?

              • 0
                1.1 уже на горизонте ;)
            • 0
              1. Почти ничего, кроме того, что Scala — это compile-time typesafe language, в котором этот самый typesafe чуть ли не самая продаваемая и продвигаемая фича, а в одном из самых популярных веб-фреймворков используется runtime DI :)
              • 0
                А можно поподробнее про compile-time typesafe language :) И что вы вкладываете в понятие type-safety?
                • +1
                  Насколько я понимаю, вы из .net?
                  В JVM есть такая приятная особенность — type erasure. Это значит, что информация о типах из дженериков вся теряется. Это справедливо для любого JVM-based языка. Вот пример из Java: http://stackoverflow.com/a/339708/1040070

                  Соответственно все штуки, которыми так богата Scala, вроде type constraints (http://stackoverflow.com/a/4870084/1040070), path-dependent types (http://stackoverflow.com/questions/2693067/what-is-meant-by-scalas-path-dependent-types), и тотальное выведение-приведение-проверка типов происходят во время компиляции. В runtime типы дженериков затерты. В Scala есть механизм typetags/classtags, который позволяет держать информацию о типах в runtime, но это скорее опция, и повсеместно оно не используется.
                  В общем всё это здорово работает и проверяется ровно до того момента, пока вместе компилируется.

                  Runtime DI подразумевает в свою очередь внедрения кода во время исполнения. Этот код также может быть написан на Scala и быть проверен тайпчекером, но результаты могут оказаться различными — например поменяли List[User] -> List[Profile]. Это вроде бы не сильно тревожно, если у нас полтора модуля для DI, но если мы собираем половину приложения через DI, то как-то это уже начинает не внушать оптимизма. При том, что за все эти тайпчеки мы по-прежнему платим временем во время сборки проекта, да и вообще, нафига городить все эти конструкции из безупречно-типизированного кода, который чуть ли не сам себя пишет (привет макросы из 2.12), если в итоге в рантайме мы будем втаскивать зависимости?

                  Всё конечно не так плохо, и Scala по прежнему работает, но, кажется последнее время свернула со своего труе-пути и пошла рука об руку с Java. А с учетом нововведений Java 8 возникает логичный вопрос — зачем всё вот это вот хозяйство, если в рантайме у нас зависимости, разработчиков всё еще не так просто найти, если есть Java 8, которая перекрывает 70% того, что есть в Scala, никуда не денется послезавтра и разработчиков под которую можно найти в любом селе необъятной?
                  • 0
                    Да, в основном .Net. Но в гетерогенном окружении, поэтому и с Java то же сталкиваюсь, читаю, портирую и тд.
                    Спасибо большое за ликбез :)
                    Согласен про Java 8. Даже стал присматриваться на тему переезда если core clr зафакапят.
        • 0
          Нужна была легковесная платформа, которая бы нативно поддерживала и java и js/nodejs. Где java-часть взяла бы на себя тяжелые вычисления и базовый функционал, а nodejs-часть была бы легко/дешево модифицируема под нужды конечного заказчика.
      • 0
        Можно и Vert.x. Просто странно при наличии Java разработчиков не использовать Akka или Vert.x, а зачем-то рисовать Actor Model на Node.js…
        • 0
          Часть прикладных задач проще решать написав модуль на Node.
          • 0
            А почему не Ruby? Python?
        • 0
          Основной сервер (написанный на java) крутится на Vert.x шаря часть шины наружу на которую уже вешаются (событийно) модули написанные на nodejs и которые реализуют дополнительный функционал. Итого вычислительно тяжелую (и медленно меняющуюся по коду/функционалу) часть имеем на java с которой прозрачно интегрируется клиент-специфик часть написанная на nodejs. Обе части объедены в единую информационную экосистему шиной данных от Vert.x. Итого получается некий неоднородный java/nodejs кластер.
  • 0
    Извиняюсь, возможно за глупый вопрос.
    >>обменивающихся между собой сообщениями через общую глобальную шину
    что имеется ввиду, когда говорится о глобальной шине?

    И все же хотелось бы подробнее узнать про эти акторы на nodeJS. Ваша статья и гугление немного вогнало в тупик.
    • 0
      Возможно, имеется в виду 0mq или rabbitmq.
    • 0
      что имеется ввиду, когда говорится о глобальной шине?

      Vertx-eventbus

      Модули NodeJS оформляются таким образом, чтобы не иметь зависимостей (в идеале вообще ни от кого). Обмениваются данными они не напрямую, а путем отправки сообщений через общую шину. Такие модули и называются акторами. При запуске инстанции конфигом определяется какие именно модули надо грузить.

      Затем заворачиваем это все в кластер или запускаем на разных машинах и живем долго и счастливо.

  • +2
    Ойвей. Расскажите про акторы на ноде немного подробнее?
    Что это такое и с чем его едят в сферическом вакууме понятно. Непонятны технические подробности реализации этого добра на node.js.
    Насколько мне известно (а применительно к node.js я начал недавно в эту сторону копать), ничего толкового в духе erlang-а нету, причем по достаточно фундаментальным для node.js причинам. Прежде всего это пресловутая однопоточность выполнения JS. Не существует одного хорошего способа запустить thread: вроде бы был/есть node-threads-a-gogo, который через native код умеет делать что-то очень похожее на нити, но если присмотреться в багтрекер, то можно заметить, что многое добро из node.js в ней не работает (например TypedArrays). Вроде бы есть webworker-threads, но тоже не внушает оптимизма реализация, хотя штука, имхо, очень правильная и в духе ноды.

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

    В общем нужно больше деталей. node.js в этом плане достаточно подходящая штука для написания акторов, но очень многих системных вещей не хватает, плюс их реализация таки идет вразрез с основной идеей node.js
    • 0
      Cluster тоже не решение, потому что точно также блокирует интерпретатор

      А зачем писать на Node что-то блокирующее? JS под это в принципе не заточен. Любую действие надо выполнять максимально быстро, все операции должны быть асинхронными — это основа программирования на JS, будь то браузерное приложение или сервер.
      Достоинством акторов в данном случае является то, что мы можем запустить любое число инстанций сервера (используя например cluster) или разнести сервер на разные машины, которые абсолютно равны между собой (не считая мастера, который немного ровнее остальных).
      При этом код остается неизменным, нагрузка размазывается по всем инстансам, повышается надежность (умер процесс либо забили либо перезапустили, если возможно) и т.д.
      Надеюсь я ответил на вопрос.
      • 0
        Хороший вопрос)
        А если вычисления есть? Вытаскивать их из ноды и рулить внешним процессом? Причем я не про числодробилку, а про что-то достаточно простое, но не отрабатывающее в течении милисекунды.
        Для типичных веб-задач конечно всё это от лукавого, но тем не менее.

        Насколько я понимаю под «акторами» на node.js в итоге подразумевалась пачка нодовых процессов, запущенных через cluster, и общающихся через некий внешний event-bus?
        Тогда эту штуку конечно по-прежнему можно назвать акторами, но тогда и классы на Java акторы, потому что «получают сообщения» при вызове методов.
        • 0
          А если вычисления есть? Вытаскивать их из ноды и рулить внешним процессом?

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

          Насколько я понимаю под «акторами» на node.js в итоге подразумевалась пачка нодовых процессов, запущенных через cluster, и общающихся через некий внешний event-bus?

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

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка