пятница, 25 декабря 2009 г.

Когда система тормозит...


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

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


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

1. Работа сервера СУБД. Понятно, что если воткнуть Oracle на PIII с 256МБ ОЗУ, то ни о какой нормальной работе и речи быть не может. По-хорошему, необходимо очертить круг поддерживаемых приложением СУБД и иметь четкие системные требования и рекомендации по настройке каждой из них. Впрочем, если заказчик говорит, что у него конкретная проблема, а в целом приложение работает нормально, то данный вариант можно считать экзотическим.

2. Подключение к СУБД. Если приложением активно пользуются и/или оно запускает много фоновых потоков, в которых используется база данных, то необходимо обеспечить пул соединений с СУБД. Само по себе установление соединения - операция довольно затратная, гораздо эффективнее при старте приложения создать несколько таких подключений и выдавать их системе по необходимости.

3. Индексы, индексы, индексы... Наиболее вероятная причина возникновения проблем. Во-первых можно банально забыть при разработке создать индекс по какому-либо полю. Например, сначала по данному полю никаких условий не накладывалось, затем условия появились, а индекс создать забыли. Во-вторых, можно не забыть создать индекс, написать соответствующую миграцию, но по какой-то причине ее не выполнить. В-третьих, при изменении структуры БД не следует полагаться на Hibenate - в режиме update Hibernate не создает индексы! Ну и в конце-концов отсутствие индексов именно на базе данных конкретного заказчика может быть следствием неосторожного поведения его/наших администраторов.

4. Тяжелые либо лишние SQL-запросы. При разработке бизнес-систем довольно часто используют OR/M-фреймворки, в частности - Hibernate. При этом данные фреймворки могут генерировать сложный SQL-код, состоящий из большого количества нетривиальных запросов. Такое поведение может быть вызвано неправильно составленным мэпингом и неверно выбранной стратегией наследования. Типичной ошибкой является отказ от ленивой стратегии загрузки объектов, вызванный неумением решать возникающие с lazy-loading проблемы. Я лично видел, как удаление из мэпинга lazy="false" ускоряло работу системы в 30 раз. Стоит помнить, что по-умолчанию для загрузки из базы данных каждого сложного поля Hibernate будет генерировать отдельный запрос на каждую сущность. Если у вас 1000 сущностей - вы получите 1000 дополнительных запросов. При этом, если используется глубокая иерархия наследования - запросы будут очень тяжелыми, включать в себя много операций соединения (JOIN). Что самое противное - сложный SQL-код может скрываться за довольно безобидным HQL-запросом.

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

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

2. Чехарда с подключениями. Само подключение к внешней системе может быть ресурсоемкой операцией, особенно если внешняя система сильно удалена. Необходимо стремиться к кэшированию подключений или создать пул таких соединений. Проблема обычно заключается в том, что если пул соединений с СУБД может создать, например, Hibernate, то пул соединений с каким-либо веб-сервисом придется реализовывать руками.

3. Низкая производительность внешней системы. Нет-нет, система доступна, но работает очень медленно + канал связи плохой. Если мы никак не можем исправить данную ситуацию и/или внешня система вообще не наша, то необходимо реализовать синхронизацию. Суть синхронизации в следующем: мы создаем локальное хранилище (возможно в терминах именно нашей предметной области) в которое помещаем объекты из внешней системы. При старте приложения, совершении действий или периодически мы синхронизируем состояние нашего хранилища с информацией, полученной от такой системы. Конечно, данный подход не всегда реализуем, но часто позволяет повысить производительность разрабатываемого ПО.

Итак, после ознакомления с проблемой нужно разобраться какое из вышеописанных узких мест присутствует на сервере заказчика. Если нет каких-либо индексов, или имеющаяся у заказчика СУБД ограничивает количество единовременных подключеий или с сервера заказчика не пингуется внешняя система, то здесь становиться все ясно и необходимо устранять обнаруженную проблему. Помимо технических аспектов есть и организационные: в идеале любое обращение заказчика за техподдержкой должно закончиться тем, что ни этот ни другие заказчики с той же самой проблемой обращаться больше не будут. Нужно написать или подкорректировать имеющиеся инструкции по воссозданию пропавших индексов; проверке связи с внешней системой и что делать в случае, если связи нет; настройке СУБД и т.д.

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

