Новый фронтенд Одноклассников: запуск React в Java. Часть I



    Многие слышали название GraalVM, но опробовать эту технологию в продакшене пока довелось не всем. Для Однокласснииков эта технология уже стала «священным Граалем», меняющим фронтенд.

    В этой статье я хочу рассказать о том, как нам удалось подружить Java и JavaScript, и начать миграцию в огромной системе с большим количеством legacy-кода, а так же как на этом пути помогает GraalVM.

    Во время написания статьи оказалось, что весь объём материала не влезает в традиционный для ХАБРа размер и если выложить публикацию целиком, то на её прочтение уйдет несколько часов. Поэтому мы решили разделить статью на 2 части.

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

    Предыстория


    Первая версия Одноклассников появилась 13 лет назад, в 2006 году. Сайт был сделан на .NET, никакого JavaScript тогда на сайте не было, всё было на серверном рендеринге.



    Через год у Одноклассников было свыше одного миллиона пользователей. В 2007 году это были невероятные цифры, и сайт, не выдержав нагрузки, начал падать. Разработчики решили проблему с помощью проекта One.lv, созданного латвийской компанией Forticom, у которой основные компетенции были в Java-разработке. Поэтому Одноклассники решено было переписать с .NET на Java.

    Со временем проект развивался, и возникла необходимость в новых клиентских решениях. Например, чтобы при навигации сайт обновлялся не полностью, а только определенные части. Или чтобы при сабмите обновлялась только форма, а не вся страница. При этом сайт работал только на Java.

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

    Конечно, без минимального JavaScript было не обойтись. Чтобы сделать pop-up, нужны манипуляции: например, по наведению курсора на div вешался display:block или он скрывался с помощью display:none.

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



    2018


    Спустя 12 лет Одноклассники превратились в гигантский сервис с более 70 миллионами пользователей. У нас больше 7 000 машин в 4 дата-центрах, и только на фронтенд OK.RU приходит 600 тысяч запросов в секунду.

    Фронт-сервер Одноклассников продолжает работать на Java, а кодовая база одних только фронтов превышает два миллиона строк.



    Технологии, реализуемые на клиентской стороне, тоже не стояли на месте: появилось много решений с использованием разных библиотек: GWT, jQuery, DotJs, RequireJS и многих других.

    В тот период не были распространены стандарты вроде React, Angular и Vue, каждый разработчик пытался найти оптимальное решение, используя все доступные средства.

    Стало понятно, что жить с этим очень трудно, потому что накопилось огромное количество проблем:

    • Много старых библиотек
    • Нет единого фреймворка
    • Нет изоморфности (поскольку бэкенд на Java, клиент на JS)
    • Нет единого структурированного приложения на клиенте
    • Плохая отзывчивость
    • Недостаточный инструментарий
    • Высокий порог входа

    В мире шел уже 2018-й год и необходимо было меняться.

    Применив всю силу технической мысли, мы продумали и сформулировали четыре основных требования к решению проблем:

    1. У Одноклассников должен быть изоморфный код для UI. Потому что невозможно постоянно писать сервер на Java, а потом, если необходимо добавить какую-то динамику, воспроизводить то же самое на клиенте.
    2. Необходим плавный переход. Потому что быстро сделать вторую версию Одноклассников и переключиться невозможно
    3. Обязательно нужен серверный рендеринг (Об этом ниже)
    4. Новое решение, работая на том же количестве железа, не должно ухудшать производительность и отказоустойчивость при наших нагрузках

    Почему серверный рендеринг?


    У Одноклассников много пользователей, которые живут далеко от Москвы и у них не всегда хороший интернет.



    Серверный рендеринг поможет таким пользователям быстрее получать контент. Пока картинки будут грузиться, они смогут начать что-то читать:



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

    Как работает сервер сейчас


    Браузер делает запрос на сайт ОК, и попадает на приложение OK-WEB, которое целиком написано на Java. Приложение идет за данными в API. Между WEB и API реализован быстрый бинарный транспорт one-nio, разработанный в Одноклассниках. Запросы осуществляются менее чем за одну миллисекунду. Можете посмотреть, что это такое отдельно. One-nio позволяет дешево делать много запросов, не беспокоясь о задержках.

    API достает данные, отдает вебу. Веб генерирует HTML-страницы движком на Java и отдает браузеру.

    Все это занимает сейчас меньше 200 мс.



    Поиск решения


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

    Приложения будут доставляться на сайт маленькими кусочками. Внутри они будут написаны на новом стеке. А для остального сайта это будет просто DOM-элемент с каким-то кастомным поведением.



    Это будет похоже на тег <video>: кастомный DOM-элемент с атрибутами, методами и событиями. В результате снаружи находится DOM API, а внутри реализуется функциональность виджета на новом стеке.

    Какой стек выбрать?


    Теперь концепцию необходимо было реализовать, стали перебирать варианты.

    Kotlin


    Первый прототип сделали на Kotlin. Идея заключалась в следующем: для новых компонентов писать логику на Kotlin, а разметку компонента описывать в XML. На сервере все можно запускать в JVM, используя существующий шаблонизатор, а для клиента транспайлить в JavaScript.



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

    Поэтому, к сожалению, от этой концепции пришлось отказаться.

    Node.js


    Другой вариант — поставить Node.js или другой рантайм, например, Dart. Но что получится?

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

    Однако, с этим подходом есть несколько проблем:

    1. Удалённый вызов Node.js предполагает сериализацию/десериализацию входных данных. Эти данные могут быть весьма объёмными, например в случае, когда новый компонент на JS является обёрткой вокруг старого компонента, реализованного на Java.
    2. Удалённый вызов, даже на локальной машине, является далеко не бесплатным, а также вносит дополнительную задержку. Если на странице будут десятки или сотни таких компонент, пусть даже очень простых, мы существенно увеличим накладные расходы и задержку на обработку запроса пользователя.
    3. Кроме того существенно усложняется эксплуатации подобной системы, так как вместо одного процесса нам надо было бы иметь процесс на Java и несколько процессов на Node.js. Соотвественно все операции становятся намного сложнее, например: развёртывание, сбор операционных показателей, анализ логов, мониторинг ошибок и т.д.

    Второй способ использования Node.js заключается в том, чтобы поставить его перед веб сервером на Java и использовать для пост-обработки HTML. Другими словами, это прокси, который разбирает HTML, находит компоненты на JS, отрисовывает их и возвращает пользователю готовый HTML. Вариант интересный, вроде бы универсальный и вполне рабочий. Недостатки такого подхода заключаются в том, что он требует основательного изменения всей инфраструктуры, существенно увеличивает накладные расходы и несёт в себе серьёзные риски – любой запрос должен проходить через Node.js, то есть мы начинам полностью от него зависеть. Это выглядит слишком дорогим решением для того, что бы решить нашу задачу.





    Получается, Node.js нельзя использовать по следующим причинам:

    • Сериализация/десериализация — это дополнительная нагрузка и задержки
    • Node.js это еще один компонент в огромной распределенной системе Одноклассников

    У нас уже работает много специалистов, знающих, как «готовить» Java, а теперь придется нанять штат сотрудников, которые будут эксплуатировать Node.js и в дополнение к существующей создать ещё одну инфраструктуру.

    JavaScript в JVM


    А что если попробовать запустить JavaScript внутри JVM? Получится, что код на Java и JavaScript будет исполняться в одном процессе и взаимодействовать с минимумом накладных расходов.

    Это позволит плавно заменять Java-куски на JavaScript внутри текущего WEB’а.
    JS-компоненты будут получать данные из Java и формировать HTML. Они смогут изоморфно работать как на клиенте, так и на сервере.

    Но как запустить JS в JVM?
    Можно использовать V8 по примеру Cloudflare. Но это — бинарный код, сторонний по отношению к Java. Поэтому в JVM невозможно будет отловить ошибки внутри V8. Любой креш V8 приведет к разрушению всего процесса. В результате использование V8 повысит риски эксплуатации, а этого допускать нельзя.

    Для JVM существует несколько JS-рантаймов: два «носорога», Nashorn и Rhino (один от Oracle, другой от Mozilla) и свежий GraalVM.



    Преимущества JS-рантаймов для JVM:

    • Все работает в JVM, а у нас в этом большая экспертиза
    • Бесплатное взаимодействие Java и JavaScript
    • Безопасный рантайм
    • Компилятор на Java в случае GraalVM

    Дальше достаточно было сравнить по скорости эти рантаймы. Оказалось, что GraalVM всех опережает с большим отрывом:



    Что такое GraalVM?


    GraalVM это рантайм высокой производительности, который поддерживает программы на разных языках. В нем есть фреймворк для написания компиляторов языков для JVM. Благодаря этому поддерживается выполнение программ на Java, Kotlin, JS, Python и других языках внутри одной JVM.

    Подробнее о возможностях GraalVM можно узнать из доклада Олега Шелаева, который работает в Oracle Labs, где разрабатывают GraalVM. Рекомендовано к посмотру бэкендерам и фронтендерам.

    GraalVM позволяет нам запустить JS для рендеринга UI на сервере. В качестве библиотеки мы используем React.

    Преимущества такой связки:

    • Не добавляется новых языков: по-прежнему Java и JavaScript
    • Большое сообщество: все знают React
    • Низкий порог входа
    • Легко искать коллег в команду
    • Эксплуатация не усложнилась

    Запуск React в GraalVM


    Внутри GraalVM можно создать Context — изолированный контейнер, в котором будет выполняться программа на гостевом языке. В нашем случае гостевым языком является JS:

    Context context = Context.create("js");
    
    // получаем global данного контекста
    Value js = context.getBindings("js");
    

    Для взаимодействия с контекстом используется его объект global:

    // можно записать в global
    js.putMember("serverProxy", serverProxy);
    
    // можно читать из global
    Value app = js.getMember("app");
    

    В контекст можно загрузить код модуля:

    // получаем метод загрузки кода
    Value load = js.getMember("load");
    
    // загружаем модуль в контекст
    load.execute(pathToModule);
    

    Или «за-eval-ить» там любой код:

    context.eval("js", someCode);
    



    Серверный рендеринг JS: концепт


    Создаем в JVM контекст JavaScript и загружаем в него код модуля приложения на React. Прокидываем из Java в JS нужные функции и методы. Затем из этого контекста извлекаем ссылку на JS функцию render() данного модуля, чтобы потом вызывать её из Java.



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



    Серверный рендеринг JS: реализация


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



    Зачем нужен пул контекстов


    Рендеринг компонента происходит синхронно в одном потоке. На это время JS-контекст рендеринга занят. Поэтому, создав несколько независимых контекстов, можно распараллелить рендеринг компонентов, используя возможности многопоточности Java.



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

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

    To be continued
    Одноклассники
    162,66
    Делимся экспертизой
    Поделиться публикацией

    Похожие публикации

    Комментарии 24

      +2
      хочется спросить насколько GraalVM опенсорсный
      +2
      ОООО, разработчики знают толк в извращениях.
      С любопытством жду продолжения

      ЗЫ интересно, получается, это свой серверный рендеринг, но внутри JVM. Любопытно. В копилку пункт по использованию graalvm
        +2
        Серверный рендеринг поможет таким пользователям быстрее получать контент. Пока картинки будут грузиться, они смогут начать что-то читать:


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


          Да, именно про это речь.
          –7
          Теперь понятно происхождение уже трех фантомных сообщений в переписке на ОК, и очень «логичное» поведение окна отправки ответного подарка (после отправки ответного подарка из ленты оповещений, закрывая окно отправки подарка (там имеется крестик) ты закрываешь и ленту оповещений (имеющую отдельный крестик)) причем разработчики считают такое поведение интерфейса абсолютно нормальным. Причем если в окне отправки ответного подарка открыть окно общения с техподдержкой, и закрыть её, то лента оповещений не закрывается.
          Короче говоря как на КДПВ (О сколько нам открытий чудных, готовит просвещенья дух)!
          image

          Надеюсь что баги таки отловят и исправят, а не превратят в гениальные фичи.
            –3
            Таки это разработчики ОК увидели описание бага и решили вместо его исправления заминусовать сообщение и в карму камень кинуть? Или это хейтеры Одноклассников? Ну бог всем судья и rm -rf /
            Кстати о птичках: Баг проявляется под всеми доступными мне ОС, и всеми доступными мне браузерами (с самыми актуальными версиями).
              –3
              В отличии от молчаливых господ минусующих, думающих что заминусовав описание ошибки они волшебным образом её исправят
              Это не те баги что вы ищете, это фичи - деплойте их в продакшн
              image

              Это не те баги что мы ищем! Это фичи! Деплоим их в продакшн!
              image

              Немедленно!
              image

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

            Опасно это в долгосрочной перспективе. Насколько я знаю, и nashorn, и rhino не поддерживаются уже. Не постигнет ли движок яваскрипта в граале та же участь? Что тогда будете делать?

              +1

              В данном случае движка js отдельно нет, ест только "описание" AST, а движок который обеспечивает работу всего этого называется truffle.

                +1
                По поводу Nashorn:

                github.com/graalvm/graaljs/blob/master/docs/user/NashornMigrationGuide.md

                никто просто так не будет выбрасывать наработки
                сдвинулись в стороны graal и truffle, так как реализовывать новые фичи из js мира достаточно ресурсоёмко, реализовывать с помощью truffle оказалось намного проще.
                +2

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


                Жду теперь реализации другой хотелки: мини-приложений (в стиле Amazon Lambda), компилируемых с помощью Native Image

                  0
                  Подход запуска js, но только в asp.net, использую лет 5. В основном для быстрых кастомизаций. А вот использование для рендеринга не знаю. Ваш подход напоминает Asp.net WebForms ). Фича ssr в том, что почти один и тот же код используется и на клиенте и в шаблонизаторе. Как реализовано у вас, я не понял. Если клиентский код и серверный код генерации html разный, то как по мне зря потрачено время
                    0

                    Об это подробно будет во второй части. Она готовится к публикации

                      0
                      Спасибо, ждем )
                    0

                    В 2006 существовал JavaScript.

                      0
                      Интересно было бы почитать про сравнение производительности, такого решения. Насколько я помню (https://github.com/graalvm/graaljs/issues/74), во многих кейсах компилируемый graal имеет намного более долгий старт, да и производительность слабее v8.
                        0
                        этот тикет оставлят неясное впечатлениие. насколько я понял прочитанное, разработчики graal не признали проблем с его производительностью.
                        тот пример долгого старта, что там указан, больше похож на запуск с недостаточным количеством памяти — для jvm в таком случае характерно пытаться непрерывно собирать мусор, что практически почти останавливает выполнение любой программы. На это часто напарываются начинающие пользователи JVM. Для пользователя, конечно, было бы удобнее увидеть нормальное сообщение, что не хватает памяти — но это больше вопрос не к graal, а к эргономике JVM.
                          0

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


                          В профильном чатике телеграмма много экспериментировали на эту тему. В некоторых тестах прогрев получался в разы дольше хотспотовского.

                            0
                            Тем не менее, если вы используете в проде, есть ли метрики в сравнении v8?
                            Очень интересно было бы почитать. Технология очень интересна.
                        0

                        Клёво конечно. Но идете против отрасли. Когда есть отдельный браузер для рендеринга js. Думаю js в будем вообще умрет или превратится в Java. Но сколько такое решение протянет лет 5 или 10. Не будет ли конфликта, когда рендеринг на клиенте будет отличаться от того что нарендерила джава.

                          0
                          Не будет ли конфликта, когда рендеринг на клиенте будет отличаться от того что нарендерила джава.


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

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

                            Самое читаемое