Наверх ▲

Сервер-агрегатор на python (а ля Xscript/FEST)

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

Андрей Сумин: Добрый день, меня зовут Андрей Сумин. Сегодня я расскажу вам о сервере-агрегаторе Frontik, который мы применяем у себя в Headhunter. Автор продукта – Андрей Татаринов. Наш доклад будет состоять из трех частей. Первую часть озвучу я. Вторую часть вы услышите от Павла Труханова. Он расскажет о внутренних особенностях продукта. Михаил Сабуренков обеспечит третью часть, поскольку он пользуется этим продуктом по 8 часов в сутки. У нас коллективный доклад. Может быть, наш опыт вам пригодится.

Каким образом мы поняли, что такой продукт, как Frontik, нам нужен?

В далеком 2007-м году сайт Headhunter чувствовал себя примерно вот так. На всякий случай поясню: красное – это плохо. В рабочее время сайт начинал работать медленно и освобождался, когда все сотрудники кадровых агентств уже уходили домой.

Было очень много смешных, но неприятных ситуаций. Люди звонили и говорили: «Запустите сайт, а то мы зарплату не получим». Оказалось, что в некоторых кадровых агентствах люди получали зарплату на основании статистики, которую выдавал наш сайт.

Горизонтальное масштабирование нам уже перестало помогать. Само большое приложение перестало умещаться на одном сервере, где оно располагалось целиком.

Мы стали пытаться разделить наше приложение на отдельные элементы. В первую очередь мы отделили поиск и "баннерку". Это прошло успешно, поэтому нам, естественно, захотелось продолжить.

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

Что нужно сделать и как устранить неполадки, чтобы попасть в середину, было совершенно непонятно.

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

К счастью, мы не были пионерами в этой области. В 2007-м году уже было 2 готовых решения. Одно из них – XScript от компании Яндекс. Другое – FEST от Mail.ru. Как раз на HighLoad-2007 представитель Mail.ru выступал. Оба решения работали по HTTP, что нас очень устраивало. Некоторые считают HTTP старым протоколом, но к нему очень много всяких "примочек".

Мы выбрали XScript, потому что в 2007-м году он был открытым решением. Уже можно было скачать, установить и собрать.

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

Кстати, если вдруг кто-то здесь от Mail.ru или от Яндекса, и я скажу неправду, то вы меня потом, пожалуйста, поправьте. У Mail.ru был собственный шаблонизатор (FEST). Это нам не очень подходило. А в XScript была возможность в качестве шаблонизации использовать XSLT, что нас устраивало.

Стоит отдать XScript должное. В сентябре-октябре 2007 года мы начали переезд самых высоко нагруженных систем, а к весне 2008 года мы его закончили. В сентябре у нас традиционно пик нагрузки на сайте. В 2008-м году мы чувствовали себя гораздо лучше - посмотрите сами.

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

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

Нам кажется, что в Яндексе тоже так считают, потому что с пятой версии в XScript встроен Lua. Скорее всего, он там появился именно для этого.

Нет доступа к работе внутри HTTP-протокола. Я искал эти возможности в исходном коде XScript, разговаривал об этом с господином Оболенским лично.

Когда была нужна полная работа с ответом на запрос (англ. Request-response) - вплоть до заголовков и полного разбора "body", - с XScript были проблемы. Возможно, что они не так уж серьезны. Есть исходники, всегда можно их доработать.

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

Далее Павел расскажет о том, как мы это сделали.

Павел Труханов: Что такое Frontik? Сейчас я постараюсь рассказать вам об этом.

Frontik – сервер-агрегатор. Вот основной сценарий его использования. К нам приходит пользовательский запрос. Мы хотим понять, чего хочет от нас пользователь. Отправить запросы каким-то нашим серверам back-end. Подождать от них ответов. Разобрать их ответы. Наложить шаблонизацию. Отдать результат (страницу) пользователю обратно.

