Наверх ▲

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

Андрей Сумин Андрей Сумин Руководитель Frontend, Mail.ru.

Андрей Сумин: Добрый день, меня зовут Андрей Сумин, я работаю в компании Mail.Ru. Есть немного лишнего времени, поэтому я подготовил небольшой бонус – как раз на оставшиеся двадцать минут. Я покажу вам возможности нашего шаблонизатора, который мы сделали, чтобы достичь тех цифр, которые нам нужны.

До этого у нас был шаблонизатор полностью на "Си". Он был довольно своеобразный. Поэтому мы очень хотели конструкций вроде таких, когда можно использовать JavaScript.

Ниже вы видите то, во что json.name превращается на сервере.

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

Safe=true. Ниже вы видите компилированный код. По "safe=true" сразу видно, что, допустим, у нас исчез "try catch".

Конструкции, которые нужны любому шаблонизатору – это "if" и, соответственно, "choose". Но обычно используется форма "if else", очень нужная разработчику. Но если вы хотите валидный XML, то, к сожалению, единственный способ – это "choose".

Здесь показан код, в который он компилируется.

Естественно, используются циклы. Куда же без циклов? Цикл по массиву, цикл по хэшу.

Еще наш шаблонизатор по умолчанию "тримит" все, что находится между тэгами.

Боюсь, что здесь могут присутствовать те, кому эти вещи не знакомы. Но в html пробел  –  это значащий символ. Поэтому желательно указывать, что вы хотите в этом месте иметь пробел. Да, есть такие специфические вещи.

Fest:script нужен, чтобы мы прямо посреди шаблонизатора могли исполнить какой-нибудь JavaScript-код.

Сейчас я попробую сделать небольшие демонстрации, чтобы вы видели, к чему это приводит. Вот, у меня есть вчерашняя заготовка. Это обычный HTTP-сервер на Node.js.

(Докладчик показывает демонстрацию.)

Он запускает некоторый сервер. Отсюда мы берем шаблон. Шаблон я, естественно, покажу. У меня тут некоторые демонстрации закомментированы. Берем шаблон, он должен превратиться в JavaScript-функцию "template". Вот здесь мы тысячу раз этот шаблон применяем. Так как основная тема моего доклада – это все-таки цифры, то нам интересно измерить его производительность в каких-то стрессовых ситуациях. Ниже, соответственно, мы выводим результаты для этого шаблона.

Давайте посмотрим повнимательнее. Первый шаблон – это цикл в десять тысяч итераций.

Давайте здесь поставим лучше html, так будет интересней. Он выводит span, внутри которого мы выводим значение "hello" из хэша входного в шаблон и занимаемся конкатенацией с текущей датой. Конкатенация строк – достаточно "дорогая" операция. Это все пройдет десять тысяч раз. Плюс я хочу вам еще показать, что и сам шаблон выполнится тысячу раз.

Давайте попробуем запустить...

Сейчас будут цифры на основе Node.js. V8 используется Node.js. Мы видим десять тысяч вот таких конкатенаций. Не волнуйтесь, я вывожу в браузер только последние, иначе у меня были бы проблемы с памятью в браузере. 10 тысяч конкатенаций выполнены тысячу раз. Они нам дали пять секунд. Это означает, что на один раз пришлось 10 тысяч конкатенаций, и это заняло у нас 5 миллисекунд на Node.js на сервере.

Можем посмотреть, сколько времени это займет в браузере (если мы этот же шаблон отправим прямо в браузер).

Цифра чуть-чуть отличается. Можем, на самом деле, еще посмотреть другой браузер. Нас же интересует JavaScript-шаблонизатор. В общем, вы уже видите, что получится. Это Opera. Значит, он должен иметь одни и те же вещи на клиенте и на сервере. Там почти минута – 40 секунд, по-моему. В любом случае, даже если здесь будет очень большая цифра, ее нужно делить на тысячу.

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

Чтобы шаблонизировать, мы этот шаблонизатор используем на клиенте для проектов, которые используются, – это могут быть мобильные версии проектов, для старых версий Android, и у нас никаких проблем с производительностью нет. В IE я, к сожалению, сейчас не могу показать, потому что у меня все-таки Mac. Но есть некоторые интересные нюансы.

У нас шаблонизатор по умолчанию со включенным "escaped". Об этом будет речь в основной части доклада. Если мы знаем, что наши данные точно не введены пользователем, то "escaped" можно выключить.

Давайте посмотрим, что произойдет с цифрами. Это сейчас на сервере. Это V8 на сервере через Node.js.

Так, видимо, я где-то ошибся. Вот здесь "save true". Это не должно влиять. А, ну, да, конечно – сервер-то надо перезапустить. Я все-таки привык с клиентом работать, где нажимаешь Ctrl+S+F5.

Вот, собственно, разница.

Очень много времени требует "html escape". Это я взял, чтобы продемонстрировать порядок проблем, с которыми приходится сталкиваться.

Что касается "save", на предыдущем слайде каждое JS-выражение, которое делает верстальщик, по умолчанию оборачивается в "try catch".

Это обходится, на самом деле, не очень дорого, по крайней мере, в V8.

Разницы особо никакой. Хотя, на самом деле, в "try catch" было обернуто, как вы понимаете, тысяча на десять тысяч. В общем, операций очень много – 10 миллионов. В современных браузерах это выполняется очень быстро, и в современных движках тоже.

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

Теперь второй интересный пример, без которого мы не смогли жить. Он не новый. Мы его позаимствовали в каком-то виде из XSLT. Тоже об этом буду говорить. Сейчас – демонстрация. Все-таки, возможно, кто-то придет к началу доклада.

