Azure-Websites

Skalieren Ihrer Webanwendung mit Azure-Websites

Yochay Kiriaty

Laden Sie die Codebeispiele herunter

Überraschenderweise wird die Skalierung bei der Entwicklung von Webanwendungen häufig vernachlässigt. Die Skalierung einer Webanwendung wird normalerweise nur dann zu einem Problem, wenn bestimmte Dinge nicht mehr funktionieren und wenn das Benutzererlebnis aufgrund von Langsamkeit oder Timeouts auf der Darstellungsschicht beeinträchtigt wird. Wenn in einer Webanwendung solche Leistungsdefizite auftreten, hat sie den Skalierbarkeitspunkt erreicht – den Punkt, an dem fehlende Ressourcen wie CPU, Arbeitsspeicher oder Bandbreite und nicht so sehr ein logischer Fehler im Code die Funktionsfähigkeit stören.

Dann ist es Zeit, die Webanwendung zu skalieren und ihr zusätzliche Ressourcen zuzuteilen, seien dies mehr Server, zusätzlicher Speicher oder eine stärkere Back-End-Datenbank. Die gängigste Form der Skalierung in der Cloud ist horizontal – es werden zusätzliche Serverinstanzen hinzugefügt, sodass eine Webanwendung auf mehreren Webservern (Instanzen) gleichzeitig ausgeführt werden kann. Mit Cloud-Plattformen wie Microsoft Azure ist es sehr einfach, die zugrunde liegende Infrastruktur für Ihre Webanwendung zu skalieren, denn mit einem Fingerschnippen können Sie eine beliebige Anzahl von Webservern in Form virtueller Computer (VMs) bereitstellen. Wenn Ihre Webanwendung jedoch nicht für die Skalierung und die Ausführung auf mehreren Instanzen konzipiert ist, kann sie die zusätzlichen Ressourcen nicht nutzen und bringt nicht die erwarteten Ergebnisse.

Dieser Artikel wirft einen Blick auf wichtige Entwurfskonzepte und Muster zum Skalieren von Webanwendungen. Die Details und Beispiele der Implementierung konzentrieren sich auf Webanwendungen, die auf Microsoft Azure-Websites ausgeführt werden.

Zunächst jedoch ist anzumerken, dass die Skalierung einer Webanwendung stark vom Kontext und vom Aufbau Ihrer Anwendung abhängt. Die in diesem Artikel verwendete Webanwendung ist einfach, betrifft jedoch die Grundlagen der Skalierung einer Webanwendung, insbesondere die Skalierung bei Ausführung der Anwendung auf Azure-Websites.

Es gibt verschiedene Skalierungsebenen, die unterschiedliche Geschäftsanforderungen bedienen. In diesem Artikel betrachte ich vier unterschiedliche Ebenen von Skalierungsmöglichkeiten. Dies reicht von Webanwendungen, die auf mehreren Instanzen ausgeführt werden können, bis zu Webanwendungen, die auf mehrere Instanzen skaliert werden können – auch über mehrere geographische Regionen und mehrere Rechenzentren hinweg.

Schritt 1: Kennenlernen der Anwendung

Zunächst werde ich die Einschränkungen der Beispiel-Webanwendung darstellen. Dabei wird der Ausgangspunkt umrissen, von dem aus die erforderlichen Änderungen zur Erweiterung der Skalierbarkeit der Anwendung vorgenommen werden. Ich habe mich entschieden, eine vorhandene Anwendung zu modifizieren, denn damit haben Sie in der Praxis häufig zu tun, nicht so sehr mit dem Erstellen einer vollkommen neuen Anwendung oder eines vollkommen neuen Entwurfs.

Die Anwendung, die ich für diesen Artikel verwenden werde, ist die WebMatrix-Fotogalerievorlage für ASP.NET-Webseiten (bit.ly/1llAJdQ). Anhand dieser Vorlage lässt sich sehr gut lernen, wie ASP.NET-Webseiten zum Erstellen realer Webanwendungen verwendet werden. Es handelt sich um eine voll funktionsfähige Webanwendung, mit der Benutzer Fotoalben erstellen und Bilder hochladen können. Jeder kann Bilder sehen, und angemeldete Benutzer können Kommentare abgeben. Die Fotogalerie-Webanwendung kann über WebMatrix oder direkt im Azure-Portal über die Azure-Websites-Galerie auf Azure-Websites bereitgestellt werden.

Die genaue Analyse des Codes der Webanwendung offenbart mindestens drei bedeutende Architekturprobleme, die die Skalierbarkeit der Anwendung begrenzen: die Verwendung eines lokalen SQL Server Express als Datenbank, die Verwendung eines In-Process-Sitzungszustands (Arbeitsspeicher des lokalen Webservers) und die Verwendung des lokalen Dateisystems zum Speichern von Fotos.

Ich werde jede dieser Einschränkungen gründlich analysieren.

