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

Создание пулов DbContext

DbContextОбычно является облегченным объектом: создание и удаление одного из них не влечет за собой операцию с базой данных, и большинство приложений могут сделать это без заметного влияния на производительность. Тем не менее, каждый DbContext из них настраивает различные внутренние службы и объекты, необходимые для выполнения своих обязанностей, и затраты на их постоянное выполнение могут быть значительными в высокопроизводительных сценариях. В таких случаях EF Core могут создавать Пулы экземпляров: при удалении DbContext EF Core сбрасывает состояние и сохраняет его во внутреннем пуле. при следующем запросе нового экземпляра возвращается экземпляр poold, а не настраивается новый экземпляр. DbContext Использование пулов позволяет платить DbContext за настройку только один раз при запуске программы, а не постоянно.

ниже приведены результаты тестирования производительности для выборки одной строки из базы данных SQL Server, работающей локально на том же компьютере, с пулом и без них DbContext . Как всегда, результаты изменятся с учетом количества строк, задержки сервера базы данных и других факторов. Важно, что это позволяет протестировать производительность многопоточного пула, в то время как реальный сценарий может иметь разные результаты; Тестирование производительности вашей платформы перед принятием любых решений. Исходный код доступен здесь, поэтому вы можете использовать его в качестве основания для собственных измерений.

Метод нумблогс Среднее значение Ошибка StdDev Gen 0 Поколение 1 Поколение 2 Allocated
висаутконтекстпулинг 1 701,6 США 26,62 США 78,48 США 11,7188 - - 50,38 КБ
висконтекстпулинг 1 350,1 США 6,80 США 14,64 США 0,9766 - - 4,63 КБ

Обратите внимание, что DbContext объединение пулов осуществляется с помощью пула подключений к базам данных, который управляется на более низком уровне в драйвере базы данных.

типичный шаблон в ASP.NET Core приложении, использующем EF Core, предполагает регистрацию пользовательского DbContext типа в контейнере DbContext через AddDbContext . Затем экземпляры этого типа получаются с помощью параметров конструктора в контроллерах или Razor Pages.

Чтобы включить DbContext объединение в пул, просто замените AddDbContext на AddDbContextPool :

services.AddDbContextPool<BloggingContext>(
    options => options.UseSqlServer(connectionString));

poolSizeПараметр AddDbContextPool задает максимальное число экземпляров, хранящихся в пуле (по умолчанию — 1024 в EF Core 6,0, а в предыдущих версиях — 128). После этого poolSize новые экземпляры контекста не кэшируются, а EF возвращается к поведению без пула, создающего экземпляры по запросу.

Ограничения

DbContext объединение в пулы имеет ряд ограничений на то, что можно сделать в OnConfiguring методе контекста.

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

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

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

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

Скомпилированные запросы

когда EF получает дерево запросов LINQ для выполнения, сначала необходимо «компилировать» это дерево, например создать SQL из него. Поскольку эта задача является большим процессом, EF кэширует запросы в виде дерева запросов, чтобы запросы с той же структурой повторно использовали внутренние кэшированные выходные данные компиляции. Такое кэширование гарантирует, что многократное выполнение одного и того же запроса LINQ происходит очень быстро, даже если значения параметров различаются.

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

EF поддерживает скомпилированные запросы, которые позволяют явно КОМПИЛИРОВАТЬ запрос LINQ в делегат .NET. После получения этого делегата его можно вызвать непосредственно для выполнения запроса без указания дерева выражений LINQ. Этот метод обходит Поиск кэша и предоставляет наиболее оптимизированный способ выполнения запроса в EF Core. Ниже приведены некоторые результаты теста производительности, сравнивающие производительность скомпилированных и нескомпилированных запросов. Тестирование производительности вашей платформы перед принятием любых решений. Исходный код доступен здесь, поэтому вы можете использовать его в качестве основания для собственных измерений.

