Antipatroon Geen caching
Antipatronen zijn veelvoorkomende ontwerpfouten die uw software of toepassingen onder stresssituaties kunnen breken en die niet over het hoofd mogen worden gezien. Er treedt geen antipatroon voor caching op wanneer een cloudtoepassing die veel gelijktijdige aanvragen verwerkt, herhaaldelijk dezelfde gegevens ophaalt. Dit kan de prestaties en schaalbaarheid verminderen.
Als gegevens niet in de cache worden opgeslagen, kan dit verschillende soorten ongewenst gedrag tot gevolg hebben, waaronder:
- Herhaaldelijk ophalen van dezelfde gegevens uit een resource met een hoge I/O-overhead of latentie.
- Herhaaldelijk samenstellen van dezelfde objecten of gegevensstructuren voor verschillende aanvragen.
- Buitensporig aantal aanroepen van een externe service waarvoor een servicequotum geldt en clients vanaf een bepaalde limiet beperkingen krijgen opgelegd.
Dit gedrag kan weer problemen veroorzaken zoals lange reactietijden, meer conflicten in het gegevensarchief en een slechte schaalbaarheid.
Voorbeelden van antipatroon voor caching
In het volgende voorbeeld wordt Entity Framework gebruikt om verbinding te maken met een database. Elke clientaanvraag resulteert in een aanroep van de database, zelfs als met verschillende aanvragen exact dezelfde gegevens worden opgehaald. De kosten van herhaalde aanvragen, qua I/O-overhead en kosten voor gegevenstoegang, kunnen snel oplopen.
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);
}
}
}
U vindt het complete voorbeeld hier.
Enkele veelvoorkomende oorzaken voor dit antipatroon:
- Geen cache gebruiken is eenvoudiger om te implementeren en werkt prima bij lage belastingen. Caching maakt de code complexer.
- De voor- en nadelen van het gebruik van een cache zijn niet helemaal helder.
- Er is bezorgdheid over de overhead van het onderhouden van de nauwkeurigheid en actualiteit van gegevens in de cache.
- Een toepassing is gemigreerd vanuit een on-premises systeem, waarin netwerklatentie niet aan de orde was, en het systeem werd uitgevoerd op dure, geavanceerde hardware, zodat caching niet is overwogen bij het originele ontwerp.
- Ontwikkelaars realiseren zich niet dat caching een mogelijkheid is in een bepaald scenario. Zo denken ontwikkelaars er bijvoorbeeld niet aan dat ze ETags kunnen gebruiken bij het implementeren van een web-API.
Het antipatroon voor opslaan in cache oplossen
De populairste cachingstrategie is on-demand of cache-aside.
- Op het moment dat gegevens worden opgevraagd, probeert de toepassing deze op te halen uit de cache. Als de gegevens niet in de cache staan, haalt de toepassing de gegevens op uit de gegevensbron en worden ze ook toegevoegd aan de cache.
- Bij het wegschrijven van de gegevens worden de wijzigingen rechtstreeks in de gegevensbron opgeslagen en worden de oude gegevens uit de cache verwijderd. De gewijzigde gegevens worden opgehaald als ze de volgende keer nodig zijn en dan ook toegevoegd aan de cache.
Deze aanpak is geschikt voor gegevens die vaak veranderen. Hier ziet u het vorige voorbeeld, maar dan bijgewerkt voor gebruik van het patroon 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;
}
}
U ziet dat de methode GetAsync
nu de klasse CacheService
aanroept, in plaats van de database rechtstreeks aan te roepen. De klasse CacheService
probeert het item eerst op te halen uit Azure Cache voor Redis. Als de waarde niet wordt gevonden in de cache, roept CacheService
een lambdafunctie aan die eerder is ontvangen van de aanroepende functie. De lambdafunctie is verantwoordelijk voor het ophalen van de gegevens uit de database. In deze implementatie wordt de opslagplaats losgekoppeld van de specifieke cacheoplossing, en de klasse CacheService
van de database.
Overwegingen voor cachingstrategie
Als de cache niet beschikbaar is, bijvoorbeeld vanwege een tijdelijke storing, moet er geen fout worden geretourneerd naar de client. Haal de gegevens in dat geval op uit de gegevensbron. Houd er wel rekening mee dat tijdens het herstellen van de cache, het originele gegevensarchief kan worden overladen met aanvragen, met time-outs en mislukte verbindingen tot gevolg. (Dit is immers een van de motivaties voor het gebruik van een cache in de eerste plaats.) Gebruik een techniek zoals het circuitonderbrekerpatroon om te voorkomen dat de gegevensbron wordt overstuurd.
Toepassingen die dynamische gegevens cachen, moeten ingebouwde ondersteuning bieden voor uiteindelijke consistentie.
In het geval van web-API‘s kunt u caching aan de clientzijde ondersteunen door een Cache-Control-header op te nemen in aanvraag- en responsberichten, en door met ETags versies van objecten te identificeren. Raadpleeg API-implementatie voor meer informatie.
Het is niet verplicht om entiteiten in hun geheel te cachen. Als het merendeel van een entiteit statisch is en slechts een klein gedeelte regelmatig verandert, plaatst u de statische elementen in de cache en haalt u de dynamische elementen op uit de gegevensbron. Deze aanpak kan ervoor zorgen dat er minder I/O-bewerkingen worden uitgevoerd op de gegevensbron.
Als vluchtige gegevens maar een korte levensduur hebben, kan het in sommige gevallen handig zijn om deze gegevens in de cache te plaatsen. Laten we het voorbeeld nemen van een apparaat dat continu statusupdates verstuurt. Het kan dan zinvol zijn om deze gegevens bij ontvangst in de cache te plaatsen en helemaal niet weg te schrijven naar een permanente opslaglocatie.
Om te voorkomen dat gegevens verouderen, bieden veel cacheoplossingen ondersteuning voor instelbare vervalperioden. Dit houdt in dat gegevens na het opgegeven interval automatisch worden verwijderd uit de cache. Het kan nodig zijn om te experimenteren met de vervaltijd voor uw specifieke scenario. Gegevens die erg statisch zijn, kunnen langer in de cache blijven staan dan vluchtige gegevens die snel verouderd zijn.
Als de cacheoplossing geen ingebouwd vervalmechanisme bevat, moet u mogelijk een achtergrondproces implementeren waarmee de cache af en toe wordt leeggemaakt, om te voorkomen dat deze veel te groot wordt.
Behalve om gegevens uit een externe gegevensbron te cachen, kunt u de cache ook gebruiken om de resultaten van complexe berekeningen op te slaan. U moet de toepassing dan wel eerst instrumenteren om te bepalen of de toepassing echt CPU-afhankelijk is.
Het kan handig zijn om de cache voor te bereiden wanneer de toepassing wordt gestart. Vul de cache met de gegevens die naar alle waarschijnlijkheid zullen worden gebruikt.
Voeg altijd instrumentatie toe waarmee cachetreffers en cachemissers kunnen worden gedetecteerd. Gebruik deze gegevens om beleidsregels voor het cachen af te stemmen, zoals welke gegevens in de cache moeten worden geplaatst en hoe lang gegevens in de cache moeten blijven staan voordat ze verlopen.
Als het ontbreken van caching een bottleneck is, kan door het toevoegen van caching het aantal aanvragen zo sterk toenemen dat de web-frontend overbelast raakt. Clients kunnen dan HTTP 503-fouten (service niet beschikbaar) krijgen. Deze zijn een indicatie dat de frontend moet worden uitgeschaald.
Een antipatroon voor opslaan in cache detecteren
U kunt de volgende stappen uitvoeren om vast te stellen of het ontbreken van caching prestatieproblemen veroorzaakt:
Bekijk het ontwerp van de toepassing. Maak een overzicht van alle gegevensarchieven die door de toepassing worden gebruikt. Bepaal voor elk archief of de toepassing een cache gebruikt. Probeer ook vast te stellen hoe vaak de gegevens veranderen. Goede kandidaten voor caching zijn gegevens die niet zo vaak veranderen en statische verwijzingsgegevens die regelmatig worden gelezen.
Instrumenteer de toepassing en monitor het live systeem om te bepalen hoe vaak de toepassing gegevens ophaalt of gegevens berekent.
Profileer de toepassing in een testomgeving om eenvoudige metrische gegevens vast te leggen van de overhead van bewerkingen voor gegevenstoegang of andere regelmatig uitgevoerde berekeningen.
Voer belastingstests uit in een testomgeving om te bepalen hoe het systeem reageert onder een normale werkbelasting en onder zware belasting. Bij de tests moet het patroon van gegevenstoegang uit de productieomgeving worden gesimuleerd met gebruikmaking van realistische werkbelastingen.
Onderzoek de statistische gegevens voor gegevenstoegang voor de onderliggende gegevensarchieven en kijk hoe vaak dezelfde gegevensaanvragen worden herhaald.
Voorbeeld van diagnose
In de volgende secties worden deze stappen toegepast op de voorbeeldtoepassing die eerder is beschreven.
De toepassing instrumenteren en het live systeem monitoren
Instrumenteer de toepassing en monitor deze om gegevens te verzamelen van de specifieke aanvragen die gebruikers versturen terwijl de toepassing in productie is.
In de volgende afbeelding ziet u monitoringgegevens die met New Relic zijn vastgelegd tijdens een belastingstest. In dit geval is Person/GetAsync
de enige uitgevoerde HTTP GET-bewerking. Maar in een live productieomgeving kunt u aan de hand van de relatieve frequentie waarmee elke aanvraag wordt uitgevoerd, een beeld krijgen van de resources die in de cache moeten worden geplaatst.
Als u een uitvoerigere analyse nodig hebt, kunt u een profiler gebruiken om algemene prestatiegegevens vast te leggen in een testomgeving (niet het productiesysteem). Kijk naar metrische gegevens zoals de frequentie van I/O-aanvragen, geheugengebruik en CPU-gebruik. Deze gegevens kunnen een groot aantal aanvragen naar een gegevensarchief of service laten zien, of herhaalde bewerkingen die dezelfde berekening uitvoeren.
De belasting van de toepassing testen
In het volgende diagram ziet u de resultaten van een belastingstest van de voorbeeldtoepassing. In de test wordt de belasting stapsgewijs opgevoerd naar maximaal 800 gebruikers die een typische reeks bewerkingen uitvoeren.
Het aantal geslaagde tests per seconde bereikt een plafond, waardoor aanvullende aanvragen langzamer worden uitgevoerd. De gemiddelde testtijd stijgt evenredig met de workload. De reactietijden stabiliseren zodra het maximale aantal gebruikers is bereikt.
Statistische gegevens van gegevenstoegang onderzoeken
Statistische gegevens van gegevenstoegang en andere gegevens afkomstig van een gegevensarchief kunnen nuttige informatie verstrekken, zoals welke query‘s het vaakst worden herhaald. Zo bevat de beheerweergave sys.dm_exec_query_stats
in Microsoft SQL Server bijvoorbeeld statistische gegevens van recent uitgevoerde query‘s. De tekst voor elke query is beschikbaar in de weergave sys.dm_exec-query_plan
. U kunt een tool zoals SQL Server Management Studio gebruiken om de volgende SQL-query uit te voeren en te bepalen hoe regelmatig query‘s worden uitgevoerd.
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)
De kolom UseCount
in de resultaten toont aan hoe vaak elke query wordt uitgevoerd. In de volgende afbeelding ziet u dat de derde query meer dan 250.000 keer is uitgevoerd, aanzienlijk meer dan elke andere query.
Dit is de SQL-query die het grote aantal databaseaanvragen veroorzaakt:
(@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
Dit is de query die Entity Framework genereert in de eerder weergegeven methode GetByIdAsync
.
De cachestrategieoplossing implementeren en het resultaat controleren
Nadat u een cache hebt geïmplementeerd, herhaalt u de belastingstests en vergelijkt u de resultaten met de eerdere belastingstests zonder een cache. Dit zijn de resultaten van de belastingstest nadat er een cache is toegevoegd aan de voorbeeldtoepassing.
Het aantal geslaagde tests bereikt nog steeds een plafond, maar bij een groter aantal gebruikers. De aanvraagsnelheid bij deze belasting is aanzienlijk hoger dan eerder. De gemiddelde testtijd neemt nog steeds toe met belasting, maar de maximale reactietijd is 0,05 ms, vergeleken met 1 ms eerder, een verbetering van 20×.