Die Datei PhotoGallery.sdf im Ordner App_Data ist die SQL Server Express-Standarddatenbank, die mit der Anwendung verteilt wird. SQL Server Express erleichtert den Einstieg in die Entwicklung einer Anwendung und bietet ein großartiges Lernerlebnis, schränkt die Skalierbarkeit der Anwendung jedoch auch stark ein. Eine SQL Server Express-Datenbank ist im Grunde genommen eine Datei im Dateisystem. Die Fotogalerie-Anwendung kann in ihrem aktuellen Status nicht sicher auf mehreren Instanzen skaliert werden. Wenn Sie eine Skalierung auf mehreren Instanzen versuchen, entstehen möglicherweise mehrere Instanzen der SQL Server Express-Datenbankdatei, bei denen es sich jeweils um eine lokale Datei handelt, die wahrscheinlich nicht mit den anderen Dateien synchronisiert ist. Auch wenn alle Webserverinstanzen das gleiche Dateisystem verwenden, kann die SQL Server Express-Datei zu unterschiedlichen Zeiten von einer beliebigen Instanz gesperrt werden, sodass die anderen Instanzen versagen.

Die Fotogalerie-Anwendung ist auch hinsichtlich der Verwaltung des Sitzungszustands eines Benutzers eingeschränkt. Eine Sitzung ist als eine Reihe von Anforderungen definiert, die von dem gleichen Benutzer innerhalb eines bestimmten Zeitraums ausgegeben werden. Um die Sitzung zu verwalten, wird jedem einzelnen Benutzer eine Sitzungs-ID zugeordnet. Die ID wird für jede nachfolgende HTTP-Anforderung verwendet und vom Client entweder in einem Cookie oder als spezielles Fragment der Anforderungs-URL bereitgestellt. Die Sitzungsdaten werden auf der Serverseite in einem der unterstützten Sitzungszustandsspeicher abgelegt, zu denen In-Process-Arbeitsspeicher, eine SQL Server-Datenbank oder ASP.NET State Server gehören.

Die Fotogalerie-Anwendung verwaltet die Benutzeranmeldung und den Benutzerzustand mit der WebSecurity-Klasse von WebMatrix, und WebSecurity verwendet den Sitzungszustand des standardmäßigen ASP.NET-Mitgliedschaftsanbieters. Der reguläre Sitzungszustandsmodus des ASP.NET-Mitgliedschaftsanbieters lautet In-Process (InProc). In diesem Modus werden die Werte und Variablen des Sitzungszustands im Arbeitsspeicher einer lokalen Webserverinstanz (VM) gespeichert. Wird der Sitzungszustand des Benutzers lokal jeweils auf einem Webserver gespeichert, wird die Möglichkeit eingeschränkt, die Anwendung auf mehreren Instanzen auszuführen, da nachfolgende HTTP-Anforderungen eines einzelnen Benutzers möglicherweise an verschiedene Instanzen eines Webservers gerichtet werden. Da jede Webserverinstanz eine eigene Kopie des Zustands in ihrem eigenen lokalen Arbeitsspeicher speichert, werden u. U. für die gleichen Benutzer verschiedene InProc-Sitzungszustandsobjekte auf unterschiedlichen Instanzen erzeugt. Dies kann zu unerwarteten und inkonsistenten Benutzererlebnissen führen. Hier sehen Sie die WebSecurity-Klasse, die zur Verwaltung des Zustands eines Benutzers verwendet wird:

_AppStart.cshtml

@{
  WebSecurity.InitializeDatabaseConnection
    ("PhotoGallery", "UserProfiles", "UserId", "Email", true);
}

Upload.cshtml

@{
  WebSecurity.RequireAuthenticatedUser();
    ...
...
}

Die WebSecurity-Klasse ist eine Hilfskomponente, die die Programmierung auf ASP.NET-Webseiten vereinfacht. Die WebSecurity-Klasse interagiert im Hintergrund mit einem ASP.NET-Mitgliedschaftsanbieter, der wiederum die grundlegenden Schritte ausführt, die zur Ausführung von Sicherheitsaufgaben erforderlich sind. Der Standard-Mitgliedschaftsanbieter auf ASP.NET-Webseiten ist die SimpleMembershipProvider-Klasse, und deren standardmäßiger Sitzungszustandsmodus ist InProc.

Die aktuelle Version der Fotogalerie-Webanwendung speichert Fotos schließlich jeweils als Bytearrays in der Datenbank. Da die Anwendung SQL Server Express verwendet, werden die Fotos im Wesentlichen auf dem lokalen Datenträger gespeichert. Eine der Hauptaufgaben einer Fotogalerie-Anwendung ist das Anzeigen von Fotos. Die Anwendung muss daher möglicherweise sehr viele Fotoanforderungen verarbeiten und anzeigen. Fotos aus einer Datenbank zu lesen, ist kaum ideal. Auch die Verwendung einer leistungsfähigeren Datenbank wie SQL Server oder Azure SQL Database ist nicht ideal, hauptsächlich deshalb, weil das Abrufen von Fotos eine so aufwändige Operation ist.

Kurz, diese Version von Fotogalerie ist eine statusbehaftete Anwendung, und Anwendungen dieses Typs lassen sich nicht gut auf mehreren Instanzen skalieren.

Schritt 2: Ändern von Fotogalerie in eine statusfreie Webanwendung

Nachdem ich einige Einschränkungen der Fotogalerie-Anwendung in Bezug auf die Skalierung erläutert habe, werde ich sie nacheinander bearbeiten, um die Skalierungsfähigkeiten der Anwendung zu verbessern. In Schritt 2 nehme ich die notwendigen Änderungen vor, um aus der statusbehafteten Fotogalerie-Anwendung eine statusfreie Anwendung zu machen. Nach Abschluss von Schritt 2 kann die aktualisierte Fotogalerie-Anwendung sicher auf mehreren Webserverinstanzen (VMs) skaliert und ausgeführt werden.

