Изоморфные шаблоны на сайте mobile.de

Краткая история

В mobile.de мы начали с шаблонизатора Apache FreeMarker, а позже переключили нашу систему шаблонов на Google Closure Templates.

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

Шаблоны закрытия написаны на языке шаблонов Soy и могут быть предварительно скомпилированы в JavaScript для рендеринга на стороне клиента или визуализированы динамически на стороне сервера. Хотя Soy поддерживает несколько бэкэндов на стороне сервера, мы использовали его на виртуальной машине Java (JVM) и иногда компилировали некоторые из этих шаблонов в JavaScript, которые нам нужно было поделиться с уровнем внешнего интерфейса. Это работало достаточно хорошо, но со временем мы заметили несколько проблем.

Проблемы с закрытием

Требование рендеринга на стороне клиента привело нас к Google Closure Library. Мы скомпилировали среду выполнения для JavaScript из Soy на основе набора имеющихся у нас шаблонов. Хотя мы очень хорошо задокументировали этот процесс, мы чувствовали себя почти единственными, кто его использовал - за исключением нескольких других компаний (в первую очередь Atlassian, одного из крупнейших пользователей Closure Templates). Было очень мало помощи и поддержки сообщества.

Во-вторых, использование Soy не решает проблему управления состоянием в пользовательском интерфейсе. Для управления состоянием на веб-странице требуются либо библиотеки, такие как jQuery, cash или Zepto.js, либо вам нужно управлять DOM через традиционный API браузера. Но для более сложных взаимодействий с пользовательским интерфейсом подобные ручные манипуляции с DOM создают запутанный спагетти-код. Для нас было ясно видно, что мы должны либо сократить сложные взаимодействия с пользовательским интерфейсом, либо перейти на другую технологию, которая упрощает работу.

Повышенная сложность интерфейса в основном была вызвана желанием нашей UX-команды перейти от традиционных HTML-виджетов (и их менее гибких шаблонов взаимодействия) к динамическим страницам, которые упрощают реализацию сложного изменения состояния и взаимодействия с пользовательским интерфейсом. Хотя, безусловно, можно реализовать эти взаимодействия с пользовательским интерфейсом и новые виджеты в одной из библиотек для манипуляций с DOM (в виде плагинов), это оказалось трудным, утомительным и подверженным ошибкам. Нам нужно было что-то получше.

Новые требования

Для нашей команды, казалось бы, невинный запрос от нашей UX-команды стал поворотным моментом. Они попросили нас реализовать новое раскрывающееся меню, чтобы лучше информировать наших пользователей о том, что они могут вводить значения вручную, набирая их, а не выбирая их из списка с помощью мыши. Он был разработан и развернут как A / B-тест для оценки его потенциального успеха.

Этот простой виджет оказался очень сложным и трудоемким для реализации из-за ряда особых требований, предъявляемых как к новому, так и к нашему старому виджету. Переходы между состояниями в обоих виджетах уже были достаточно сложными - а в новом еще сложнее. Было сложно избежать ошибок, связанных с переходом между состояниями. Кроме того, это обычная панацея, открытый исходный код - у нас не было возможности взять существующий виджет с открытым исходным кодом, поэтому мы в конечном итоге адаптировали наш плагин jQuery и перекодировали его половину с учетом новых требований. Мы оставили ощущение, что может быть лучший способ.

В ходе этого эксперимента мы заметили, что этот виджет будет намного проще реализовать в React.js. Мы поняли, что для того, чтобы идти в ногу с этими запросами от UX и доставлять их в разумные сроки, нужно что-то делать.

Новые библиотеки - или новый подход?

Хотя наши эксперименты заставили нас искать более продвинутые библиотеки, фронтенд-сообщество в компании решило стандартизировать React.js.

Хотя React.js очень хорошо работает для рендеринга на стороне клиента (где код шаблона отображается исключительно в браузере пользователя) и может отображаться на стороне сервера через Node.js и встроенный движок JavaScript V8 Google, в то время не было доказано масштабирование для серьезных случаев использования с высоким трафиком.

