Диагностика производительности

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

Определение медленных команд базы данных с помощью ведения журнала

В конце дня EF подготавливает и выполняет команды, которые будут выполняться в базе данных; с реляционной базой данных это означает выполнение инструкций SQL с помощью API базы данных ADO.NET. Если определенный запрос занимает слишком много времени (например, из-за отсутствия индекса), это можно увидеть, проверяя журналы выполнения команд и наблюдая, сколько времени они на самом деле занимает.

EF упрощает запись времени выполнения команд с помощью простого ведения журнала или Microsoft.Extensions.Logging:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True;ConnectRetryCount=0")
        .LogTo(Console.WriteLine, LogLevel.Information);
}

Если уровень ведения журнала установлен на LogLevel.Information уровне, EF выдает сообщение журнала для каждого выполнения команды с указанным временем выполнения.

info: 06/12/2020 09:12:36.117 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [b].[Id], [b].[Name]
      FROM [Blogs] AS [b]
      WHERE [b].[Name] = N'foo'

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

Предупреждение

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

Сопоставление команд базы данных с запросами LINQ

Одна из проблем с ведением журнала выполнения команд заключается в том, что иногда трудно сопоставить запросы SQL и запросы LINQ: команды SQL, выполняемые EF, могут выглядеть очень иначе от запросов LINQ, из которых они были созданы. Чтобы помочь с этой проблемой, может потребоваться использовать функцию тегов запросов EF, которая позволяет внедрить небольшой комментарий в SQL-запрос:

var myLocation = new Point(1, 2);
var nearestPeople = await (from f in context.People.TagWith("This is my spatial query!")
                     orderby f.Location.Distance(myLocation) descending
                     select f).Take(5).ToListAsync();

Тег отображается в журналах:

-- This is my spatial query!

SELECT TOP(@__p_1) [p].[Id], [p].[Location]
FROM [People] AS [p]
ORDER BY [p].[Location].STDistance(@__myLocation_0) DESC

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

Другие интерфейсы для записи данных о производительности

Существуют различные варианты функции ведения журнала EF для записи времени выполнения команд, которые могут быть более мощными. Базы данных обычно используют собственные средства трассировки и анализа производительности, которые обычно предоставляют гораздо более широкие сведения о конкретной базе данных за пределами простого времени выполнения; Фактические настройки, возможности и использование значительно различаются в разных базах данных.

Например, SQL Server Management Studio — это мощный клиент, который может подключаться к экземпляру SQL Server и предоставлять ценные сведения об управлении и производительности. Это выходит за рамки этого раздела, чтобы перейти к деталям, но две возможности, которые стоит упомянуть, — монитор действий, который предоставляет динамическую панель мониторинга действий сервера (включая самые дорогие запросы), а также функцию расширенных событий (XEvent ), которая позволяет определить произвольные сеансы сбора данных, которые можно адаптировать в соответствии с вашими точными потребностями. Документация по SQL Server по мониторингу содержит дополнительные сведения об этих функциях, а также о других функциях.

Другой подход к сбору данных о производительности заключается в том, чтобы автоматически собирать сведения, создаваемые EF или драйвером базы данных через DiagnosticSource интерфейс, а затем анализировать эти данные или отображать их на панели мониторинга. Если вы используете Azure, то приложение Azure Insights предоставляет такой мощный мониторинг вне поля, интегрируя производительность базы данных и время выполнения запросов в анализ времени выполнения веб-запросов. Дополнительные сведения об этом доступны в руководстве по производительности Application Insights и на странице аналитики SQL Azure.

Проверка планов выполнения запросов

После указания проблемного запроса, требующего оптимизации, следующий шаг обычно анализирует план выполнения запроса. Когда базы данных получают инструкцию SQL, обычно они создают план выполнения этого плана; Иногда требуется сложное принятие решений на основе определенных индексов, объем данных, существующих в таблицах и т. д. (кстати, сам план обычно должен кэшироваться на сервере для оптимальной производительности). Реляционные базы данных обычно предоставляют пользователям способ просмотра плана запроса, а также вычисляемой стоимости для различных частей запроса; это бесценно для улучшения запросов.