Zuerst ersetze ich SQL Server Express durch einen leistungsfähigeren Datenbankserver – Azure SQL Database, einen cloudbasierten Dienst von Microsoft, der im Rahmen der Azure-Diensteplattform Datenspeicherungsfunktionen zur Verfügung stellt. Die SKUs Azure SQL Database Standard und Premium bieten erweiterte Funktionen für die Geschäftskontinuität, die ich in Schritt 4 verwenden werde. Zunächst aber migriere ich die Datenbank einfach von SQL Server Express nach Azure SQL Database. Sie können dies ohne weiteres mit dem WebMatrix-Datenbankmigrationstool oder einem von Ihnen bevorzugten Tool erledigen, das die SDF-Datei in das Azure SQL Database-Format konvertiert.

Die Migration der Datenbank ist eine gute Gelegenheit, einige Schemaänderungen vorzunehmen, die großen Einfluss auf die Skalierungsfähigkeiten der Anwendung haben werden, auch wenn sie nur geringfügig sind.

Zuerst werde ich den Typ der ID-Spalte einiger Tabellen (Galleries, Photos, UserProfiles usw.) von INT in GUID konvertieren. Diese Änderung wird sich in Schritt 4 als hilfreich erweisen, wenn ich die Anwendung aktualisiere, damit sie in mehreren Regionen ausgeführt werden kann, und die Datenbank- und Fotoinhalte synchronisiert bleiben müssen. Es ist darauf hinzuweisen, dass diese Änderung keine Codeänderungen in der Anwendung erfordert. Alle SQL-Abfragen in der Anwendung bleiben gleich.

Anschließend stoppe ich das Speichern von Fotos als Bytearrays in der Datenbank. Diese Änderung umfasst sowohl Schema- als auch Codeänderungen. Ich entferne die Spalten FileContents und FileSize aus der Tabelle Photos, speichere die Fotos direkt auf dem Datenträger und verwende die Foto-ID, die jetzt eine GUID ist, zum Unterscheiden der Fotos.

Der folgende Codeausschnitt zeigt die INSERT-Anweisung vor der Änderung (beachten Sie, dass sowohl fileBytes als auch fileBytes.Length direkt in der Datenbank gespeichert werden):

db.Execute(@"INSERT INTO Photos
  (Id, GalleryId, UserName, Description, FileTitle, FileExtension,
  ContentType, FileSize, UploadDate, FileContents, Likes)
  VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10)",
  guid.ToString(), galleryId, Request.GetCurrentUser(Response), "",
  fileTitle, fileExtension, fileUpload.ImageFormat, fileBytes.Length,
  DateTime.Now, fileBytes, 0);

Und so sieht der Code nach den Datenbankänderungen aus:

using (var db = Database.Open("PhotoGallery"))
{
  db.Execute(@"INSERT INTO Photos
  (Id, GalleryId, UserName, Description, FileTitle, FileExtension,
  UploadDate, Likes)
  VALUES (@0, @1, @2, @3, @4, @5, @6, @7)", imageId, galleryId,
  userName, "", imageId, extension,
  DateTime.UtcNow, 0);
}

In Schritt 3 werden ich ausführlicher darstellen, wie ich die Anwendung geändert habe. Vorläufig soll es genügen festzustellen, dass die Fotos an einem zentralen Ort gespeichert werden, z. B. auf einem freigegebenen Datenträger, auf den alle Webserverinstanzen zugreifen können.

Die letzte Änderung, die ich in Schritt 2 vornehme, besteht darin, den Sitzungszustand InProc nicht mehr zu verwenden. Wie bereits erwähnt, ist WebSecurity eine Hilfsklasse, die mit ASP.NET-Mitgliedschaftsanbietern interagiert. Der standardmäßige ASP.NET-SimpleMembership-Sitzungszustandsmodus ist InProc. Mit SimpleMembership können Sie verschiedene prozessexterne Optionen verwenden, einschließlich SQL Server und des Diensts ASP.NET State Server. Diese beiden Optionen bieten die Möglichkeit, den Sitzungsstatus auf mehreren Webserverinstanzen gemeinsam zu verwenden und eine Serveraffinität zu vermeiden; d. h., die Sitzung muss nicht an einen bestimmten Webserver gebunden werden.

Mein Ansatz dient auch dazu, den prozessexternen Zustand zu verwalten, insbesondere mithilfe einer Datenbank und eines Cookies. Ich verlasse mich jedoch eher auf meine eigene Implementierung als auf ASP.NET, im Wesentlichen deshalb, weil ich alles möglichst einfach gestalten möchte. Die Implementierung verwendet einen Cookie und speichert die Sitzungs-ID und den Sitzungszustand in der Datenbank. Wenn sich ein Benutzer anmeldet, weise ich eine neue GUID als Sitzungs-ID zu, die ich in der Datenbank speichere. Diese GUID wird außerdem als Cookie an den Benutzer zurückgesendet. Der folgende Code zeigt die CreateNewUser-Methode, die aufgerufen wird, sobald sich ein Benutzer anmeldet:

private static string CreateNewUser()
{
  var newUser = Guid.NewGuid();
  var db = Database.Open("PhotoGallery");
db.Execute(@"INSERT INTO GuidUsers (UserName, TotalLikes) VALUES (@0, @1)",
  newUser.ToString(), 0);
  return newUser.ToString();
}

