Vorhersage: Bewölkt

Zwischenspeicherungsstrategien für Windows Azure

Joseph Fultz

Joseph FultzMeine Zwischenspeicherungsmethode aus zwei Schritten entstand während des Dotcom-Booms. Natürlich musste ich damals hier und da kleine Datenmengen beim Kunden oder im Arbeitsspeicher zwischenspeichern, damit die Anwendungen, die ich zu diesem Zeitpunkt entwickelt hatte, einfacher oder schneller liefen. Aber erst als das Internet und vor allem der Internethandel sich immer weiter ausbreiteten, entwickelten sich wirklich meine Überlegungen bezüglich der Zwischenspeicherungsstrategien für meine Anwendungen, sowohl im Web- als auch im Desktopbereich.

In dieser Spalte ordne ich verschiedene Zwischenspeicherungsfunktionen von Windows Azure den Zwischenspeicherungsstrategien für die Ausgabe, speicherinterne Daten und Dateiressourcen unter dem Gesichtspunkt des Ausgleichs zwischen neuen Daten und dem Wunsch nach optimaler Leistung zu. Am Ende werde ich kurz über Dereferenzierung als eine Möglichkeit der intelligenten Zwischenspeicherung sprechen.

Zwischenspeichern von Ressourcen

Wenn ich von der Zwischenspeicherung von Ressourcen spreche, beziehen sich diese auf alle serialisierten Daten in einem Dateiformat, das am Endpunkt genutzt wird. Dazu gehört alles von serialisierten Objekten (z. B. XML und JSON) bis zu Bildern und Videos. Sie können versuchen, das Cacheverhalten des Browsers mithilfe von Headern und Metatags zu beeinflussen, aber nur zu oft werden die entsprechenden Vorschläge nicht wirklich berücksichtigt. Vielmehr steht eigentlich von vornherein fest, dass die Header von den Dienstschnittstellen ignoriert werden. Angesichts der schwindenden Hoffnung, dass wir sich langsam ändernde Ressourceninhalte zumindest zur Leistungs- und Verhaltenssicherstellung unter Last erfolgreich zwischenspeichern könnten, müssen wir einen Schritt zurück gehen. Anstatt diese jedoch auf den Webserver zurück zu verschieben, können wir für die meisten Ressourcen ein Netzwerk für die Inhaltsübermittlung (Content Delivery Network, CDN) nutzen.

Eine Möglichkeit, sich etwas vom Client zu entfernen und die Inhalte den Benutzern näher zu bringen, besteht darin, zwischen den Front-End-Webservern und dem Client eine Art Wegpunkt zu nutzen, vor allem in großen geografischen Gebieten. Die Inhalte werden so nicht nur an den betreffenden Punkten zwischengespeichert, sondern vor allem näher an den Endbenutzern, was noch wichtiger ist. Die für die Verteilung verwendeten Server werden zusammengefasst als Inhaltsübermittlungs-/Inhaltsverteilungsnetzwerk bezeichnet. Zu Beginn der Internetverbreitung waren die Ideen und Implementierungen bezüglich der verteilten Zwischenspeicherung von Ressourcen für das Web noch ziemlich neu, und Unternehmen wie Akami Technologies bot sich eine großartige Chance für den Verkauf von Diensten zur Skalierung von Websites. Zehn Jahre später ist die Strategie in einer Welt, in der uns das Web trotz physischer Trennung zusammenbringt, wichtiger als je zuvor. Für Windows Azure stellt Microsoft das Windows Azure Content Delivery Network (CDN) bereit. Auch wenn es sich dabei um eine funktionierende Strategie für die Zwischenspeicherung von Inhalten und deren Näherbringung an den Benutzer handelt, wird das CDN tatsächlich aber vor allem von Websites verwendet, die mengen- und/oder größenmäßig umfassende Ressourcen zu verarbeiten haben. Ein guter Beitrag zur Verwendung des Windows Azure CDN ist im Blog von Steve Marx (bit.ly/fvapd7) zu finden, der im Windows Azure-Team arbeitet.

Bei der Bereitstellung einer Website erscheint es in den meisten Fällen ziemlich offensichtlich, dass die Dateien auf den Servern für die Website platziert werden müssen. In einer Windows Azure-Webrolle werden die Websiteinhalte im Paket bereitgestellt – mal überprüfen, erledigt. Moment, die letzten Bilder vom Marketing wurden nicht mit dem Paket übertragen; Zeit für eine erneute Bereitstellung. Eine Aktualisierung dieser Inhalte bedeutet – realistisch gesehen – eine erneute Bereitstellung des Pakets. Natürlich können sie auch stufenweise bereitgestellt und dann gewechselt werden, dabei muss der Benutzer jedoch in der Regel eine Verzögerung oder eine eventuelle Störung in Kauf nehmen.