Все это происходит по HTTP-протоколу. Frontik ориентирован на то, что серверы back-end отдают XML.

Как сделан Frontik?

Frontik работает поверх Tornado. Это сервер приложений (англ. Application Server). Вы пишете для него свое приложение, и он его запускает.

Tornado – это асинхронный HTTP-сервер. Frontik его запускает в одном потоке (англ. thread). В одном модуле "ioloop" он производит выборку файловых дескриптеров. За счет этого осуществляется прием новых соединений, их обработка и хождение по серверам back-end. Для этого у Tornado есть AsyncHttpClient, который использует Frontik.

Какое место занимает здесь Frontik?

Он является надстройкой над Tornado и занимается диспетчеризацией запросов. Сейчас реализована прямая переадресация (англ. mapping) URL запроса на файловую систему от корневой www.ru-директории вашего приложения. Если в нужном месте лежит файл на языке Python, в котором объявлен "handler" (класс "handler" указывает на следующую обработку через Frontik), то Frontik найдет его, запустит и отдаст ему запрос на обработку.

Используя средства Frontik, вы можете отправлять запросы серверам back-end по протоколу HTTP, собирать их вместе, добавлять новые запросы (в зависимости от проверки парсером и обработки результатов ответов). Также можно агрегировать XML, который вам нужен, от всех серверов back-end, накладывать шаблонизацию и отдавать пользователю.

Вот реальный пример. Тривиальный пример – понятно. Там один сервер back-end - мы пошли к нему, получили XML, сверху наложили шаблонизацию.

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

Как это работает?

Есть два сервера back-end – сервер поиска и "опечаточник". Поисковый сервер занимается поиском. "Опечаточник" занимается поиском в запросе возможных исправлений и вариантов замены для них.

Соответственно, мы отправляем первоначальный пользовательский запрос и серверу поиска, и "опечаточнику". В нашем примере "опечаточник" возвращает данные об ошибке - нужно добавить букву "т". Мы тут же отправляем исправленный запрос на поиск еще раз. Поиск асинхронный, он возвращает два ответа. Гипотетически может сложиться ситуация, когда второй ответ (отправленный на поиск повторно после хождения на "опечаточник") придет раньше, чем первый.

В общем, мы получаем два ответа от поиска. В одном (по оригинальному запросу) найдено 4 вакансии. В другом найдено 2000 вакансий, что мы и показываем пользователю.

Какие функциональные возможности предлагает Frontik?

Это, например, отображение приветствия ("helloworld"). В файле на языке Python есть класс "Page", который наследуется от обработчика страницы (англ. Page Handler). Декоратор Set_xsl устанавливает, что какой-то XSL назначается ему для шаблонизации.

Соответственно, определяется функция class get_Page. Эта функция отвечает за получение запросов (get_request), которые придут на этот обработчик.

Что мы здесь делаем? Мы создаем XML-узел (англ. XML node) ‘hello’. Добавляем в него текст ‘world’. Кладем в собственный документ (у обработчика страницы есть self.doc), который потом обрабатывается посредством XSL.

Теперь мы хотим перейти на сервер back-end. Для этого есть функция get_url. Вы передаете запрос и можете передать свой обратный вызов (англ. callback). Обратный вызов получает проверенное парсером тело запроса в виде переменной XML и сам запрос. Вы можете, соответственно, как-то это обработать.

В нашем случае, если нет узлов, то пишем: «Ничего не найдено».

Функция get_url

Соответственно, метод возвращает объект типа "будущий местозаполнитель" (англ. future placeholder). Если у вас нет задачи обрабатывать результат ответа от сервера back-end, и вы просто хотите добавить его на конечную страницу (это могут быть справочники, общие куски), то местозаполнитель можно сразу "положить" в конечный документ.