Метод нумблогс Среднее значение Ошибка StdDev Gen 0 Allocated
вискомпиледкуери 1 564,2 США 6,75 США 5,99 США 1,9531 9 КБ
висауткомпиледкуери 1 671,6 США 12,72 США 16,54 США 2,9297 13 КБ
вискомпиледкуери 10 645,3 США 10,00 США 9,35 США 2,9297 13 КБ
висауткомпиледкуери 10 709,8 США 25,20 США 73,10 США 3,9063 18 КБ

Чтобы использовать скомпилированные запросы, сначала Скомпилируйте запрос со EF.CompileAsyncQuery следующим параметром (используется EF.CompileQuery для синхронных запросов):

private static readonly Func<BloggingContext, int, IAsyncEnumerable<Blog>> _compiledQuery
    = EF.CompileAsyncQuery(
        (BloggingContext context, int length) => context.Blogs.Where(b => b.Url.StartsWith("http://") && b.Url.Length == length));

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

await foreach (var blog in _compiledQuery(context, 8))
{
    // Do something with the results
}

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

Ограничения

  • Скомпилированные запросы можно использовать только для одной модели EF Core. Иногда разные экземпляры контекста одного типа могут быть настроены для использования разных моделей. выполнение скомпилированных запросов в этом сценарии не поддерживается.
  • При использовании параметров в скомпилированных запросах используйте простые скалярные параметры. Более сложные выражения параметров, такие как доступ членов или методов к экземплярам, не поддерживаются.

Кэширование запросов и параметризация

когда EF получает дерево запросов LINQ для выполнения, сначала необходимо «компилировать» это дерево, например создать SQL из него. Поскольку эта задача является большим процессом, EF кэширует запросы в виде дерева запросов, чтобы запросы с той же структурой повторно использовали внутренние кэшированные выходные данные компиляции. Такое кэширование гарантирует, что многократное выполнение одного и того же запроса LINQ происходит очень быстро, даже если значения параметров различаются.

Рассмотрим следующие два запроса:

var post1 = context.Posts.FirstOrDefault(p => p.Title == "post1");
var post2 = context.Posts.FirstOrDefault(p => p.Title == "post2");

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

SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] = N'blog1'

SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] = N'blog2'

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

Небольшое изменение в запросах может значительно измениться:

var postTitle = "post1";
var post1 = context.Posts.FirstOrDefault(p => p.Title == postTitle);
postTitle = "post2";
var post2 = context.Posts.FirstOrDefault(p => p.Title == postTitle);

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

SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] = @__blogName_0

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

Примечание

Счетчики событий EF Core отчет о доле попаданий в кэш запросов. В обычном приложении этот счетчик достигает 100% вскоре после запуска программы, как только один раз выполняется большинство запросов. Если значение счетчика остается стабильным ниже 100%, это указывает на то, что приложение может сделать что-то, что не нарушает кэш запросов. это хороший смысл исследовать это.

Примечание

Управление базой данных в кэшах планов запросов зависит от базы данных. например, SQL Server неявно поддерживает кэш планов запросов LRU, а PostgreSQL — нет (но подготовленные инструкции могут привести к очень похожему результату). Дополнительные сведения см. в документации по базе данных.

Динамически сконструированные запросы

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

В следующем примере используются два метода для динамического создания запроса. Мы добавляем Where оператор в запрос, только если данный параметр не равен null. Обратите внимание, что это не хороший вариант использования для динамического создания запроса, но мы используем его для простоты:

[Benchmark]
public int WithConstant()
{
    return GetBlogCount("blog" + Interlocked.Increment(ref _blogNumber));

    static int GetBlogCount(string url)
    {
        using var context = new BloggingContext();

        IQueryable<Blog> blogs = context.Blogs;

        if (url is not null)
        {
            var blogParam = Expression.Parameter(typeof(Blog), "b");
            var whereLambda = Expression.Lambda<Func<Blog, bool>>(
                Expression.Equal(
                    Expression.MakeMemberAccess(
                        blogParam,
                        typeof(Blog).GetMember(nameof(Blog.Url)).Single()
                    ),
                    Expression.Constant(url)),
                blogParam);

            blogs = blogs.Where(whereLambda);
        }

        return blogs.Count();
    }
}

Тестирование этих двух методов дает следующие результаты.