Beim Beantworten einer HTTP-Anforderung wird die GUID in die HTTP-Antwort als Cookie eingebettet. Der Benutzername, welcher der AddUser-Methode übergeben wird, ist das Produkt der soeben gezeigten CreateNewUser-Funktion, etwa so:

public static class ResponseExtensions
{
  public static void AddUser(this HttpResponseBase response, 
    string userName)
  {
    var userCookie = new HttpCookie("GuidUser")
    {
      Value = userName,
      Expires = DateTime.UtcNow.AddYears(1)
    };
    response.Cookies.Add(userCookie);
  }
}

Bei der Behandlung einer eingehenden HTTP-Anforderung versuche ich zuerst, die als GUID dargestellte Benutzer-ID aus dem Cookie GuidUser zu extrahieren. Anschließend suche ich in der Datenbank nach dieser Benutzer-ID (GUID) und extrahiere benutzerspezifische Informationen. Abbildung 1 zeigt einen Ausschnitt der Get­CurrentUser-Implementierung.

Abbildung 1: GetCurrentUser

public static string GetCurrentUser(this HttpRequestBase request,
   HttpResponseBase response = null)
  {
    string userName;
    try
    {
      if (request.Cookies["GuidUser"] != null)
        {
          userName = request.Cookies["GuidUser"].Value;
          var db = Database.Open("PhotoGallery");
          var guidUser = db.QuerySingle(
            "SELECT * FROM GuidUsers WHERE UserName = @0", userName);
          if (guidUser == null || guidUser.TotalLikes > 5)
            {
              userName = CreateNewUser();
            }
        }
      ...
      ...
}

Sowohl CreateNewUser als auch GetCurrentUser gehören zur RequestExtensions-Klasse. Entsprechend gehört AddUser zur ResponseExtensions-Klasse. Beide Klassen greifen auf die ASP.NET-Anforderungsverarbeitungspipeline zu, die Anforderungen bzw. Antworten verarbeitet.

Meine Herangehensweise zur Verwaltung des Sitzungszustands ist eher einfach, denn er ist nicht sicher und erzwingt keine Authentifizierung. Sie verdeutlicht jedoch den Vorteil, Sitzungen prozessextern zu verwalten, und sie ist skalierbar. Wenn Sie Ihre eigene Sitzungszustandsverwaltung implementieren, verwenden Sie unabhängig davon, ob sie auf ASP.NET beruht, auf jeden Fall eine sichere Lösung mit Authentifizierung und einem sicheren Verfahren zum Verschlüsseln des zurückgesendeten Cookies.

An diesem Punkt kann ich mit Sicherheit behaupten, dass die aktualisierte Fotogalerie-Anwendung jetzt eine statusfreie Webanwendung ist. Durch Ersetzen der lokalen SQL Server Express-Datenbankimplementierung durch Azure SQL Database und Ändern der Implementierung des Sitzungszustands von InProc in eine prozessexterne Operation, durch Verwenden eines Cookies und einer Datenbank habe ich aus der statusbehafteten erfolgreich eine statusfreie Anwendung gemacht, wie in Abbildung 2 dargestellt ist.

Logical Representation of the Modified Photo Gallery Application
Abbildung 2: Logische Darstellung der geänderten Fotogalerie-Anwendung

Die notwendigen Schritte zu ergreifen, um die Webanwendung statusfrei zu machen, ist wahrscheinlich die wichtigste Aufgabe bei der Entwicklung einer Webanwendung. Die Möglichkeit, eine Anwendung sicher auf mehreren Webserverinstanzen auszuführen, ohne sich um Benutzerzustand, Datenbeschädigung oder ordnungsgemäßes Funktionieren Gedanken machen zu müssen, ist einer der wichtigsten Faktoren bei der Skalierung einer Webanwendung.

Schritt 3: Zusätzliche Verbesserungen mit erheblichen Auswirkungen

Die in Schritt 2 vorgenommenen Änderungen an der Fotogalerie-Webanwendung sorgen dafür, dass die Anwendung statusfrei ist und sicher auf mehreren Webserverinstanzen skaliert werden kann. Jetzt werde ich einige weitere Verbesserungen vornehmen, welche die Skalierungseigenschaften der Anwendung zusätzlich erweitern können, denn damit kann die Webanwendung größere Lasten mit weniger Ressourcen verarbeiten. In diesem Schritt untersuche ich die Speicherstrategien und befasse mich mit asynchronen Entwurfsmustern, welche die UX-Leistung verbessern.

Eine der in Schritt 2 diskutierten Änderungen war das Speichern von Fotos an einem zentralen Ort, beispielsweise auf einem freigegebenen Datenträger, auf den alle Webserverinstanzen zugreifen können, anstatt in einer Datenbank. Die Azure-Websites-Architektur sorgt dafür, dass alle Instanzen einer Webanwendung, die auf mehreren Webservern ausgeführt werden, den gleichen Datenträger verwenden, wie Abbildung 3 zeigt.

With Microsoft Azure Web Sites, All Instances of a Web Application See the Same Shared Disk
Abbildung 3: Mit Microsoft Azure-Websites verwenden alle Instanzen einer Webanwendung denselben freigegebenen Datenträger

Aus der Perspektive der Fotogalerie-Webanwendung bedeutet „freigegebener Datenträger“ Folgendes: Wenn ein Foto von einem Benutzer hochgeladen wird, wird es im Ordner …/uploaded gespeichert, der wie ein lokaler Ordner aussieht. Wenn das Bild auf den Datenträger geschrieben wird, wird es jedoch nicht „lokal“ auf dem jeweiligen Webserver gespeichert, der die HTTP-Anforderung verarbeitet, sondern an einem zentralen Speicherort, auf den alle Webserver zugreifen können. Jeder Server kann also ein Foto auf den freigegebenen Datenträger schreiben, und alle anderen Webserver können dieses Bild lesen. Die Fotometadaten werden in der Datenbank gespeichert und von der Anwendung verwendet, um die Foto-ID – eine GUID – zu lesen und die Bild-URL im Rahmen der HTML-Antwort zurückzusenden. Der folgende Codeausschnitt ist Teil von view.cshtml, die Seite, die ich verwende, um das Anzeigen von Bildern zu ermöglichen:

    <img class="large-photo" src="@ImagePathHelper.GetFullImageUrl(photoId,
      photo.FileExtension.ToString())" alt="@Html.AttributeEncode(photo.FileTitle)" />

