Хабрахабр

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

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

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

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

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

Предыстория

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

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

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

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

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

Но при этом содержимое поп-апа запрашивалось с сервера, вся бизнес-логика находилась там и была на 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 позволяет дешево делать много запросов, не беспокоясь о задержках.

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

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

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

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

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

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

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

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

Kotlin

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

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

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

Node.js

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

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

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

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

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

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

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

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

JavaScript в JVM

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

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

Но это — бинарный код, сторонний по отношению к Java. Но как запустить JS в JVM?
Можно использовать V8 по примеру Cloudflare. Любой креш V8 приведет к разрушению всего процесса. Поэтому в JVM невозможно будет отловить ошибки внутри 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.

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

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

  • Не добавляется новых языков: по-прежнему 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.

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

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

To be continued

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть