Share via


Antimönstret Ingen cachelagring

Antimönster är vanliga designfel som kan bryta din programvara eller dina program under stresssituationer och bör inte förbises. Ett antipattern för cachelagring sker när ett molnprogram som hanterar många samtidiga begäranden upprepade gånger hämtar samma data. Detta kan minska prestanda och skalbarhet.

När data inte cachelagras kan det orsaka ett antal oönskade beteenden, som:

  • Upprepad hämtning av samma information från en resurs som är dyr att komma åt, när det gäller I/O-overhead eller svarstider.
  • Upprepad konstruktion av samma objekt eller datastrukturer för flera begäranden.
  • Överdrivet antal anrop till en fjärrtjänst som har en tjänstkvot och begränsar klienter efter en viss gräns.

De här problemen kan i sin tur leda till långa svarstider, ökad konkurrens i datalagret och dålig skalbarhet.

Exempel på inget cachelagringsantipattern

I följande exempel används Entity Framework till att ansluta till en databas. Varje klientbegäran resulterar i ett anrop till databasen, även om flera begäranden hämtar exakt samma data. Kostnaden för upprepade begäranden, när det gäller I/O-overhead och avgifter för dataåtkomst, kan ackumuleras snabbt.

public class PersonRepository : IPersonRepository
{
    public async Task<Person> GetAsync(int id)
    {
        using (var context = new AdventureWorksContext())
        {
            return await context.People
                .Where(p => p.Id == id)
                .FirstOrDefaultAsync()
                .ConfigureAwait(false);
        }
    }
}

Du hittar hela exemplet här.

Det här antimönstret inträffar normalt eftersom:

  • Att inte använda en cache är lättare att implementera och fungerar bra under låg belastning. Cachelagring gör koden mer komplicerad.
  • För- och nackdelarna med att använda en cache förstås inte helt och hållet.
  • Det finns problem när det gäller overhead för att underhålla exakta och uppdaterade cachelagrade data.
  • Ett program migrerades från ett lokalt system, där nätverksfördröjning inte var ett problem och systemet kördes på dyr maskinvara med höga prestanda, så cachelagring övervägdes inte i ursprungsdesignen.
  • Utvecklare är inte medvetna om att cachelagring är en möjlighet i ett givet scenario. Till exempel kanske utvecklarna inte tänker på att använda ETags vid implementering av ett webb-API.

Så här åtgärdar du antipattern för ingen cachelagring

Den mest populära cachelagringsstrategin är på begäran eller cache-aside.

  • Vid läsning försöker programmet läsa data från cachen. Om data inte finns i cachen hämtar programmet dem från datakällan och lägger till dem i cachen.
  • Vid skrivning skriver programmet ändringen direkt till datakällan och tar bort det gamla värdet från cachen. Det hämtas och läggs till i cachen nästa gång det krävs.

Den här metoden lämpar sig för data som ändras ofta. Här är det tidigare exemplet som uppdaterats för att använda mönstret Cache-Aside.

public class CachedPersonRepository : IPersonRepository
{
    private readonly PersonRepository _innerRepository;

    public CachedPersonRepository(PersonRepository innerRepository)
    {
        _innerRepository = innerRepository;
    }

    public async Task<Person> GetAsync(int id)
    {
        return await CacheService.GetAsync<Person>("p:" + id, () => _innerRepository.GetAsync(id)).ConfigureAwait(false);
    }
}

public class CacheService
{
    private static ConnectionMultiplexer _connection;

    public static async Task<T> GetAsync<T>(string key, Func<Task<T>> loadCache, double expirationTimeInMinutes)
    {
        IDatabase cache = Connection.GetDatabase();
        T value = await GetAsync<T>(cache, key).ConfigureAwait(false);
        if (value == null)
        {
            // Value was not found in the cache. Call the lambda to get the value from the database.
            value = await loadCache().ConfigureAwait(false);
            if (value != null)
            {
                // Add the value to the cache.
                await SetAsync(cache, key, value, expirationTimeInMinutes).ConfigureAwait(false);
            }
        }
        return value;
    }
}