Die Quelle des HTML-Bildelements wird mit dem Rückgabewert der GetFullImageUrl-Hilfsfunktion gefüllt, die eine Foto-ID und eine Dateierweiterung (.jpg, .png usw.) nimmt und eine Zeichenfolge zurückgibt, die die URL des Bilds darstellt.

Durch das Speichern der Fotos an einem zentralen Ort wird sichergestellt, dass die Webanwendung statusfrei ist. Mit der aktuellen Implementierung wird ein bestimmtes Bild jedoch direkt von einem der Webserver zur Verfügung gestellt, auf denen die Webanwendung ausgeführt wird. Insbesondere verweist die URL jeder Bildquelle auf die URL der Webanwendung. Folglich wird das Bild selbst von einem der Webserver zur Verfügung gestellt, auf denen die Webanwendung ausgeführt wird. Das heißt, dass die tatsächlichen Bytes des Bildes als HTTP-Antwort des Webservers gesendet werden. Der Webserver stellt also zusätzlich zur Verarbeitung dynamischer Webseiten auch statische Inhalte wie Bilder zur Verfügung. Webserver können statische Inhalte in großem Umfang zur Verfügung stellen, dadurch werden jedoch Ressourcen wie CPU, EA und Arbeitsspeicher belastet. Wenn Sie dafür sorgen könnten, dass statische Inhalte wie Fotos nicht direkt vom Webserver zur Verfügung gestellt werden, auf dem die Webanwendung ausgeführt wird, sondern von einem anderen Ort, könnten Sie die Anzahl der an die Webserver gesendeten HTTP-Anforderungen reduzieren. Dadurch würden auf dem Webserver Ressourcen zur Verarbeitung weiterer dynamischer HTTP-Anforderungen freigegeben.

Die erste dazu erforderliche Änderung ist die Verwendung eines Azure-Blob-Speichers (bit.ly/TOK3yb) zum Speichern und Bereitstellen von Benutzerfotos. Wenn ein Benutzer ein Bild anzeigen möchte, verweist die von der aktualisierten GetFullImageUrl zurückgegebene URL auf ein Azure-Blob. Das Endergebnis entspricht dem folgenden HTML-Code, in dem die Bild-URL auf den Blob-Speicher verweist:

    <img class="large-photo" alt="764beb6b-1988-42d7-9900-03ee8a60749b"
      src="http://photogalcontentwestus.blob.core.windows.net/
      full/764beb6b-1988-42d7-9900-03ee8a60749b.jpg">

Das Bild wird also direkt aus dem Blob-Speicher und nicht von den Webservern bereitgestellt, auf denen die Webanwendung ausgeführt wird.

Mit der folgenden Anweisung werden hingegen Fotos gezeigt, die auf einem freigegebenen Azure-Websites-Datenträger gespeichert sind:

<img class="large-photo" alt="764beb6b-1988-42d7-9900-03ee8a60749b"
  src="http:// builddemophotogal2014.websites.net/
  full/764beb6b-1988-42d7-9900-03ee8a60749b.jpg">

Die Fotogalerie-Webanwendung verwendet zwei Container, full und thumbnail. Wie zu erwarten, werden die Fotos in full in der Originalgröße gespeichert, während in thumbnail kleinere Bilder gespeichert werden, die in der Galerieansicht angezeigt werden.

public static string GetFullImageUrl(string imageId, 
  string imageExtension)
{
  return String.Format("{0}/full/{1}{2}",
    Environment.ExpandEnvironmentVariables("%AZURE_STORAGE_BASE_URL%"),
    imageId, imageExtension);
}

AZURE_STORAGE_BASE_URL ist eine Umgebungsvariable, die die Basis-URL für das Azure-Blob enthält, in diesem Fall http://­photogalcontentwestus.blob.core.windows.net. Diese Umgebungsvariable kann im Azure-Portal auf der Registerkarte „Site Config“ festgelegt werden, sie kann aber auch Teil der Anwendung web.config sein. Das Festlegen von Umgebungsvariablen im Azure-Portal bietet jedoch mehr Flexibilität, da sie leichter geändert werden kann, ohne dass eine erneute Bereitstellung erforderlich wird.

