JavaScript на сервере, 1ms на трансформацию

в 10:10, , рубрики: javascript, mail.ru, Блог компании Mail.Ru Group, метки: ,

Зачем?

Вопрос “Зачем?” — самый главный при принятии любого решения. В нашем случае причин было несколько.

Во-первых, люди. Текущий шаблонизатор обрабатывался Си. Все вопросы о его изменениях решались не быстро. А самое главное, что писали шаблонизатор одни люди, а использовали совсем другие.

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

Но в Mail.Ru Group есть целая команда высококвалифицированных людей, знающих JS, способных самостоятельно написать инструмент, а самое главное — они же им и будут пользоваться.

Во-вторых, задачи. Возьмем проект Почта@Mail.ru. Мы не можем отказаться от шаблонизации на сервере – нам нужна быстрая загрузка при первом входе. Мы не можем отказаться от шаблонизации на клиенте – люди должны видеть высокую скорость реакции на их действия, а значит, обязателен AJAX и шаблонизация на клиенте.

Проблема очевидна: два набора совершенно разных шаблонов на сервере и на клиенте. А самое обидное, что решают они одну и ту же задачу. Дублирование логики нас просто измотало.

v8 — это интерпретатор JavaScript, а значит, мы можем получить один шаблон, который работает как на сервере, так и на клиенте.

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

Что нужно

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

Я давно и много использую для трансформации XSLT (не самый быстрый вариант трансформации), хотя, если его правильно использовать, он показывает хорошие цифры (я говорю про libxslt). Но у XSLT есть очень мощный инструмент шаблонизации – переопределение шаблонов. Мы решили реализовать нечто похожее, но намного проще.

/head.xml
	…
	<title><fest:get name=”title”/></title>
	<fest:set name=”title”>Mail.ru</fest:set>
/mail.xml
	…
	<fest:include src=”head.xml”/>
	<fest:set name=”title”>Почта Mail.ru</fest:set>

Странно было бы использовать v8 и не дать в шаблонах доступ к JavaScript.

<fest:script>
	var text = “mail.ru”
</fest:script>
<fest:value>text</fest:value>

И еще много чего по мелочи, помимо стандартных условий и циклов.

XML

В качестве синтаксиса для шаблонизатора мы взяли XML.

Поддержка базового функционала в IDE. Сто процентов популярных IDE знают, что такое XML. Расстановку переносов, подсветку, базовое автодополнение вы получаете бесплатно.

Валидация на уровне IDE. Валидный HTML получается за счет валидных шаблонов. Опять же, все IDE умеют валидировать xml как таковой.

Модульность из коробки (name spaces). Базовые возможности шаблонизатора очень быстро захочется расширить. Например, добавить тег, который позволяет делать проекты на нескольких языках. Система Name Spaces позволяет легко это сделать.

Широкий набор готовых инструментов. За много лет скопилось множество инструментов для обработки XML, например, библиотеки по обработке XML посредством XSL или целый класс SAX парсеров, XSD и DTD схемы для валидации и т.п.

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

Реализация

JavaScript на сервере, 1ms на трансформацию

Ковбойство. Я сам недавно узнал, что есть такой термин.

До этого я был уверен, что самый надежный способ выполнения задачи — парное программирование и тесты. Тесты, как всегда, себя оправдали, а вот парному программированию нашлись альтернативы.

Отличие этой задачи от типичной: был известен шаблон и результирующий HTML, который этот шаблон должен выдавать. Мы с Костей (Frontend разработчик почты) начали писать свои реализации. Раз в неделю мы сравнивали замеры скорости трансформации.

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

01	[
02		{action:"template"},
03		"<html>...",
04		{action:"value"},
05		"json.value"
06	]

Вторая строка означает начало шаблона. Третью надо просто отдать браузеру. Четвертая говорит, что пятую надо исполнить как JavaScript и результат исполнения отдать браузеру.

01	[	
02		{action:"template"},
03		"<html>....”,
04		{action:"if"},
05		"json.value",
06		"<span>true</span>",
07		"<span>false</span>"
08	]

Вариант немного сложнее. Четвертая строчка означает, что пятую надо исполнить и если результат истина или ложь, то отдать шестую или седьмую строку соответственно.

Вариант с функцией особо пояснять не надо.

01	function template(json){
02		var html = "";
03		html += "<html>…";
04		html += json.value;
05		return html;
07	}

Забегая вперед, скажу, что его вариант оказался быстрее. Некоторое время мы шли почти ровно, но вариант со структурой уперся в потолок намного раньше.