У нас есть отдельный шаблон, который выводит span. Внутри span у нас есть вызов (вот, я его выделил) – fest:get. Означает, что в этом месте вывести блок с именем "word". А ниже идет определение того, что это содержимое того блока, соответственно, и равно слово "word". 

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

Дальше мы хотим, на самом деле, переопределить это.

Понятно, что поменялось? Это для примера, что в браузере, собственно, эти же шаблоны точно также отрабатывают. Вот это то самое наследование, которое есть во всех современных шаблонизаторах (и не очень современных). Очень близко, по-моему, к "Django" – там практически то же самое.

Я хочу сказать, что вот этот XML-синтаксис – это "syntactic sugar" для верстальщиков. У нас некоторые люди, которые знакомы с XSL, когда видели этот синтаксис, начинали писать на нем как на родном. Но это приводило к проблемам. Вот, у нас есть "fest value", которое я вот здесь вывожу. Вот оно, допустим – "fest value".

Они приходили и спрашивали, есть ли у него атрибут "формат", чтобы можно было сразу в "fest value" это как-то сформатировать. На что я им отвечал: "Вот в этом конкретно пункте забудьте про XSL. У вас есть внутри JavaScript". В том числе, про этот же самый синтаксис. Он сделан исключительно для того, чтобы рядовые задачи приводили к очень быстрым решениям по скорости. Поэтому никаких особых "наворотов" там нет. Если "навороты" есть, это должно быть осознанное решение конкретного разработчика. Оно должно быть выражено в JavaScript.

Вот поинтереснее задача. Допустим, у нас есть JavaScript-библиотека… Вот эта функция умеет склонять слова. На входе она получает число и массив слов, которые надо склонять. Мы в этой строчке ее подключаем. Значит, на сервере в этот момент этот JavaScript выполнится, создаст эту функцию.

Создаем массив слов. Вот небольшой массив, который выведет результат от нуля до девяти, как эта функция себя будет вести с такими входными параметрами.

Вот функция слова просклоняла.

Само по себе это не очень интересно, если бы не следующий пример.

Теперь она – шаблон, который генерирует html для браузера. Внимание: здесь уже не "fest include", а "fest insert". В этом месте вставляем этот JavaScript. Браузер его получит именно как JavaScript-программу.

Вот получаем ровно тот же пример. Вот она наша функция пришла на клиент, и мы можем ее использовать на клиенте.

Самый большой бонус, ради которого мы бились – чтобы одни и те же библиотеки, шаблоны, программы на JavaScript могли использоваться на клиенте и на сервере. Раньше это было невозможно как раз потому, что было медленно и непонятно. Вдруг кто помнит первые попытки использовать JavaScript на сервере – это был, по-моему, какой-то IDE. Jagser, по-моему, он назывался.

Реплика из зала: Это Aptana?

Андрей Сумин: Да, Aptana. Там был, конечно, полный провал. Я его честно попробовал, честно старался, но сдался. С появлением отдельных движков от разных браузеров, с появлением конкуренции, когда они соревнуются друг с другом по производительности, мы получили возможность использовать JavaScript на сервере.

У вас есть вопросы по моим примерам?

Реплика из зала: Можно посмотреть скомпилированный код?

Андрей Сумин: Да. Скомпилированный код можем посмотреть. Вот, скомпилированный код шаблона. Тут некоторые служебные функции идут сначала, конечно же. Как я говорил, нужен escape.

Вот, допустим, наш цикл. Вот я его выделил. Давайте попробую приблизить. Вот тот самый hello+Date. Как я и обещал, у нас все по умолчанию в "try catch". Так что если вдруг вам попался верстальщик, который не очень соображает в JavaScript, он, по крайней мере, вам ничего не сломает. Escape html – по-честному, все без обмана. 

Реплика из зала: А у вас в команде есть кто-то, кто разбирается в JavaScript?

Андрей Сумин: Да. У меня в команде есть больше, наверное, чем три или четыре (может, даже чем пять людей), которые умеют "готовить" java script. Они понимают, что это такое, и как его "готовить". Гораздо большая опасность – это такая архитектурная "травма" JavaScript – так называемое слово "var". Если вы его не объявили, то у вас будут проблемы. На сервере это фактически утечка памяти, потому что по умолчанию переменная попадет в список глобальных и там и останется, по крайней мере, до перезагрузки контекста.

Но у нас есть Straight mode, который не позволяет этого сделать. Текущий шаблонизатор по умолчанию работает со Straight mode. Я пытался вызвать утечки памяти – у меня не получилось.

Начнем рассказ про выполнение JavaScript на сервере.

Зачем это нужно?

Естественно, первый вопрос, который возникает, когда мы говорим про JavaScript на сервере – это вопрос "зачем?". Я очень много программировал на JavaScript , я его очень люблю. Но этого недостаточно. Надо все-таки заниматься делом.

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

При этом есть вторая особенность, особенно актуальная сейчас. Есть очень много людей, которые знают JavaScript. Конкретно у меня очень много людей, которые знают JavaScript. А текущий шаблонизатор у меня на "Си", но у меня в подчинении нет ни одного человека, который знает "Си".

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

При этом у нас получалась такая ситуация, когда у нас есть шаблонизация на клиенте и шаблонизация на сервере, логика одна и та же, а шаблоны разные. Дублирование кода, куча проблем. Непонятно, что с этим делать. Реально очень много ошибок.

Чего же мы хотели?

Как я уже говорил, помимо JavaScript, я еще очень много писал на XSL. Это тоже очень хороший шаблонизатор, возможно, самый мощный. Но тоже не без "родовых травм". Хотя какие-то его возможности нужны.