Azure Storage wird fast genauso wie ein Netzwerk für die Inhaltsübermittlung (CDN) verwendet, hauptsächlich deswegen, weil HTTP-Anforderungen für Bilder nicht von den Webservern der Anwendung, sondern direkt von einem Azure Storage-Container zur Verfügung gestellt werden. Dadurch wird der Verkehr mit statischen HTTP-Anforderungen verringert, der Ihre Webserver jemals erreicht, sodass die Webserver mehr dynamische Anforderungen verarbeiten können. Beachten Sie auch, dass Azure Storage weitaus mehr Verkehr verarbeiten kann als ein durchschnittlicher Webserver – ein einzelner Container kann mehrere zehntausend Anforderungen pro Sekunde bedienen.

Zur Verarbeitung statischer Inhalte können Sie zusätzlich zum Blob-Speicher auch das Microsoft Azure CDN hinzufügen. Wird der Webanwendung ein CDN hinzugefügt, wird die Leistung noch mehr erhöht, da das CDN den gesamten statischen Inhalt verarbeitet. Anforderungen für ein Foto, das bereits im CDN zwischengespeichert ist, erreichen den Blob-Speicher gar nicht erst. Darüber hinaus verbessert ein CDN auch die wahrgenommene Leistung, da das CDN in der Regel über einen peripheren Server verfügt, der dem Endkunden näher ist. Die einzelnen Schritte zum Hinzufügen eines CDN zur Beispielanwendung würden den Rahmen dieses Artikels sprengen, da die Änderungen hauptsächlich die DNS-Registrierung und -Konfiguration betreffen. Wenn Sie jedoch eine Produktion in großem Maßstab vorsehen und sicherstellen möchten, dass Ihre Kunden in den Genuss einer schnell reagierenden Benutzeroberfläche kommen, sollten Sie die Verwendung des CDN in Betracht ziehen.

Ich habe den Code, der die hochgeladenen Bilder eines Benutzers verarbeitet, nicht geprüft, doch dies ist eine gute Gelegenheit, ein grundlegendes asynchrones Muster anzusprechen, das sowohl die Leistung der Webanwendung als auch das Benutzererlebnis verbessert. Wie Sie in Schritt 4 sehen werden, wird dadurch auch die Datensynchronisierung zwischen zwei unterschiedlichen Regionen unterstützt.

Als nächstes werde ich der Fotogalerie-Webanwendung eine Azure Storage-Warteschlange hinzufügen, um das Front-End der Anwendung (die Website) von der Back-End-Geschäftslogik (WebJob und Datenbank) zu trennen. Ohne Warteschlange musste der Fotogalerie-Code sowohl Front-End als auch Back-End verarbeiten, während der Hochladecode das Bild in Originalgröße im Speicher ablegte, eine Miniaturansicht erzeugte und im Speicher ablegte und die SQL Server-Datenbank aktualisierte. Während dieser Zeit musste der Benutzer auf eine Antwort warten. Mit der Einführung einer Azure Storage-Warteschlange jedoch schreibt das Front-End einfach eine Nachricht in die Warteschlange und sendet sofort eine Antwort an den Benutzer zurück. Ein Hintergrundprozess, WebJob (bit.ly/1mw0A3w), nimmt die Nachricht aus der Warteschlange auf und führt die erforderliche Back-End-Geschäftslogik aus. In Bezug auf Fotogalerie umfasst dieser Prozess das Bearbeiten der Bilder, das Speichern der Bilder am richtigen Ort und das Aktualisieren der Datenbank. Abbildung 4 veranschaulicht die in Schritt 3 vorgenommenen Änderungen einschließlich der Verwendung von Azure Storage und des Hinzufügens einer Warteschlange.

Logical Representation of Photo Gallery Post Step ThreeAbbildung 4: Logische Darstellung von Schritt 3 des Fotogalerie-Beitrags

Da ich nun über eine Warteschlange verfüge, muss ich den Code in upload.cshtml ändern. Im folgenden Code sehen Sie, dass ich mithilfe von StorageHelper eine Nachricht in die Warteschlange einreihe, anstatt eine komplizierte Geschäftslogik mit Bildbearbeitung auszuführen (die Nachricht umfasst die Foto-ID, die Erweiterung der Fotodatei und die Galerie-ID):

var file = Request.Files[i];
var fileExtension = Path.GetExtension(file.FileName).Trim();
guid = Guid.NewGuid();
using
var fileStream = new FileStream(
 Path.Combine( HostingEnvironment.MapPath("~/App_Data/Upload/"),
 guid + fileExtension), FileMode.Create))
{
  file.InputStream.CopyTo(fileStream);
  StorageHelper.EnqueueUploadAsync(
    Request.GetCurrentUser(Response),
     galleryId, guid.ToString(), fileExtension);
}

StorageHelper.EnqueueUploadAsync erzeugt einfach eine CloudQueueMessage und lädt sie asynchron zur Azure Storage-Warteschlange hoch:

public static Task EnqueueUploadAsync
  (string userName, string galleryId, string imageId, 
    string imageExtension)
{
  return UploadQueue.AddMessageAsync(
    new CloudQueueMessage(String.Format("{0}, {1}, {2}, {3}",
    userName, galleryId, imageId,
    imageExtension)));
}

