Показаны сообщения с ярлыком lift. Показать все сообщения
Показаны сообщения с ярлыком lift. Показать все сообщения

среда, 2 ноября 2011 г.

Опыт работы с Lift: часть 2

Продолжая начатое в прошлом посте выкладывание материалов ScalaSPB расскажу ещё два занятных эпизода из своего почти годового общения с Lift. Дальше будут интехнические подробности и даже куски кода. Не пугайтесь совсем, дальше в серии будет больше абстракции и философии.

Проблема с локализацией

В один светлый день функционал проекта показался близок к полному а архитектура к устоявшейся. В этот день было решено провести нагрузочное тестирование, чтобы понять как скоро станет плохо, если проектом кто-то начнёт пользоваться. Идея была с энтузиазмом подхвачена и воплощена в жизнь. К сожалению результат оказался не очень радужным. Сервер, использующий отличную кэширующую БД, написанный на замечательном, масштабируемом языке и использующий топовую EC2 конфигурацию оказался способен переварить не более 20 запросов в секунду (наиболее тяжёлых из возможных надо заметить).

Обдумывая возвращение и прекрасного мира стартапов в кровавый энтерпрайз я начал применять все подручные средства для поиска причин такой небодрой работы сервера. Первым попавшимся тулом был munin, отчёты о загрузке CPU если конкретно. Он был весьма обнадёживающим: загрузка была около 15% на сервере и 5% на БД. После недолгой медитации на эти отчёты мне пришла гениальная идея: погрепать код на слово "synchronized". Это правда ничего не дало, но немного подняло мораль.

Следующим шагом стало настроить на сервере доступа к JMX и воспользоваться им для подвода информации в VisualVM. И это был частичный win. Кусочек кода внутри Lift (ладно, не точно этот, в том момент использовался Lift 2.2, но его исходники кажется остались только в виде архивов) пользовался дикой популярностью среди заблокированных потоков. Собственно выбора особо небыло, все обращения к S.? были заменены на свежедобавленный ResourceBundle.

Потребление памяти

Ещё одним вопросом который меня долго занимал было куда сервер девает память. Дело в том, что под максимальной загрузкой он выедал все отведённые 4GB примерно за 3-4 секунды. Признаков того, что именно это ограничивало производительность не было, но любопытство не давало покоя и выкроив пару свободных часов я занялся поиском причин.

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

Результат был достаточно предсказуем - больше половины занимали вариации append из StringBuilder и скальный :: (конструктор списка то есть). Ещё подозрительно близко к вершине обретался некий "scala.text.DocCons", но на тот момент должное внимание ему уделено не было. Беглый осмотр самых толстых веток того самого дерева пожирателей памяти показал, что все они растут из метода JsonAST.render.

Для того чтобы понять причину пришлось проследить нелёгкий путь, который переживает объект перед тем как отправиться к клиенту в виде Json. Всё обилие вариантов представлено на картинке в докуметации. Наша ветка - от "case class" до "string". Хм... две промежуточные формы представления. Кажда требующая создание своей копии данных. (Ну не совесм своей, строки ре-используются и то не всегда - эскейпинг вынуждает посимвольно копировать строки на стрелке JSON AST -> Document). Практически двухкратный оверхед.

Отдельно стоит рассказать про scala.text.Document, которую я тогда попутно для себя открыл. Это мини-библиотека для форматирования текста отступами. Кажется нигде, кроме как в собственных исходниках она не описана. Очень простая и компактная (один файл, 120 строк). Как ей пользоваться очевидно из исходников, если не до конца очевидно можно посмотреть на всё тот же render. Единственная её проблема - это выделение памяти на каждый чих, что впрочем и не удивительно - красиво форматируют обычно для людей, как следствие не гигабайтами.

Не буду приводить все рассчёты, но у меня оказалось, что паразитные копии строк + AST из пакета scala.text были повинны примерно в 20% потребляемой памяти. Подумываю о замене всей этого хозяйства на прямое формирование строк, при случае естественно. Кроме того этот способ более не является рекомендуемым в Lift и ему предложены некоторые альтернативы.


Мораль

Для начала надо отметить, что это всё-таки не столько проблема, сколько особенность Lift'а. Он создан для поддержки AJAX-COMET сайтов, но не сервисов для массовой перекачки данных. Вывод прост: надо брать простые инструменты сквозь которые видно что происходит. Ещё полезно изучать не только тех. документацию, но и мысли авторов относительно области применения их детищ. Эту область хорошо сопоставлять со своей задачей и делать выводы.

суббота, 15 октября 2011 г.

Опыт работы с Lift: часть 1

После доклада на ScalaSPB образовалась подборка материалов, которыми грех не воспользоваться. Заодно включу в них то, что никак не вписывалось в доклад: примеры с кодом и философские рассуждения. И начну я пожалуй с наиболее простого и короткого кусочка, а именно опыта общения с Lift.

(Тут нужна небольшая вводная: речь идёт об опыте разработки REST сервиса. Он во-первых заведует преобразованиями между domain model и схемой хранения данных. Во-вторых по ходу дела планирует и выполняет всякие дополнительные задачи вроде отправки оповещений на почту или шаринга контента в facebook.)

Итак, от Lift'a используется практически два модуля. Это поддержка REST-сервисов и сериализация case классов в JSON (что однако тянет с собой всю инфраструктуру работы с HTTP). В работе с ними было три эпизода, которые представляют интерес. Ниже привожу первый из них.

Нестандартная авторизация

Первая проблема, которую пришлось решить это не совсем стандартная схема авторизации, используемая в нашем приложении. А именно: все экспортируемые методы делятся на два класса: приватные и публичные. Приватные защищаются на уровне файрвола от внешнего доступа и к ним на уровне приложения доступ свободный. Публичные доступны всем и соответственно на них висит авторизация. Авторизация не простая а по токену, добавленному одновременно в куку и параметр запроса. Это не что иное как издревле известный метод борьбы с CSRF. Придумать легко, а вот обучить Lift такому поведению потребовало немного нестандартного приёма.

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

Для того, чтобы под неё подстроиться была выбрана следующая стратегия:

  1. Помечать часть путей внутри сервиса, как требующие авторизации требуя для них определённую "волшебную" роль;
  2. Выполнять аутентификацию при наличии токенов путём присваивания текущему пользователю двух ролей: "волшебной" и роли с его идентификатором в качестве имени;
  3. Предоставлять нашему коду абстракцию над предыдущими двумя шагами вида "получить авторизованного для данного метода пользователя". Этот метод изящно реализовывался паттерн-матчингом над списком ролей :)

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

Ну а рассказ про замеченые проблемы с производительностью будет в следующем псто...