Плюс, если у нас есть XSL, один из его недостатков – это то, что в него не встроен какой-то алгоритмически полный язык. Когда мы хотим пересортировать там массив – это еще решаемая задача. Но когда мы хотим там пройтись "регуляркой", в XSL это настоящий ад.

Поэтому мы захотели лучшего от этих двух вещей?

 

Вот пример, который я вам показывал, он очень похож на "Django". Мы объявляем некоторый блок. Вот здесь у нас есть "title". Его содержимое сразу же определяется – что это "Mail.ru". Если мы подключаем этот шаблон как есть на странице, то у нас выводится "title" с заголовком "Mail.ru". Все проекты, которые есть на Mail.ru, могут его подключить и иметь единый заголовок.

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

Сам JavaScript.

Нам очень хочется, чтобы с входными данными можно было работать алгоритмически полным языком. Вот есть такая конструкция, как "fest script". В ней можно что-то поделать именно на JavaScript, а ниже через "fest value", собственно, это вынести.

Вот этот шаблон выведет в Mail.ru.

Сам шаблонизатор.

XML

На самом деле, у нас было очень много споров по поводу XML-синтаксиса. XML-синтаксис избыточен – с этим глупо спорить. Однако наша основная задача состояла в том, чтобы научиться "готовить" JavaScript на сервере. Поэтому не хотелось параллельно решать еще и задачи "давайте мы придумаем в нем синтаксис", "давайте мы в нем придумаем эскейпинг", "давайте мы в нем напишем всякие плагины к IDE". Не хотелось этого решать. Поэтому мы взяли XML, получились с короткой поддержкой IDE.

Любой уважающий себя IDE без всяких настроек вам скажет, что у вас XML не валиден. А валидность XML шаблонов автоматом нам, получается, отдает валидность выходного html. Вот валидация из коробки.

Плюс там еще во всех IDE есть подсветка, автоматическая табуляция. Не хотелось заморачиваться.

Еще в XML есть по умолчанию такая хорошая вещь, как пространства имен. Это расширяемость.

У нас есть шаблонизатор, он не очень-то много умеет. У вас есть реальные проекты. Вдруг вам стала нужна мультиязычность. "Зашивать" ее в fest-шаблон по умолчанию – это как-то странно. Он "разбухнет" и перестанет поддерживаться. А если вы объявляете, допустим, пространство имен своего проекта, то вы можете "перехватить" в компиляторе это событие и обработать это по-своему. Допустим, у вас могут быть специфичные пространства имен fest и Mail.ru.

За многие годы у нас появилась куча инструментов работы с XML. Когда я занимался компиляцией из fest-шаблона в XML, у меня был богатый выбор того, чем можно воспользоваться. В результате я воспользовался SAX-парсером, но не факт, что на этом остановлюсь.

Преобразование XML to XML

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

Реализация

Когда мы определились, как должен выглядеть шаблон (примерно поняли, что нам нужно для тестового стенда, чтобы он начал работать), мы приступили к реализации самого компилятора XML в JavaScript.

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

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

Структура против функции

Мы выбрали два принципиально разных подхода. Я решил компилировать XML в структуру. А Костя компилировал сразу в функцию. Мне это поначалу показалось не очень безопасным.

Чтобы вы понимали, компиляция в структуру – это примерно такой массив.

Хеши означают действие, а строки можно либо вывести сразу к клиенту, либо что-то с ними сделать.

Для пояснения. Первый хеш – "action":"template" означает, что начинается шаблон. Со второй строкой ничего делать не надо, ее можно прямо так вывести к клиенту. Третья строчка означает, что четвертую строку надо пропустить через <i>val</i>, и результат <i>val</i> уже вывести к клиенту.

Или, например (наверное, так будет понятнее), рассмотрим вариант с "if".

Первые, вторые строчки – то же самое, что на предыдущем слайде. Третья строка означает, что следующее выражение надо выполнить. Если оно является истинным, то, соответственно, нужно вывести "true". Если оно является ложным, то надо вывести "false".

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

С реализацией в функцию все понятно. Просто по каким-то условиям начинает конкатенироваться изначальная переменная, которая выставлена в пустую строку.

Результат мы хотели получить вот такой.

Это один из наших главных проектов. Естественно, думая о шаблонизаторе, надо было думать об этом проекте. Список писем с достаточным количеством папок.

Скажу честно: первая реализация, которую я сделал, решала эту задачу за 200 миллисекунд.

Мы с Костей "бодались", по-моему, в течение месяца или полутора месяцев. Приходили после выходных, и один говорил: "А у меня 180!" Второй отвечал: "А у меня 150!" И так далее, и тому подобное. На самом деле, в какой-то момент я сдался, потому что понял, что уже не догоню. Мы начали делать реализацию с функцией, она победила. Когда мы все-таки "вылизали" все до конца, у нас на эту задачу уходило 3 миллисекунды.

Список писем рисовался за 3 миллисекунды. Трансформация примерно такая, это максимально близко к самым простым конструкциям JavaScript. "For" – в "for", "if" – в "if". Choose – это "if {} else".

Нам пришлось немного помучиться с fest:set, поскольку я понимал, что нам нельзя дальше жить без этого переопределения. Оно делается тоже не очень сложно. У нас по мере первого выполнения функции шаблона сначала создается как раз объект "set". По мере того, как ему попадаются XML-блоки "set", переписывается функция, которая, собственно, переопределяет содержимое этого блока.

Поэтому, если "set" много, то при отрабатывании шаблона, который вернет XML, выполнятся только последние из них, а не все. Это тоже дало небольшой прирост.

Очень интересный эффект дало следующее. С первой реализацией мы не очень сильно заморачивались. Когда было 200 миллисекунд – мы, в общем, этого не видели. Но компилировали мы сначала в такую структуру. У нас есть исходный HTML, который компилировался просто в конкатенацию строчка за строчкой.

