![](https://habrastorage.org/webt/-r/o9/iz/-ro9izrzg1xlvkkewuua1wqu3dg.png)
Многие слышали название GraalVM, но опробовать эту технологию в продакшене пока довелось не всем. Для Однокласснииков эта технология уже стала «священным Граалем», меняющим фронтенд.
В этой статье я хочу рассказать о том, как нам удалось подружить Java и JavaScript, и начать миграцию в огромной системе с большим количеством legacy-кода, а так же как на этом пути помогает GraalVM.
Во время написания статьи оказалось, что весь объём материала не влезает в традиционный для ХАБРа размер и если выложить публикацию целиком, то на её прочтение уйдет несколько часов. Поэтому мы решили разделить статью на 2 части.
Из первой части вы узнаете об истории фронтенда в Одноклассниках и познакомитесь с его историческими особенностями, пройдете путь поиска решения проблем, которые накопились у нас за 11 лет существования проекта, а в самом конце окунетесь в технические особенности серверной реализации принятого нами решения.
Предыстория
Первая версия Одноклассников появилась 13 лет назад, в 2006 году. Сайт был сделан на .NET, никакого JavaScript тогда на сайте не было, всё было на серверном рендеринге.
![](https://habrastorage.org/webt/dl/zv/ik/dlzvikklims5fo3osgeyr-4pbik.jpeg)
Через год у Одноклассников было свыше одного миллиона пользователей. В 2007 году это были невероятные цифры, и сайт, не выдержав нагрузки, начал падать. Разработчики решили проблему с помощью проекта One.lv, созданного латвийской компанией Forticom, у которой основные компетенции были в Java-разработке. Поэтому Одноклассники решено было переписать с .NET на Java.
Со временем проект развивался, и возникла необходимость в новых клиентских решениях. Например, чтобы при навигации сайт обновлялся не полностью, а только определенные части. Или чтобы при сабмите обновлялась только форма, а не вся страница. При этом сайт работал только на Java.
Для этого придумали систему, в которой сайт размечался именованными блоками. При переходе по навигации производился запрос на сервер, который принимал решение, о том что нужно поменять по этой ссылке, и клиенту отдавались нужные куски. Клиентский движок просто заменял нужные части, и так реализовывалась клиентская динамика. Очень удобно, потому что вся бизнес-логика находится на сервере. Небольшой движок позволил компании по-прежнему писать Java-код, управляющий клиентом.
Конечно, без минимального JavaScript было не обойтись. Чтобы сделать pop-up, нужны манипуляции: например, по наведению курсора на div вешался display:block или он скрывался с помощью display:none.
Но при этом содержимое поп-апа запрашивалось с сервера, вся бизнес-логика находилась там и была на Java.
![](https://habrastorage.org/webt/aj/0h/9i/aj0h9iklirl7jtnjfib1jwaowxo.jpeg)
2018
Спустя 12 лет Одноклассники превратились в гигантский сервис с более 70 миллионами пользователей. У нас больше 7 000 машин в 4 дата-центрах, и только на фронтенд OK.RU приходит 600 тысяч запросов в секунду.
Фронт-сервер Одноклассников продолжает работать на Java, а кодовая база одних только фронтов превышает два миллиона строк.
![](https://habrastorage.org/webt/ew/dg/zh/ewdgzh7z3n7pf40yqgaluttxkyk.jpeg)
Технологии, реализуемые на клиентской стороне, тоже не стояли на месте: появилось много решений с использованием разных библиотек: GWT, jQuery, DotJs, RequireJS и многих других.
В тот период не были распространены стандарты вроде React, Angular и Vue, каждый разработчик пытался найти оптимальное решение, используя все доступные средства.
Стало понятно, что жить с этим очень трудно, потому что накопилось огромное количество проблем:
- Много старых библиотек
- Нет единого фреймворка
- Нет изоморфности (поскольку бэкенд на Java, клиент на JS)
- Нет единого структурированного приложения на клиенте
- Плохая отзывчивость
- Недостаточный инструментарий
- Высокий порог входа
В мире шел уже 2018-й год и необходимо было меняться.
Применив всю силу технической мысли, мы продумали и сформулировали четыре основных требования к решению проблем:
- У Одноклассников должен быть изоморфный код для UI. Потому что невозможно постоянно писать сервер на Java, а потом, если необходимо добавить какую-то динамику, воспроизводить то же самое на клиенте.
- Необходим плавный переход. Потому что быстро сделать вторую версию Одноклассников и переключиться невозможно
- Обязательно нужен серверный рендеринг (Об этом ниже)
- Новое решение, работая на том же количестве железа, не должно ухудшать производительность и отказоустойчивость при наших нагрузках
Почему серверный рендеринг?
У Одноклассников много пользователей, которые живут далеко от Москвы и у них не всегда хороший интернет.
![](https://habrastorage.org/webt/55/k3/a-/55k3a-kmkkvhtrpyjsmbvtrg1j8.jpeg)
Серверный рендеринг поможет таким пользователям быстрее получать контент. Пока картинки будут грузиться, они смогут начать что-то читать:
![](https://habrastorage.org/webt/ig/ga/ww/iggawwqkjpsknbpzdtddoj3lu3c.jpeg)
Мы провели ряд экспериментов, пытаясь понять что произойдёт, если какие-то данные (например, ленту) доставлять уже на клиенте, с ожиданием. В результате оказалось, что это негативно сказывалось на пользовательской активности.
Как работает сервер сейчас
Браузер делает запрос на сайт ОК, и попадает на приложение OK-WEB, которое целиком написано на Java. Приложение идет за данными в API. Между WEB и API реализован быстрый бинарный транспорт one-nio, разработанный в Одноклассниках. Запросы осуществляются менее чем за одну миллисекунду. Можете посмотреть, что это такое отдельно. One-nio позволяет дешево делать много запросов, не беспокоясь о задержках.
API достает данные, отдает вебу. Веб генерирует HTML-страницы движком на Java и отдает браузеру.
Все это занимает сейчас меньше 200 мс.
![](https://habrastorage.org/webt/px/lt/tw/pxlttwxxg5fddebl4lumank7w2e.jpeg)
Поиск решения
Сперва была выработана концепция миграции, основанная на виджетах.
Приложения будут доставляться на сайт маленькими кусочками. Внутри они будут написаны на новом стеке. А для остального сайта это будет просто DOM-элемент с каким-то кастомным поведением.
![](https://habrastorage.org/webt/0a/7b/9j/0a7b9jtqv4vm1zsfpwdb93jlzpm.jpeg)
Это будет похоже на тег <video>: кастомный DOM-элемент с атрибутами, методами и событиями. В результате снаружи находится DOM API, а внутри реализуется функциональность виджета на новом стеке.
Какой стек выбрать?
Теперь концепцию необходимо было реализовать, стали перебирать варианты.
Kotlin
Первый прототип сделали на Kotlin. Идея заключалась в следующем: для новых компонентов писать логику на Kotlin, а разметку компонента описывать в XML. На сервере все можно запускать в JVM, используя существующий шаблонизатор, а для клиента транспайлить в JavaScript.
![](https://habrastorage.org/webt/al/il/q6/alilq6rigfkuh5jz4yuerp8kvgk.jpeg)
Помимо привнесения нового языка, с высоким порогом входа, у Kotlin оказался не достаточно развитый инструментарий работы с JavaScript, и пришлось бы многое дорабатывать самостоятельно.
Поэтому, к сожалению, от этой концепции пришлось отказаться.
Node.js
Другой вариант — поставить Node.js или другой рантайм, например, Dart. Но что получится?
Использовать Node.js можно двумя способами.
Первый способ заключается в том, что бы делегировать отрисовку компонента серверу на Node.js запущенному на том же сервере, где и Java приложение. Таким образом мы сохраняем приложение на Java и просто в процессе отрисовки HTML делаем вызов к сервису локально запущенному на Node.js.
Однако, с этим подходом есть несколько проблем:
- Удалённый вызов Node.js предполагает сериализацию/десериализацию входных данных. Эти данные могут быть весьма объёмными, например в случае, когда новый компонент на JS является обёрткой вокруг старого компонента, реализованного на Java.
- Удалённый вызов, даже на локальной машине, является далеко не бесплатным, а также вносит дополнительную задержку. Если на странице будут десятки или сотни таких компонент, пусть даже очень простых, мы существенно увеличим накладные расходы и задержку на обработку запроса пользователя.
- Кроме того существенно усложняется эксплуатации подобной системы, так как вместо одного процесса нам надо было бы иметь процесс на Java и несколько процессов на Node.js. Соотвественно все операции становятся намного сложнее, например: развёртывание, сбор операционных показателей, анализ логов, мониторинг ошибок и т.д.
Второй способ использования Node.js заключается в том, чтобы поставить его перед веб сервером на Java и использовать для пост-обработки HTML. Другими словами, это прокси, который разбирает HTML, находит компоненты на JS, отрисовывает их и возвращает пользователю готовый HTML. Вариант интересный, вроде бы универсальный и вполне рабочий. Недостатки такого подхода заключаются в том, что он требует основательного изменения всей инфраструктуры, существенно увеличивает накладные расходы и несёт в себе серьёзные риски – любой запрос должен проходить через Node.js, то есть мы начинам полностью от него зависеть. Это выглядит слишком дорогим решением для того, что бы решить нашу задачу.
![](https://habrastorage.org/webt/t_/yx/s4/t_yxs4aycowstwwyyhpptcv711y.jpeg)
Получается, 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.
![](https://habrastorage.org/webt/lh/q0/pz/lhq0pzsjpcd-d8c18ebae99flcu.jpeg)
Преимущества JS-рантаймов для JVM:
- Все работает в JVM, а у нас в этом большая экспертиза
- Бесплатное взаимодействие Java и JavaScript
- Безопасный рантайм
- Компилятор на Java в случае GraalVM
Дальше достаточно было сравнить по скорости эти рантаймы. Оказалось, что GraalVM всех опережает с большим отрывом:
![](https://habrastorage.org/webt/rw/tn/fa/rwtnfafh52ttkgbukp2fbfjdess.jpeg)
Что такое 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);
![](https://habrastorage.org/webt/nl/ku/w6/nlkuw6ihae8x0gjy2ciz3_rbbnw.jpeg)
Серверный рендеринг JS: концепт
Создаем в JVM контекст JavaScript и загружаем в него код модуля приложения на React. Прокидываем из Java в JS нужные функции и методы. Затем из этого контекста извлекаем ссылку на JS функцию render() данного модуля, чтобы потом вызывать её из Java.
![](https://habrastorage.org/webt/vx/_5/yx/vx_5yxc9yqy_4yzzuobsdo06cua.jpeg)
Когда пользователь запрашивает страницу, запускается серверный шаблонизатор, он вызывает функцию render() нужных компонент с необходимыми данными, получает из них HTML-код и отдает его вместе с HTML всей страницы пользователю.
![](https://habrastorage.org/webt/bn/51/-l/bn51-lrlqnt2zl53ssv3h3rusgq.jpeg)
Серверный рендеринг JS: реализация
В серверном шаблонизаторе Одноклассников верстка написана в виде HTML-разметки. Для того чтобы отличить приложения на JS от обычной разметки мы используем кастомные теги.
Когда шаблонизатор встречает кастомный тег, то создается задача на рендеринг соответствующего модуля. Она отправляется в пул потоков, каждый из которых имеет свой JS-контекст, исполняется на свободном потоке, рендерит в нём компонент и отдает его клиенту.
![](https://habrastorage.org/webt/mf/yg/mc/mfygmcckcz8qga5fbr9stygigjw.png)
Зачем нужен пул контекстов
Рендеринг компонента происходит синхронно в одном потоке. На это время JS-контекст рендеринга занят. Поэтому, создав несколько независимых контекстов, можно распараллелить рендеринг компонентов, используя возможности многопоточности Java.
![](https://habrastorage.org/webt/uq/oe/x6/uqoex6amlj9qbhrbjwx4pm1mpe4.jpeg)
Java-функции получения данных передаются по ссылке в каждый контекст. В результате получается классный многопоточный JavaScript внутри одного процесса.
Как на этой концепции построена реализация клиентской части нового фронтенда, мы расскажем в следующей статье.
Продолжение: habr.com/ru/company/odnoklassniki/blog/486810