Entwurf und Architektur skalierbarer Software

Spätestens mit Veröffentlichung von Windows Azure wird deutlich, mit welcher Entschiedenheit Microsoft den Weg in Richtung Cloud Computing einschlägt. Neben dem Outsourcing von Infrastruktur verfolgen Unternehmen, die auf Cloud Computing setzen, ein zentrales Ziel: Skalierung von Anwendungen durch Nutzung der Infrastruktur eines Service Providers, die ebenfalls massiv skaliert.

Grundsätzlich gibt es zwei Ansätze, um Anwendungen zu skalieren

  • Scale-up
    Einsatz stärkerer Hardware (ohne die Anwendung, die skaliert werden soll, selbst zu verändern). Vorteil ist, dass die Anwendung selbst nicht verändert werden muss, Nachteil ist, dass diese Variante Grenzen hat: Zum einen lässt sich Hardware nicht unbegrenzt vergrößern, zum anderen steigen die Kosten
  • Scale-out
    Einsatz redundanter Hardware mit paralleler Ausführung der Anwendung bzw. Anwendungsteilen (wobei die Anwendung in der Regel angepasst wird, um die Parallelausführung zu erlauben). Dieser Ansatz hat den Vorteil, dass das Gesamtsystem nahezu beliebig skaliert, zugleich

Windows Azure setzt intern klar auf den zweiten Ansatz, Scale-out, und unterstützt auch für Anwendungen, die auf Windows Azure betrieben werden, diesen Ansatz.

Aufbau skalierbarer Services

Die Windows Azure Services Plattform bietet mit SQL Services und .NET Services Dienste, die nahelegen, selbstgeschriebene Services, die auf Windows Azure betrieben werden sollen, in entsprechende Tiers zu unterteilen. Folgende Unterteilung empfiehlt sich:

  • Communication Tier - verantwortlich für die Kommunikation, spricht Protokolle
  • Logic Tier - enthält die Anwendungslogik (und wird auf .NET Services betrieben)
  • Storage Tier - speichert die Daten (und wird auf SQL Services betrieben)

Durch eine klare Trennung dieser Tiers wird eine Skalierbarkeit auf einzelnen Schichten ermöglicht, d.h. Skalierung kann pro Tier entschieden, geplant und umgesetzt werden.

Ein paar Definitionen...

Wird nun eine dieser Schichten auf mehrere Server verteilt, stellt sich die Frage, wie die einzelnen Server konsistent gehalten werden. Hierbei lassen sich drei Level für die Konsistenzherstellung (Consistency Level) unterscheiden:

  • Strong: Konsistenz zwischen den Instanzen muss unmittelbar hergestellt werden (Beispiel: Sensorsteuerung, bei der die Sensordaten sofort überall verfügbar sein müssen)
  • Eventual: Konsistenz muss hergestellt werden, aber nicht notwendigerweise sofort (Beispiel: Adressänderung)
  • Optimistic: Konsistenz sollte hergestellt werden (Beispiel: Aktienkursdarstellung, da Kurse laufend kommen, fällt eine fehlende Aktualisierung nicht ins Gewicht)

Um die einzelnen Instanzen abzugleichen, müssen Nachrichten ausgetauscht werden. Hierbei stellt sich die Frage, welche Anforderungen an die Nachrichtenzustellung (Message Assurance) gestellt werden. Dabei gibt es vier grundlegende Anforderungsstufen:

  • Exactly Once - Nachrichten werden genau einmal zugestellt.
  • At Least Once - Nachrichten werden sicher zugestellt, dabei kann eine Nachricht im Rahmen der Zustellungssicherung auch mehrfach ausgeliefert werden.
  • At Most Once - Es wird sichergestellt, dass die Nachricht nicht mehrfach zugestellt wird.
  • Best Effort - in der Regel ein Zustellversuch, Einsatz bei Optimistic Consistency Level (Beispiel: http-Protokoll).