Frontik подождет, пока запрос придет от сервера back-end и положит его в указанное место в документе. Вы могли его "завернуть" в какие-то узлы XML или что-то еще с ним сделать, как с гипотетическим будущим XML-ответом.

SyncGroups

Иногда складывается ситуация (как в случае с сервером-"опечаточником"), когда нам нужны ответы от нескольких серверов back-end. Так как мы ходим на них асинхронно, нужна точка синхронизации.

Мы здесь объявляем такую группу, передаем ей в конструктор функцию finish_callback. Она будет вызвана после того, как все запросы, которые в нее добавлены, будут выполнены.

Делаем два запроса к серверам back-end: один – запрос "get", другой – запрос "post". Обратные вызовы к этим запросам объединяем в одну группу. В "опечаточнике" (если "Callback 2" – это "опечаточник", мы можем добавить еще что-то в это группу). Получается очень гибкое хождение по серверам back-end.

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

Во-вторых, как я уже сказал, у него гибкая архитектура. У нас были задачи, когда казалось, что нам мало XSLT. Все это быстро решается.

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

Михаил Сабуренков: Меня зовут Михаил Сабуренков. Андрей и Павел рассказали, как мы пришли к разработке продукта Frontik, как он устроен изнутри. Я вам сейчас поведаю, как мы его используем в реальной жизни, какие интересные задачи мы смогли на нем решить.

Изначально Frontik использовался как сервер-агрегатор XML, который собирал XML и накладывал на него XSLT. В какой-то момент мы поняли, что XSLT не подходит для решения некоторых задач.

Мы добавили в серверную часть Frontik еще этап обработки документов после XSLT, это было достаточно дешево. XSLT выдает нам уже результирующий документ, который проходит еще один вариант пост-обработки.

Первое, что мы реализовали на пост-обработке, это переводы. Сайт Headhunter.ru многоязычный (есть как английская, так и русская версии). Весь статический текст на сайте хранится в базе. Нужно, чтобы предоставить доступ, чтобы модераторам, пользователям, администраторам было легко вносить правки. Поэтому на этапе XSLT мы вставляем все необходимые переводы по ключу.

Для этого нам нужно, чтобы в XML уже были все возможные переводы. Для этого мы должны сходить в сервис переводов, получить результат и при необходимости вставить.

Если перевод какой-то динамический, логика вставки перевода описана на XSLT, мы должны уже иметь на XML все возможные переводы. Поэтому этап вставки переводов мы вынесли на пост-процессинг.

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

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

Чтобы рассчитать, на какой сайт ставить ссылку какой вакансии, нам необходимо было выгрузить в XML весь список региональных сайтов. Как показано, этот список занял порядка 200 килобайт в XML, 7000 узлов. Нужно было довольно простой XSL-логикой разобраться, какой поставить.

Естественно, средство XSLT при таком объеме XML стало не очень хорошо себя чувствовать. Левая сторона графика – это время XSLT-трансформации с этим огромным XML. Квантиль в среднем было 200 миллисекунд.

Когда мы вынесли это в пост-процессинг (из XML у нас ушло 200 килобайт XML, вся эта логика начала обрабатываться на Python), XSLупал на порядок.

Есть еще одна задача, которую мы достаточно легко и просто решили на Frontik. Для реализации примеров с предыдущих двух слайдов нам необходимо было доработать сервер Frontik, добавить само понятие пост-процессинга, а потом написать пост-процессор в Application Summit. В случае с кэшем нам не пришлось делать никаких патчей для Frontik.

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

Мы добавили этап "memcached". Теперь Frontik ходит к HTTP XML серверов back-end и "достает" из базы только то, чего нет в "memcached". За счет этого мы смогли в два раза уменьшить время обработки запросов.

Самое главное, что Frontik позволил нам сделать, - это избавиться от дублирования кода верстки.