Obs! Metoden GetAsync anropar nu klassen CacheService, istället för att anropa databasen direkt. Klassen CacheService försöker först hämta objektet från Azure Cache for Redis. Om värdet inte hittas i cacheminnet anropar CacheService en lambda-funktion som skickades av anroparen. Lambda-funktionen ansvarar för att hämta data från databasen. Den här implementeringen frikopplar lagringsplatsen från den specifika cachelagringslösningen och frikopplar CacheService från databasen.

Överväganden för cachelagringsstrategi

  • Returnera inget fel till klienten om cachen inte är tillgänglig, kanske på grund av ett tillfälligt fel. Hämta istället data från den ursprungliga datakällan. Men var medveten om att medan cachen återställs kan den ursprungliga datakällan översköljas med begäranden, vilket resulterar i uppnådda tidsgränser och misslyckade anslutningar. (Detta är trots allt en av motiveringarna för att använda en cache i första hand.) Använd en teknik som kretsbrytarmönstret för att undvika att överbelasta datakällan.

  • Program som cachelagrar dynamiska data ska designas för att stödja slutlig konsekvens.

  • För webb-API:er kan du stödja cachelagring på klientsidan genom att inkludera ett Cache-Control-huvud i begäran och svarsmeddelanden och använda ETags för att identifiera versioner av objekt. Mer information finns i API-implementering.

  • Du måste inte cachelagra hela entiteter. Om det mesta av en entitet är statisk men bara en liten bit ändras ofta cachelagrar du de statiska elementen och hämtar de dynamiska elementen från datakällan. Den här metoden kan hjälpa till att minska mängden I/O som utförs mot datakällan.

  • I vissa fall, om föränderliga data är kortvariga, kan det vara användbart att cachelagra dem. Anta till exempel en enhet som kontinuerligt skickar statusuppdateringar. Det kan vara bra att cachelagra den här informationen när den kommer, och inte skriva den till ett beständigt lager alls.

  • För att förhindra data från att bli inaktuella stöder många cachelagringslösningar konfigurerbara giltighetsperioder, så att data automatiskt tas bort från cachen efter ett visst intervall. Du kan behöva justera giltighetstiden för scenariot. Data som är mycket statiska kan vara kvar i cachen längre än föränderliga data som kan bli inaktuella snabbt.

  • Om cachelagringslösningen inte har inbyggd giltighet kan du behöva implementera en bakgrundsprocess som ibland rensar cachen, för att förhindra att den växer gränslöst.

  • Förutom att cachelagra data från en extern datakälla kan du använda cachelagring för att spara resultatet av komplexa beräkningar. Men innan du gör det instrumenterar du programmet för att fastställa om programmet verkligen är processorsbundet.

  • Det kan vara användbart att preparera cachen när programmet startar. Fyll i cachen med de data som mest troligt kommer att användas.

  • Inkludera alltid instrumentering som identifierar cacheträffar och cachemissar. Använd den här informationen till att justera cachelagringsprinciper, som vilka data som ska cachelagras och hur länge data ska lagras i cachen innan de förfaller.

  • Om bristen på cachelagring är en flaskhals kan att lägga till cachelagring öka mängden begäranden så mycket att webbklienten blir överbelastad. Klienter kan börja få HTTP 503-fel (tjänsten är inte tillgänglig). Det är en indikation på att du bör skala ut klientdelen.

Så här identifierar du ett antipattern för cachelagring

Du kan utföra följande steg för att hjälpa dig att identifiera om bristen på cachelagring orsakar prestandaproblem:

  1. Granska programdesignen. Gör en förteckning av alla datalager som programmet använder. Fastställ för varje program om det använder en cache. Fastställ om möjligt hur ofta data ändras. Bra första kandidater för cachelagring är data som ändras långsamt och statiska referensdata som läses ofta.

  2. Instrumentera programmet och övervaka det aktiva systemet för att få reda på hur ofta programmet hämtar data eller beräknar information.

  3. Profilera programmet i en testmiljö för att registrera mått på låg nivå om overhead kopplat till dataåtkomståtgärder eller andra ofta utförda beräkningar.

  4. Utför belastningstestning i en testmiljö för att identifiera hur systemet svarar under normal arbetsbelastning och under hård belastning. Belastningstestning ska simulera mönstret med dataåtkomst som observeras i produktionsmiljön med realistiska arbetsbelastningar.

  5. Undersök statistiken för dataåtkomst för de underliggande datalagren och granska hur ofta samma databegäranden upprepas.