WebJob ist jetzt für die Back-End-Geschäftslogik verantwortlich. Das neue WebJobs-Feature von Azure-Websites vereinfacht das Ausführen von Programmen wie Diensten oder Hintergrundaufgaben auf einer Website. Der WebJob achtet auf Änderungen der Warteschlange und nimmt jede neue Nachricht auf. Die in Abbildung 5 dargestellte ProcessUploadQueueMessages-Methode wird aufgerufen, sobald die Warteschlange mindestens eine Nachricht enthält. Das QueueInput-Attribut gehört zum Microsoft Azure WebJobs SDK (bit.ly/1cN9eCx), ein Framework, das das Hinzufügen von Hintergrundverarbeitung zu Azure-Websites vereinfacht. Das WebJobs SDK ist zwar nicht Gegenstand dieses Artikels, aber in diesem Zusammenhang müssen Sie auch lediglich wissen, dass das WebJobs SDK ohne weiteres die Bindung an eine Warteschlange ermöglicht, in meinem Fall uploadqueue, und auf eingehende Nachrichten wartet.

Abbildung 5: Eine Nachricht aus einer Warteschlange lesen und die Datenbank aktualisieren

public static void ProcessUploadQueueMessages
  ([QueueInput(“uploadqueue”)] string queueMessage, IBinder binder)
{
  var splited = queueMessage
    .Split(‘,’).Select(m => m.Trim()).ToArray();
  var userName = splited[0];
  var galleryId = splited[1];
  var imageId = splited[2];
  var extension = splited[3];
  var filePath = Path.Combine(ImageFolderPath, 
    imageId + extension);
  UploadFullImage(filePath, imageId + extension, binder);
  UploadThumbnail(filePath, imageId + extension, binder);
  SafeGuard(() => File.Delete(filePath));
  using (var db = Database.Open(“PhotoGallery”))
  {
    db.Execute(@”INSERT INTO Photos Id, GalleryId, UserName,
      Description, FileTitle, FileExtension, UploadDate, Likes)
      VALUES @0, @1, @2, @3, @4, @5, @6, @7)”, imageId,
      galleryId, userName, “”, imageId, extension, DateTime.UtcNow, 0);
  }
}

Jede Nachricht wird decodiert, wobei die Eingabezeichenfolge in ihre einzelnen Teile zerlegt wird. Anschließend ruft die Methode zwei Hilfsfunktionen auf, um die Bilder zu bearbeiten und zu den Blob-Containern hochzuladen. Zum Schluss wird die Datenbank aktualisiert.

Jetzt kann die aktualisierte Fotogalerie-Webanwendung viele Millionen HTTP-Anforderungen pro Tag verarbeiten.

Schritt 4: Globale Reichweite

Ich habe die Skalierungsfähigkeit der Fotogalerie-Webanwendung bereits enorm verbessert. Wie erwähnt, kann die Anwendung jetzt viele Millionen HTTP-Anforderungen verarbeiten, wobei nur wenige große Server auf Azure-Websites verwendet werden. Derzeit befinden sich all diese Server in einem einzigen Azure-Rechenzentrum. Zwar wird die Skalierung durch die Verwendung nur eines Rechenzentrums eigentlich nicht eingeschränkt – zumindest nicht nach der Standarddefinition von Skalierung –, doch wenn Ihre Kunden von überall auf der Welt eine niedrige Latenz verlangen, müssen Sie die Webanwendung in mehr als einem Rechenzentrum ausführen. Dadurch werden auch die Stabilität sowie die Geschäftskontinuität der Webanwendung verbessert. Falls ein Rechenzentrum ausfällt, was sehr selten geschieht, kann ihre Webanwendung den Verkehr über den zweiten Standort weiter bedienen.

In diesem Schritt ändere ich die Anwendung so, dass sie in mehreren Rechenzentren ausgeführt werden kann. Für diesen Artikel konzentriere ich mich auf die Ausführung an zwei Standorten im Aktiv/Aktiv-Modus, wobei die Anwendung in beiden Rechenzentren dem Benutzer die Möglichkeit gibt, Fotos anzuzeigen sowie Fotos und Kommentare hochzuladen.

Da ich den Kontext der Fotogalerie-Webanwendung kenne, weiß ich, dass die meisten Benutzeroperationen Leseoperationen, das Anzeigen von Fotos, sind. Nur eine kleine Anzahl von Benutzeranforderungen umfasst das Hochladen neuer Fotos oder das Aktualisieren von Kommentaren. In Bezug auf die Fotogalerie-Webanwendung kann ich mit Bestimmtheit behaupten, dass Lesevorgänge am Lese-/Schreib-Verhältnis einen Anteil von mindestens 95 Prozent haben. Dies gestattet mir, einige Annahmen zu machen, z. B. dass eine letztendliche Konsistenz (eventual consistency) im gesamten System sowie eine langsamere Reaktion bei Schreiboperationen akzeptabel ist.

Es ist wichtig zu verstehen, dass diese Annahmen den Kontext berücksichtigen, von den spezifischen Eigenschaften einer Anwendung abhängen und sich wahrscheinlich von Anwendung zu Anwendung ändern.