Некоторые команды использовали React.js либо только на стороне клиента, либо исключительно для страниц с низким трафиком. Но страницы, за которые отвечает наша команда, далеки от трафика с низким уровнем трафика, и наше приложение написано на Java, работающем на JVM. Это заставило нас больше, чем обычно, ломать голову над тем, как поддерживать рендеринг React.js на стороне сервера на JVM.

Хотя мы хотели использовать React.js, чтобы извлечь выгоду из его преимуществ, мы точно знали, что переписывать все приложение на Node.js и JavaScript - это не вариант. Это заняло бы слишком много времени, и нам нужно было тем временем принести пользу для бизнеса (а не проводить рефакторинг и переписывание). Более того, хотя Node.js захватывает все больше и больше веб-интерфейсов, он определенно небезупречен.

Верховая езда на носороге

Наша первая идея заключалась в использовании Oracle движка Nashorn JavaScript, который якобы достаточно хорошо работает в последних версиях Java 8, поэтому мы попробовали его. Он работал достаточно хорошо после того, как JIT-компилятор скомпилировал и оптимизировал пакет серверного JavaScript. Эта компиляция или предварительная компиляция занимала в среднем 5–6 секунд. Поскольку JavaScript не является потокобезопасным, эту компиляцию нужно было выполнять для каждого экземпляра v8.

Это могло бы сработать в производственной среде, поскольку мы могли бы предварительно подогреть и предварительно скомпилировать пакет JS на стороне сервера перед запуском и обслуживанием этого экземпляра JVM для наших клиентов. Этот подход, однако, был полным провалом с точки зрения жизненного цикла разработки (изменить шаблон React.js, регенерировать пакет с помощью сборщика модуля JavaScript webpack и наблюдать изменения локально). Принуждение разработчика подождать 5 секунд, прежде чем он сможет увидеть изменение локально, не подходил. Нам нужно было найти что-то получше или отказаться от всего эксперимента ...

Визуализация обходов

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

Встраивание V8 в JVM

Наша третья идея возникла из некоторых прототипов, которые мы нашли в Eclipse J2V8, мосте Java Native Interface (JNI) между JVM и движком Google V8. Прямое использование подхода Патрика Гримарда вообще не сработало. Он решил создать V8 и выпускать его по каждому запросу. Весь процесс с созданием, компиляцией серверного пакета и выпуском экземпляра занял около 500 мс. Но полсекунды для каждого запроса клиента - это не то, что мы могли себе позволить - это могло сработать для страниц интрасети или вариантов использования с низким трафиком, но не в таком случае, как наш.

Вместо того, чтобы создавать и выпускать по каждому запросу, мы решили использовать подход объединения, в данном случае пул воркеров v8. Для пула мы использовали Apache Commons Pool. Первоначальные показатели производительности и масштабируемости выглядели впечатляюще, и после подключения решения к нашему коду и его тестирования мы реализовали полностью новую страницу только с этим J2V8. Подробнее о J2V8 позже.

Механизмы шаблонов и модели параллелизма

Как только он начал работать, стали проявляться некоторые различия в масштабируемости J2V8 и Google Closure Templates. Одна из первых концепций, которую вы усваиваете при изучении функционального программирования, - это преимущества неизменяемых структур данных. В зависимости от проекта может потребоваться некоторое время, чтобы увидеть, когда это будет выгодно, но рендеринг шаблонов на стороне сервера - это тот случай, который сразу демонстрирует силу этой концепции. Именно здесь различаются классические технологии серверного рендеринга (SSR), такие как Google Soy или FreeMarker, и подходы на основе JavaScript, такие как React.js. Каждый поток создает блокировку для процесса V8, пока выполняется функция JavaScript. В Soy это неизменяемая чистая функция, в которую передаются данные, отображается шаблон и возвращается HTML. Модель параллелизма V8 требует блокировки, а это означает, что пострадает масштабируемость, и для обслуживания одного и того же трафика с одинаковой производительностью потребуется больше процессов / машин / экземпляров. Обратите внимание, что масштабируемость не следует путать с производительностью - на самом деле производительность J2V8 для одного пользователя, отправляющего последующие HTTP-запросы, была весьма впечатляющей - иногда даже немного лучше, чем с Google Soy.