Infrastrukturoptionen

  • Einzelne Maschine - eine Maschine ist für alle Anfragen verantwortlich
  • Partitioniert - Mehrere Maschinen arbeiten parallel und bedienen jeweils feste Segmente des Anforderungsspektrums
  • Hochverfügbarkeit - Mehrere Maschinen arbeiten parallel, bedienen primär feste Segmente des Anforderungsspektrums, replizieren sich aber gegenseitig, so dass sie sich im Fehlerfall gegenseitig "vertreten" können.

Die letztgenannte Option vereint die Vorteile von Load Balancing und Ausfallsicherheit, stellt an die Software und die Infrastruktur allerdings auch die höchsten Anforderungen: die einzelnen Maschinen müssen sich synchronisieren, im Fall des Ausfalls einer Maschine muss ein Umleiten von Anfragen an die ausgefallene Maschine erfolgen.

Architekturtipps

Für die Architektur einer hoch skalierbaren Anwendung können folgende Empfehlungen gegeben werden:

Tip 1: Auswahl eines high-level-Frameworks

"the higher level the framework you use, the more portable your service will get"

So simpel dieser Satz ist, so groß ist doch auch seine Bedeutung. Bei Einsatz eines Frameworks wie der Azure Services, wird die Entscheidung für ein Hostingmodell (vor-Ort, beim Hoster, bei Microsoft) zu einer Entscheidung, die zum Deploymentzeitpunkt getroffen werden kann. Deshalb empfiehlt sich der Einsatz eines high-level-Frameworks (low-level-Frameworks treffen häufig zu viele Annahmen über die Umgebung, in der sie eingesetzt werden).

Tip 2: Separierung von Service- und Hosting-Codes

Infrastruktur-Code sollte in Konfigurationsdaten und dedizierte, pro Umgebung verwaltbare Konfigurationskomponenten ausgelagert werden. Damit sind bei einem Transfer in eine andere (skalierbarere) Umgebung nur die Hosting-Codes anzupassen.

Tip 3: Partitionierung des Codes (horizontal in Tiers, vertikal z.B. in Schemata)

Schichtenbildung in der Software erlaubt, wie oben bereits beschrieben, Skalierungsmaßnahmen auf den einzelnen Schichten durchzuführen. Wird in der Datenschicht hohe Skalierung gewünscht, so kann diese beispielsweise auf SQL Data Services betrieben werden.

Designtipps

Tip 1: Einsatz loser Koppelung

Lose Kopplung und die Einhaltung von Prinzipien serviceorientierter Architektur schafft die Mobilität von Services und z,B. auch die Voraussetzung, Services auf eine skalierbare Umgebung zu verschieben. Wo die Datenschicht beispielsweise zunächst lokal betrieben werden kann, kann diese bei entsprechend loser Kopplung auch auf SQL Data Services verschoben werden. Solange die Schnittstelle konstant bleibt, bekommen die anderen Schichten hiervon nichts mit.

Tip 2: Verwendung von Caches und "stale" Data

Caches können den Ausfall von Schichten abfangen, indem sie Daten solange bereithalten, solange der "Live" Speicher unverfügbar ist.

Tip 3: Implementierung einiger zentraler "recovery paths"

Nachdem der Ausfall einzelner Serverkomponenten möglich ist, muss es möglich sein, nach Ersatz der fehlerhaften Komponente wieder einen konsistenten Systemzustand herzustellen. Hierfür sollten entsprechende Funktionalitäten bereitgestellt werden.

Tip 4: Vermeidung von Shutdown-Code

Insbesondere im Falle von Cloud Computing sollte immer damit gerechnet werden, dass die Hardware, auf der eine Instanz der Software läuft, (ohne Zeitverzögerung) ausfällt.

Tip 5: Unabhängigkeit von der zugrundeliegenden Topologie

Es sollte vermieden werden, sich Informationen zu verlassen, die vom Administrator, d.h. dem Betreiber der Software zur Laufzeit geändert werden könnten. Also: keine hardcodierten Servernamen, IP-Adressen etc. Diese Informationen sollten, wenn sie benötigt werden, in Konfigurationsdateien ausgelagert werden.