Когда мы подбирались к трем миллисекундам, мы нехитрой "регуляркой" в скомпилированном шаблоне заменили эти "плюс-равно" на одну строку – мы получили, на самом деле, приличную цифру – плюс 30 % производительности.

Еще раз поговорим о безопасности.

Безопасность

Под безопасностью я понимаю не только XSS: нам обязательно нужно, чтобы шаблонизатор минимизировал его "из коробки". Причем я надеюсь, что он их исключает "из коробки". Но "try catch" должен быть, если вдруг нерадивый верстальщик возьмет свойства без определения (англ. undefined).

Поэтому по умолчанию все в try catch. Тем более, по нашим тестам это "бесплатно".

Это escape. Там есть и escape JavaScript, escape HTML. Пользовательские данные, чтобы не было доступа.

Опять же, как мы видели, это самая "дорогая" операция в нашем шаблонизаторе.

Конечно, используем "strict mode", чтобы не было утечек памяти.

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

Как я уже говорил, компиляция по умолчанию – это как раз "try catch" плюс "escape".


Интеграция

Мы получили цифры, которые хотели. Или, по крайней мере, получили какие-то приличные цифры, с которыми не стыдно прийти.

Следующий интересный момент. У нас в компании, как минимум, есть вот эти языки. Я уверен, что их сильно больше. Но это те, с которым я сталкивался.

Это "Си" – у нас HTTP-сервер на "Си", и на нем много логики написано. Perl: большая часть почты написана на Perl. Какие-то проекты пишутся на Python. Node.js у нас на продакшне нет, но мы его тоже используем в разработке, естественно, с ним надо жить.

Казалось бы, когда мы добавляем к этому многообразию еще и V8, то мы сделаем только хуже. Но когда на части проектов мы добавили V8 ко всем этим технологиям… На самом деле, для это несложно. Нужно, чтобы V8 (там есть Google API) опрокинула вот эти три функции для логирования.

V8 – это просто библиотека, сама по себе она ничего не умеет. Это библиотека, которая принимает что-то на вход, что-то дает на выход. Естественно, когда у вас внутри что-то происходит, вы хотите об этом сообщать внешнему миру. Этот самый fest_log – уже на усмотрение технологии. Он либо в консоль, либо в браузер, либо письмом, либо СМС-кой системному администратору может сообщать, что внутри V8 что-то не так. А V8 эту функцию внутри получает и может как-то взаимодействовать с ней и через нее с внешним миром.

Fest_file, естественно, шаблонизация. Она бьется на файлы, "include", "insert" и так далее, и тому подобное. Поэтому когда V8 собирает и компилирует файлы, ей нужно обрабатывать "include" и "insert". Поэтому внешняя среда должна предоставить V8 возможность прочитать файл.

Dirname – это такая мелочь. Когда вы хотите прочитать файл, вам нужно на что-то опереться. Это директория текущего исполняемого JavaScript-файла. Вот этих трех конструкций достаточно, чтобы V8 заработала.

Произошло следующее. У нас на сервере back-end целый зоопарк технологий. Но есть проект, который на сервере использует fest. Есть проект, который на клиенте использует fest. Есть проект, который на клиенте и на сервере использует fest. Везде единый синтаксис, и верстальщики мигрируют между проектами, никаких проблем с этим нет. Конечно, мы "из коробки" получили интеграцию с браузером.

Fest компилируется в примитивные JavaScript-конструкции, которые (я уверен) принимаются даже Explorer 5. Просто негде было попробовать, извините.

Работа с реальными пользователями

Итак, у нас есть реализация, и нам все-таки хочется добраться до пользователей, узнать, как все заработает у них. Я с этой реализацией пришел к Игорю и сказал, что у нас тестово-нагрузочные прогоны укладываются в три миллисекунды. Он ответил: "Пляши, как хочешь – а там на все четыре".

Вы должны понимать, что продакшн – это, помимо трансформации, еще какие-то данные, которые надо получить, что-то с ними делать. А 4 миллисекунды – это на все.

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

Наш HTTP-сервер написан на "Си", мы его называем Light. Когда он получает данные от сервера back-end, то он хранит их в плоском хэше. На самом деле, это примитивный вариант. Он должен легко читаться. На всякий случай уточню, что это список писем. Первая строчка означает, что его длина равна пяти, вторая строчка означает, что у первого письма заголовок "letter", а третья строка означает, что оно не прочтенное. 

Естественно, когда мы говорим о JavaScript, то хотим видеть внутри него вот такой V8.

Каким-то образом нам нужно это засунуть внутрь V8. Мы пробовали очень много вариантов. Пересмотрели всякие Binding, PerlOuI и свои решения – по-моему, около двух недель мучились. Вот вариант, который мы точно попробовали – это решение проблемы через V8 API.

Я пробовал, но, к сожалению, не получилось привести код на "Си". Его реально очень много. Я попробую объяснить на словах.

Через V8 API можно было сделать так: если вы в JavaScript пишете JSON, в этот момент V8 обращается к вашему коду на "Си". Код на "Си" в этот момент может вернуть то, что он считает нужным. Он может написать JSON.name. Это будет в два подхода – первый раз он обратится за JSON, а второй – за name. Это очень долго.

Второй вариант – это JSON.parse. Когда вы поднимаете контекcт V8, то на "Си" вы собираете строчку, которая один в один похожа на JSON. Отправляете ее в контекст, и прямо в контексте накладываете на это V8 JSON.parse "из коробки". Соответственно, уже внутри контекста у вас есть хеш, который можно отправлять в шаблон. Это тоже оказалось медленно.

Самый быстрый вариант – это как раз третий вариант. Я попробую все-таки кусок кода на "Си" от него показать.

