К релизу нового корпоративного решения ONLYOFFICE Enterprise Edition мы переписали код серверной части наших онлайн-редакторов на Node.JS и теперь, собственно, хотим поделиться опытом освобождения от ASP.Net, в ловушке которого мы оказались еще пять лет назад.

Переход на Node.JS стал логичным продолжением развития облачного офиса на Linux. Первая версия для него появилась почти год назад — тогда мы приняли решение использовать проект Mono. О проблемах, возникших при портировании на Mono системы для совместной работы, мы уже рассказывали. На тот момент работа над редакторами для Linux'а только начиналась. Сначала вышла бета-версия ONLYOFFICE Document Server, также написанная с использованием Mono. Сейчас она доступна в open source версии 3.0.

В новое серверное решение ONLYOFFICE Enterprise Edition мы включили обновленные редакторы ONLYOFFICE Document Editors 3.5, уже на Node.JS. Почему, как и что получилось расскажем далее.

image

Мотивы ухода от Mono

1. Снижение числа багов

Использование чужого продукта, пусть и хорошего (сейчас мы говорим о Mono), чревато размножением багов в геометрической прогрессии. Мы запускаем свой код, условно говоря, в «чёрный ящик», в котором с ним происходит почти всё что угодно. Число наших багов умножается на число багов чужого продукта и, как результат, начинается бесконечная борьба за стабильность, которую часто приходится начинать заново с выходом очередных (и внеочередных) обновлений.

2. Кроссплатформенность

Нам был нужен кроссплатформенный код для десктопных редакторов, работающих на всех ОС, в том числе и на мобильных. Совсем недавно вышло приложение ONLYOFFICE Documents для iOS с возможностью редактирования текстов и просмотра таблиц, pdf-файлов и презентаций. Останавливаться на этом мы не планируем, однако о планах позднее.

В общем и целом, мы осознали, что ASP.Net и Mono перестали отвечать нашим потребностям, и задались вопросом: на чем писать дальше?

Почему Node.JS?

Рассмотрев несколько вариантов (среди которых Ruby, Java, Python, Node.JS), мы выбрали последний. Причины выбора достаточно очевидны: у нас уже был неплохой опыт работы с этой платформой — на Node.JS написаны серверные части редакторов: службы совместного редактирования (CoAuthoring) и проверки орфографии (SpellChecker).

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

Сложности при переходе на Node.JS

1. Проблема с битовыми операциями

Что было: В JavaScript перед битовыми операциями число преобразуется в 32-bit signed int

Например, у нас был C# код:

var byteCount = Math.Min(5, data.Length - i);

ulong buffer = 0;

for (var j = 0; j < byteCount; ++j)

{

	buffer = (buffer << 8) | data[i + j];

}


Проблема: В Node.JS после цикла переменная buffer становится 2^40 и содержит некорректное число.

Решение в Node.JS: Чтобы избежать «превращений», мы переписали подобные места без битовых операций

buffer = (buffer * 256) + data[i + j];


2. Проблема со скачиванием файла

Что было: Файлы скачиваются по ссылке при конвертации документов. На C# для скачивания использовался класс System.Net.HttpWebRequest.

Проблема: В Node.JS есть два стандартных модуля для веб-запросов — http и https. Интерфейсы модулей абсолютно одинаковы, и разработчику необходимо вручную выбирать, какой модуль использовать в каждом конкретном случае. При этом стандартные модули не поддерживают автоматический переход по адресу, указанному в заголовке переадресации, если сервер вернул статус 301 или 302.

Решение в Node.JS: Мы использовали дополнительный модуль, который оборачивает стандартные модули и автоматически решает проблему с выбором между http и https и редиректами. В Node.JS есть несколько подобных модулей — из них наш выбор пал на «request».

3. Проблема с обработкой запросов

Что было: В C# для обработки запросов мы создавали класс-наследник от IHttpAsyncHandler или IHttpHandler. Таким образом, вся обработка запросов была «из коробки».

Проблема: В Node.JS нет встроенных модулей для обработки запросов, поэтому её пришлось собирать «под себя».

