Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Antimuster sind gängige Entwurfsmängel, die Ihre Software oder Anwendungen in Belastungssituationen unterbrechen können und nicht übersehen werden sollten. Ein Kein-Caching-Antimuster tritt auf, wenn eine Cloudanwendung, die viele gleichzeitige Anforderungen bearbeitet, wiederholt dieselben Daten abruft. Dies kann die Leistung und Skalierbarkeit verringern.
Wenn die Daten nicht zwischengespeichert werden, kann eine Reihe unerwünschter Verhaltensweisen auftreten, wie z.B. folgende:
- Wiederholtes Abrufen der gleichen Informationen aus einer Ressource, deren Zugriff viel E/A-Overhead oder Latenz kostet
- Wiederholtes Erstellen der gleichen Objekte oder Datenstrukturen für mehrere Anforderungen
- Übermäßig viele Abrufe für einen Remotedienst, für den ein Dienstkontingent gilt und der Clients ab einem bestimmten Grenzwert drosselt
Diese Probleme können zu langen Antwortzeiten, vermehrten Konflikten im Datenspeicher und unzureichender Skalierbarkeit führen.
Beispiele für das „Kein Caching“-Antimuster
Das folgende Beispiel verwendet Entity Framework, um eine Verbindung mit einer Datenbank herzustellen. Jede Clientanforderung führt zu einem Aufruf in der Datenbank, selbst wenn mehrere Anforderungen exakt die gleichen Daten abrufen. Die Kosten wiederholter Anforderungen hinsichtlich E/A-Overhead und Datenzugriffsgebühren können sich schnell summieren.
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);
}
}
}
Dieses Antimuster tritt üblicherweise aus folgenden Gründen auf:
- Wenn Sie keinen Cache verwenden, ist die Implementierung einfacher, und das Verfahren funktioniert gut bei geringen Lasten. Die Verwendung eines Caches macht den Code komplizierter.
- Die Vor- und Nachteile eines Caches sind nicht klar.
- Es gibt Bedenken hinsichtlich des Overheads für die Sicherstellung der Genauigkeit und Aktualität der zwischengespeicherten Daten.
- Eine Anwendung wurde aus einem lokalen System migriert, in dem die Netzwerklatenz kein Problem war und das System auf teurer Hochleistungshardware ausgeführt wurde, daher wurde im ursprünglichen Entwurf kein Caching berücksichtigt.
- Entwicklern ist nicht bewusst, dass Caching in einem bestimmten Szenario eine Möglichkeit sein kann. Entwickler können sich beispielsweise nicht vorstellen, ETags beim Implementieren einer Web-API zu verwenden.
Beheben des „Kein Caching“-Antimusters
Die am häufigsten verwendete Zwischenspeicherungsstrategie ist die On-Demand- oder Cache-Aside-Strategie .
- Während eines Lesevorgangs versucht die Anwendung, die Daten aus dem Cache zu lesen. Wenn sich die Daten nicht im Cache befinden, ruft die Anwendung sie aus der Datenquelle ab und fügt sie dem Cache hinzu.
- Bei einem Schreibvorgang schreibt die Anwendung die Änderung direkt in die Datenquelle und entfernt den alten Wert aus dem Cache. Wenn die Daten das nächste Mal benötigt werden, werden sie abgerufen und dem Cache hinzugefügt.
Diese Vorgehensweise eignet sich für Daten, die sich häufig ändern. Nachfolgend sehen Sie das vorherige Beispiel, das zum Verwenden des Cache-Aside-Musters aktualisiert wurde.
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;
}
}
Beachten Sie, dass die GetAsync
-Methode jetzt die CacheService
-Klasse aufruft, anstatt die Datenbank direkt aufzurufen. Die CacheService
-Klasse versucht zuerst, das Element aus Azure Cache für Redis abzurufen. Wenn der Wert im Cache nicht gefunden wird, ruft CacheService
eine Lambdafunktion auf, die vom Aufrufer an sie übergeben wurde. Die Lambdafunktion ist dafür zuständig, die Daten aus der Datenbank abzurufen. Diese Implementierung entkoppelt das Repository von der jeweiligen Cachinglösung und den CacheService
von der Datenbank.
Erwägungen für Zwischenspeicherungsstrategien
Wenn der Cache nicht verfügbar ist – möglicherweise aufgrund eines vorübergehenden Fehlers – geben Sie keinen Fehler an den Client zurück. Rufen Sie die Daten stattdessen aus der ursprünglichen Datenquelle ab. Denken Sie jedoch daran, dass während der Wiederherstellung des Caches der ursprüngliche Datenspeicher mit Anforderungen überschwemmt werden könnte, sodass Zeitüberschreitungen und Verbindungsfehler die Folge wären. (Schließlich ist dies eine der Motivationen für die Verwendung eines Caches von vornherein.) Verwenden Sie eine Technik wie das Schaltkreistrennmuster, um zu vermeiden, dass die Datenquelle überlastet wird.
Anwendungen, die dynamische Daten zwischenspeichern, sollte für die Unterstützung der letztlichen Konsistenz entworfen werden.
Bei Web-APIs können Sie das clientseitige Cachen unterstützen, indem Sie einen Cache-Control-Header in die Anforderungs- und Antwortnachricht einschließen und ETags zum Identifizieren der Version von Objekten verwenden. Weitere Informationen finden Sie unter API-Implementierung.
Sie müssen keine vollständigen Entitäten zwischenspeichern. Wenn der größte Teil einer Entität statisch ist und sich nur ein kleiner Teil häufig ändert, speichern Sie die statischen Elemente zwischen, und rufen Sie die dynamischen Elemente aus der Datenquelle ab. Mit dieser Vorgehensweise können Sie die Menge an E/A-Vorgängen reduzieren, die für die Datenquelle ausgeführt werden.
In einigen Fällen, wenn flüchtige Daten kurzlebig sind, kann es nützlich sein, diese zwischenzuspeichern. Nehmen wir als Beispiel ein Gerät, das kontinuierlich Statusupdates sendet. Es kann sinnvoll sein, diese Informationen bei Eingang zwischenzuspeichern und gar nicht erst in den dauerhaften Speicher zu schreiben.
Um zu verhindern, dass Daten veralten, unterstützen viele Cachinglösungen konfigurierbare Ablaufzeiträume, sodass diese Daten nach dem angegebenen Zeitraum automatisch aus dem Cache entfernt werden. Möglicherweise müssen Sie die Ablaufzeit für Ihr Szenario optimieren. Daten, die hochgradig statisch sind, können länger im Cache verbleiben als veränderliche Daten, die schnell veraltet werden können.
Wenn die Caching-Lösung keinen integrierten Ablauf bereitstellt, müssen Sie möglicherweise einen Hintergrundprozess implementieren, der gelegentlich den Cache bereinigt, um zu verhindern, dass er ohne Grenzen wächst.
Sie können nicht nur Daten aus einer externen Datenquelle zwischenspeichern, Sie können den Cache auch zum Speichern der Ergebnisse komplexer Berechnungen verwenden. Bevor Sie dies tun, instrumentieren Sie die Anwendung, um zu ermitteln, ob sie tatsächlich CPU-gebunden ist.
Es kann nützlich sein, den Cache vorzubereiten, wenn die Anwendung startet. Füllen Sie den Cache mit den Daten auf, die am wahrscheinlichsten verwendet werden.
Schließen Sie immer eine Instrumentierung ein, die Cachetreffer und Cachefehler erkennt. Verwenden Sie diese Informationen, um Cachingrichtlinien anzupassen, beispielsweise dahingehend, welche Daten zwischengespeichert werden und wie lange Daten im Cache aufbewahrt werden, bevor sie ablaufen.
Wenn der Mangel an Zwischenspeicherung ein Engpass ist, kann das Hinzufügen der Zwischenspeicherung das Volumen der Anforderungen so stark erhöhen, dass das Web-Front-End überlastet wird. Fehler vom Typ HTTP 503 (Dienst nicht verfügbar) können bei Clients auftreten. Diese sind ein Hinweis darauf, dass Sie das Front-End aufskalieren sollten.
Erkennung eines „No-Caching“-Antipatterns
Sie können die folgenden Schritte ausführen, um herauszufinden, ob ein unzureichendes Caching Leistungsprobleme verursacht:
Überprüfen Sie den Anwendungsentwurf. Erfassen Sie den Bestand aller Datenspeicher, die von der Anwendung verwendet werden. Ermitteln Sie für jede Instanz, ob die Anwendung einen Cache verwendet. Sofern möglich, ermitteln Sie, wie häufig sich die Daten ändern. Gute Anfangskandidaten für das Caching sind Daten, die sich nur langsam ändern, und statische Verweisdaten, die häufig gelesen werden.
Instrumentieren Sie die Anwendung, und überwachen Sie das Livesystem, um herauszufinden, wie häufig die Anwendung Daten abruft oder Informationen berechnet.
Erstellen Sie ein Profil der Anwendung in einer Testumgebung, um Low-Level-Metriken zum Overhead zu erfassen, der mit Datenzugriffsvorgängen oder anderen häufig ausgeführten Berechnungen in Verbindung steht.
Führen Sie einen Auslastungstest in einer Testumgebung durch, um zu ermitteln, wie das System unter normaler und schwerer Workload reagiert. Der Auslastungstest sollte das Datenzugriffsmuster simulieren, das in der Produktionsumgebung mit realistischen Workloads beobachtet wurde.
Untersuchen Sie die Datenzugriffsstatistiken für die zugrunde liegenden Datenspeicher, und untersuchen Sie, wie häufig die gleichen Datenanforderungen wiederholt werden.
Beispieldiagnose
In den folgenden Abschnitten werden diese Schritte auf die zuvor beschriebene Beispielanwendung angewendet.
Instrumentiere die Anwendung und überwache das Live-System
Instrumentieren Sie die Anwendung, und überwachen Sie sie, um Informationen zu den spezifischen Anforderungen zu erhalten, die Benutzer während der Produktionsphase der Anwendung senden.
Die folgende Abbildung zeigt die Von New Relic erfassten Überwachungsdaten während eines Auslastungstests. In diesem Fall ist Person/GetAsync
der einzige ausgeführte HTTP GET-Vorgang. In einer Liveproduktionsumgebung kann die Kenntnis der Häufigkeit, mit der jede Anforderung ausgeführt wird, Ihnen Einblicke darin geben, welche Ressourcen zwischengespeichert werden sollten.
Wenn Sie eine detailliertere Analyse benötigen, können Sie einen Profiler verwenden, um in einer Testumgebung (nicht im Produktionssystem) Leistungsdaten auf niedriger Ebene zu erfassen. Sehen Sie sich Metriken wie E/A-Anforderungsraten, Arbeitsspeichernutzung und CPU-Auslastung an. Diese Metriken können eine große Anzahl von Anforderungen an einen Datenspeicher oder Dienst oder eine wiederholte Verarbeitung anzeigen, die dieselbe Berechnung ausführt.
Auslastungstest der Anwendung
Das folgende Diagramm zeigt die Ergebnisse des Auslastungstests der Beispielanwendung. Der Auslastungstest simuliert eine Schrittauslastung von bis zu 800 Benutzern, die eine typische Reihenfolge von Vorgängen ausführen.
Die Anzahl von pro Sekunden ausgeführten erfolgreichen Tests stabilisiert sich auf einem Niveau, und dadurch werden weitere Anforderungen verlangsamt. Die durchschnittliche Testzeit steigt stetig mit der Workload. Die Antwortzeit sinkt, sobald die Benutzerauslastung einen Spitzenwert erreicht.
Untersuchen der Datenzugriffsstatistiken
Datenzugriffsstatistiken und andere von einem Datenspeicher bereitgestellte Informationen, z.B. dazu, welche Abfragen am häufigsten wiederholt werden, können sehr nützlich sein. In Microsoft SQL Server bietet die sys.dm_exec_query_stats
-Verwaltungssicht statistische Informationen zu kürzlich ausgeführten Abfragen. Der Text für jede Abfrage ist in der sys.dm_exec-query_plan
-Sicht verfügbar. Sie können ein Tool wie z.B. SQL Server Management Studio verwenden, um die folgende SQL-Abfrage auszuführen und zu ermitteln, wie häufig Abfragen ausgeführt werden.
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)
Die UseCount
-Spalte in den Ergebnissen gibt an, wie häufig jede Abfrage ausgeführt wird. Die folgende Abbildung zeigt, dass die dritte Abfrage über 250.000-mal ausgeführt wurde – deutlich mehr als jede andere Abfrage.
Dies ist die SQL-Abfrage, die so viele Datenbankanforderungen verursacht:
(@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
Dies ist die Abfrage, die Entity Framework in der oben gezeigten GetByIdAsync
-Methode generiert.
Implementieren der Lösung für die Zwischenspeicherungsstrategie und Überprüfen des Ergebnisses
Nachdem Sie einen Cache implementiert haben, wiederholen Sie die Auslastungstests, und vergleichen Sie die Ergebnisse mit den vorherigen Auslastungstests ohne Cache. Dies sind die Ergebnisse des Auslastungstests nach dem Hinzufügen eines Caches zur Beispielanwendung.
Das Volumen erfolgreicher Tests erreicht weiterhin ein Plateau, aber bei einer höheren Benutzerauslastung. Die Anforderungsrate bei dieser Auslastung ist deutlich höher als vorher. Die durchschnittliche Testzeit steigt weiterhin mit der Auslastung, aber die maximale Antwortzeit beträgt 0,05 ms (im Vergleich zu bisher 1 ms eine 20-fache Verbesserung).