Примерно 3 года назад, когда мы начали отходить от монолитного приложения на XML, серверам-сборщикам нужно было собирать данные, потом обрабатывать их на XSL, - появилась проблема дублирования кода верстки. Часть проекта уже была написана на XSLT, часть проекта еще была на JSP, унаследованный код был очень старым.

Например, обвязка сайта (заголовок, "подвал" сайта и левое меню) – этот код верстки дублировался как на XSLT, так и на JSP. Мы пытались решить эту проблему, избежать этого дублирования. Frontik позволил нам сделать это.

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

Теперь все страницы проходят через Frontik. Frontik ходит за обвязкой, статическими файлами заголовка, "подвала" или как-то генерит левое меню. Потом от JSP получает именно контент страницы и накладывает на все это XSLT. В XSLT этот контент просто пробрасывается в результирующий документ.

Но тут появилось несколько проблем (вполне ожидаемых). Зачастую технология JSP нам отдавала HTML. Не XHTML, не XML – это просто какой-то суп из тегов. Неважно. Важно то, что зачастую она просто выставляла необходимые заголовки (или выставляла настраиваемые), "пробрасывала" куки. Иногда JSP вообще отдавала не текст HTML, а какие-то PGF, по сути - просто бинарные данные.

Поэтому на Frontik нам пришлось (и мы смогли это сделать) полностью разобрать весь HTTP-запрос, который приходит к нам от JSP. Мы "пробросили" и поставили нужные заголовки, установили куки. Сделали редирект при необходимости, если там трехсотые коды, выставили нужные статус-коды, 404 – не знаю.

Также в каких-то случаях мы запроксировали данные. Если приходил бинарный ответ на тело запроса, то мы его просто "пробрасывали"  сразу в браузер, минуя все XSLT-трансформации.

Конечно, из-за того, что изначально Frontik не был под это "заточен" (все-таки он в первую очередь сервер-агрегатор XML-данных и наложений на XSLT), у нас возникли некоторые проблемы. Одна из главных проблем возникла с обвязкой сайта: выяснилось, что Frontik не умеет делать подгрузку в фоновом режиме (англ. streaming). Если к нему пришли огромные бинарные данные, то ему придется загрузить их все в память, и только потом отдать.

Есть другие проблемы, о которых расскажет Павел.

Павел Труханов: Frontik все еще является разрабатываемым проектом. Он у нас в продакшне. На нем уже работает весь Headhunter. Данные от JSP тоже идут через него. Весь HeadHunter работает через Frontik. Но так как это разрабатываемый проект, то, соответственно, есть проблемные места (они же – планы развития).

Frontik запускает Tornado в режиме одного потока (англ. single thread). Соответственно, если у вас многоядерная машина, то вам нужно запускать количество экземпляров (англ. instances) Frontik по количеству ядер, чтобы машина работала на полную.

К чему это приводит? Например, если у вас экземпляр держит в памяти тяжелые объекты (у нас это скомпилированный XSL), то, запуская количество экземпляров по количеству ядер, вы умножаете память, занимаемую под эти объекты. Хотя они работают только на чтение, и нет смысла держать много их копий в памяти.

Вторая проблема – блокировка на время вычислений. Например, если у вас в процессе обработки страницы запроса идут вычисления, затратные с точки зрения ресурсов процессора, то модуль "ioloop" блокируется, пока все это считается. Например, таким затратным действием является наложение XSL.

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

Сейчас создан пул потоков, имеющих общую память. Все наложение XSL туда вынесено.

Об отсутствии подгрузки в фоновом режиме (англ. streaming) Михаил уже рассказал. Если Frontik работает как прокси, и приходит большой кусок бинарных данных в файле pdf, сгенерированный сервером back-end, то надо "пробросить" насквозь. В настоящий момент этого нет - Frontik сначала в память загрузит, потом отдаст.

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