Операции с привязкой к ЦП и многое другое для производительности и масштабируемости

Отрисовка шаблонов - это операция, связанная с процессором, тогда как JVM, с которой мы работаем для определенных случаев использования, имеет много потоков (с использованием пула потоков Tomcat). Пул теоретически делает безболезненным блокировку потока при обслуживании HTTP-запроса клиента. Проблемы масштабируемости не уникальны для J2V8, поскольку разница заключается в базовой модели параллелизма. Использование Node.js и React.js для рендеринга на стороне сервера оказывается одинаково сложным - или тем более для веб-сайтов с высоким трафиком. Причина в том, что при одновременном обслуживании нескольких запросов цикл обработки событий узла может быть заблокирован и привести к перегрузке при обслуживании HTTP-запросов. На самом деле, насколько мне известно (на момент бета-версии 16), даже Facebook не использует React.js на стороне сервера. Другие успешно использовали его (иногда с помощью возможности кэширования или других уловок) либо для веб-сайтов с низким трафиком, либо с плагином кластера Node.js и масштабированием Node.js для каждого процесса вместо более традиционной масштабируемости по потокам.

Такие решения могут привести к относительно небольшому количеству запросов в секунду на процесс Node.js. В прежние времена накладные расходы процесса на потоки были настолько высоки, что была разработана вся концепция потоков. В наши дни для компании может быть вполне нормально позволить себе такую ​​стоимость, если использование React.js дает серьезные преимущества. Node.js упрощает для операций сайта возможность скрыть количество процессов за одним портом, к которому могут получить доступ подсистемы балансировки нагрузки. Многие разработчики Node.js знакомы с плагином кластера.

Несколько решений предоставляют различные формы помощи, позволяющие избежать блокировки цикла событий Node.js, в том числе:

В JVM есть встроенная поддержка потоковой передачи, поэтому с J2V8 мы действительно могли бы продолжать использовать модель масштабирования потоков вместо модели масштабирования процесса. Поскольку J2V8 встроен как библиотека в JVM, в решениях, в которых разработчик JVM должен избегать блокировки любой ценой, нет необходимости.

Хотя рендеринг на стороне сервера был абсолютно нормальным в течение многих лет, эти технические проблемы и трудности с рендерингом React.js на стороне сервера заставляют некоторых людей в сообществе React.js полагать, что вам может даже не понадобиться SSR. Эта техническая трудность приводит к ухудшению пользовательского опыта с точки зрения воспринимаемой скорости сайта и очевидного отсутствия уверенности в SEO.

В нашей команде мы отвергли это понятие - отсутствие уверенности в SEO, счетчики и индикаторы загрузки - все это обходные пути. Очень заманчиво использовать процессоры наших клиентов для рендеринга страниц React.js на стороне клиента, но нет «бесплатного обеда». При этом рендеринг на стороне клиента может привести к медленному восприятию времени рендеринга страниц, черту прядильщиков, отсутствию первоклассной поддержки SEO [сканирующих ботов] - особенно для веб-сайтов, которые все еще страдают от синхронной интеграции рекламы или блокировки HTTP-запросов в заголовках страниц (которые мы иногда не можем избежать по разным причинам).

Такие архитектуры часто запрашивают серверные модели через AJAX, что приводит к еще более медленному времени рендеринга (AJAX туда и обратно по сравнению с серверной моделью данных, получающей туда и обратно). Индикаторы загрузки могут привлечь внимание пользователя к контенту на странице, но за все это нужно платить. Хотя сканер Google может понимать простые страницы с расширенным JavaScript, он все еще официально не поддерживается, а другие роботы-роботы (BingBot, Yahoo) могут видеть еще меньше контента - если вообще какое-либо релевантное содержание.

