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

в 6:00, , рубрики: graalvm, java, javascript, ok.ru, React, react.js, ReactJS, Блог компании Одноклассники, одноклассники, ок.tech, Разработка веб-сайтов

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

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

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

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

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

Предыстория

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

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

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

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

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

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

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

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

2018

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Kotlin

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

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

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

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

Получается, 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.

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

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

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

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

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

Что такое 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);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

To be continued

Автор: Олег Коровин

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js