Antipattern nesprávného vytváření instancí
Někdy se neustále vytvářejí nové instance třídy, kdy se mají vytvořit jednou a pak sdílet. Toto chování může poškodit výkon a označuje se jako nesprávný antipattern vytváření instancí. Antipattern je běžná reakce na opakující se problém, který je obvykle neefektivní a může být dokonce kontraproduktivní.
Popis problému
Mnoho knihoven poskytuje abstrakce externích prostředků. Interně tyto třídy obvykle spravují svá vlastní připojení k prostředku, fungují jako zprostředkovatelé, které mohou uživatelé použít, pokud chtějí k prostředku získat přístup. Tady jsou některé příklady tříd zprostředkovatelů, které se mohou týkat aplikací Azure:
System.Net.Http.HttpClient
. Komunikuje s webovou službou pomocí protokolu HTTP.Microsoft.ServiceBus.Messaging.QueueClient
. Odesílá a přijímá zprávy do fronty služby Service Bus.Microsoft.Azure.Documents.Client.DocumentClient
. Připojí se k instanci služby Azure Cosmos DB.StackExchange.Redis.ConnectionMultiplexer
. Připojuje se k Redisu, včetně služby Azure Cache for Redis.
Instance těchto tříd by se měly vytvořit jednou a pak by se měly v rámci životního cyklu aplikace opakovaně používat. Častým omylem je ale předpoklad, že by se tyto třídy měly načítat pouze podle potřeby a že by se měly rychle uvolňovat. (Tady uvedené jsou knihovny .NET, ale vzor není jedinečný pro .NET.) Následující ASP.NET příklad vytvoří instanci HttpClient
pro komunikaci se vzdálenou službou. Kompletní ukázku najdete tady.
public class NewHttpClientInstancePerRequestController : ApiController
{
// This method creates a new instance of HttpClient and disposes it for every call to GetProductAsync.
public async Task<Product> GetProductAsync(string id)
{
using (var httpClient = new HttpClient())
{
var hostName = HttpContext.Current.Request.Url.Host;
var result = await httpClient.GetStringAsync(string.Format("http://{0}:8080/api/...", hostName));
return new Product { Name = result };
}
}
}
Ve webové aplikaci se tato technika nedá škálovat. Pro každý požadavek uživatele se vytvoří nový objekt HttpClient
. V případě velkého zatížení může webový server vyčerpat počet dostupných soketů a může dojít k chybám SocketException
.
Tento problém se neomezuje na třídu HttpClient
. Jiné třídy, které balí prostředky nebo je jejich vytvoření náročné, můžou způsobit podobné problémy. Následující příklad vytvoří instanci třídy ExpensiveToCreateService
. Problémem tu není nezbytně vyčerpání soketů, ale jednoduše doba potřebná k vytvoření každé instance. Neustálé vytváření a ničení instancí této třídy může negativně ovlivnit škálovatelnost systému.
public class NewServiceInstancePerRequestController : ApiController
{
public async Task<Product> GetProductAsync(string id)
{
var expensiveToCreateService = new ExpensiveToCreateService();
return await expensiveToCreateService.GetProductByIdAsync(id);
}
}
public class ExpensiveToCreateService
{
public ExpensiveToCreateService()
{
// Simulate delay due to setup and configuration of ExpensiveToCreateService
Thread.SpinWait(Int32.MaxValue / 100);
}
...
}
Oprava nesprávného antipatternu vytváření instancí
Pokud se třída, která balí externí prostředek, dá sdílet a je bezpečná pro přístup z více vláken, vytvořte sdílenou instanci typu singleton nebo fond opakovaně použitelných instancí třídy.
Následující příklad používá statickou instanci HttpClient
, a proto sdílí připojení napříč všemi požadavky.
public class SingleHttpClientInstanceController : ApiController
{
private static readonly HttpClient httpClient;
static SingleHttpClientInstanceController()
{
httpClient = new HttpClient();
}
// This method uses the shared instance of HttpClient for every call to GetProductAsync.
public async Task<Product> GetProductAsync(string id)
{
var hostName = HttpContext.Current.Request.Url.Host;
var result = await httpClient.GetStringAsync(string.Format("http://{0}:8080/api/...", hostName));
return new Product { Name = result };
}
}
Důležité informace
Klíčovým prvkem tohoto antipatternu je opakované vytváření a ničení instancí objektu ke sdílení. Pokud třída není ke sdílení (není bezpečná pro přístup z více vláken), potom tento antipattern neplatí.
Typ sdíleného prostředku může diktovat, jestli se má použít typ singleton nebo se má vytvořit fond. Třída
HttpClient
je určená spíše ke sdílení než k použití ve fondu. Jiné objekty můžou podporovat použití ve fondu a umožňovat tak systému, aby rozložil zatížení na více instancí.Objekty, které sdílíte mezi více požadavků, musí být bezpečné pro přístup z více vláken. Třída
HttpClient
je navržená pro použití tímto způsobem, jiné třídy ale nemusí souběžné požadavky podporovat. Podívejte se proto do dostupné dokumentace.Při nastavování vlastností u sdílených objektů buďte opatrní, protože může vést ke konfliktům časování. Konflikt časování může způsobit například nastavení
DefaultRequestHeaders
pro tříduHttpClient
před jednotlivými žádostmi. Vlastnosti tohoto typu nastavte jednou (například během spuštění). Pokud budete později potřebovat jiné nastavení, vytvořte samostatné instance.Některé typy prostředků jsou omezené a neměli byste na ně spoléhat. Příkladem jsou připojení k databázi. Pokud budete udržovat otevřené připojení k databázi, které se nevyžaduje, můžete tak bránit jiným souběžným uživatelům v získání přístupu k této databázi.
V rozhraní .NET Framework se velké množství objektů vytvářejících připojení k externím prostředkům vytváří pomocí statických metod pro vytváření objektů jiných tříd, které tato připojení spravují. Tyto objekty se mají ukládat a opakovaně používat, neměly by se vyřazovat a vytvářet znovu. Například ve službě Azure Service Bus se objekt
QueueClient
vytvoří prostřednictvím objektuMessagingFactory
.MessagingFactory
interně spravuje připojení. Další informace najdete v tématu Osvědčené postupy pro zlepšení výkonu pomocí zasílání zpráv Service Bus.
Zjištění nesprávného antipatternu vytváření instancí
Mezi příznaky tohoto problému patří pokles propustnosti nebo vyšší míra chyb a také některé z těchto situací:
- Zvýšený počet výjimek naznačující, že došlo k vyčerpání prostředků, jako jsou třeba sokety, připojení k databázi, popisovače souborů a další
- Vyšší využití a uvolňování paměti
- Zvýšení síťových aktivit, aktivit disku nebo databáze
Následující postup vám pomůže tento problém identifikovat:
- Proveďte monitorování procesů produkčního systému. Můžete tak identifikovat body, kdy se doby odezvy zpomalí nebo dojde k chybě systému kvůli nedostatku prostředků.
- Prozkoumejte telemetrická data zachycená v těchto bodech, abyste mohli určit, které operace mohou vytvářet a ničit objekty spotřebovávající prostředky.
- Proveďte zátěžový test každé podezřelé operace v řízeném testovacím prostředí (místo v produkčním systému).
- Zkontrolujte zdrojový kód a zkontrolujte, jak se spravují zprostředkovací objekty.
V trasování zásobníku vyhledejte operace, které v zatíženém systému běží pomalu nebo generují výjimky. Tyto informace vám pomáhají určit, jakým způsobem tyto operace využívají prostředky. Výjimky vám můžou pomoct zjistit, jestli jsou chyby způsobeny vyčerpanými sdílenými prostředky.
Ukázková diagnostika
V následujících částech se tento postup použije u ukázkové aplikace popsané výše.
Identifikace bodů zpomalení nebo chyb
Následující obrázek ukazuje výsledky vygenerované pomocí New Relic APM a zobrazuje operace s dlouhou dobou odezvy. V tomto případě stojí za to prozkoumat podrobněji metodu GetProductAsync
v kontroleru NewHttpClientInstancePerRequest
. Všimněte si, že při běhu těchto operací se také zvýší chybovost.
Prozkoumání telemetrických dat a hledání korelací
Následující obrázek zobrazuje data zachycená pomocí profilace vláken během období odpovídajícího předchozímu obrázku. Systém stráví značnou dobu otvíráním připojení soketů a ještě více času jejich zavíráním a zpracováváním výjimek soketů.
Provedení zátěžového testování
Pomocí zátěžového testování simulujte obvyklé operace, které můžou uživatelé provádět. Můžete tak identifikovat části systému, které trpí při různém zatížení vyčerpáním prostředků. Tyto testy provádějte v řízeném prostředí (ne v produkčním systému). Následující graf ukazuje propustnost požadavků zpracovávaných kontrolerem NewHttpClientInstancePerRequest
, když se zatížení uživatelů zvýší na 100 souběžných uživatelů.
Objem požadavků zpracovaných za sekundu zpočátku při rostoucím zatížení stoupá. Při zhruba 30 uživatelích ale objem úspěšných požadavků dosáhne limitu a systém začne generovat výjimky. Od té chvíle se objem výjimek společně s uživatelským zatížením postupně zvyšuje.
Zátěžový test tyto chyby ohlásil jako chyby HTTP 500 (Interní server). Kontrola telemetrických dat ukázala, že tyto chyby byly způsobeny tím, že systému došly při vytváření dalších a dalších objektů HttpClient
prostředky soketů.
Následující graf zobrazuje podobný test pro kontroler, který vytváří vlastní objekt ExpensiveToCreateService
.
Tentokrát kontroler negeneruje žádné výjimky, propustnost ale stejně dosáhne limitu, zatímco se průměrná doba odezvy zvýší 20 x. (Graf používá logaritmické měřítko pro dobu odezvy a propustnost.) Telemetrie ukázala, že hlavní příčinou problému bylo vytvoření nových instancí ExpensiveToCreateService
.
Implementace řešení a ověření výsledku
Po přepnutí metody GetProductAsync
na sdílení jedné instance HttpClient
ukázal druhý zátěžový test zlepšení výkonu. Nebyly hlášeny žádné chyby a systém byl schopen zpracovat zvyšující se zatížení až do 500 požadavků za sekundu. Průměrná doba odezvy se v porovnání s předchozím testem zkrátila na polovinu.
Následující obrázek zobrazuje pro porovnání telemetrická data trasování zásobníku. Tentokrát systém tráví většinu času prováděním skutečné práce (místo otvírání a zavírání soketů).
Následující graf zobrazuje podobný zátěžový test s použitím sdílené instance objektu ExpensiveToCreateService
. Objem zpracovaných požadavků se opět společně s uživatelským zatížením zvyšuje, ale doba odezvy zůstává nízká.