Der Arbeitsaufwand, der nötig ist, um Fotogalerie an zwei verschiedenen Standorten auszuführen, ist überraschenderweise klein, da der größte der Teil der schwierigsten Aufgaben in den Schritten 2 und 3 erledigt wurde. Abbildung 6 zeigt ein zusammenfassendes Blockdiagramm der Anwendungstopologie, die in zwei Rechenzentren ausgeführt wird. Die Anwendung in West U.S. ist die „Hauptanwendung“ und erzeugt im Wesentlichen die Ausgabe von Schritt 3. Die Anwendung in East U.S. wird am „sekundären“ Standort ausgeführt, und Azure Traffic Manager ist beiden Standorten übergeordnet. Azure Traffic Manager hat verschiedene Konfigurationsoptionen. Ich verwende die Option „Performance“. Diese bewirkt, dass Traffic Manager an beiden Standorten die Latenz in den jeweiligen Regionen überwacht und den Verkehr auf Basis der niedrigsten Latenz weiterleitet. In diesem Fall werden Kunden aus New York (Ostküste) zum Standort East U.S. weitergeleitet, während Kunden aus San Francisco (Westküste) zum Standort West U.S. weitergeleitet werden. Beide Standorte sind gleichzeitig aktiv und verarbeiten den Verkehr. Falls die Anwendung in einer Region aus irgendeinem Grund Leistungseinbußen zu verzeichnen hat, leitet Traffic Manager den Verkehr zur anderen Anwendung weiter. Da die Daten synchronisiert sind, dürften keine Daten verloren gehen.

Logical Representation of Photo Gallery Post Step Four
Abbildung 6: Logische Darstellung von Schritt 4 des Fotogalerie-Beitrags

Ich betrachte die Änderungen der West U.S.-Anwendung. Die einzige Codeänderung betrifft den WebJob, der auf Nachrichten in der Warteschlange wartet. Anstatt Fotos in einem Blob zu speichern, speichert der WebJob Fotos im lokalen und im „entfernten“ Blob-Speicher. In Abbildung 5 ist UploadFullImage eine Hilfsmethode, mit der Fotos im Blob-Speicher abgelegt werden. Um ein Foto in ein entferntes sowie in ein lokales Blob kopieren zu können, habe ich am Ende von UploadFullImage die ReplicateBlob-Hilfsfunktion hinzugefügt, wie Sie hier sehen können:

private static void UploadFullImage(
  string imagePath, string blobName, IBinder binder)
{
  using (var fileStream = new FileStream(imagePath, FileMode.Open))
  {
    using (var outputStream =
      binder.Bind<Stream>(new BlobOutputAttribute(
      String.Format("full/{0}", blobName))))
    {
      fileStream.CopyTo(outputStream);
    }
  }
  RemoteStorageManager.ReplicateBlob("full", blobName);
}

Die ReplicateBlob-Methode im folgenden Code enthält eine wichtige Zeile – die letzte Zeile ruft die StartCopyFromBlob-Methode auf, die den Dienst auffordert, sämtliche Inhalte, Eigenschaften und Metadaten eines Blobs in ein neues Blob zu kopieren (die restlichen Schritte lasse ich vom Azure SDK und vom Speicherdienst ausführen):

public static void ReplicateBlob(string container, string blob)
{
  if (sourceBlobClient == null || targetBlobClient == null)
    return;
  var sourceContainer = 
    sourceBlobClient.GetContainerReference(container);
  var targetContainer = 
    targetBlobClient.GetContainerReference(container);
  if (targetContainer.CreateIfNotExists())
  {
    targetContainer.SetPermissions(sourceContainer.GetPermissions());
  }
  var targetBlob = targetContainer.GetBlockBlobReference(blob);
  targetBlob.StartCopyFromBlob(sourceContainer.GetBlockBlobReference(blob));
}

Am Standort East U.S. verarbeitet die ProcessLikeQueueMessages-Methode nichts, sondern leitet die Nachricht einfach an die West U.S.-Warteschlange weiter. Die Nachricht wird am Standort West U.S. verarbeitet, Bilder werden wie weiter oben erläutert repliziert, und die Datenbank wird synchronisiert, wie ich jetzt erklären werde.

Dies ist der letzte noch fehlende Trick – die Synchronisierung der Datenbank. Dazu verwende ich das Vorschaufeature Active Geo-Replication (fortlaufende Kopie) von Azure SQL Database. Mit diesem Feature können Sie sekundäre schreibgeschützte Replikate der Masterdatenbank erstellen. In die Masterdatenbank geschriebene Daten werden automatisch in die sekundäre Datenbank kopiert. Die Masterdatenbank ist als Lese-Schreib-Datenbank konfiguriert, während alle sekundären Datenbanken schreibgeschützt sind. Aus diesem Grund werden in meinem Szenario Nachrichten aus der East U.S.-Warteschlange zum Standort West U.S. verschoben. Sobald Sie Active Geo-Replication (über das Portal) konfigurieren, sind die Datenbanken synchronisiert. Über die bereits genannten Codeänderungen hinaus sind keine weiteren Änderungen erforderlich.

Zusammenfassung

Mit Microsoft Azure können Sie mit sehr geringem Aufwand hoch skalierbare Web-Apps erstellen. In diesem Artikel habe ich gezeigt, wie Sie in wenigen Schritten eine Webanwendung, die gar nicht skaliert werden kann, da sie nicht auf mehreren Instanzen ausgeführt werden kann, in eine Webanwendung umwandeln können, die nicht nur auf mehreren Instanzen, sondern auch in mehreren Regionen ausgeführt werden und dabei Millionen (viele zig Millionen) HTTP-Anforderungen verarbeiten kann. Die Beispiele beziehen sich auf die spezielle Anwendung, das Konzept gilt jedoch für jede Webanwendung und kann in jeder Webanwendung implementiert werden.

Yochay Kiriaty ist Principal Program Manager Lead im Microsoft Azure-Team und arbeitet an Azure-Websites. Sie erreichen ihn unter yochay@microsoft.com und können ihm auf Twitter unter twitter.com/yochayk folgen.

Unser Dank gilt dem folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Mohamed Ameen Ibrahim