После того, как удалось воспроизвести проблему, нужно запускать профайлер. Для Java существуют такие профайлеры, как Eclipse TPTP и YourKit Java Profiler. Нужно настроить фильтрацию таким образом, чтобы показывались вызовы методов именно вашего приложения и искать тот метод, который выполняется дольше всех. Я думаю, что в львиной доле случаев медленными будут методы, в которых выполняется обращение к базе данных (DAO, HibernateHandler'ы и т.д.).

Если используется Hibernate, то в томозящем методе может выполняться вполне себе безобидный запрос. Следует посмотреть какой именно SQL-код генерируется. Для этого в настройках Hibernate-сессии следует выставить параметр hibernate.show_sql в true, а чтобы удобнее было читать - параметр hibernate.format_sql тоже в true.

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

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

Понравилось сообщение - подпишитесь на блог или читайте меня в twitter

7 комментариев:

intr комментирует...

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

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

Несколько проблем из личного опыта:
1. Были очень сильные тормоза при выполнение одной части системы. Выяснилось что тормозили SQL запросы к БД (firebird). Разработчик неправильно заджойнил таблицы :)

2. Была БД изменять которую было нельзя. При выполнении одного запроса система подвисала намертво, запрос в свою очередь делал джойн двух таблиц. В итоге пришлось переписать все на два разных запроса и все стало быстрее на порядок :)

3. Проблема не связанная с производительностью: в jetty и tomcat по разному организованы класслоадеры. Если мне не изменяет память в jetty классы загружаются постоянно, а в tomcat повторяющиеся классы повторно не загружаются. Отсюда пришлось ловить жуткий jar-hell на tomcat :)

Dawn Architect комментирует...

По описываемым методам вопросов нет, детально и по делу. Но вот:

Очень редко когда приложение реализует сложную бизнес-логику.

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

Не планируешь в будущем осветить?

Pavel Samolisov комментирует...

"Процесс поиска и устранения ошибок всегда был стандартным"

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

"В итоге пришлось переписать все на два разных запроса и все стало быстрее на порядок"

Хм, даже так оказывается бывает. Интересно.

"классы загружаются постоянно"
В OSGi загрузка класса может быть довольно долгой операцией, поэтому с производительностью тоже могут возникнуть проблемы.

"Не планируешь в будущем осветить?"
Я занимаюсь разработкой именно бизнес-системы и пока с профилированием сложной логики не сталкивался.

Andrey комментирует...

Была упомянута проблема с lazy-loading для Hibernate. Недавно сам столкнулся с таким - объекты, которые возвращает DAO слой нельзя полноценно использовать. Для решения текущих задач в некоторых Criteria-запросах выставил fetching по нужных данным:
.setFetchMode(".", FetchMode.JOIN)
Но, блин, криво это. Стоит кому-то начать вызывать другие поля - снова повылетают LazyInitialization exception-ы. Надо править hibernateDAO.. В обратном случае будут join-ться лишние данные.
Есть ли более нормальный способ?

Pavel Samolisov комментирует...

Если ваше приложение построено на базе Spring, то может помочь эта статья: http://samolisov.blogspot.com/2009/01/lazy-loading-hibernate-spring.html

Михаил комментирует...

У нас были ещё следующие оптимизации Hibernate:
1) в маппингах при ссылке на другие классы всегда указывать класс максимально точно. то есть, если есть класс Foo, а класс Something наследуется от Foo, то нужно указывать Something
2) указывать опцию outer-join="false" и fetch="select". fetch - это стратегия фетчинга(в самом запросе или генерировать отдельный select).
3) отследить использование flush() в приложении. его использование приводит к записи всех изменений из сессии в БД, что не всегда нужно.
3) При больших выборках у сессии использовать метод iterate(), заместо list().
4) при выборке по нескольким полям один индекс скорее всего не спасет. необходимо создавать составной индекс. вообще это сильно зависит от типа СУБД. и вероятно это не сильно будет выручать, если не отслеживать реальные SQL запросы, которые генерит hibernate, т.к. они могут очень отличаться от того, что вы ожидаете.

Pavel Samolisov комментирует...

Сегодня только выносил flush() из цикла, получил некоторый прирост производительности.

Про 1-е и 3-е не знал, точнее с 1-м пока вообще не сталкивался, а за 4-е большое спасибо.

Про то, что Хибернейт может нагенерировать ого-го чего я упомянул в статье. Еще хочу добавить, что когда нужно количество объектов или проверить наличие какого-то объекта в БД то ни в коем случае не следует делать Query#list().

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!