Eine unkomplizierte Möglichkeit der Bereitstellung eines aktualisierbaren Front-End-Webcaches mit Inhalten besteht darin, die meisten Inhalte im Windows Azure-Speicher zu speichern und alle URIs an die Windows Azure Storage-Container zu verweisen. Aus verschiedenen Gründen ist es jedoch vorzuziehen, die Inhalte bei den Webrollen zu belassen. Eine Möglichkeit zur Gewährleistung, dass der Webrolleninhalt aktualisiert oder neuer Inhalt hinzugefügt werden kann, besteht darin, die Dateien im Windows Azure-Speicher zu belassen und bei Bedarf in einen Speichercontainer für lokale Ressourcen der Webrollen zu verschieben. Zu diesem Thema stehen mehrere Varianten zur Verfügung. Eine davon ich habe in einem Blogbeitrag vom März 2010 (bit.ly/u08DkV) besprochen.

Zwischenspeicherung im Arbeitsspeicher

Während der Schwerpunkt der bisherigen Ausführungen zur Zwischenspeicherung tatsächlich auf der Verschiebung von dateibasierten Ressourcen lag, konzentriere ich mich als Nächstes auf alle Daten und dynamisch dargestellten Inhalte der Website. Ich habe bereits massenweise Leistungstests und schwerpunktmäßig auf die Leistung der Website und die dahinterstehende Datenbank bezogene Optimierungsmaßnahmen durchgeführt. Mithilfe eines soliden Zwischenspeicherungsplans und einer Implementierung, die eine Zwischenspeicherung der Ausgabe (HTML-Darstellung ohne Notwendigkeit der erneuten Darstellung zur einfachen Übertragung an den Client) sowie Daten (in der Regel vom Typ Zusatzcache) abdeckt, werden Sie ausnahmslos sowohl hinsichtlich der Skalierung als auch der Leistung eine erhebliche Verbesserung erzielen – vorausgesetzt, die Datenbankimplementierung ist nicht von Natur aus defekt.

Die schwierigste Arbeit bei der Implementierung einer Zwischenspeicherungsstrategie innerhalb einer Website besteht darin, festzulegen, was zwischengespeichert werden soll und wie oft eine Aktualisierung erfolgen soll bzw. was bei jeder Anforderung dynamisch dargestellt bleiben soll. Neben den von Microsoft .NET Framework für den Ausgabecache und von System.Web.Caching bereitgestellten Standardfunktionen bietet Windows Azure einen verteilten Cache namens Windows Azure App­Fabric Cache (AppFabric Cache).

Verteilter Cache

Mithilfe eines verteilten Caches können mehrere Probleme gelöst werden. So wird beispielsweise, obwohl Zwischenspeicherung immer für die Websiteleistung empfohlen wird, in der Regel die Nutzung des Sitzungsstatus kontraindiziert, auch wenn dadurch ein kontextbezogener Cache bereitgestellt wird. Der Grund dafür liegt darin, dass zum Abrufen des Sitzungsstatus ein Client mit einem Server verbunden sein muss, was die Skalierbarkeit negativ beeinflusst, oder eine Synchronisierung unter den Servern einer Farm erfolgen muss, wodurch – aus gutem Grund – in der Regel Probleme und Einschränkungen zu erwarten sind. Das Sitzungsstatusproblem wird mithilfe eines leistungsfähigen und stabilen verteilten Caches für die Sicherung gelöst. Dadurch können die Server die Daten empfangen, ohne diese dafür ständig abrufen müssen. Gleichzeitig werden die Daten mit einem entsprechenden Mechanismus beschrieben und nahtlos an alle Cacheclients weitergeleitet. So erhält der Entwickler einen umfangreichen kontextabhängigen Cache ohne Einbüßung der Skalierungsqualitäten einer Webfarm.

Das Beste an AppFabric Cache ist, dass für dessen Verwendung kaum mehr getan werden muss, als einige Konfigurationseinstellungen zu ändern, sobald der Sitzungsstatus erreicht ist. Außerdem umfasst es eine einfache API für die programmgesteuerte Nutzung. Detaillierte Informationen zur Verwendung des Caches finden Sie im Artikel von Karandeep Anand und Wade Wegner der Ausgabe vom April 2011 (msdn.microsoft.com/magazine/gg983488).