Метод Среднее значение Ошибка StdDev Gen 0 Поколение 1 Поколение 2 Allocated
висконстант 1 096,7 США 12,54 США 11,12 США 13,6719 1,9531 - 83,91 КБ
виспараметер 570,8 США 42,43 США 124,43 США 5,8594 - - 37,16 КБ

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

Примечание

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

Скомпилированные модели

Примечание

Скомпилированные модели появились в EF Core 6,0.

Скомпилированные модели могут улучшить время запуска EF Core для приложений с большими моделями. Большая модель обычно означает сотни и тысячи типов сущностей и отношений. Время запуска — это время выполнения первой операции с DbContext , когда этот DbContext тип используется впервые в приложении. Обратите внимание, что только создание DbContext экземпляра не приводит к инициализации модели EF. Стандартные первые операции, которые приводят к инициализации модели, включают вызов DbContext.Add или выполнение первого запроса.

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

Для создания скомпилированной модели используется новая команда dbcontext optimize. Пример:

dotnet ef dbcontext optimize

Параметры --output-dir и --namespace можно использовать для указания каталога и пространства имен, в которых будет создаваться скомпилированная модель. Пример:

PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>
  • Дополнительные сведения см. в разделе dotnet ef dbcontext optimize.
  • если вам удобнее работать в Visual Studio, можно также использовать Optimize-DbContext .

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

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseModel(MyCompiledModels.BlogsContextModel.Instance)
        .UseSqlite(@"Data Source=test.db");

Начальная загрузка скомпилированной модели

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

[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
    private static BlogsContextModel _instance;
    public static IModel Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BlogsContextModel();
                _instance.Initialize();
                _instance.Customize();
            }

            return _instance;
        }
    }

    partial void Initialize();

    partial void Customize();
}

Это разделяемый класс с разделяемыми методами, которые можно реализовать для настройки модели по мере необходимости.

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

public static class RuntimeModelCache
{
    private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
        = new();

    public static IModel GetOrCreateModel(string connectionString)
        => _runtimeModels.GetOrAdd(
            connectionString, cs =>
                {
                    if (cs.Contains("X"))
                    {
                        return BlogsContextModel1.Instance;
                    }

                    if (cs.Contains("Y"))
                    {
                        return BlogsContextModel2.Instance;
                    }

                    throw new InvalidOperationException("No appropriate compiled model found.");
                });
}

Ограничения

У скомпилированных моделей есть некоторые ограничения:

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

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

Сокращение затрат времени выполнения

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

  • Включите использование пулов DbContext; Наши тесты показывают, что эта функция может оказать деЦисиве воздействие на приложения с высоким уровнем производительности и низкой задержкой.
    • Убедитесь, что maxPoolSize соответствует вашему сценарию использования. Если он слишком мал, DbContext экземпляры будут постоянно созданы и уничтожены, что ухудшает производительность. Слишком большое значение параметра может не занимать память, так как неиспользуемые DbContext экземпляры сохраняются в пуле.
    • Для дополнительного незначительного увеличения производительности рассмотрите возможность использования PooledDbContextFactory вместо внедрения экземпляров контекста непосредственно (EF Core 6 и выше). Управление DbContext пулами в организациях влечет за собой небольшие издержки.
  • Используйте предварительно скомпилированные запросы для оперативных запросов.
    • Чем сложнее LINQ-запрос, тем больше операторов, которые он содержит, а результирующее дерево выражения, тем больше прибыли могут ожидать от использования скомпилированных запросов.
  • Рассмотрите возможность отключения проверок безопасности потоков, установив значение EnableThreadSafetyChecks false в конфигурации контекста (EF Core 6 и выше).
    • Одновременное использование одного и того же DbContext экземпляра из разных потоков не поддерживается. EF Core имеет функцию безопасности, которая обнаруживает эту ошибку программирования во многих случаях (но не все) и немедленно создает информативное исключение. Однако эта функция безопасности добавляет некоторые затраты времени выполнения.
    • Предупреждение: Отключайте только проверки потокобезопасности после тщательного тестирования того, что приложение не содержит таких ошибок параллелизма.