Exempeldiagnos

I följande avsnitt används stegen på exempelprogrammet som beskrivs ovan.

Instrumentera programmet och övervaka det aktiva systemet

Instrumentera programmet och övervaka det för att få information om specifika begäranden som användare gör när programmet är i produktion.

På följande bild visas övervakningsdata som registrerats av New Relic under ett belastningstest. I det här fallet är den enda HTTP GET-åtgärd som utförs Person/GetAsync. Men i en aktiv produktionsmiljö kan vetskapen om den relativa frekvens som varje begäran utförs ge dig en inblick i vilka resurser som bör cachelagras.

New Relic showing server requests for the CachingDemo application

Om du behöver en djupare analys kan du använda en profil för att registrera prestandadata på låg nivå i en testmiljö (inte i produktionssystemet). Se på mått som I/O-begäranhastigheter, minnesförbrukning och processoranvändning. De här måtten kan visa ett stort antal begäranden till ett datalager eller en tjänst, eller upprepad bearbetning som utför samma beräkning.

Belastningstesta programmet

I följande diagram visas resultatet av belastningstest av exempelprogrammet. Belastningstestet simulerar en stegbelastning på upp till 800 användare som utför ett antal typiska åtgärder.

Performance load test results for the uncached scenario

Antalet test som utförs varje sekund når en platå och ytterligare begäranden går därför långsammare. Den genomsnittliga testtiden ökar stadigt med arbetsbelastningen. Svarstiden planar ut när användarbelastningen når sin topp.

Undersöka dataåtkomststatistik

Dataåtkomststatistik och annan information som tillhandahålls av ett datalager kan ge användbar information, till exempel vilka frågor som upprepas oftast. I Microsoft SQL Server har till exempel hanteringsvyn sys.dm_exec_query_stats statistisk information om de senast utförda frågorna. Texten för varje fråga är tillgänglig i vyn sys.dm_exec-query_plan. Du kan använda ett verktyg som SQL Server Management Studio till att köra följande SQL-fråga och fastställa hur ofta frågor utförs.

SELECT UseCounts, Text, Query_Plan
FROM sys.dm_exec_cached_plans
CROSS APPLY sys.dm_exec_sql_text(plan_handle)
CROSS APPLY sys.dm_exec_query_plan(plan_handle)

Kolumnen UseCount i resultatet anger hur ofta varje fråga körs. Följande bild visar att den tredje frågan kördes över 250 000 gånger, betydligt mer än någon annan fråga.

Results of querying the dynamic management views in SQL Server Management Server

Här är SQL-frågan som orsakar så många databasbegäranden:

(@p__linq__0 int)SELECT TOP (2)
[Extent1].[BusinessEntityId] AS [BusinessEntityId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName]
FROM [Person].[Person] AS [Extent1]
WHERE [Extent1].[BusinessEntityId] = @p__linq__0

Det här är frågan som Entity Framework genererar i metoden GetByIdAsync som visades tidigare.

Implementera cachestrategilösningen och verifiera resultatet

När du har skapat en cache upprepar du belastningstesterna och jämför resultatet med de tidigare belastningstesterna utan cache. Här är belastningstestresultatet efter att ha lagt till en cache i exempelprogrammet.

Performance load test results for the cached scenario

Antalet utförda tester når fortfarande en platå men vid en högre användarbelastning. Begäranhastigheten vid den här belastningen är betydligt högre än tidigare. Den genomsnittliga testtiden ökar fortfarande med belastningen, men den maximala svarstiden är 0,05 ms, jämfört med 1 ms tidigare – en förbättring på 20×.