Вот у вас есть вторая строчка. Это как раз шаблон. Та JavaScript функция, в которую компилируется fest. Она на вход ждет JSON. Вы собираете такую строчку, вызов этой функции, и первый и единственный ее аргумент – это тот JSON, который надо использовать. Даете команду выполнить это.

Цифры не очень были. При ограничении в четыре миллисекунды, две из которых – это трансформация. Мы их замеряли отдельно. Четыре секунды занимала подготовка данных.

Вы сейчас смотрите: казалось бы, четыре миллисекунды на подготовку данных – ну и что? А это 67 % от трансформации.

67 % времени у нас занимает подготовка данных для генерации этого html. У нас была задача уложиться в те же мощности, что сейчас есть. 6 миллисекунд вместо 4, это близко. Но все-таки было обидно. На самом деле, в этот момент мы почти забросили эту идею.

Но я все-таки нашел в себе силы взять тот хеш, который вы помните. Я взял его в текстовом виде, пришел домой. Честно говоря, сейчас не помню код. Но я из текстового вида его как-то превратил в JSON Node.js.

Дальше я его прогнал через шаблон, через который мы тестировали. На всю эту операцию у меня уходило 4 миллисекунды. Ровно те цифры, в которые нам нужно было уложиться. Но это был Node.js.

Я пришел к Игорю с этими цифрами и ожидал услышать от него: "Ты, конечно, молодец. Node.js тоже молодец. В Node.js мы писать не будем. Давай все-таки трезво оценивать мысли". Но услышал абсолютно обратное. Если на Node.js можно, почему мы не можем его использовать? В этот момент я понял, что мы решим эту задачу.

Что мы сделали? Давайте вернемся к тем данным, которые у нас есть в HTTP-сервере.

На самом деле, у нас в HTTP-сервере все данные уже есть. Поэтому мы сделали следующий трюк. Вместо того, чтобы конвертировать данные, в V8 "пробросили" функцию, которая по ключу будет забирать эти данные.

Мы выбросили сразу любые конвертации, которые у нас занимали 4 миллисекунды. Осталось время на проброс. Но оказалось, что это время – 1 миллисекунда. При ограничении в 4 миллисекунды… Напоминаю, что это список писем. Мы получили те миллисекунды, к которым стремились. Допустили к этому реальных пользователей. Не то чтобы они это видели, но ситуация стандартная.

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

Да, кстати, эти результаты показало одно ядро, не сервер, хочу отметить. Сервер обработал 10 миллионов хитов за 30 часов. Среднее время трансформации составило 1,6 миллисекунды. Не то чтобы сервер оказался быстрее. Просто у реальных пользователей разные настройки… Мы-то все тестировали на 25-ти письмах, а реальных пользователей просто местами меньше. Цифра ниже как раз показывает, что полпроцента, допустим, вообще-то укладывались в десять миллисекунд. Но, видимо, просто у них стоит в настройках 200 писем.

Думаю, что цифры прямо пояснять не надо. Тут видно, что у большинства как раз меньше двух миллисекунд. 90 % запросов меньше двух миллисекунд.

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

Когда мы с Игорем с цифрами, которые мы получили, пришли к Ермакову (тогда он был заместителем технического директора), я сказал: "Нагрузочные тесты хорошие, цифры хорошие, задача реальная. Хотим запускать это на пользователей". Как в любой большой компании, у нас есть пара проектов, которые лежат. Вроде как и надо их делать, а вроде и не надо. Я говорю: "Давай мы сделаем какой-нибудь из таких проектов на V8 на fest. Если что-то пойдет не так, то ничего страшного. Забудем про этот проект точно так же, как и забывали последний год. Но если все будет хорого, мы получим работающую штуку, от которой можно отталкиваться".

Игорь посмотрел на меня, посмотрел на цифры и говорит: "Цифры хорошие. Но ты правда хочешь V8 на продакшне?" Я говорю: "Хочу". Он ответил: "Тогда начинай с главной страницы. Иначе эти "сопли" так и будут размазываться дальше". Это была одна страница. Верстка заняла три дня. Мы опять выделили один из серверов front-end и запустили туда половину обычной нагрузки. Получили потребление ресурсов процессора в три раза больше, чем соседний front-end, который сейчас работает на пользователей.

Выглядело это вообще как полнейший провал. На самом деле, мы очень крупно ошиблись (я потом покажу, где). Но это нам дало еще оптимизации. Все было очень грустно, потому что мы настолько были погружены в эту задачу, что у меня весь код шаблонизатора был в голове. Я мог его перебирать в мыслях, не обращаясь к IDE. Я не понимал, где мы могли так проиграть по времени, потому что во всех предыдущих тестах все было хорошо.

Мы проиграли в шесть раз по сравнению с текущим сервером.

Не первый раз, что называется. Стали смотреть. Главная страница занимает 165 килобайт. Из них V8 генерирует 65 килобайт. Остается 100 килобайт. У нас есть технология RB внутри. Чтобы вы понимали, главная страница – это все-таки витрина остальных проектов. Поэтому RB – это средство доставки данных от этих проектов на главную страницу. RB в HTTP-сервер отдает уже, в общем-то, в виде html. Его шаблонизировать не надо. Если надо, то этим RB занимается внутри себя. Поэтому происходит следующее.

У нас есть технология RB. Она "общается" с HTTP-сервером, HTTP-сервер отдает результат V8. V8 конкатенирует это со своими данными, отдает обратно. Еще одно примечание для тех, кто ничего не читал про V8. Он внутри у себя все держит в utf-16. Это означает, что мы… А у нас шла, естественно, от utf-8. Utf-8 utf-16 обратно в utf8.