Wenn Sie eine bestehende Website verwenden, die System.Web.Caching direkt im Code aufruft, ist die Einbindung von AppFabric Cache leider etwas aufwendiger. Dafür gibt es zwei Gründe:

  1. Die Verschiedenheit der APIs (siehe Abbildung 1)
  2. Die Strategie hinsichtlich dessen, was wo zwischengespeichert werden soll

Abbildung 1: Hinzufügen von Inhalten per Cache-API

Hinzufügen zu Cache AppFabric Cache Hinzufügen zu Cache System.Web.Caching

DataCacheFactory cacheFactory=

  new DataCacheFactory(configuration);

DataCache appFabCache =

  cacheFactory.GetDefaultCache();

string value =

  "This string is to be cached locally.";

appFabCache.Put("SharedCacheString", value);

System.Web.Caching.Cache LocalCache =

  new System.Web.Caching.Cache();

string value =

  "This string is to be cached locally.";

LocalCache.Insert("localCacheString", value);

Abbildung 1 macht deutlich, dass es sogar hinsichtlich der grundlegenden Elemente der APIs eindeutig einen Unterschied gibt. Durch die Erstellung einer Dereferenzierungsschicht zur Vermittlung der Aufrufe wird die Flexibilität des Codes in Ihrer Anwendung unterstützt. Natürlich bedarf es einiger Arbeit, bis die erweiterten Funktionen der drei Cachetypen problemlos bereitgestellt werden können, die Vorteile überwiegen jedoch auf jeden Fall über den für die Implementierung der Funktionalität erforderlichen Aufwand.

Auch wenn der verteilte Cache durchaus einige im Allgemeinen schwierige Probleme löst, sollte er dennoch nicht als Allheilmittel eingesetzt werden, andernfalls wird er wahrscheinlich genauso wenig wirksam sein. Je nach dem jeweiligen Ausgleich und den beim Cache eingehenden Daten ist es möglich, dass für die Speicherung von Daten auf den lokalen Cacheclient mehr Abrufe bei abgeschaltetem Gerät erforderlich sind, was wiederum die Leistung negativ beeinflussen würde. Noch mehr fallen die Bereitstellungskosten ins Gewicht. Ab dem Zeitpunkt dieser Abhandlung betragen die Kosten für 4 GB freigegebenem AppFabric Cache monatlich 325 US-Dollar. Auch wenn das an sich keine große Summe ist und 4 GB sich nach einem relativ großen Cacheplatz anhören, könnten auf einer Website mit hohem Datenverkehr, vor allem wenn diese zum Sichern des Sitzungsstatus mit AppFabric Cache sowie zahl- und umfangreicher gezielter Inhalte genutzt wird, problemlos mehrere Caches dieser Größe gefüllt werden. In den Produktkatalogen sind die Preisunterschiede je nach Kundenschichten sowie angepasste Vertragspreise zu finden.

Dereferenzierung außerhalb des Caches

Wie viele andere Dinge in dieser Technologiebranche – und mir fallen wirklich viele andere ein – ist Design eine gewisse Mischung aus den idealen technischen, gemäß der finanztechnischen Realität geänderten Implementierungen. Somit gibt es, auch wenn Sie nur die Windows Server 2008 R2 AppFabric-Zwischenspeicherung verwenden, Gründe für die weitere Verwendung der von System.Web.Caching bereitgestellten lokalen Zwischenspeicherung. Als ersten Schritt der Dereferenzierung habe ich die Aufrufe in die jeweiligen Zwischenspeicherungsbibliotheken verpackt und für jeden eine Funktion bereitgestellt, wie beispielsweise AddtoLocalCache(key, object) und AddtoSharedCache(key, object). Das bedeutet jedoch, dass jedes Mal, wenn ein Cachevorgang erforderlich ist, der Entwickler eine ziemlich undurchsichtige und persönliche Entscheidung hinsichtlich dessen, wo die Zwischenspeicherung stattfinden soll, trifft. Eine derartige Logik scheitert unter Wartungsbedingungen und bei größeren Teams schnell und führt unvermeidlich zu unvorhergesehenen Fehlern, da der Entwickler eventuell ein Objekt zu einem ungeeigneten Cache hinzufügen oder es versehentlich von einem anderen als dem für die Hinzufügung abrufen könnte. Somit würden viele zusätzliche Datenabrufaktionen erforderlich werden, da sich die Daten nicht im Cache befinden würden bzw. beim Abruf im falschen Cache wären. Dadurch würden Szenarien wie die Feststellung einer unerwartet schwachen Leistung entstehen, bei deren Untersuchung nur festgestellt werden würde, dass die Hinzufügungsvorgänge in einem Cache durchgeführt wurden, die Abrufvorgänge aber unerklärlicherweise in einem anderen stattfanden, weil der Entwickler einfach den richtigen vergessen oder den falschen eingegeben hatte. Darüber hinaus werden bei der korrekten Planung eines Systems die betreffenden Datentypen (Entitäten) im Voraus identifiziert, und aus dieser Definition sollte geschlossen werden, wo die jeweilige Entität verwendet wird, welche Konsistenzanforderungen bestehen (vor allem unter Lastenausgleich-Servern) und wie aktuell sie sein muss. So kann der Zwischenspeicherungsort (mit oder ohne Freigabe) entsprechend bestimmt und das Ablaufdatum im Voraus als Teil der Deklaration festgelegt werden.