Решение в Node.JS: Для обработки запросов в Node.JS мы используем модуль «express», а чтобы иметь доступ к телу POST запроса — модуль «body-parser».

Для обработки POST c multipart/form-data мы выбирали из множества модулей с похожей функциональностью. Возможные варианты (multiparty, busboy, formidable) отличаются интерфейсом, настройками, возможностью отключить запись временного файл в temp и поддержкой потоков. Мы выбрали «multiparty» — с его помощью можно напрямую записывать данные как поток в хранилище Amazon S3.

4. Проблема с особенностями языка и стандартных библиотек

Что было: Одним из главных плюсов C# является большая стандартная библиотека и разнообразный синтаксис.

Например, в C# можно передать параметр в функцию по ссылке (ref)

private int CreateIndexByOctetAndMovePosition(ref string data, int currentPosition, ref int[] index)


Проблема: Если переписать эту функцию на JavaScript “в лоб”, то строка, переданная в качестве аргумента, будет скопирована. При работе с большими строками это приведет к лишней трате процессорного времени на их копирование и увеличению потребления памяти.

Решение в Node.JS: Чтобы предотвратить копирование больших строк при передаче в функцию, пришлось либо переводить их в бинарный формат (Buffer), либо оборачивать в объект и передавать его.

Кроме того, в Node.JS отсутствуют «из коробки» функции для удаления непустых директорий, создания директорий без родительских поддиректорий, работа с xml и т.д. Их пришлось дописывать руками.

Результаты и планы

Мы получили новый сервер, не использующий ASP.Net, способный выдерживать большие нагрузки. Плюсы для конечного пользователя: он не падает и не висит. Плюсы для нас как разработчиков: он не падает и не висит.

Но, естественно, всё затевалось не только ради серверов на Node.JS — нам нужен был кроссплатформенный код, и мы его получили. Теперь наши редакторы смогут работать на любых ОС.