Чтобы понимать, что дал такой подход: моя первая реализация выполняла задачу за 200ms. Когда мы выжали все, что можно, а потом соединили лучшее из двух наших программ, то получили 3ms.

Если описать текущую реализацию кратко, то мы циклы транслируем в циклы, условные операторы в условные операторы и т.д.

fest:forearch			for(i = 0; i < l; i++) {}
fest:if				if(value) {}
fest:choose			if(value) {} else {}

Нет сужений контекста. Да, это ограничение, но зато нет накладных расходов на ограничение контекста, а самое главное, что, как только сужаешь контекст, сразу возникает задача достать что-то из глобального контекста или из контекста уровнем выше.

Важно, что шаблоны транслируются в JS-функцию, которая работает в режиме strict mode. Это не дает верстальщикам шансов написать код, который приведет к утечкам памяти.

Везде, где нужна логическая работа с данными, доступен JavaScript.

<fest:if test=”_javascript_”></fest:if>
<fest:value>_javascript_</fest:value>

Все конструкции, которые предполагают исполнение JavaScript, оборачиваются в try catch.

Все конструкции, которые предполагают вывод в HTML после выполнения JavaScript, по умолчанию проходят HTML escape.

<fest:value>json.name</fest:value>

try {
	html += __escape(json.name)
} catch(e) {}

С самого начала разработка шаблонизатора ведется в открытом виде.
https://github.com/mailru/fest

Возможности интеграции

С одной стороны, v8 — это только библиотека, которая позволяет интерпретировать JavaScript. Сама по себе она кажется бесполезной – никакого доступа к системе. Но, с другой стороны, она легко прикручивается к другим языкам.

Имея нулевой опыт программирования на Cи и Perl, я сделал тестовые примеры на обоих языках. Плюс на текущий момент у нас есть связка с Python.

Ну и, конечно, NodeJS для прототипов и браузеры — среды, где JavaScript шаблоны работают из коробки.

Условия, близкие к боевым

Получив 3ms, я пошел к сервер-сайд программистам. На вопрос, сколько у меня есть времени на запрос, который отдает список писем пользователя, они сказали: не больше 4ms. У меня уже было 3ms на трансформацию, надо было пробовать.

Список писем у нас отдает наш собственный http-сервер, написанный на Cи. Получение данных — операции, которые не конкурируют за процессор, поэтому их не замеряли. Остановились на подготовке данных к трансформации и на самой трансформации.

По историческим причинам наш http сервер хранит данные в плоском хеше.

msg_length = 5
msg_1_title = “letter”
msg_1_Unread = 1

Так как мы говорим о JavaScript, то первое, что приходит на ум, — это JSON

msg = [ {title: “letter”, Unread: true} ]

Мы взяли строку с плоским хешом, поместили ее в память и стали добиваться результата, когда при трансформации шаблона в v8 JavaScript оперировал с JSON.

Вариантов перебрали много. Пробрасывать объект, пробрасывать строку и парсить ее на JavaScript, пробрасывать строку и пропускать ее через JSON.parse.

Как ни странно, самым быстрым оказалось преобразовать плоский хеш в строку, которая совпадает с JSON, и в v8 отдать строку

“template([ {title: “letter”, Unread: true} ])”

Но, несмотря на все, мы уперлись в 6ms при трансформации 2ms. Все были готовы сдаться. Я все же решил взять исходные данные, строку с плоским хешом и, используя тот же скомпилированный шаблон, получить нужный HTML на NodeJS.

Получил 4ms. Когда пришел с этой цифрой к нашим сишникам, честно говоря, ожидал фразы “Классно, но NodeJS писать у нас нет ресурсов” Но вместо этого услышал “Если NodeJS может за 4ms, значит мы тоже сможем!”.

JavaScript на сервере, 1ms на трансформацию

Именно в этот момент я понял — мы доведем это до продакшена. Появилось второе дыхание!

Решение оказалось простым. Раз мы 67% времени теряем на подготовке данных, а данные в принципе у нас уже есть, надо выкинуть подготовку данных.

Мы пробросили в v8 фукцию __get(‘key’). Таким образом, мы из v8 забирали данные напрямую из хеша нашего http сервера. Нет конвертации данных в нужный формат. Нет преобразования этой строки в объект внутри v8. Мы вышли на 3ms и имели запас 1ms.

Почти продакшен

Итак, выглядит все замечательно, но мы еще и близко не были на продакшене. Чешутся руки попробовать.