Вот как выглядел изначально скомпилированный код в очень упрощенном виде.

У нас есть некоторая строка. С этой строкой конкатенируется результат работы RB, и дальше он конкатенируется со следующей строкой V8.

Возникает вопрос: если с RB мы ничего не делаем, зачем ее вообще отдавать V8 и тратить на это ресурсы? Поэтому мы сделали небольшой хак. Из "Си" пробросили еще две функции. Это функция push.К счастью, главная страница у нас сплошная. Поэтому там не надо мучиться с "set" и "get".

Не заостряйте на этом внимание. Если кто-то будет досконально разбираться, вы поймете, о чем я говорю. Там шаблонизация сплошная, вернее, линейная. Как только шаблон отработал "кусок", мы точно знаем, что его можно отдавать пользователям.

У нас следующее происходило: библиотека V8 генерировала по своей логике часть строки и отдавала ее сразу в HTTP-сервер. Он его может сразу отдавать клиенту. Дальше, по логике, нужно выдать кусок из RB. Мы этот кусок даже не получаем. Мы HTTP-серверу сразу говорим: "Бери этот кусок и отдавай сразу клиенту – нам от него ничего не нужно". Получили еще 30 %. Но, на самом деле, оставался проигрыш в четыре раза.

Я заподозрил что-то неладное. Взял вот эту нагрузку, количество хитов, которое мы отправляем на тестовый сервер. Умножил на количество серверов, которое у нас сейчас "поднято", и получил вот это количество хитов в сутки.

440 000 000

А по всем TNS-счетчикам у нас вот столько хитов в сутки.

110 000 000

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

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

Сейчас вся главная страница Mail.ru отдается через V8.

V8 на данный момент генерирует 65 килобайт данных. Может быть, сегодня уже чуть-чуть больше – я все-таки две недели готовил доклад. Время, которое библиотеке нужно для генерации этих данных – одна миллисекунда. Плюс 40 мегабайт на контекст с учетом того, что у нас количество ядер на сервере доходит до 8, не знаю, или до 16-ти. Это, в принципе, не очень напрягает, проблем с этим нет.

Это почти история успеха. Здесь присутствует Игорь Сысоев. Все, кто когда-нибудь думал про V8 на сервере, конечно, должны были прочитать его статью о проблемах V8.

Если я не прав, надеюсь, он меня поправит. Насколько я помню, там речь идет вот о чем. В V8, во-первых, контекст поднимается не мгновенно. Если говорить про реальные проекты, то у нас контекст поднимается с поднятием HTTP-сервера и только один раз. Это занимает около 2 миллисекунд, и, в общем, с этим можно жить.

Вторая проблема. Если вдруг случилось так, что библиотека V8 хотела выделить себе память и не смогла, она в этот момент "падает". В момент, когда мы занимались этим шаблонизатором c JavaScript на сервере, мне удалось выйти на разработчика V8 – Вячеслава Егорова. Он, кстати, часто выступает, это известная личность. Можно легко найти информацию про него. Он эти догадки почти подтвердил. Он утверждает (мы не проверяли, честно), что в текущей реализации V8, если ему не удалось аллоцировать память, выбрасывает исключение (англ. exeption), которое можно перехватить.

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

Что касается Nginx, скорее всего, такое поведение критично. У нас в Mail.ru, по заверениям одного из коллег, в том же самом RB работа в условиях нехватки памяти считается штатной ситуацией. А там, где нехватка памяти считается штатной ситуацией, скорее всего, будут проблемы. Но если мы говорим про обычный проект, то если у вас вдруг начнет заканчиваться память на машине, вы будете думать не про V8, а совсем про другие вещи.

Еще один неприятный момент.

Это статья про V8. Trunk V8, оказывается, очень активно разрабатывается. Самому Вячеславу не удалось воспроизвести эту ситуацию. Но у нас она воспроизводится "на ура". Я надеюсь, что мы поможем ему ее решить.

В какой-то момент мы запустили V8, и у нас обнаружалась утечка памяти. Мы очень долго искали утечку у себя, а потом переключились с trunk на версию 3.6.8, и проблема исчезла. Trunk проблемный. Стабильная версия Node.js – это, на данный момент, 06.14, и она тоже живет на 3.6. Если вы работаете с trunk, помните: вы работаете с чем-то, что не очень стабильно себя ведет. Все-таки переключайтесь на стабильные версии, за которые разработчики отвечают.

Все предыдущие ссылки я привел.

Как раз ссылка на API V8, которая очень понадобится, если вдруг этим займетесь. Ссылка на наш шаблонизатор. Я не могу сказать, что он OpenSource по одной простой причине. На самом деле, OpenSource предполагает некоторую ответственность за то, что ты делаешь. Мы ее, скорее всего, ее на себя возьмем, но конкретно сейчас мы к этому не готовы. Поэтому я просто говорю, что мы просто в открытую разрабатываемся.

Все, что я говорил про V8 trunk, может случиться и у нас. На данный момент мы ничего не стабилизируем. Это продукт, который мы разрабатываем для себя, но в открытом виде. С оглядкой на то, что мы его хотим выложить в OpenSource. Если будут какие-то просьбы типа "pull request" (они уже были, кстати, от некоторых людей), то такую заинтересованность мы, конечно, приветствуем.

У меня все. Я готов ответить на ваши вопросы.  

Вопросы и ответы

Реплика из зала: Спасибо за доклад. У меня такой вопрос. Я не очень понял предысторию, почему нужно было использовать V8. У вас был сайт…

Андрей Сумин: Чуть больше, чем один сайт.

Реплика из зала: У вас были скрипты. Эти скрипты на серверах back-end стали тормозить. Так было или не так?