Wie ich bereits erwähnte, sollte es einen Zwischenspeicherungsplan geben. Nur zu oft wird dieser willkürlich am Ende eines Projekts hinzugefügt, ihm sollten aber die gleiche Berücksichtigung und derselbe Designaufwand zukommen wie jedem anderen Aspekt der Anwendung. Dies ist vor allem bei der Arbeit mit der Cloud von Bedeutung, da unzureichend überlegte Entscheidungen neben den Defiziten im Anwendungsverhalten häufig auch noch Extrakosten verursachen. Bei der Überlegung, welche Datentypen zwischengespeichert werden sollten, können zum einen die betroffenen Entitäten (Datentypen) und deren Lebenszyklus innerhalb der Anwendung und Benutzersitzung berücksichtigt werden. Angesichts dessen wird schnell klar, dass es hilfreich wäre, wenn die Entität selbst auf der Basis des jeweiligen Typs eine intelligente Zwischenspeicherung vornehmen könnte. Mithilfe eines benutzerdefinierten Attributs ist dies glücklicherweise eine leichte Aufgabe.

Ich überspringe die Einrichtung für alle Caches, da diese im zuvor erwähnten Material ausführlich genug behandelt wird. Für meine Zwischenspeicherungsbibliothek habe ich einfach eine statische Klasse mit statischen Methoden in meinem Beispiel erstellt. Bei anderen Implementierungen gibt es gute Gründe, dafür Instanzobjekte zu verwenden, aber um das Beispiel möglichst einfach zu gestalten, wähle ich ein statisches.

Ich deklariere eine Aufzählung zur Angabe des Speicherorts und eine Klasse mit Übernahme des Attributs zur Implementierung meines benutzerdefinierten Attributs, wie in Abbildung 2 dargestellt.

Abbildung 2: Deklarieren einer Aufzählung zur Implementierung eines benutzerdefinierten Attributs

public enum CacheLocationEnum
{
  None=0,
  Local=1,
  Shared=2
}
public class CacheLocation:Attribute
{
  private CacheLocationEnum _location = CacheLocationEnum.None;
  public CacheLocation(CacheLocationEnum location)
  {
    _location = location;
  }
  public CacheLocationEnum Location { get { return _location; } }
}

Durch die Übergabe des Speicherorts an den Konstruktor kann dieser später problemlos im Code verwendet werden, ich erläutere aber auch eine schreibgeschützte Methode zum Abrufen des Werts, da ich diese für eine Case-Anweisung brauche. In meiner CacheManager-Bibliothek habe ich einige private Methoden zum Hinzufügen zu den zwei Caches erstellt:

private static bool AddToLocalCache(string key, object newItem)
{...}
private static bool AddToSharedCache(string key, object newItem)
{...}

Für eine tatsächliche Implementierung benötige ich wahrscheinlich noch weitere Informationen (z. B. den Cachenamen, Abhängigkeiten, das Ablaufdatum usw.), für den Moment reicht das aber aus. Die öffentliche Hauptfunktion für das Hinzufügen von Inhalten zum Cache ist eine Vorlagenmethode, die es mir leicht macht, den Cache nach dem Typ zu bestimmen, wie in Abbildung 3 dargestellt.

Abbildung 3: Hinzufügen von Inhalten zum Cache

public static bool AddToCache<T> (string key, T newItem)
{
  bool retval = false;
  Type curType = newItem.GetType();
  CacheLocation cacheLocationAttribute =
    (CacheLocation) System.Attribute.GetCustomAttribute(typeof(T), 
    typeof(CacheLocation));
  switch (cacheLocationAttribute.Location)
  {
    case CacheLocationEnum.None:
      break;
    case CacheLocationEnum.Local:
      retval = AddToLocalCache(key, newItem);
      break;
    case CacheLocationEnum.Shared:
      retval = AddToSharedCache(key, newItem);
      break;
  }
  return retval;
}