Берем отдельный сервер, поднимаем на нем версию http сервера, который работает с v8, и дублируем реальные запросы на него. Оставляем на 30 часов одно ядро 2.2 ГГц Xeon.

10 000 000+ хитов
1.6ms среднее время трансформации

992 422		10% между	2 и 5ms
208 464		2% между	5 и 10ms
39 649		0,4%		больше 10ms

Только 12% были больше 2ms. v8 стабильно ведет себя по памяти.

Продакшен

Я пришел с последними цифрами к заместителю технического директора, сказав, что v8 готов к продакшену, надо сделать отдельный небольшой проект, который, если что, можно и забыть в случае неудачи. В ответ получил «цифры хорошие, отдельный проект, провал которого не страшен, — это правильно, но ты правда хочешь запустить v8? Начни с главной страницы Mail.Ru». Вопрос поставлен правильно – либо мы делаем дело, либо развлекаемся в сторонке.

Верстка главной страницы на fest заняла три дня. Выключили один сервер из балансера, залили туда версию с v8 и продублировали запросы. Все выкладки будут происходить в контексте одного демона/ядра.

Дальше я расскажу очень поучительную историю с хорошим концом.

Всегда детально выясняйте, что показывают ваши графики. Мы пустили на тестовый сервер половину нагрузки. Потребление процессора было в три раза выше обычного. Выглядело как провал, проигрыш по ресурсам в шесть раз по сравнению с текущим шаблонизатором.

Стали смотреть. Тут я немного расскажу про архитектуру главной. На ней собирается информация от разных проектов. Собирает ее внутренняя разработка, мы ее называем RB. Из 165кб, которые генерируются для отдачи главной, 100кб собирает RB. И происходит следующее: RB отдает куски HTML через http сервер в v8, v8 конкатинирует их со своими строками, а результат возвращает все это обратно в http-сервер.

Налицо двойной проброс данных. Сделали оптимизацию. Теперь v8 вместо построения одной большой строки, включающей в себя данные от RB, отдает данные сразу в http-сервер.

__push_string(‘foo’);
__push_rb(id);
__push_string(‘bar’);

Как плюс, нет конкатинации строк на v8, нет двойного проброса RB из сервера в v8 и обратно, а самое главное, любой проброс данных — это конвертация из utf-8 в utf-16 и обратно. V8 все хранит в utf-16.

Был профит, ресурсы потреблялись в два раза больше, чем обычно, а не в три. Т.е. мы все еще проигрывали в четыре раза, хотя вроде бы выжали все до капли.

А теперь поучительная часть. Я интереса ради взял нагрузку, которую мы тестировали, умножил на два, на количество демонов на машине и на количество машин. Получил 440 000 000 хитов. При этом у нас в сутки 110 000 000 хитов. Закрались смутные сомнения.

Пошли смотреть логии. Оказалось, что на каждый запрос с нагрузкой мы получали три запроса с отчетами в логи для статистики! Реальная нагрузка на один http-сервер в четыре раза ниже той, на которой тестируемся мы!

На следующее утро мы раскатили версию главной страницы с v8.

Данные на сегодня:
Размер отдаваемого HTML, который генерирует v8 65кб.
Время, работы v8 на запрос 1ms.
В среднем v8 требует 40MB на контекст.

Пара уточнений

Все, кто думают про v8, натыкались на статью Игоря Сысоева sysoev.ru/prog/v8.html

За все время работы над этой задачей нам очень сильно помогал разработчик v8 Вячеслав Егоров (http://mrale.ph).

Опасения про память оправданны. Если для вас критична корректная работа (при условии, что нехватка памяти — это штатная ситуация), то у вас будут проблемы. Корректно перехватить ошибку аллокации можно, но все, что можно сделать по этому поводу, — это корректно перезапуститься.

Но скажу честно, у нас есть только один продукт, где это критично. Что касается главной страницы — у нас многократный запас по памяти и мы его очень хорошо мониторим.

Оказалось, что у нас v8 trunk течет. Вячеславу воспроизвести это не удалось, но, я думаю, мы соберем тестовый пример, который поможет разработчикам найти утечку. Версия 3.6 ведет себя по памяти прекрасно.

Полезные ссылки

github.com/mailru/fest шаблонизатор
code.google.com/p/v8/ v8 API
sysoev.ru/prog/v8.html статья Игоря Сысоева

Андрей Сумин, руководитель разработки клиентской части Mail.ru

Автор: AndrewSumin

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


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