J2V8 Insights

J2V8 предоставляет привязки Java для V8 и был разработан Ian Bull в EclipseSource для поддержки клиентских инициатив проекта tabrisjs.com. Из проекта ясно видно, что он не был протестирован в боевых условиях на стороне сервера, но относительно хорошо протестирован и успешно используется в продакшене для сценариев использования tabris-JS (код пользовательского интерфейса).

На сегодняшний день самая последняя версия J2V8 использует Node.js 7.4.0 и V8 5.4. Его можно скомпилировать против 7.9.x (с V8 5.5.x) в macOS и Linux, но компиляция Windows там не работает. Также возможно скомпилировать J2V8 против NodeJS 8.2.1 с V8: 5.8.x как для Linux, так и для macOS, но, к сожалению, компиляция на Android в настоящее время является проблемой.

Неровная езда с J2V8

Итак, наш первоначальный прототип и решение работали не очень хорошо, не масштабируемо и, что еще хуже, нестабильно.

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

Вторая проблема была обнаружена в производственной среде, и ее было намного сложнее воспроизвести и в конечном итоге исправить. После развертывания, после начального набора успешных тестов масштабируемости и производительности в нашей среде контроля качества, процесс JVM будет время от времени аварийно завершаться каждый час. Со временем нам удалось сократить количество сбоев до одного раза в 6 часов, но мы знали, что это не то, что мы можем серьезно использовать для производственного веб-сайта, такого как mobile.de, входящего в 50 лучших сайты в Германии по посещаемости ».

Мы работали напрямую с сопровождающим ядра J2V8, чтобы проанализировать проблему, и после нескольких дней работы мы нашли решение. Проблема заключалась в очень тонкой ошибке C ++ распределения стека по сравнению с распределением кучи. Это было невозможно воспроизвести в нашей среде контроля качества, и это было настолько тонко, что это было в проекте с самого начала - незамеченным и неактивным для случаев использования пользовательского интерфейса. На сегодняшний день версия с исправлениями еще не выпущена, но вы можете выполнить сборку из основной ветки J2V8, чтобы воспользоваться исправлением стабильности.

Мы также столкнулись со спонтанными glibc ошибками связывания: если система, в которой запущено приложение, имеет несовместимую glibc версию, приложение Java запустится, но при использовании моста J2V8 возникают ошибки времени выполнения. Эту проблему легко решить с помощью контейнеров Docker, но некоторые пользователи действительно старых устаревших систем Linux, таких как Alpine (с musl вместо glibc), сообщали о серьезных препятствиях.

Заключение

Хотя J2V8 определенно работает с React.js SSR на нашей специализированной странице поиска с низким трафиком, еще предстоит доказать, будет ли она работать достаточно надежно для наших самых важных страниц на mobile.de: Страница результатов поиска и Просмотр страницы товара.

Пока результаты впечатляют. J2V8 оказался успешным при обслуживании наших страниц / компонентов React.js на стороне сервера в сценариях использования, не связанных с высоким трафиком / критических ситуациях. Это было немного ухабисто, но он быстро обслуживает страницы, достаточно хорошо масштабируется и стабилен. Общий недостаток - отсутствие возможности отладки также не относится к J2V8, поскольку мы можем отлаживать JS на стороне сервера с помощью IDE через разъем сокета.

Преимущества J2V8

  • Производительный и достаточно масштабируемый
    (все еще хуже, чем шаблоны без блокировки, использующие неизменяемые структуры данных)
  • Быстрые циклы перекомпиляции при разработке
    (для транспиляции всего серверного пакета JavaScript требуется 400 мс)
  • Встроенный подход V8, уровень быстрой связи через JNI между JVM и виртуальной машиной Google V8
  • Последние версии масштабируются линейно с количеством входящих запросов, используя все ядра ЦП для рендеринга.
  • Возможна отладка кода JavaScript из IDE (!)