Андрей Сумин: Нет, совсем. Под "сайтом" вы понимаете проекты "Mail.ru Почта"?

Реплика из зала: Да.

Андрей Сумин: Основных проблем несколько. Первое – у нас двойная шаблонизация. Шаблоны, которые применяются на сервере, нельзя использовать на клиенте. Это проблема. Список писем мы обязаны отрендерить и на сервере, и на клиенте. Поэтому нам нужны были шаблоны, которые работают везде. Нужно, чтобы один и тот же шаблон выдавал один и тот же результат и на клиенте, и на сервере. Это первая и самая главная предпосылка для перехода на V8.

Вторая предпосылка. У меня в команде много людей, которые знают JavaScript. Но нет ни одного человека, который в достаточной для Mail.ru квалификации знает, допустим, "Cи", Python, или что-либо подобное. У меня есть люди, которые способны писать быстрый, хороший JavaScript, без утечек памяти и так далее, и тому подобное. Но нет людей, которые способны это писать на другом языке. Не потому, что они, в принципе, не способны, а потому, что они этим не занимаются каждый день. Плюс к тому, что нужны единые шаблоны на клиенте и на сервере, еще нужны специалисты, которые могут это сделать. Это две основные причины.

Реплика из зала: Нет, вот, смотрите. Если мне нужно список отрендерить на сервере, я там рендерю этот список, и каждому элементу там присваиваю какой-нибудь идентификатор. Или группе элементов присваиваю класс. Пишу на JavaScript скрипт, который что-то там делает. Может, замены какие-то, не знаю… Просто не совсем понятна мотивация использования. Почему двойная шаблонизация получается?

Андрей Сумин: У нас на "Почте" есть список писем. Он составляется из каких-то данных, его надо отрисовать. Дальше мы приходим на клиент, и список писем у нас обновляется раз в какое-то время (потому что человеку могут прийти новые письма). Соответственно, его надо повторно рендерить. Он приходит, естественно, на клиент в виде данных. "Гонять" html затратно по времени. Он приходит в виде какого-то JSON.

Плюс у нас есть разного рода оптимизации. Пришел JSON, его надо превратить в html. Это, опять же, шаблонизация. Плюс списки писем. Для каждого письма достаточно информации, чтобы при клике на письмо пользователю уже можно было что-то отобразить – допустим, заголовок письма. У нас есть тема, автор, мы можем это отобразить, дожидаясь контента письма. Это еще одна шаблонизация.

На клиенте у нас очень много шаблонизаций. Всякие ухищрения типа "я добавлю идентификатор и напишу простенький код на JavaScript" реально не работают. А когда человек находится на странице с письмом, он там может нажать F5, и в этот момент мы должны отрендерить это письмо на сервере, потому что так быстрее, как ни странно.

Письмо – это такой непростой объект. Там очень много всякой логики шаблонизации. "От кого", "Кому", "Прочтенное", "Непрочтенное", "Важное", "Не важное". Есть вложения, нет вложений. Много всего.

Реплика из зала: Ясно. Спасибо.

Реплика из зала: У меня два вопроса. Первый: почему вы выбрали V8. Смотрели ли вы в сторону JavaScript от Mozilla (SpiderMonkey, TraceMonkey)?

Андрей Сумин: Честно говоря, мы в его сторону посмотрели. Лично я выбрал V8, скорее, по политическим соображениям. Большая компания делает продукт для себя. Причина была такая. Когда мы стали результирующие шаблоны запускать на клиенте, в частности, шаблон со списком писем в Chrome "экспандился" за 6 миллисекунд, а в Mozilla – за 3 миллисекунды. Я задумался: может быть, неправильно был сделан выбор?

Мы в результате все-таки достигли 1 миллисекунды (для сравнения: у нас сервер в Gzip тратит сильно больше времени, чем V8). Я решил пока остановиться на V8. Может быть, V8 еще обгонит SpiderMonkey. Хотя на данный момент на нашем шаблонизаторе SpiderMonkey быстрее.

Причины пока политические. С учетом того, что по скорости нам некуда дальше гнаться. Другие проблемы сейчас надо решать.

Реплика из зала: Второй вопрос. Как сервер выглядит с точки зрения архитектуры? Это один процесс, который обрабатывает кучу соединений, или как?

Андрей Сумин: Игорь ответит – он писал это. Лучше ему право ответа отдать.

Игорь Сысоев: Да, это один процесс, который обрабатывает кучу соединений.

Реплика из зала: Понятно. Epoll Linux у вас?

Игорь Сысоев: Да. Один процесс.

Реплика из зала: Все соединения обрабатываются в одном контексте?

Игорь Сысоев: Да, один контекст.

Реплика из зала: Понятно. Спасибо.

Реплика из зала: У меня вопрос, во-первых, про переопределение шаблонов. Как решить такую задачу? Скажем, у нас есть какой-нибудь блок, мы хотим на какой-нибудь странице вывести этот блок в точности и добавить к нему еще что-то. Добавить какую-то дополнительную информацию. Переопределиться с вызовом базового блока. Ты сказал, что у вас есть JavaScript. Как это будет выражаться? Придется написать ассемблерную вставку в этот XML-синтаксис на JavaScript?

Андрей Сумин: Вариантов несколько. Естественно, это может быть ассемблерная вставка, либо можно самому расширить язык шаблонов. Если нам это понадобится, и мы не проиграем по скорости, мы это сделаем, может быть, в текущем шаблонизаторе. Если это нужно вам, и вам требуются такие же скорости, то можно просто расширить текущий синтаксис своим. Либо прислать нам патч.

Реплика из зала: Нет, я-то, конечно, понимаю, что такие семантические удобства "бесплатно" относительно скорости не даются. У меня вопрос в другом. Какие у вас тогда библиотеки блоков, что вам не нужно переопределяться с вызовом базового метода?

