среда, 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 сайтов, но не сервисов для массовой перекачки данных. Вывод прост: надо брать простые инструменты сквозь которые видно что происходит. Ещё полезно изучать не только тех. документацию, но и мысли авторов относительно области применения их детищ. Эту область хорошо сопоставлять со своей задачей и делать выводы.

Комментариев нет: