Pokročilá témata týkající se výkonu
Sdružování DbContext
A DbContext
je obecně lehký objekt: vytvoření a odstranění není součástí databázové operace a většina aplikací to může udělat bez znatelného dopadu na výkon. Každá instance kontextu ale nastavuje různé interní služby a objekty nezbytné pro plnění svých povinností a režijní náklady na průběžné provádění tohoto postupu můžou být významné ve scénářích s vysokým výkonem. V těchto případech může EF Core sdružovat vaše kontextové instance: když odstraníte kontext, EF Core resetuje jeho stav a uloží ho do interního fondu. Při další žádosti o novou instanci se tato instance ve fondu vrátí místo nastavení nové instance. Sdružování kontextu umožňuje platit náklady na nastavení kontextu pouze jednou při spuštění programu, a ne nepřetržitě.
Všimněte si, že sdružování kontextu je orthogonální pro sdružování připojení k databázi, které se spravuje na nižší úrovni v ovladači databáze.
Typický vzor v aplikaci ASP.NET Core pomocí EF Core zahrnuje registraci vlastního DbContext typu do kontejneru injektáže závislostí prostřednictvím AddDbContext. Potom se instance tohoto typu získávají prostřednictvím parametrů konstruktoru v kontroleru nebo Razor Pages.
Pokud chcete povolit sdružování kontextu, jednoduše nahraďte AddDbContext
:AddDbContextPool
builder.Services.AddDbContextPool<WeatherForecastContext>(
o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));
Parametr poolSize
AddDbContextPool nastaví maximální počet instancí uchovávaných fondem (výchozí hodnota je 1024). Po poolSize
překročení se nové instance kontextu neukládají do mezipaměti a ef se vrátí k chování při vytváření instancí na vyžádání bez sdružování.
Srovnávací testy
Následují výsledky srovnávacího testu pro načtení jednoho řádku z databáze SQL Serveru spuštěné místně na stejném počítači s kontextovým fondem a bez toho. Stejně jako vždy se výsledky změní s počtem řádků, latencí databázového serveru a dalšími faktory. Důležité je, že tento testuje výkon sdružování s jedním vláknem, zatímco skutečný scénář může mít různé výsledky; na vaší platformě před provedením jakýchkoli rozhodnutí. Zdrojový kód je zde k dispozici, můžete ho použít jako základ pro vlastní měření.
metoda | NumBlogs | Střední hodnota | Chyba | Směrodatná odchylka | Gen 0 | Gen 1 | Gen 2 | Přiděleno |
---|---|---|---|---|---|---|---|---|
BezcontextPoolingu | 0 | 701.6 nás | 26.62 us | 78,48 nás | 11.7188 | - | - | 50,38 kB |
WithContextPooling | 0 | 350.1 nás | 6,80 nás | 14.64 nás | 0.9766 | - | - | 4,63 kB |
Správa stavu ve fondových kontextech
Sdružování kontextu funguje opětovným použitím stejné instance kontextu napříč požadavky; to znamená, že se efektivně zaregistruje jako Singleton a stejná instance se znovu použije napříč několika požadavky (nebo obory DI). To znamená, že je potřeba věnovat zvláštní pozornost v případě, že kontext zahrnuje jakýkoli stav, který se může mezi požadavky změnit. Zásadní je, že kontext OnConfiguring
se vyvolá jen jednou – při prvním vytvoření kontextu instance – a proto se nedá použít k nastavení stavu, který se musí lišit (např. ID tenanta).
Typickým scénářem souvisejícím se stavem kontextu by byla aplikace s více tenanty ASP.NET Core, kde instance kontextu má ID tenanta, které se bere v úvahu pomocí dotazů (další podrobnosti najdete v tématu Globální filtry dotazů). Vzhledem k tomu, že id tenanta se musí s každou webovou požadavkem změnit, musíme projít některými dalšími kroky, aby všechno fungovalo s sdružováním kontextu.
Předpokládejme, že vaše aplikace zaregistruje vymezenou ITenant
službu, která zabalí ID tenanta a všechny další informace související s tenanty:
// Below is a minimal tenant resolution strategy, which registers a scoped ITenant service in DI.
// In this sample, we simply accept the tenant ID as a request query, which means that a client can impersonate any
// tenant. In a real application, the tenant ID would be set based on secure authentication data.
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenant>(sp =>
{
var tenantIdString = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request.Query["TenantId"];
return tenantIdString != StringValues.Empty && int.TryParse(tenantIdString, out var tenantId)
? new Tenant(tenantId)
: null;
});
Jak je uvedeno výše, věnujte zvláštní pozornost tomu, odkud získáte ID tenanta – to je důležitý aspekt zabezpečení vaší aplikace.
Jakmile máme službu s vymezeným ITenant
oborem, zaregistrujte jako službu Singleton objekt pro sdružování kontextu jako obvykle:
builder.Services.AddPooledDbContextFactory<WeatherForecastContext>(
o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));
Dále napište vlastní kontextovou továrnu, která získá fondový kontext z objektu pro vytváření Singleton, který jsme zaregistrovali, a vloží ID tenanta do kontextových instancí, které předává:
public class WeatherForecastScopedFactory : IDbContextFactory<WeatherForecastContext>
{
private const int DefaultTenantId = -1;
private readonly IDbContextFactory<WeatherForecastContext> _pooledFactory;
private readonly int _tenantId;
public WeatherForecastScopedFactory(
IDbContextFactory<WeatherForecastContext> pooledFactory,
ITenant tenant)
{
_pooledFactory = pooledFactory;
_tenantId = tenant?.TenantId ?? DefaultTenantId;
}
public WeatherForecastContext CreateDbContext()
{
var context = _pooledFactory.CreateDbContext();
context.TenantId = _tenantId;
return context;
}
}
Jakmile máme vlastní kontextovou továrnu, zaregistrujte ji jako službu s vymezeným oborem:
builder.Services.AddScoped<WeatherForecastScopedFactory>();
Nakonec uspořádejte kontext, který se vloží z naší továrny s vymezeným oborem:
builder.Services.AddScoped(
sp => sp.GetRequiredService<WeatherForecastScopedFactory>().CreateDbContext());
V tomto okamžiku se kontrolery automaticky vloží do kontextové instance, která má správné ID tenanta, aniž by o něm museli něco vědět.
Úplný zdrojový kód pro tuto ukázku je k dispozici zde.
Poznámka:
I když se EF Core stará o resetování interního stavu a DbContext
souvisejících služeb, obecně se nenuluje stav v podkladovém ovladači databáze, který je mimo EF. Pokud například ručně otevřete a použijete nebo jinak manipulujete se stavem DbConnection
ADO.NET, je na vás, abyste tento stav obnovili před vrácením instance kontextu do fondu, například zavřením připojení. Pokud to neuděláte, může to způsobit únik stavu mezi nesouvisejícími požadavky.
Kompilované dotazy
Když EF obdrží strom dotazu LINQ ke spuštění, musí nejprve "zkompilovat" tento strom, například vytvořit z něj SQL. Vzhledem k tomu, že je tento úkol náročným procesem, ef ukládá dotazy do mezipaměti podle obrazce stromu dotazu, takže dotazy se stejnou strukturou opakovaně používají výstupy kompilace v interní mezipaměti. Toto ukládání do mezipaměti zajišťuje, že provádění stejného dotazu LINQ několikrát je velmi rychlé, i když se hodnoty parametrů liší.
Ef však musí ještě před použitím interní mezipaměti dotazů provádět určité úlohy. Například strom výrazů dotazu musí být rekurzivně porovnán se stromy výrazů dotazů uložených v mezipaměti, aby bylo možné najít správný dotaz uložený v mezipaměti. Režie za toto počáteční zpracování je ve většině aplikací EF zanedbatelná, zejména v porovnání s jinými náklady souvisejícími se spouštěním dotazů (vstupně-výstupní operace sítě, skutečné zpracování dotazů a vstupně-výstupní operace disku v databázi...). V některých vysoce výkonných scénářích však může být žádoucí ho odstranit.
EF podporuje kompilované dotazy, které umožňují explicitní kompilaci dotazu LINQ do delegáta .NET. Po získání tohoto delegáta je možné ho vyvolat přímo ke spuštění dotazu bez poskytnutí stromu výrazů LINQ. Tato technika obchází vyhledávání v mezipaměti a poskytuje nejoptimaličtější způsob spuštění dotazu v EF Core. Následuje několik výsledků srovnávacích testů, které porovnávají kompilovaný a nekopilovaný výkon dotazů; na vaší platformě před provedením jakýchkoli rozhodnutí. Zdrojový kód je zde k dispozici, můžete ho použít jako základ pro vlastní měření.
metoda | NumBlogs | Střední hodnota | Chyba | Směrodatná odchylka | Gen 0 | Přiděleno |
---|---|---|---|---|---|---|
WithCompiledQuery | 0 | 564.2 us | 6,75 nás | 5,99 nás | 1.9531 | 9 kB |
WithoutCompiledQuery | 0 | 671.6 us | 12.72 nás | 16,54 nás | 2.9297 | 13 kB |
WithCompiledQuery | 10 | 645.3 nás | 10.00 nás | 9,35 nás | 2.9297 | 13 kB |
WithoutCompiledQuery | 10 | 709,8 nás | 25.20 nás | 73.10 nás | 3.9063 | 18 kB |
Pokud chcete použít kompilované dotazy, nejprve zkompilujte dotaz EF.CompileAsyncQuery následujícím způsobem (použijte EF.CompileQuery pro synchronní dotazy):
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));
V této ukázce kódu poskytujeme EF s lambda, která DbContext
přijímá instanci, a libovolný parametr, který se má předat dotazu. Delegáta teď můžete vyvolat při každém spuštění dotazu:
await foreach (var blog in _compiledQuery(context, 8))
{
// Do something with the results
}
Všimněte si, že delegát je bezpečný pro přístup z více vláken a lze ho vyvolat souběžně v různých kontextových instancích.
Omezení
- Kompilované dotazy je možné použít pouze pro jeden model EF Core. Někdy je možné nakonfigurovat různé kontextové instance stejného typu tak, aby používaly různé modely; spuštění kompilovaných dotazů v tomto scénáři není podporováno.
- Při použití parametrů v kompilovaných dotazech použijte jednoduché skalární parametry. Složitější výrazy parametrů , jako jsou přístupy k členům nebo metodám v instancích, se nepodporují.
Ukládání dotazů do mezipaměti a parametrizace
Když EF obdrží strom dotazu LINQ ke spuštění, musí nejprve "zkompilovat" tento strom, například vytvořit z něj SQL. Vzhledem k tomu, že je tento úkol náročným procesem, ef ukládá dotazy do mezipaměti podle obrazce stromu dotazu, takže dotazy se stejnou strukturou opakovaně používají výstupy kompilace v interní mezipaměti. Toto ukládání do mezipaměti zajišťuje, že provádění stejného dotazu LINQ několikrát je velmi rychlé, i když se hodnoty parametrů liší.
Zvažte následující dva dotazy:
var post1 = context.Posts.FirstOrDefault(p => p.Title == "post1");
var post2 = context.Posts.FirstOrDefault(p => p.Title == "post2");
Vzhledem k tomu, že stromy výrazů obsahují různé konstanty, strom výrazů se liší a každý z těchto dotazů bude kompilován zvlášť ef Core. Každý dotaz navíc vytvoří trochu jiný příkaz SQL:
SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = N'post1'
SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = N'post2'
Vzhledem k tomu, že se SQL liší, bude váš databázový server pravděpodobně také muset vytvořit plán dotazu pro oba dotazy, a ne opakovaně používat stejný plán.
Malé změny dotazů můžou výrazně změnit:
var postTitle = "post1";
var post1 = context.Posts.FirstOrDefault(p => p.Title == postTitle);
postTitle = "post2";
var post2 = context.Posts.FirstOrDefault(p => p.Title == postTitle);
Vzhledem k tomu, že název blogu je teď parametrizovaný, oba dotazy mají stejný tvar stromu a EF je potřeba zkompilovat pouze jednou. Vygenerovaný SQL je také parametrizován a umožňuje databázi opakovaně používat stejný plán dotazů:
SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = @__postTitle_0
Všimněte si, že není nutné parametrizovat každý a každý dotaz: je naprosto v pořádku mít některé dotazy s konstantami, a databáze (a EF) mohou někdy provádět určitou optimalizaci kolem konstant, které nejsou možné, když je dotaz parametrizován. V části o dynamicky vytvořených dotazech se podívejte na příklad, ve kterém je zásadní správné parametrizace.
Poznámka:
Metriky EF Core hlásí rychlost dosažení mezipaměti dotazů. V normální aplikaci tato metrika brzy po spuštění programu dosáhne 100 %, jakmile se většina dotazů alespoň jednou spustí. Pokud tato metrika zůstává stabilní pod 100 %, znamená to, že vaše aplikace může dělat něco, co porazí mezipaměť dotazů – je vhodné ji prozkoumat.
Poznámka:
Způsob správy plánů dotazů v mezipaměti je závislý na databázi. SQL Server například implicitně udržuje mezipaměť plánu dotazů LRU, zatímco PostgreSQL ne (ale připravené příkazy můžou způsobit velmi podobný koncový efekt). Další podrobnosti najdete v dokumentaci k databázi.
Dynamicky vytvořené dotazy
V některých situacích je nutné dynamicky vytvářet dotazy LINQ a nezadávat je přímo ve zdrojovém kódu. K tomu může dojít například na webu, který přijímá libovolné podrobnosti dotazu z klienta s operátory dotazů s otevřeným koncem (řazení, filtrování, stránkování...). V zásadě, pokud je to správně, dynamicky vytvořené dotazy mohou být stejně efektivní jako běžné dotazy (i když není možné použít zkompilovanou optimalizaci dotazů s dynamickými dotazy). V praxi jsou však často zdrojem problémů s výkonem, protože je snadné náhodně vytvářet stromy výrazů s obrazci, které se pokaždé liší.
Následující příklad používá tři techniky k vytvoření výrazu Where
lambda dotazu:
- Rozhraní API výrazu s konstantou: Dynamicky sestavte výraz pomocí rozhraní API výrazu pomocí konstantního uzlu. Jedná se o častou chybu, když dynamicky vytváří stromy výrazů a způsobí, že EF dotaz pokaždé, když je vyvolán s jinou konstantní hodnotou (obvykle také způsobuje znečištění mezipaměti plánu na databázovém serveru).
- Rozhraní API výrazu s parametrem: Lepší verze, která nahradí konstantu parametrem. Tím se zajistí, že se dotaz zkompiluje pouze jednou bez ohledu na zadanou hodnotu a vygeneruje se stejný (parametrizovaný) SQL.
- Jednoduchý s parametrem: Verze, která nepoužívá rozhraní API výrazu, pro porovnání, která vytvoří stejný strom jako výše uvedená metoda, ale je mnohem jednodušší. V mnoha případech je možné dynamicky sestavovat strom výrazů bez použití rozhraní API pro výrazy, což je snadné se pokazit.
Where
Operátor přidáme do dotazu pouze v případě, že daný parametr nemá hodnotu null. Všimněte si, že se nejedná o vhodný případ použití pro dynamické vytváření dotazu, ale pro jednoduchost ho používáme:
[Benchmark]
public int ExpressionApiWithConstant()
{
var url = "blog" + Interlocked.Increment(ref _blogNumber);
using var context = new BloggingContext();
IQueryable<Blog> query = context.Blogs;
if (_addWhereClause)
{
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);
query = query.Where(whereLambda);
}
return query.Count();
}
Srovnávací testy těchto dvou technik poskytují následující výsledky:
metoda | Střední hodnota | Chyba | Směrodatná odchylka | Gen0 | Gen1 | Přiděleno |
---|---|---|---|---|---|---|
ExpressionApiWithConstant | 1 665,8 nás | 56,99 nás | 163.5 nás | 15.6250 | - | 109,92 KB |
ExpressionApiWithParameter | 757.1 nás | 35.14 nás | 103.6 nás | 12.6953 | 0.9766 | 54,95 KB |
SimpleWithParameter | 760.3 us | 37,99 nás | 112.0 us | 12.6953 | - | 55.03 kB |
I když je rozdíl v milisekundách malý, mějte na paměti, že konstantní verze nepřetržitě znečišťuje mezipaměť a způsobuje opětovné kompilaci jiných dotazů, zpomaluje je i a má obecný negativní dopad na celkový výkon. Důrazně doporučujeme vyhnout se opakovanému dokončování konstantních dotazů.
Poznámka:
Vyhněte se vytváření dotazů pomocí rozhraní API stromu výrazů, pokud opravdu nepotřebujete. Kromě složitosti rozhraní API je velmi snadné neúmyslně způsobit významné problémy s výkonem při jejich použití.
Kompilované modely
Kompilované modely můžou zlepšit dobu spouštění EF Core pro aplikace s velkými modely. Velký model obvykle znamená stovky až tisíce typů entit a relací. Čas spuštění je čas provést první operaci při DbContext
prvním použití tohoto DbContext
typu v aplikaci. Všimněte si, že pouhé vytvoření DbContext
instance nezpůsobí inicializaci modelu EF. Místo toho typické první operace, které způsobují inicializaci modelu, zahrnují volání DbContext.Add
nebo spuštění prvního dotazu.
Kompilované modely se vytvářejí pomocí nástroje příkazového dotnet ef
řádku. Než budete pokračovat, ujistěte se, že jste nainstalovali nejnovější verzi nástroje .
K vygenerování kompilovaného modelu se používá nový dbcontext optimize
příkaz. Příklad:
dotnet ef dbcontext optimize
Pomocí --output-dir
možností --namespace
lze určit adresář a obor názvů, do kterého se bude kompilovaný model generovat. Příklad:
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>
- Další informace najdete tady:
dotnet ef dbcontext optimize
. - Pokud jste v sadě Visual Studio pohodlnější, můžete také použít Optimize-DbContext.
Výstup spuštění tohoto příkazu zahrnuje část kódu, která se má zkopírovat a vložit do DbContext
konfigurace, aby EF Core používala zkompilovaný model. Příklad:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseModel(MyCompiledModels.BlogsContextModel.Instance)
.UseSqlite(@"Data Source=test.db");
Bootstrapping kompilovaného modelu
Obvykle není nutné se podívat na vygenerovaný kód bootstrappingu. Někdy ale může být užitečné přizpůsobit model nebo jeho načítání. Kód bootstrappingu vypadá přibližně takto:
[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();
}
Jedná se o částečnou třídu s částečnými metodami, které je možné implementovat pro přizpůsobení modelu podle potřeby.
Kromě toho lze vygenerovat více kompilovaných modelů pro DbContext
typy, které mohou v závislosti na určité konfiguraci modulu runtime používat různé modely. Ty by se měly umístit do různých složek a oborů názvů, jak je znázorněno výše. Informace o modulu runtime, jako je například připojovací řetězec, je pak možné prozkoumat a podle potřeby vrátit správný model. Příklad:
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.");
});
}
Omezení
Kompilované modely mají určitá omezení:
- Globální filtry dotazů nejsou podporovány.
- Opožděné načítání a proxy servery pro sledování změn se nepodporují.
- Model se musí ručně synchronizovat tak, že ho znovu vygeneruje pokaždé, když se změní definice modelu nebo konfigurace.
- Vlastní implementace IModelCacheKeyFactory nejsou podporovány. Podle potřeby ale můžete zkompilovat několik modelů a načíst odpovídající modely.
Z důvodu těchto omezení byste měli použít pouze kompilované modely, pokud je čas spuštění EF Core příliš pomalý. Kompilace malých modelů obvykle nestojí za to.
Pokud je podpora některé z těchto funkcí pro váš úspěch důležitá, hlasujte prosím o odpovídajících problémech, které jsou propojené výše.
Snížení režie za běhu
Stejně jako u jakékoli vrstvy přidává EF Core v porovnání s kódováním přímo proti rozhraním API databáze nižší úrovně režijní náklady za běhu. Tato režie za běhu pravděpodobně významně neovlivní většinu reálných aplikací; další témata v tomto průvodci výkonem, jako je efektivita dotazů, využití indexů a minimalizace zaokrouhlení, jsou mnohem důležitější. Kromě toho platí, že i u vysoce optimalizovaných aplikací bude latence sítě a vstupně-výstupní operace databáze obvykle dominovat kdykoliv stráveným uvnitř samotného EF Core. U vysoce výkonných aplikací s nízkou latencí, kde je důležitý každý bit výkonu, se ale dají použít následující doporučení ke snížení režie EF Core na minimum:
- Zapněte sdružování DbContext. Naše srovnávací testy ukazují, že tato funkce může mít rozhodující dopad na aplikace s vysokou výkonem a nízkou latencí.
- Ujistěte se, že
maxPoolSize
odpovídá vašemu scénáři použití. Pokud je příliš nízká,DbContext
instance se budou neustále vytvářet a odstraňovat, což snižuje výkon. Nastavení příliš vysoké může zbytečně spotřebovávat paměť, protože se ve fondu spravují nepoužívanéDbContext
instance. - Pokud chcete zvýšit výkon navíc, zvažte použití namísto přímé
PooledDbContextFactory
vkládání instancí kontextu DI. Správa di fondůDbContext
způsobuje mírné režijní náklady.
- Ujistěte se, že
- Používejte předkompilované dotazy pro horké dotazy.
- Čím složitější je dotaz LINQ – čím více operátorů obsahuje, a čím větší je výsledný strom výrazů, tím větší je možné od použití kompilovaných dotazů očekávat další zisky.
- Zvažte zakázání kontrol zabezpečení vláken nastavením
EnableThreadSafetyChecks
na false v konfiguraci kontextu.- Použití stejné
DbContext
instance souběžně z různých vláken se nepodporuje. EF Core má bezpečnostní funkci, která detekuje tuto chybu programování v mnoha případech (ale ne všechny) a okamžitě vyvolá informativní výjimku. Tato bezpečnostní funkce ale přidává určitou režii za běhu. - UPOZORNĚNÍ: Po důkladném testování, že vaše aplikace neobsahuje takové chyby souběžnosti, zakažte pouze bezpečnostní kontroly vláken.
- Použití stejné