Андрей Сумин: Сейчас я не очень понял вопрос.

Реплика из зала: У вас есть какие-то библиотеки, которые потом используются и в которых есть переопределение? Или просто все написано на одном проекте в один слой, и никаких переопределений нет, потому и никакой богатой семантики не нужно.

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

Реплика из зала: У меня еще один вопрос. Ты рассказывал, как здорово, что есть классные JavaScript-специалисты. Для них все это понятно и удобно. Зачем тогда XML-синтаксис, почему бы и синтаксис не уложить в JavaScript-синтаксис? Тогда специалист будет использовать "родной" JavaScript-редактор, например. Банально код ему будет проще читать, поскольку это один и тот же код. Мы вроде бы унифицировали исполнение этих шаблонов с точки зрения движка. Но с точки зрения синтаксиса мы, наоборот, создали вторую сущность.

Андрей Сумин: Сущность создали. Но тут несколько причин. Это следствие нескольких решений. Во-первых, я упоминал, что задача, которую нам точно надо было решить, – это научиться "готовить" JavaScript на сервере. Для того, чтобы ее решить, не хотелось отвлекаться на придумывание синтаксисов, решение проблем с "эскапированием" и так далее.

Реплика из зала: Но в JavaScript придуман синтаксис, и решены проблемы с "эскапированием". Точно так же, как с XML.

Андрей Сумин: Нет, когда ты шаблонизируешь на JavaScript, то в любом случае придумаешь свой язык.

Реплика из зала: Почему? Почему я придумываю язык, а не пишу на JavaScript?

Андрей Сумин: Потому что конкатенировать строчки на JavaScript – это очень неблагодарное занятие.

Реплика из зала: Ладно. Не понимаю.

Андрей Сумин: Второе. Может быть, когда JavaScript на сервере станет обычным делом на многих проектах (может быть, не для всего проекта), по крайней мере, когда к этому будут относиться как к обычной практике, скорее всего, появится другой шаблонизатор, не fest. Очень может быть. Почему нет?

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

Второе. 99 % наших выходных документов – это XML. Когда у тебя управляющая конструкция XML и на выходе XML, проекту сильно проще, потому что XML понимают редакторы. Они же это валидируют, плюс есть куча инструментов по работе с XML. На текущий момент это финальное решение. Что будет через год, я не могу сказать.

Реплика из зала: Окей, спасибо. Я заканчиваю. Да, правильно ты говоришь, на мой взгляд. Самая главная проблема – это привнести JavaScript на сервер, а дальше это можно уже оптимизировать. Это, грубо говоря, принципиальный шаг. А дальше идут уже непринципиальные мелкие улучшения, которые можно сделать в будущем. Спасибо за то, что ты тоже такой опыт провел.

Реплика из зала: У меня вопрос в продолжение одного из предыдущих – про один процесс. Это действительно один процесс, к которому идут все запросы? Или это несколько процессов на разных портах, куда через upstream, допустим, Nginx передает различные запросы от разных пользователей?

Игорь Сысоев: Нет. Там нет Nginx. Там – да, один процесс, куда все идет. На самом деле, там на одной машине несколько форков. Все они на ядрах "висят". Туда как-то балансируются клиенты.

Реплика из зала: Понятно. Спасибо.

Реплика из зала: Еще такой вопрос. Несколько лет назад я тоже пытался заниматься JavaScript на сервере. С отладкой был вообще сущий ад. Какие инструменты отладки используете, чтобы все это профилировать?

Андрей Сумин: У нас сейчас есть очень большой бонус – это Node.js. Все, что мы запускаем, "из коробки" работает в Node.js. Там достаточно хороший инструмент отладки. Плюс, если мы говорим именно про шаблонизатор, мы всегда можем свой шаблон отправить на клиент. Там еще больше инструментов отладки.

Третье: как я уже говорил, могут быть какие-то специфические накладки со средой. Можно сделать так, чтобы среда просто "пробрасывала" функцию. У нас функция лог "пробрасывает". На самом деле она "log", "var" и "error" "пробрасывает", и V8 может "общаться" с внешним миром. Там уже среда куда-то выведет эти данные. Плюс еще в самом V8 API есть очень много средств работы с V8 через API V8. По крайней мере, когда он "падает" – он через API говорит, что он "падает".

Реплика из зала: Спасибо большое.

Реплика из зала: Спасибо за внимание.

Андрей Сумин: Спасибо всем.

Комментарии

Нет ни одного комментария

Только пользователи могут оставлять комментарии

Возможно, вам будет интересно:

Александр Быков

Александр Быков

Старший программист в Yandex, Москва.

Александр Быков (Mail.Ru) рассмотривает типичные узкие места систем мониторинга и различные варианты их оптимизации.

Евгений Лисицкий

Евгений Лисицкий

Ведущий разработчик компании "Спорт Сегодня".

Евгений Лисицкий рассказывает о стремительно развивающейся технологии WebSocket, которая позволяет создавать веб-приложения, работающие в режиме, близком к реальному времени.

Дмитрий Тарахно

Дмитрий Тарахно

В Webprofiters руководит проектированием и разработкой сайтов, занимается технической частью аналитических проектов. До прихода в компанию в течение 6 лет работал в ряде софтверных компаний и системных интеграторов (ЛАНИТ, Upscale Soft, Optima) на проектах по разработке и внедрению корпоративных информационных систем федерального масштаба.

Доклад о систематизации опыта в области того, как жить с клиентами, с которыми мы уже начали работать, чтобы не было нам самим печально. Интересно это будет компаниям, которые оказывают какие-то услуги – консалтинг, разработка и т.п.