Ich werde einfach den übergebenen Typ zum Abrufen des benutzerdefinierten Attributs verwenden und meinen benutzerdefinierten Attributtyp über die GetCustomAttribute(type, type)-Methode anfordern. Sobald das erledigt ist, bedarf es nur noch eines einfachen Aufrufs der schreibgeschützten Eigenschaft und einer Case-Anweisung, dann ist der Aufruf erfolgreich an den betreffenden Cacheanbieter weitergeleitet. Zur Gewährleistung einer fehlerfreien Ausführung muss ich meine Klassendeklarationen entsprechend im Detail darstellen:

[CacheLocation(CacheLocationEnum.Local)]
public class WebSiteData
{
  public int IntegerValue { get; set; }
  public string StringValue { get; set; }
}
[CacheLocation(CacheLocationEnum.Shared)]
public class WebSiteSharedData
{
  public int IntegerValue { get; set; }
  public string StringValue { get; set; }
]}

Nach der Einrichtung meiner gesamten Anwendungsinfrastruktur steht einer Nutzung im Anwendungscode nichts mehr im Weg. Ich öffne die Datei default.aspx.cs, um die Beispielaufrufe zu erstellen und Code zur Erstellung der Typen hinzuzufügen, weise einige Werte zu und füge diese zum Cache hinzu:

WebSiteData data = new WebSiteData();
data.IntegerValue = 10;
data.StringValue = "ten";
WebSiteSharedData sharedData = new WebSiteSharedData();
sharedData.IntegerValue = 50;
sharedData.StringValue = "fifty";
CachingLibrary.CacheManager.AddToCache<WebSiteData>("localData", data);
CachingLibrary.CacheManager.AddToCache<WebSiteSharedData>(
  "sharedData", sharedData);

An meinen Typennamen ist ersichtlich, wo die Daten zwischengespeichert werden. Ich könnte die Typennamen jedoch auch ändern, und mit der durch die Prüfung des benutzerdefinierten Attributs gesteuerten Zwischenspeicherung wäre es weniger ersichtlich. Bei Verwendung dieses Musters werden die Details hinsichtlich des Zwischenspeicherorts der Daten sowie weitere Details bezüglich der Konfiguration des Cacheelements gegenüber dem Seitenentwickler verborgen. Somit bleiben derartige Entscheidungen dem Teil des Teams überlassen, der die Datenwörterbücher erstellt und den gesamten Lebenszyklus der besagten Daten festlegt. Erfassen Sie den Typ, der an die Aufrufe von AddToCache<t>(string, t) übergeben werden. Die Implementierung der restlichen Methoden für die CacheManager-Klasse (also GetFromCache) würde nach demselben Muster erfolgen, das hier für die AddToCache-Methode verwendet wurde.

Abgleichen der Kosten mit Leistung und Skalierung

Windows Azure bietet die notwendige Softwareinfrastruktur, um Sie bei Ihrer Implementierung in jeder Hinsicht zu unterstützen. Dies gilt sowohl für die Zwischenspeicherung an sich als auch für die Festlegung, ob die Zwischenspeicherung für Ressourcen – zum Beispiel per CDN verteilte – erfolgen soll oder für Daten, die in AppFabric Cache erfasst werden könnten. Das Wichtigste für ein gutes Design und eine anschließende zufriedenstellende Implementierung ist die Abgleichung der Kosten mit der Leistung und Skalierung. Ein letzter Hinweis: Wenn Sie jetzt gerade an einer neuen Anwendung arbeiten und die Erstellung einer Zwischenspeicherung dafür planen, sollten Sie die Dereferenzierungsschicht gleich jetzt einbinden. Dafür ist etwas zusätzliche Arbeit erforderlich, da aber neue Funktionen wie die AppFabric-Zwischenspeicherung online bereitgestellt werden, wird es Ihnen mit dieser Praktik leichter fallen, die neuen Funktionen überlegt und effektiv in Ihrer Anwendung zu integrieren.    

Joseph Fultz ist Softwarearchitekt bei Hewlett-Packard Co. und Mitglied der HP.com Global IT-Gruppe. Zuvor war er Softwarearchitekt bei Microsoft und arbeitete gemeinsam mit dessen wichtigsten Unternehmens- und ISV-Kunden an der Definition von Architekturen und dem Entwurf von Lösungen.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Wade Wegner.