Сейчас готовятся к выпуску наши десктопные редакторы под Windows, MacOS и Linux (да, работа в браузерах также связана с некоторыми ограничениями, которые мы хотим преодолеть). Кроме того, мы продолжаем улучшать наши онлайн-редакторы. Например, совсем скоро ONLYOFFICE Documents для iOS, о котором мы писали выше, появятся редактор таблиц (сейчас в стадии тестирования) и редактор презентаций (там же). Редакторы под Android выйдут в 2016 году.

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


  1. Envy
    01.12.2015 14:35
    +12

    1. Снижение числа багов
    Использование чужого продукта, пусть и хорошего (сейчас мы говорим о Mono), чревато размножением багов в геометрической прогрессии. Мы запускаем свой код, условно говоря, в «чёрный ящик», в котором с ним происходит почти всё что угодно. Число наших багов умножается на число багов чужого продукта и, как результат, начинается бесконечная борьба за стабильность, которую часто приходится начинать заново с выходом очередных (и внеочередных) обновлений.

    В случае Node.JS вы не запускаете свой код в чёрном ящике? А учитывая уход от статическтой типизации.

    2. Кроссплатформенность

    Нам был нужен кроссплатформенный код для десктопных редакторов, работающих на всех ОС, в том числе и на мобильных. Совсем недавно вышло приложение ONLYOFFICE Documents для iOS с возможностью редактирования текстов и просмотра таблиц, pdf-файлов и презентаций. Останавливаться на этом мы не планируем, однако о планах позднее.

    В общем и целом, мы осознали, что ASP.Net и Mono перестали отвечать нашим потребностям, и задались вопросом: на чем писать дальше?

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


    1. xkorolx
      01.12.2015 17:31
      +1

      В случае Node.JS вы не запускаете свой код в чёрном ящике? А учитывая уход от статической типизации.

      Любой framework можно отчасти считать «чёрным ящиком». Но тут ситуация такая: изначально код писался под ASP.Net («черный ящик»), его портировали с помощью Mono («черный ящик»). В итоге получаем произведение «черных ящиков».
      Да, можно было править баги в самом Mono. Но когда выходит его новая версия и появляются новые?
      По поводу статической типизации — хотелось бы, но с другой стороны у нас основная часть редакторов была на JS (клиентская), поэтому привыкли.
      То чувство, когда выходит новая версия Mono
      image


      1. impwx
        01.12.2015 18:36
        +3

        А заюзать typescript для поддержки статической типизации не планируете?


        1. xkorolx
          01.12.2015 18:44

          В ближайшее время не планируем.


  1. k12th
    01.12.2015 14:44
    +3

    Кроме того, в Node.JS отсутствуют «из коробки» функции для удаления непустых директорий, создания директорий без родительских поддиректорий, работа с xml и т.д. Их пришлось дописывать руками.

    Точно так же, как с request и express, все это есть в npm. Почему дописывать руками?


    1. xkorolx
      01.12.2015 17:37

      Точно так же, как с request и express, все это есть в npm. Почему дописывать руками?

      Собственно мы и используем пакеты из npm (mkdirp, например).
      Про «дописывать руками» имелось ввиду, что «в коробке» c Node.JS не идет.


      1. k12th
        01.12.2015 18:19
        +1

        Понятно. Просто неудачно сформулировано, написали бы уж, что третьесторонние модули еще были для того и этого.


  1. Gorniv
    01.12.2015 15:10
    +4

    Я понимаю, что Вы уже давно это переписывали, но Вы смотрели в сторону ASP.NET 5 (Vnext)?
    Там должно быть меньше багов, ведь реализация идет от Microsoft?


    1. Razaz
      01.12.2015 15:18
      +2

      Там еще и производительность обещают огогошеньки какую https://github.com/aspnet/benchmarks.


    1. xkorolx
      01.12.2015 17:44

      Я понимаю, что Вы уже давно это переписывали, но Вы смотрели в сторону ASP.NET 5 (Vnext)?
      Там должно быть меньше багов, ведь реализация идет от Microsoft?

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


      1. Razaz
        01.12.2015 21:58

        В смысле заложили?


        1. xkorolx
          01.12.2015 23:10

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


          1. Razaz
            02.12.2015 12:14

            Вы стэйт в памяти держали? Ну это и на Asp.Net можно было легко решить :)


            1. xkorolx
              02.12.2015 14:03

              Что-то держали в памяти, что-то в базе. Но самое главное, что мы держали большое число соединений (изначально использовали Node.JS на сервере совместного редактирования) и не могли перекидывать сообщения для пользователей одного документа между серверами. Теперь можем, благодаря Rabbit.
              И да, сколько одновременных соединений может держать Asp.Net?


              1. Gorniv
                02.12.2015 16:04

                А каких конкретно соединениях идет речь?
                И почему нельзя использовать Rabbit с ASP.NET так же как Node.JS?


                1. xkorolx
                  02.12.2015 16:24

                  Речь о concurrent connections. Вот, например, статья на эту тему
                  Я и не говорил, что Rabbit нельзя использовать с ASP.NET.

                  Я уже писал, что у нас изначально было два сервера:
                  — совместного редактирования, изначально написанный на Node.JS
                  — вторая часть, которая конвертировала, собирала, раздавала документы (он был на ASP.NET)
                  Так вот, мы не просто так транслировали на другой язык, но и добавляли необходимый функционал.


                  1. RaveNoX
                    02.12.2015 16:35

                    В моём проекте мы держим порядка 20к на 4-ядерном xeon + HT, 8GB ram, ASP.Net MVC + Signalr


                  1. Razaz
                    02.12.2015 17:02
                    +1

                    Кто мешает попробовать на Asp.Net 5? Opt-In Async/IOCP(Привет неблокирующий IO) и похудевший HttpContext(Раньше был около 30 кб минимум, а сейчас минимум 2кб), плюс хостить можете очень гибко(Тут уже кто-то биндинги для proxygen пилит). Выше уже скидывал бенчмарки. Думаю с релизом кто-нибудь попробует сделать аналог этого коня в вакууме.

                    С приходом Owin Asp.Net превратился в ту же Ноду на C#. Только шансов, что кто-то психанет, форкнет и оттянет часть ресурсов разработки меньше :) Странно, что вы его не иcпользовали. Могли бы на Nancy сделать бодрый сервер поверх HttpListener.

                    Это конечно ваш выбор, но аргументы какие-то натянутые в статье. Тот же SO как-то тянет нагрузку вполне небольшим кол-вом серверов.


  1. PQR
    01.12.2015 15:41
    +13

    Статья хорошая, всё по полочкам, но аргументы не показались мне достаточными, чтобы сменить .NET/Mono на Node.js. Вы жаловались на баги и «чёрный ящик» платформы, но Node.js тоже не идеален — это такой же «чёрный ящик» со своими утечками, багами и однопоточностью.

    Плюс вы потеряли статическую типизацию и богатую и удобную библиотеку .NET (о чём сами и написали). Знаю, что статическую типизацию в какой-то степени можно вернуть с помощью TypeScript, а .NET библиотеку заменить отдельными npm пакетами, но используете ли вы TypeScript? И, опять же, на сколько используемые npm пакеты хороши и содержат меньше багов по сравнению с .NET/Mono?

    Как отметил Envy в первом комментарии, складывается впечатление о погоне за модой, а не о хладнокровном расчёте.

    Из оставшихся упомянутых вариантов:
    Ruby — медленно и динамически типизированно;
    Java — те же яйца, что и .NET/Mono, только в профиль, хотя, может меньше багов чем в Mono?
    Python — медленно и динамически типизированно;

    Можно было бы расширить список кросплатформенных языков, которые на слуху:
    Go — быстро, статически типизированно, но сыро на мобильных
    Rust — быстро, статически типизированно, но сложно и не знаю, работает ли оно на мобильных вообще?
    C++ — быстро, статически типизированно, работает везде, но сложно++

    На мой вкус, выходит либо Java/Go — если хочется попроще, либо Rust/C++ если команда готова на это.
    Ваши конкуренты из Мой Офис потянули и сделали всё кросплатформенно на C++/Qt habrahabr.ru/company/ncloudtech/blog/263719

    Интересно было бы обсудить эти или другие варианты — комментируйте!


    1. xkorolx
      01.12.2015 18:43
      -2

      Да, потеряли типизацию. Но как я уже писал, у нас основная часть редакторов была на JS (клиентская), поэтому привыкли. TypeScript — не используем, пишем на чистом JS.

      И, опять же, на сколько используемые npm пакеты хороши и содержат меньше багов по сравнению с .NET/Mono?

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

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


    1. justmara
      02.12.2015 08:30

      Автор плачется про "баги" и "чёрный ящик" в контексте проекта, в котором нет ни одного теста. Да что там тестов — комментариев даже нет!
      Это хорошо, конечно, что вы такой развёрнутый ответ написали. Вот только не по адресу, боюсь.


      1. Marazmatik
        02.12.2015 14:06
        +2

        От лица отдела тестирования хочу отметить, что у нас около 30 тысяч тестов, которые лежат в отдельном репозитории. Комменты просто вырезали для публикации


        1. justmara
          02.12.2015 18:46
          +4

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


  1. alemiks
    01.12.2015 15:46
    +1

    можно было ничего не делать всё это время, а немного подождать, пока ms выкатит production ready своего кроссплатформенного аспнета ) Как раз ваш релиз совпал по времени с их


    1. impwx
      01.12.2015 15:54
      +7

      Ждем статью «От Node.JS обратно к ASP.NET vNext» :)


    1. leschenko
      02.12.2015 00:54
      +1

      Уже выпустили. ASP.NET 5 еще в статусе RC, но с пометной о том, что уже можно на продакшен.


      1. RaveNoX
        02.12.2015 01:17
        -1

        Ага и mvc в нём под Linux не работает


        1. Razaz
          02.12.2015 12:18

          А что там не работает? Там вроде на красношапке был какой-то косяк временный.


          1. RaveNoX
            02.12.2015 12:22

            github.com/aspnet/Home/issues/1093 проблема с OSX и Linux
            По поводу красношапки — она вообще в RC1 не поддерживается, о чём написано в информации по релизу


            1. Razaz
              02.12.2015 12:41

              Про нее как раз и читал. Так какая-то фигня с пакетами. А этот ишью уже пофиксили вроде github.com/aspnet/KestrelHttpServer/commit/e4fd91bb68f535801ca8a79aa453ea3fb3f448fe


  1. justmara
    01.12.2015 15:52
    +8

    Как мы писали-писали, нишмагли, чот взгрустнулось и мы решили переписать на модном.
    Аргументы против .net указаны просто смешные. Т.е. реально абсурдные. По некоторым косвенным признакам сложилось впечатление, что в c# у вас творился тот ещё говнокод и вместо вдумчивого рефакторинга вы решили переписать на модном-стильном-молодёжном.
    Чтож, увеличение энтропии — тоже похвальное занятие.


    1. xkorolx
      01.12.2015 19:00
      +2

      Самый главный аргумент — это кроссплатформенность. Да, согласен, портирование простого проекта и Mono потянет. На что-то сложное его не хватает.
      Наш C# код открытый, можно посмотреть вот тут — github.com/ONLYOFFICE/DocumentServer


      1. perfectdaemon
        02.12.2015 06:47
        +3

        Судя по всему, ваши C#-программисты раньше вовсю писали на C/C++. Венгерская нотация, «безопасные» и бесполезные в C# сравнения наоборот: if(true == oReader.Read()).

        Тем удивительнее выбор JS.


  1. Sevlyar
    01.12.2015 18:46
    -1

    Проблема: Если переписать эту функцию на JavaScript “в лоб”, то строка, переданная в качестве аргумента, будет скопирована. При работе с большими строками это приведет к лишней трате процессорного времени на их копирование и увеличению потребления памяти.

    Решение в Node.JS: Чтобы предотвратить копирование больших строк при передаче в функцию, пришлось либо переводить их в бинарный формат (Buffer), либо оборачивать в объект и передавать его.

    Раза 4 перечитал это место, так и не понял где «собака зарыта». В JS строки неизменяемые и передаются в функции по ссылке. При этом никакого копирования самого значения строки не происходит. Для чего их «оборачивать в объект»?


    1. olen
      01.12.2015 19:41

      Я тоже на этом моменте затормозил.

      > CreateIndexByOctetAndMovePosition(ref string data…

      Насколько я понял из статьи, ref используется для быстродействия? Возможно, я сейчас торможу, но, по моему, без ref не будет никакого копирования строки (речь про C#), т.к. строки являются классами. Разница только в том, что в функции с ref можно присвоить параметру другое значение.


  1. olen
    01.12.2015 19:46

    А как команда (программисты) приняли идею перехода с C# на JS? Ведь речь идет не про небольшой проект на другой технологии, которым интересно заняться ради общего развития. Тут получается, что переходишь с одной технологии на другую.

    P.S. Я так понял, что весь код портировали руками? Или есть какие-то тулзы?


    1. impwx
      01.12.2015 21:04

      Тулзы есть, но они подразумевают, что проект продолжает развиваться в виде .net-приложения, а js-версия является портом. Получаемый в результате JS-код очень сложно дебажить и абсолютно не резонно поддерживать.

      У меня на одном проекте потребовалось переписать приложение с Silverlight на HTML5, максимально сохранив исходную структуру кода. В итоге я потратил день на написание конвертера с помощью Roslyn, который обрабатывал все описания (классы, поля, сигнатуры методов и т.д.) — тела методов портировались все равно вручную. Это оказалось наиболее разумным компромиссом.


    1. xkorolx
      01.12.2015 21:51

      Не просто портировали, а переписывали руками. И это было достаточно волевым решением, несмотря на то, что наша команда и на C#, и на C++, и на JS умеет писать.


  1. Shablonarium
    02.12.2015 16:12
    +2

    Сколько программистов повесилось в процессе переезда?


    1. xkorolx
      03.12.2015 13:23

      Ни одного программиста не пострадало.