Сведения о начале работы с SQL Server см. в документации по планам выполнения запросов. Типичный рабочий процесс анализа — использовать SQL Server Management Studio, вставляя SQL медленного запроса, определяемого одним из указанных выше средств, и создавая графический план выполнения:

Отображение плана выполнения SQL Server

Хотя планы выполнения могут показаться сложными вначале, стоит потратить немного времени на знакомство с ними. Особенно важно отметить затраты, связанные с каждым узлом плана, и определить, как индексы используются (или нет) в различных узлах.

Хотя приведенные выше сведения относятся к SQL Server, другие базы данных обычно предоставляют те же средства с аналогичной визуализацией.

Внимание

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

Метрики

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

Дополнительные сведения см. на выделенной странице метрик EF.

Тестирование с помощью EF Core

В конце дня иногда необходимо знать, является ли определенный способ написания или выполнения запроса быстрее, чем другой. Важно никогда не предполагать или догадываться о ответе, и очень просто собрать быстрый бенчмарк, чтобы получить ответ. При написании тестов настоятельно рекомендуется использовать общеизвестную библиотеку BenchmarkDotNet , которая обрабатывает множество ошибок, с которыми сталкиваются пользователи при попытке написать собственные тесты: вы выполнили некоторые итерации прогрева? Сколько итераций на самом деле выполняет ваш тест, и почему? Давайте рассмотрим, как выглядит тест с EF Core.

Совет

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

В качестве простого сценария теста давайте сравним следующие различные методы вычисления среднего рейтинга всех блогов в нашей базе данных:

  • Загрузите все сущности, суммируйте их отдельные рейтинги и вычислите среднее значение.
  • Так же, как и выше, используйте только запрос без отслеживания. Это должно быть быстрее, так как разрешение идентичностей не выполняется, и сущности не фотографируются для целей отслеживания изменений.
  • Старайтесь ни при каких условиях не загружать все экземпляры сущностей блога, отображая только ранжирование. Позволяет избежать передачи других ненужных столбцов типа сущности Блога.
  • Вычислите среднее значение в базе данных, сделав его частью запроса. Это должен быть самый быстрый способ, так как все вычисляется в базе данных, и только результат передается клиенту.

При помощи BenchmarkDotNet вы пишете код, который необходимо протестировать, в виде простого метода — по аналогии с модульным тестом, — и BenchmarkDotNet автоматически выполняет каждый метод необходимое количество итераций, надежно измеряя время выполнения и объем выделенной памяти. Ниже приведены различные методы (полный код теста можно увидеть здесь):

[Benchmark]
public async Task<double> LoadEntities()
{
    var sum = 0;
    var count = 0;
    using var ctx = new BloggingContext();
    await foreach (var blog in ctx.Blogs.AsAsyncEnumerable())
    {
        sum += blog.Rating;
        count++;
    }

    return (double)sum / count;
}

Ниже приведены результаты, как показано в BenchmarkDotNet:

Способ Среднее Ошибка StdDev Медиана Коэффициент RatioSD 0-го поколения Поколение 1 Поколение 2 Распределено
LoadEntities 2,860,4 нас 54.31 мы 93.68 нас 2,844,5 нас 4,55 0,33 210.9375 70.3125 - 1309.56 КБ
LoadEntitiesNoTracking 1,353.0 мы 21.26 мы 18.85 нас 1,355.6 нас 2,10 0,14 87.8906 3.9063 - 540.09 КБ
РейтингТолькоПроектов 910.9 мы 20.91 us 61.65 нас 892.9 нас 1,46 0,14 41.0156 0.9766 - 252.08 КБ
РассчитатьВБазеДанных 627.1 нас 14.58 нас 42.54 нас 626.4 нас 1,00 0.00 4.8828 - - 33.27 КБ

Примечание.

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

Одним из ограничений BenchmarkDotNet является то, что он измеряет простую, однопоточную производительность предоставляемых методов и поэтому не подходит для тестирования параллельных сценариев.

Внимание

Всегда убедитесь, что в базе данных есть данные, аналогичные рабочим данным при тестировании, в противном случае результаты теста могут не представлять фактическую производительность в рабочей среде.