Недостатки J2V8

  • Нативные привязки, требуют перекомпиляции исходного кода C ++ для каждой платформы (потеря преимущества JVM «писать один раз, запускать где угодно»)
  • Не тестировался в бою на стороне сервера, но последняя основная сборка очень стабильна
  • Меньшее сообщество с открытым исходным кодом
  • Требует, чтобы разработчик создавал и вручную выпускал промежуточные объекты V8 (предоставляет инструменты для обнаружения и предотвращения потенциальных утечек памяти)
  • Запуск нескольких V8 внутри процесса JVM эффективно означает, что у нас столько сборщиков мусора, сколько у нас есть V8 + 1 для самой JVM, что приводит к увеличению сложности.
  • J2V8 необходимо запустить и скомпилировать с определенной версией glibc (необходимо синхронизировать)

Однако независимо от того, использует ли вы Node.js или J2V8, React.js SSR сталкивается с проблемами. Исходя из традиционных механизмов рендеринга шаблонов на стороне сервера, наши тесты показали, что рендеринг JavaScript на сервере менее масштабируем.

Ситуация, похоже, перекликается с битвой между C ++ и Java 20 лет назад. Раньше писать на C ++ было намного быстрее, масштабируемее и эффективнее с точки зрения памяти. Java / JVM считалась медленной и похожей на свинью платформой. С точки зрения эксплуатационных расходов запуск приложения Java был намного дороже, чем скомпилированный код C ++. Но, несмотря на свои недостатки, в то время Java выиграла на стороне сервера для сценариев использования разработки веб-приложений, часто финансовых систем и многого другого. Есть еще несколько примеров использования, в которых C ++ не имеет себе равных: торговые системы с низкой задержкой и, разумеется, компьютерные игры.

Ситуация с React.js SSR сравнима. У него есть недостатки с точки зрения масштабируемости (блокировка доступа потока к V8 или блокировка цикла событий), но он намного лучше с точки зрения простоты использования и управления сложными состояниями пользовательского интерфейса для разработчиков внешнего интерфейса / инженеров полного стека. Для определенных веб-сайтов с высоким трафиком это на самом деле, скорее всего, приведет к более высоким эксплуатационным расходам на сервер, стоимость, которую одни компании будут без проблем оплатить, в то время как другие могут столкнуться с трудностями (например, стартапы или малобюджетные компании).

Есть случаи, когда React.js и SSR могут быть излишними - почти все случаи, когда сайт прост, требуется очень мало взаимодействий с пользовательским интерфейсом на стороне клиента, нет необходимости в изоморфных шаблонах, изоморфной маршрутизации и т. Д. Библиотека манипуляций с DOM и страницы, созданные сервером, по-прежнему работают нормально. Что еще более важно, они не заставляют использовать для этого JavaScript, существует множество языков шаблонов для всех видов платформ, которые поддерживают традиционные SSR для веб-разработки.

Наши коллеги из Marktplaats пошли именно по этому пути: сначала они использовали React.js с рендерингом на стороне сервера, а затем поняли, что им это не нужно для своих шаблонов взаимодействия UX - это добавляло сложности, но не давало заметных преимуществ. Осознав это, они вернулись к традиционным решениям и в конечном итоге отказались от React.js.

Как обычно в ИТ, все нужно рассматривать с точки зрения затрат и выгод для отдельной команды / компании / сценария использования, а не в поисках шумихи. И последнее, но не менее важное: ничто не вечно. Возможно, со временем Marktplaats снова рассмотрит возможность использования React.js. По мере того, как страницы становятся более интерактивными, пользователям могут потребоваться шаблоны UX и производительность, которые ощущаются как родное приложение. В качестве альтернативы мы можем прийти к выводу, что нам не нужно такое продвинутое решение для того набора проблем, который у нас есть.

Особая благодарность:

  • Войтек Сельски (поддержка DevOps)
  • Сергей Швагер (поддержка React.js)
  • Томас Виз (поддержка C ++)
  • Роджер Шин (грамматико-стилистический обзор)