Этот раздел я считаю самым важным, т.к. именно умение делать веб приложения с хорошей производительностью является отличительным признаком мастера от простого веб-разработчика. Основной принцип при оптимизации – сначала измерить и только потом менять. Не сделав замер, вы не сможете понять, улучшили вы производительность и на сколько.
Поэтому первый момент – это получить точное число, характеризующее тот или иной участок кода. Обычно это время выполнения в миллисекундах. Это время можно получить, например, с помощью обычной встроенной трассировки.
Второй момент – это где искать узкие места в приложениях. В нашей системе есть специальный код в Global.asax, который отслеживает все большие запросы. В итоге мы можем пройтись по таким запросам и изучить почему они выполняются так долго.
Вот код, который вы можете для этого использовать:
public DateTime startTime = DateTime.Now; protected void Application_BeginRequest() { if (System.Web.Configuration.WebConfigurationManager.AppSettings["TracePagePerfomance"] == "1") { startTime = DateTime.Now; } } protected void Application_EndRequest() { if (!HttpContext.Current.Request.IsLocal && System.Web.Configuration.WebConfigurationManager.AppSettings["TracePagePerfomance"] == "1") { var endTime = DateTime.Now; var duration = (endTime - startTime).TotalSeconds; if(duration>0.5){ CMS.BLL.Tracer.Write(HttpContext.Current.Request.RawUrl, duration.ToString(), "perf" + (int)HttpContext.Current.Response.StatusCode); } } }
Важный момент – используйте этот код только на некоторое время, иначе ваша база может очень быстро разрастись (либо увеличить порог до 1 сек – т.е. в базу будут записываться запросы с длиной более 1 секунды).
После того, как вы установили список медленных запросов, вы можете для каждого запроса расставить вызовы Trace.Warn и понять какие именно операторы вызывают наибольшие замедления. Про трассировку мы говорили ранее. Здесь можно только сказать, что вам надо просто расставлять метки вида [functionName.i] – т.е. в функции GetItems вы ставите такие точки GetItems1, GetItems2, …. Тем самым вы сразу по трассировке поймете какой участок самый медленный (по trace.axd). Таким нехитрым способом вы решите 1 задачу оптимизации – вы найдете те места, которые вызывают замедления.
Допустим у вас есть проблемный небольшой участок, который по каким-то причинам выполняется долго.
В типовых ситуациях это может быть:
Вы должны представлять примерно все типовые решения для каждого типа проблем.
В LINQ запросе это могут быть, например:
Давайте в целом рассмотрим методы борьбы с долгими операциями:
Дорогие операции:
EXEC sp_tableoption ‘mytable’, ‘large value types out of row’, ‘1’
IF EXISTS(SELECT * FROM dbo.Book)
print ‘Records found’
Еще несколько советов по оптимизации SQL:
Для замеров времени запросов используйте:
Tell SQL Server to show I/O and timing details of each query we run:
SET STATISTICS IO ON SET STATISTICS TIME ON
Also, before each query, flush the SQL Server memory cache:
CHECKPOINT DBCC DROPCLEANBUFFERS
Также будет хорошим советом использовать отдельные хранимые процедуры для сложной бизнес-логики. Т.е. вы делаете операции не через C# и объекты, а в базе и в итоге выдаете результаты в виде каких-либо объектов. Согласен, что это немного неудобно для тех, кто привык работать с объектами EF и оперировать ими через LINQ. Но здесь также возможно приведение к этим объектам. Более того, обычно вам нужна проекция объектов (т.е. для вывода JSON через ajax), поэтому вы можете сразу доставлять в C# уже только нужные для этого данные. Попробуйте оценить на практике подобный подход для узких мест.
Правила для индексов:
Поиск, где можно установить индексы:
select d.name AS DatabaseName, mid.* from sys.dm_db_missing_index_details mid join sys.databases d ON mid.database_id=d.database_id Также можно использовать sql profiler и DB Tuning Advisor
Убрать лишние индексы:
SELECT d.name, t.name, i.name, ius.* FROM sys.dm_db_index_usage_stats ius JOIN sys.databases d ON d.database_id = ius.database_id JOIN sys.tables t ON t.object_id = ius.object_id JOIN sys.indexes i ON i.object_id = ius.object_id AND i.index_id = ius.index_id ORDER BY user_updates DESC
там где user_updates больше чем user_lookup – можно удалить индексы
Просматривайте периодически event log (applications and system) (или event viewer в Winsows Server 2012) на сервере. Там вы можете увидеть исключения, возникающие в веб приложениях, а также можно увидеть ошибки SQL и IIS – все эти моменты требуют самого тщательно рассмотрения. Также вы можете просмотреть аудит доступа к SQL Server. Очень часто его пытаются взломать методом прямого перебора паролей к SA. Это означает, что вам надо во-первых заблокировать запить sa. Во-вторых, надо устанавливать сложные пароли для учетных записей SQL Server.
Также хороший вариант – ограничить внешний доступ по IP к SQL Server через брандмауэр (обычно этот доступ нужен только для локальной разработки).
Также вы можете искать медленные места с помощью специальных запросов SQL – они позволяют выявить самые затратные SQL запросы по памяти и по процессору:
Запрос по загрузке процессора.
SELECT TOP (25) qs.sql_handle, qs.execution_count, qs.total_worker_time AS Total_CPU, total_CPU_inSeconds = --Converted from microseconds qs.total_worker_time/1000000, average_CPU_inSeconds = --Converted from microseconds (qs.total_worker_time/1000000) / qs.execution_count, qs.total_elapsed_time, total_elapsed_time_inSeconds = --Converted from microseconds qs.total_elapsed_time/1000000, st.text, qp.query_plan FROM sys.dm_exec_query_stats AS qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st CROSS apply sys.dm_exec_query_plan (qs.plan_handle) AS qp ORDER BY qs.total_worker_time DESC OPTION (RECOMPILE);
Запрос по поиску тяжелых запросов (по времени выполнения):
описание полей – http://www.sql.ru/blogs/decolores/946
set transaction isolation level read uncommitted select top 100 creation_time, last_execution_time, execution_count, total_worker_time/1000 as CPU, convert(money, (total_worker_time))/(execution_count*1000)as [AvgCPUTime], qs.total_elapsed_time/1000 as TotDuration, convert(money, (qs.total_elapsed_time))/(execution_count*1000)as [AvgDur], total_logical_reads as [Reads], total_logical_writes as [Writes], total_logical_reads+total_logical_writes as [AggIO], convert(money, (total_logical_reads+total_logical_writes)/(execution_count + 0.0))as [AvgIO], case when sql_handle IS NULL then ' ' else(substring(st.text,(qs.statement_start_offset+2)/2,( case when qs.statement_end_offset =-1 then len(convert(nvarchar(MAX),st.text))*2 else qs.statement_end_offset end - qs.statement_start_offset)/2 )) end as query_text, db_name(st.dbid)as database_name, object_schema_name(st.objectid, st.dbid)+'.'+object_name(st.objectid, st.dbid) as object_name from sys.dm_exec_query_stats qs cross apply sys.dm_exec_sql_text(sql_handle) st where total_logical_reads > 0 order by AvgDur desc
Запрос, который показывает какие запросы выдают очень много данных (строк).По хорошему каждый sql запрос должен выдавать только самые необходимые данные (т.е. до 500-700 строк за 1 запрос. Если больше – значит есть проблема с фильтрацией).
set transaction isolation level read uncommitted select top 100 max_rows, case when sql_handle IS NULL then ' ' else(substring(st.text,(qs.statement_start_offset+2)/2,( case when qs.statement_end_offset =-1 then len(convert(nvarchar(MAX),st.text))*2 else qs.statement_end_offset end - qs.statement_start_offset)/2 )) end as query_text, creation_time, last_execution_time, execution_count, total_worker_time/1000 as CPU, convert(money, (total_worker_time))/(execution_count*1000)as [AvgCPUTime], qs.total_elapsed_time/1000 as TotDuration, convert(money, (qs.total_elapsed_time))/(execution_count*1000)as [AvgDur], db_name(st.dbid)as database_name, object_schema_name(st.objectid, st.dbid)+'.'+object_name(st.objectid, st.dbid) as object_name from sys.dm_exec_query_stats qs cross apply sys.dm_exec_sql_text(sql_handle) st --where total_logical_reads > 0 order by max_rows desc
Пару слов о методе ToList: по сути именно при его вызове по факту выполняется SQL запрос. Поэтому постарайтесь как можно больше данных отрезать до его вызова в операторе Where. После его вызова вы уже работаете с коллекцией объектов, а не с SQL запросом.
Cамая распространенная ошибка, которая влияет на быстродействие LINQ запросов – это так называемая ошибка n+1 запроса.
Мы используем EntityFramework, по умолчанию для загрузки связанных записей он использует lazy loading. Общий принцип – мы загружаем в объект, содержащийся в ObjectContext, запись из основной таблицы. В момент, когда мы обращаемся к свойству дочернего объекта, EntityFramework делает отдельный запрос в базу, загружает связанную запись и соответственно нужное свойство. В большинстве случаев это работает. В каком случае это плохо? Представим ситуацию:
1) Мы загружаем некоторую коллекцию объектов одной таблицы (db.entities.ToList())
2) Делаем обращение к дочерней записи каждого из объектов в цикле либо в операторе Select (db.entities.Select(e => e.childEntity.childProperty))
Представим, что мы загрузили 100 записей. Далее, для каждой из 100 записей мы делаем отдельный запрос в базу, чтобы загрузить childEntity. Итого считаем, запрос на список пусть будет 80 ms, запрос на каждую запись – 50 ms, итого время работы – 80 + 50 * 100
Запрос в базу – это одна из самых тяжелых операций, их количество надо сводить к минимуму.
Для этого нужно за меньшее количество запросов выбрать максимальное количество данных. В конкретном случае можно использовать оператор Include. В его параметрах надо указать, какие связанные записи мы должны загрузить вместе с запросом. Если это обычное свойство, то указать просто e.ChildEntity, если надо загрузить коллекцию, то e.ChildEnityt.Select(c => c.ChildCollection)
В данном случае запрос надо выполнить так: db.entities.Include(e => e.childEntity).ToList(). В этом случае время запроса немного вырастет, допустим будет 100 ms, зато остальные 100 запросов выполняться не будут.
Все это показывает MiniProfiler – он находит повторяющиеся запросы и помечает их Duplicated, их видно если нажать на ссылку sql.
Есть простой способ проверить, что у вас не выполняются дополнительные запросы в базу для загрузки связанных свойств. У ObjectContext (LocalSqlServer) есть свойство db.Configuration.LazyLoadingEnabled. Если поставить его в false, то связанные записи не будут загружаться автоматически, а будут оставаться null. Но соответственно все записи, которые включены в Include, будут загружаться. Это можно использовать только для проверки запросов! Если вдруг где-то связанный объект не добавлен в Include, при обращении к его свойству будет выдано NullReferenceException.
Еще момент – после использования Include надо обязательно проверять, не выросло ли время основного запроса настолько, что он стал выполняться дольше, нежели 100 мелких запросов. При сложном запросе это вполне может получиться.
Теперь по использованию хранимых процедур: да, использование хранимой процедуры уменьшает время запроса. Но при этом – EntityFramework не умеет мапить данные, полученных хранимой процедурой, на связанные объекты (но для этого мы можем использовать Dapper – он может мапить либо в объекты Entity, либо в динамические объекты). Т.е. даже если добавить Include и дописать в теле процедуры inner join <…>, связанные объекты все равно будут подгружаться дополнительными запросами. Это может привести к потере преимущества.
Ну и последнее – если добавляете Include в некоторый запрос, обязательно смотрите, не используется ли этот запрос еще где-то. Возможно, в остальных случаях достаточно загрузить только основную запись. В этом случае лучше сделать отдельный метод с Include.
var allTypes = mng.GetTypes(); var res = items.Select(x=> new { typeName = x.typeID!=null ? allTypes.FirstOrDefault(z=>z.id==x.typeID).name });
вместо
var res = items.Select(x=> new { typeName = x.types.name });
в этом случае не создаются лишние обращений к базе.
Важно: этот метод надо использовать аккуратно и только для небольших коллекций. При неверно использовании он довольно сильно нагружает процессор и может подвесить весь сайт. В самых критичных запросах используйте лучше Dapper + хранимые процедуры + dynamic объекты. Пример использования такого запроса – в документации по ark (для метода get items в as crud). Dapper на сервере работает значительно быстрее чем локально – учитывайте это (т.е. проверяйте конечную оптимизацию именно на сервере).
Еще один частый кейс – это вывод каких-то данных к строке в таблице (например, файлов). Понятно, что сразу загружать их не вариант, т.е. потребуется множество обращений к диску (в цикле). Наиболее предпочтительный вариант – это подгрузка файлов только по мере необходимости. Т.е. будет ссылка для каждой строки Файла. При нажатии на нее показывается popover с нужными файлами. Файлы грузятся только для этого элемента и только при нажатии на эту ссылку. Дополнительный момент – для строки данных в базе вы можете отдельно хранить количество файлов, соответствующих этой строке – чтобы не делать прогрузку файлов при выводе таблицы (вы просто берете данные о количестве файлов из таблицы).
Альтернативный вариант – это сначала грузим только основные данные, а вторым ajax запросом грузим дополнительные, более “медленные” данные.
Несколько советов по общей оптимизации ASP.NET:
Самое главное что вы должны попробовать – это Google PageSpeed.
Это плагин для Chrome (или можно использовать через сайт https://developers.google.com/speed/pagespeed/insights/). Вы просто вводите адрес страницы и сервис выдает вам рекомендации что и как надо улучшить на сайте. Очень и очень полезная вещь.
Какие рекомендации по оптимизации клиентской части приложения:
Последнее что мы обсудим в плане оптимизации – это оптимизация JS. Для большинства приложений она не нужна. Но если вы разрабатываете приложение, где будут использоваться гигантские таблицы или большое количество разметки (свыше 1000 элементов), то обязательно следуйте этим правилам.
$('#id') $("#id").find(".class1"); $(".class", parent)
псевдоселекторы вроде :visible – самые медленные
т.е. ставим перед циклом var cont = $(el).closest(‘table’) и используем этот cont далее.
http://www.slideshare.net/AddyOsmani/jquery-proven-performance-tips-tricks
http://jonraasch.com/blog/10-advanced-jquery-performance-tuning-tips-from-paul-irish
http://jsperf.com/for-vs-while21221
http://jonraasch.com/blog/10-advanced-jquery-performance-tuning-tips-from-paul-irish
Далее рассмотрим клиентский сервис.