Над всем этим мы собираемся работать. Еще один пункт, который сюда можно внести, это возможность транспортировки данных по URL (англ. URL mapping). Мы собираемся сделать полноценную поддержку URL Rewrite, потому что таблица Rewrite на нашем сервере nginx на продакшне сильно "разрослась". Нас это не устраивает. Это произошло из-за Frontik. Мы собираемся это переписывать, частично уносить во Frontik.

Андрей скажет несколько слов в заключение.

Андрей Сумин: Это последняя смена актеров.

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

По первой ссылке вы можете скачать сам проект. Какую-то минимальную документацию, типа "как сделать "helloworld", Андрей написал.

По второй ссылке можно скачать проект, работающий на Frontik. Он раскрывает большинство возможностей Frontik. Он, конечно, побольше, чем "helloworld". Он хорош тем, что там практически нет бизнес-логики. Там не нужно в коде разбираться с логикой самого проекта. Он демонстрирует именно возможности Frontik, по большей части. Там есть интеграция с API ya.ru. Кстати, по секрету, там у нас еще API от Headhunter.

Посмотреть, что же этот проект рисует, можно по последней ссылке.

Наверное, все. У кого-то есть вопросы?

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

Вопрос из зала:
У меня вопрос по поводу манипуляций с пост-обработкой. Зачем она нужна? У нас подобная задача, мы подобным образом ее решали. Те два случая использования пост-обработки, которые я запомнил (переводы и большие XML, которые надо обрабатывать), решаются совершенно элементарно.
XSLT-процессоры поддерживают две вещи: кастомные Xpas-функции и можно писать кастомные элементы. Когда в трансформации встречается кастомный элемент, будет вызываться обратный вызов, чтобы обработать этот элемент. Обратный вызов туда, куда укажешь при компиляции XSLT-трансформации.
В итоге (можно даже без кастомных элементов) можно взять кастомные Xpas-функции. Можно взять решение, которым пользуется весь цивилизованный мир, типа get text. Просто написать в XSLT-трансформации. Там, где нужно вставить текст, вызов get text. Все это красиво и быстро работает, без всяких "регулярок". Это раз.
Второе. У вас была проблема: большой XML, на который очень долго накладывается XSLY. Тут вопрос решается в пост-трансформации, но не готовой страницы, а изначально из самого XML. Надо выкинуть из него все до того, как накладывать трансформацию.
Михаил Сабуренков:
По поводу первого вопроса, первой пост-обработки с переводами. Да, можно было на уровне XSLT обращаться к выполнению какого-нибудь кода или ходить динамически. Просто расскажу, как это было изначально и почему мы пришли к этому.
До Frontik у нас было так: чтобы перевод был в базе, нужно было прописать его в XML (до этого был XScript). Соответственно, когда он там динамически, то на странице 100 ненужных переводов.
Нам было просто лень все это вбивать в трех местах. Мы это вынесли в пост-процессинг. Вынесли в таком виде - для нас это было очевидно. Мы не хотели лезть именно в XSLT, чтобы вызывать из него кастомные вещи. У нас XSLT "крутится" первым. По-моему, там кастомного минимум.


Комментарии

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

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

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

Сергей Туленцев

Сергей Туленцев

Разработчик популярных приложений для социальных сетей, где имел возможность испытать MongoDB под реальной нагрузкой.

Сергей Туленцев рассказывает, в каких проектах и сценариях не стоит использовать MongoDB.

Ярослав Городецкий

Ярослав Городецкий

Генеральный директор CDNvideo. В Интернете и телекоме с 1996 года. Обожает придумывать и запускать новые продукты и услуги.

Ярослав Городецкий (CDNvideo) делится своим опытом построения сети доставки контента, работающей в России, СНГ и Западной Европе.

Адам Кнапп (Adam Knapp)

Адам Кнапп (Adam Knapp)

Директор по развитию технологий облачного хранения в GoDaddy.com.

Рассказ о технологиях облачного хранения в GoDaddy.com, о проблемах, их решениях, также немного о процессах и управлении проектом.