(0) exportieren Drucken
Alle erweitern

Methoden zum Maximieren der Skalierbarkeit und Wirtschaftlichkeit von warteschlangenbasierten Messaging-Lösungen unter Windows Azure

Letzte Aktualisierung: Juli 2014

Erstellt von: Valery Mizonov

Überprüft von: Brad Calder, Sidney Higa, Christian Martinez, Steve Marx, Curt Peterson, Paolo Salvatori und Trace Young

Dieser Artikel ist ein normativer Leitfaden mit Best Practices zum Erstellen warteschlangenbasierter Messaginglösungen auf der Windows Azure-Plattform, die skalierbar, hoch effizient und kosteneffektiv sind. Dieser Artikel richtet sich an Lösungsarchitekten und -entwickler, die Cloud-basierte Lösungen entwerfen und implementieren, die die Warteschlangen-Speicherdienste der Windows Azure-Plattform nutzen.

Eine herkömmliche warteschlangenbasierte Messaginglösung verwendet das Konzept des Nachrichtenspeicherorts, das als Nachrichtenwarteschlange bezeichnet wird. Dabei handelt es sich um ein Repository für Daten, die an Teilnehmer gesendet oder von diesen empfangen werden – in der Regel mithilfe eines asynchronen Kommunikationsmechanismus.

Der warteschlangenbasierte Datenaustausch stellt die Basis für eine zuverlässige und hochgradig skalierbare Messagingarchitektur dar, die eine Reihe von leistungsstarken Szenarien im Distributed Computing Environment, einer Umgebung für verteilte Datenverarbeitung, unterstützen kann. Bei Aufgaben wie der Verteilung von einem hohem Arbeitsvolumen oder dem permanenten Messaging stellt die Message Queuing-Technologie erstklassige Funktionen bereit, die auf die unterschiedlichen Anforderungen der asynchronen Kommunikation bedarfsgerecht eingehen.

In diesem Artikel wird untersucht, wie Entwickler bestimmte Entwurfsmuster in Verbindung mit Funktionen nutzen können, die von der Windows Azure-Plattform bereitgestellt werden, um optimierte und kostengünstige warteschlangenbasierte Messaginglösungen zu erstellen. Der Artikel bietet einen tieferen Einblick in häufig verwendete Ansätze zum Implementieren von warteschlangenbasierten Interaktionen in Windows Azure-Lösungen und enthält Empfehlungen zum Verbessern der Leistung, Erhöhen der Skalierbarkeit und Reduzieren der Betriebsausgaben.

Die zugrunde liegende Erläuterung wird durch relevante Best Practices, Hinweise und Empfehlungen ergänzt. Das in diesem Artikel beschriebene Szenario behandelt eine technische Implementierung, die auf einem Kundenprojekt aus der Praxis basiert.

Das folgende konkrete Beispiel basiert auf einem verallgemeinerten Kundenszenario aus der Praxis.

Ein Anbieter von SaaS-Lösungen führt ein neues Abrechnungssystem ein, das als Windows Azure-Anwendung implementiert ist und die Geschäftsanforderungen bei der Verarbeitung von Kundentransaktionen bedarfsgerecht erfüllt. Die Schlüsselvoraussetzung der Lösung dreht sich um die Fähigkeit, rechenintensive Arbeitslasten in die Cloud auszulagern und die Flexibilität der Windows Azure-Infrastruktur zu nutzen, um die rechenintensiven Vorgänge auszuführen.

Das lokale Element der End-to-End-Architektur konsolidiert und verteilt große Mengen von Transaktionen in einem von Windows Azure gehosteten Dienst in regelmäßigen Abständen während des ganzen Tags. Pro Übermittlung werden zwischen einigen Tausenden und Hunderttausenden von Transaktionen und pro Tag Millionen von Transaktionen gesendet. Darüber hinaus wird angenommen, dass die Lösung eine SLA-basierte Anforderung nach garantierter maximaler Verarbeitungslatenzzeit erfüllen muss.

Die Lösungsarchitektur basiert auf dem verteilten MapReduce-Entwurfsmuster. Sie umfasst eine workerrollenbasierte Cloud-Ebene mit mehreren Instanzen, wobei der Windows Azure-Warteschlangenspeicher für die Arbeitsverteilung verwendet wird. Transaktionsbatches werden von der Prozessinitiator-Workerrolleninstanz empfangen, in kleinere Arbeitsaufgaben zerlegt und zum Zweck der Lastenverteilung in eine Sammlung von Windows Azure-Warteschlangen eingereiht.

Die Arbeitslastverarbeitung wird von mehreren Instanzen der Verarbeitungsworkerrolle gehandhabt, die Arbeitsaufgaben von Warteschlangen abrufen und durch die Berechnungsprozeduren leiten. Zur Leistungsoptimierung implementieren die Verarbeitungsinstanzen mithilfe von Multithread-Warteschlangenüberwachung eine parallele Datenverarbeitung.

Die verarbeiteten Arbeitsaufgaben werden in eine dedizierte Warteschlange weitergeleitet, aus der sie von der Prozesscontroller-Workerrolleninstanz wieder entfernt werden. Anschließend werden sie in einem Datenspeicher für Data Mining, Berichterstellung und Analyse aggregiert und permanent gespeichert.

Die Lösungsarchitektur kann wie folgt dargestellt werden:

Das Diagramm oben zeigt eine typische Architektur für das horizontale Skalieren von großen oder komplexen Berechnungsarbeitslasten. Das von dieser Architektur übernommene warteschlangenbasierte Nachrichtenaustauschmodell ist auch für viele andere Windows Azure-Anwendungen und -Dienste typisch, die für die Kommunikation Warteschlangen verwenden müssen. Dadurch wird ein kanonischer Ansatz für die Untersuchung von bestimmten wesentlichen Komponenten, die bei einem warteschlangenbasierten Nachrichtenaustausch beteiligt sind, ermöglicht.

Eine typische Messaginglösung für den Datenaustausch zwischen den verteilten Komponenten mithilfe von Nachrichtenwarteschlangen umfasst Verleger, die Nachrichten in Warteschlangen einreihen, und einen oder mehrere Abonnenten, für den bzw. die die Nachrichten gedacht sind. In den meisten Fällen werden die Abonnenten, die manchmal auch als Warteschlangenlistener bezeichnet werden, als Single- oder Multithreadprozesse implementiert, die gemäß einem Zeitplan nach Bedarf fortlaufend ausgeführt oder initiiert werden.

Auf höherer Ebene gibt es zwei primäre Verteilungsmechanismen, mit denen ein Warteschlangenlistener die in einer Warteschlange gespeicherten Nachrichten empfangen kann:

  • Abrufen (PULL-basiertes Modell): Ein Listener überwacht eine Warteschlange, indem er die Warteschlange regelmäßig auf neue Nachrichten hin überprüft. Wenn die Warteschlange leer ist, setzt der Listener das Abrufen der Warteschlange fort, wobei er in regelmäßigen Abständen in den Energiesparmodus übergeht, und der Abrufvorgang dadurch unterbrochen wird.

  • Auslösen (PUSH-basiertes Modell): Ein Listener abonniert ein Ereignis, das ausgelöst wird (entweder vom Verleger selbst oder von einem Warteschlangendienst-Manager), wenn eine Nachricht in einer Warteschlange eingeht. Der Listener kann dann die Nachrichtenverarbeitung initiieren, sodass er nicht die Warteschlange abrufen muss, um festzustellen, ob neue Arbeitsaufgaben verfügbar sind.

Es sollte auch erwähnt werden, dass es von beiden Mechanismen unterschiedliche Arten gibt. Beispielsweise kann das Abrufen blockierend und nicht blockierend sein. Eine blockierende Anforderung wird in eine Wartestellung versetzt, bis eine neue Nachricht in der Warteschlange eingeht (oder ein Timeout auftritt). Dagegen wird eine nicht blockierende Anforderung vollständig durchgeführt, auch wenn die Warteschlange leer ist. Wenn das Auslösen verwendet wird, kann eine Benachrichtigung an die Warteschlangenlistener weitergegeben werden, wenn eine neue Nachricht eingeht, nur wenn die erste Nachricht in einer leeren Warteschlange eingeht oder wenn die Warteschlangentiefe eine bestimmte Ebene erreicht.

noteHinweis
Die Vorgänge, die Elemente aus der Warteschlange entfernen und die von der Warteschlangendienst-API von Windows Azure unterstützt werden, sind nicht blockierend. Das bedeutet, dass API-Methoden wie z. B. GetMessage oder GetMessages sofort zurückgegeben werden, wenn in einer Warteschlange keine Nachrichten gefunden werden. Im Gegensatz dazu bieten Windows Azure Service Bus-Warteschlangen blockierende Empfangsvorgänge, die den aufrufenden Thread blockieren, bis eine Nachricht in einer Warteschlange eingeht oder ein angegebenes Timeout abgelaufen ist.

Die nachfolgende Zusammenfassung beschreibt, wie heute am häufigsten an das Implementieren von Warteschlangenlistenern in Windows Azure-Lösungen herangegangen wird:

  1. Ein Listener wird als Anwendungskomponente implementiert, die als Teil einer Workerrolleninstanz instanziiert und ausgeführt wird.

  2. Der Lebenszyklus der Warteschlangenlistener-Komponente ist häufig an die Laufzeit der Hostingrolleninstanz gebunden.

  3. Die wesentliche Verarbeitungslogik besteht aus einer Schleife, in der Nachrichten aus der Warteschlange entfernt und für die Verarbeitung verteilt werden.

  4. Wenn keine Nachrichten empfangen werden, geht der Listening-Thread in einen Energiesparmodus über, dessen Dauer häufig durch einen anwendungsspezifischen Backoffalgorithmus gesteuert wird.

  5. Die Empfangsschleife wird ausgeführt und eine Warteschlange wird abgerufen, bis der Listener benachrichtigt wird, die Schleife und den Vorgang zu beenden.

Das folgende Flussdiagramm stellt die Logik dar, die häufig verwendet wird, wenn ein Warteschlangenlistener mit einem Abrufmechanismus in Windows Azure-Anwendungen implementiert wird:

Best Practices für Messaginglösungen Azure2
noteHinweis
Im Rahmen dieses Artikels werden keine komplexeren Entwurfsmuster – beispielsweise Muster, die einen zentralen Warteschlangen-Manager (Broker) erfordern – verwendet.

Die Verwendung eines klassischen Warteschlangenlisteners mit einem Abrufmechanismus ist möglicherweise nicht die optimale Wahl bei Windows Azure-Warteschlangen, da das Windows Azure-Preismodell Speichertransaktionen in Form von Anwendungsanforderungen misst, die für die Warteschlange, unabhängig davon, ob sie leer ist oder nicht, ausgeführt werden. In den nächsten Abschnitten werden verschiedene Techniken zum Maximieren der Leistung und Minimieren der Kosten für warteschlangenbasierte Messaginglösungen auf der Windows Azure-Plattform erläutert.

In diesem Abschnitt wird überprüft, wie die relevanten Entwurfsaspekte verbessert werden können, um höhere Leistung, bessere Skalierbarkeit und Kosteneffizienz zu erreichen.

Die einfachste Methode, um zu ermitteln, ob ein Implementierungsmuster eine "effizientere Lösung" darstellt, besteht vermutlich darin, festzustellen, ob der Entwurf die folgenden Zielsetzungen erfüllt:

  • Reduziert Betriebsausgaben, indem ein beträchtlicher Teil der Speichertransaktionen entfernt wird, aus denen sich keine verwendbaren Arbeitsaufgaben ableiten lassen.

  • Eliminiert übermäßige Latenzzeit, die durch ein Abrufintervall auferlegt wird, wenn eine Warteschlange auf neue Nachrichten hin überprüft wird.

  • Skaliert dynamisch aufwärts und abwärts, indem die Rechenleistung an veränderliche Arbeitsmengen angepasst wird.

Das Implementierungsmuster sollte diese Ziele auch erreichen, ohne die Komplexität in einem Maß zu erhöhen, das durch die verbundenen Vorteile nicht gerechtfertigt ist.

Bei der Beurteilung der Gesamtkosten (TCO) und Rendite (ROI) für eine Lösung, die auf der Windows Azure-Plattform bereitgestellt wird, stellt die Menge der Speichertransaktionen eine der wichtigsten Variablen in der TCO-Gleichung dar. Eine Verringerung der Anzahl von Transaktionen in Verbindung mit Windows Azure-Warteschlangen reduziert die Betriebskosten, da diese mit dem Ausführen von Lösungen auf Windows Azure in Beziehung stehen.

Im Kontext einer warteschlangenbasierten Messaginglösung kann die Anzahl der Speichertransaktionen über eine Kombination aus den folgenden Methoden reduziert werden:

  1. Wenn Sie Nachrichten in einer Warteschlange einreihen, gruppieren Sie zusammengehörige Nachrichten in einem einzigen größeren Batch, komprimieren und speichern Sie das komprimierte Bild in einem BLOB-Speicher, und verwenden Sie die Warteschlange als Referenz auf das BLOB, das die tatsächlichen Daten enthält.

  2. Wenn Sie Nachrichten aus einer Warteschlange abrufen, gruppieren Sie mehrere Nachrichten in einem Batch in einer einzelnen Speichertransaktion. Mit der GetMessages-Methode in der Warteschlangendienst-API kann die angegebene Anzahl von Nachrichten in einer einzelnen Transaktion aus der Warteschlange entfernt werden (siehe Hinweis unten).

  3. Wenn Se das Vorhandensein von Arbeitsaufgaben in einer Warteschlange überprüfen, vermeiden Sie aggressive Abrufintervalle, und implementieren Sie eine Backoffverzögerung, die die Zeitdauer zwischen Abrufanforderungen erhöht, wenn die Warteschlange kontinuierlich leer bleibt.

  4. Reduzieren Sie die Anzahl der Warteschlangenlistener – verwenden Sie bei einem PULL-basierten Modell nur einen Warteschlangenlistener pro Rolleninstanz, wenn die Warteschlange leer ist. Um die Anzahl von Warteschlangenlistenern pro Rolleninstanz noch weiter auf Null (0) zu reduzieren, verwenden Sie einen Benachrichtigungsmechanismus, um Warteschlangenlistener zu instanziieren, wenn die Warteschlange Arbeitsaufgaben empfängt.

  5. Wenn Warteschlangen die meiste Zeit leer bleiben, skalieren Sie die Anzahl der Rolleninstanzen automatisch abwärts, und setzen Sie die Überwachung relevanter Systemmetriken fort, um zu bestimmen, ob und wann die Anwendung die Anzahl der Instanzen aufwärts skalieren sollte, um zunehmende Arbeitslasten bewältigen zu können.

Die meisten der oben erwähnten Empfehlungen können in eine relativ allgemeine Implementierung übersetzt werden, die Nachrichtenbatches verarbeitet und viele der zugrunde liegenden Warteschlangen-/BLOB-Speicher und Threadverwaltungsvorgänge kapselt. Die entsprechende Vorgehensweise wird an einer späteren Stelle in diesem Artikel untersucht.

ImportantWichtig
Beim Abrufen von Nachrichten über die GetMessages-Methode ist die maximale Batchgröße, die von der Warteschlangendienst-API in einem einzelnen Vorgang zum Entfernen von Elementen aus der Warteschlange unterstützt wird, auf 32 beschränkt.

Im Allgemeinen steigen die Kosten von Windows Azure-Warteschlangentransaktionen linear, wenn die Anzahl der Warteschlangendienst-Clients zunimmt – wenn z. B. die Anzahl der Rolleninstanzen aufwärts skaliert wird oder die Anzahl von Threads zum Entfernen von Elementen aus der Warteschlange erhöht wird. Das folgende Beispiel, das auf konkreten Zahlen basiert, veranschaulicht, welche Auswirkungen ein Lösungsentwurf, in dem die oben genannten Empfehlungen nicht umgesetzt sind, auf die Kosten auswirken kann.

Wenn der Lösungsarchitekt keine relevanten Optimierungen implementiert, werden durch die oben beschriebene Abrechnungssystemarchitektur mit großer Wahrscheinlichkeit übermäßig hohe Betriebskosten verursacht, wenn die Lösung auf der Windows Azure-Plattform bereitgestellt und ausgeführt wird. Die Ursachen für die möglicherweise übermäßig hohen Kosten werden in diesem Abschnitt beschrieben.

Wie in der Szenariodefinition erwähnt, gehen die Daten zu geschäftlichen Transaktionen regelmäßig ein. Wir gehen jedoch davon aus, dass die Lösung nur während 25 % eines normalen achtstündigen Geschäftstags mit der Verarbeitung von Arbeitslasten beschäftigt ist. Das ergibt eine "Leerlaufzeit" von 6 Stunden (8 Stunden * 75 %), in der möglicherweise keine Transaktionen durch das System weitergeleitet werden. Darüber hinaus empfängt die Lösung überhaupt keine Daten während der 16 Stunden eines jeden Tags, die keine Geschäftsstunden sind.

Auch in dem Leerlaufzeitraum von insgesamt 22 Stunden versucht die Lösung, Arbeitsaufgaben aus der Warteschlange zu entfernen, da nicht bekannt ist, wann neue Daten eingehen. In diesem Zeitfenster führt jeder einzelne Thread zum Entfernen von Elementen aus der Warteschlange bis zu 79.200 Transaktionen (22 Stunden * 60 Minuten * 60 Transaktionen/Minute) für eine Eingabewarteschlange aus, wenn ein standardmäßiges Abrufintervall von 1 Sekunde angenommen wird.

Wie bereits erwähnt wurde, basiert das Preismodell auf der Windows Azure-Plattform auf einzelnen "Speichertransaktionen". Eine Speichertransaktion ist eine Anforderung einer Benutzeranwendung, um Speicherdaten hinzuzufügen, zu lesen, zu aktualisieren oder zu löschen. Zum Zeitpunkt, als dieses Whitepaper verfasst wird, werden Speichertransaktionen mit einer Rate von 0,01 USD pro 10.000 Transaktionen berechnet (hierbei sind keine Werbeangebote oder besondere Preisvereinbarungen berücksichtigt).

ImportantWichtig
Wenn Sie die Anzahl der Warteschlangentransaktionen berechnen, beachten Sie, dass das Einreihen einer einzelnen Nachricht in eine Warteschlange als eine Transaktion gezählt wird, das Abrufen einer Nachricht jedoch oft ein Prozess mit zwei Schritten ist, in dem das Abrufen von einer Anforderung gefolgt wird, die Nachricht aus der Warteschlange zu entfernen. Infolgedessen ist ein erfolgreicher Vorgang zum Entfernen von Elementen aus der Warteschlange mit zwei Speichertransaktionen verbunden. Beachten Sie, dass auch eine Anforderung zum Entfernen von Elementen aus der Warteschlange, bei der keine Daten abgerufen werden, als abrechenbare Transaktion gezählt wird.

Die Speichertransaktionen, die im Szenario oben von einem einzelnen Thread zum Entfernen von Elementen aus der Warteschlange generiert werden, erhöhen die monatliche Rechnung um ungefähr 2,38 USD (79.200 / 10.000 * 0,01 USD * 30 Tage). Bei 200 Threads zum Entfernen von Elementen aus der Warteschlange (oder alternativ 1 entsprechenden Thread in 200 Workerrolleninstanzen) erhöhen sich die Kosten um 457,20 USD pro Monat. Diese Kosten wurden verursacht, obwohl die Lösung keine Berechnungen ausgeführt hat, sondern nur überprüft hat, ob in den Warteschlangen Arbeitsaufgaben verfügbar sind. Bei dem obigen Szenario handelt es sich um ein abstraktes Beispiel, da der Dienst nie auf diese Weise implementiert werden würde. Aus diesem Grund ist es wichtig, die als Nächstes beschriebenen Optimierungen umzusetzen.

Ein Ansatz zum Optimieren der Leistung von warteschlangenbasierten Windows Azure-Messaginglösungen besteht darin, die Veröffentlichungs-/Abonnement-Messagingschicht zu verwenden, die mit Windows Azure Service Bus bereitgestellt wird, wie in diesem Abschnitt beschrieben ist.

Bei dieser Vorgehensweise müssen sich Entwickler auf das Erstellen einer Kombination aus Abrufvorgängen und PUSH-basierten Echtzeitbenachrichtigungen konzentrieren, damit die Listener ein Benachrichtigungsereignis (Trigger) abonnieren können, das bei bestimmten Bedingungen ausgelöst wird und anzeigt, dass eine neue Arbeitslast in der Warteschlange eingegangen ist. Dieser Ansatz erweitert die herkömmliche Warteschlangen-Abrufschleife mit einer Veröffentlichungs-/Abonnement-Messagingschicht zum Verteilen von Benachrichtigungen.

In einem komplexen verteilten System würde dieser Ansatz die Verwendung eines "Nachrichtenbusses" oder von "nachrichtenbezogener Middleware" erfordern, um eine lose verbundene Übertragung der Benachrichtigungen an einen oder mehrere Abonnenten sicherzustellen. Windows Azure Service Bus ist eine gute Wahl, um auf die Messaginganforderungen von lose verbundenen verteilten Anwendungsdiensten einzugehen, die auf Windows Azure und am lokalen Standort ausgeführt werden. Windows Azure Service Bus ist auch ideal für eine "Nachrichtenbus"-Architektur, die den Austausch von Benachrichtigungen zwischen Prozessen ermöglicht, die Teil einer warteschlangenbasierten Kommunikation sind.

Die Prozesse, die während eines warteschlangenbasierten Nachrichtenaustauschs durchgeführt werden, können das folgende Muster nutzen:

Best Practices für Messaginglösungen Azure3

Insbesondere in Bezug auf die Interaktion zwischen Warteschlangendienst-Verlegern und -Abonnenten erfüllen dieselben Prinzipien, die für die Kommunikation zwischen Windows Azure-Rolleninstanzen gelten, die meisten Anforderungen an den Nachrichtenaustausch mit PUSH-basierter Benachrichtigung. Diese Grundlagen werden bereits in Vereinfachen und Skalieren der Kommunikation zwischen Rollen mit Windows Azure Service Bus behandelt.

ImportantWichtig
Für die Verwendung von Windows Azure Service Bus gilt ein Preismodell, das die Menge der Messagingvorgänge im Zusammenhang mit einer Service Bus-Messagingentität, wie z. B. einer Warteschlange oder eines Themas, berücksichtigt.

Aus diesem Grund ist eine Kosten-Nutzen-Analyse wichtig, um die Vorteile und Nachteile zu beurteilen, die die Implementierung von Service Bus in einer bestimmten Architektur mit sich bringt. In diesem Zusammenhang empfiehlt sich auch eine Auswertung, ob die Implementierung einer Schicht zum Verteilen von Benachrichtigungen, die auf Service Bus basiert, tatsächlich eine Kostensenkung ermöglicht, die die Investition und den zusätzlichen Entwicklungsaufwand rechtfertigt.

Weitere Informationen zum Preismodell für Service Bus finden Sie in den relevanten Abschnitten unter FAQ für Windows Azure.

Mit einer Veröffentlichungs-/Abonnement-Messagingschicht ist es relativ einfach, den Auswirkungen auf die Latenzzeit entgegenzuwirken. Sie können die Kosten jedoch noch weiter senken, wenn Sie die dynamische (flexible) Skalierung verwenden, wie im nächsten Abschnitt beschrieben ist.

Die Windows Azure-Plattform ermöglicht es den Kunden, schneller und einfacher als je zuvor aufwärts und abwärts zu skalieren. Die Möglichkeit der Anpassung an variable Arbeitslasten und variablen Datenverkehr ist einer der primären Wertvorschläge der Cloud-Plattform. Das bedeutet, dass "Skalierbarkeit" kein mit hohen Kosten verbundener IT-Begriff mehr ist, sondern eine Standardfunktion, die programmgesteuert und nach Bedarf in einer gut geplanten Cloud-Lösung aktiviert werden kann.

Dynamische Skalierung bezeichnet im technischen Sinn die Anpassungsfähigkeit einer bestimmten Lösung an sich ständig wechselnde Arbeitslasten, indem Kapazität und Verarbeitungsleistung während der Laufzeit erhöht und reduziert werden. Die Windows Azure-Plattform unterstützt die dynamische Skalierung systemintern durch die Bereitstellung einer verteilten Computinginfrastruktur, die den Erwerb von Computingstunden nach Bedarf ermöglicht.

Es ist wichtig, zwischen den beiden folgenden Typen der dynamischen Skalierung auf der Windows Azure-Plattform zu unterscheiden:

  • Rolleninstanzskalierung bezieht sich auf das Hinzufügen und Entfernen von zusätzlichen Web- oder Workerrolleninstanzen, um die Arbeitslasten zu einem bestimmten Zeitpunkt bewältigen zu können. Zu diesem Zweck wird oft die Anzahl der Instanzen in der Dienstkonfiguration geändert. Die Erhöhung der Instanzenzahl bewirkt, dass die Windows Azure-Laufzeit neue Instanzen startet, während das Reduzieren der Instanzenzahl dazu führt, dass Instanzen beendet werden.

  • Prozessskalierung (Threadskalierung) bezieht sich auf die Bewahrung von ausreichender Kapazität im Hinblick auf die Verarbeitung von Threads in einer bestimmten Rolleninstanz, indem die Anzahl der Threads abhängig von der aktuellen Arbeitsauslastung nach oben oder unten angepasst wird.

Für die dynamische Skalierung in einer warteschlangenbasierten Messaginglösung gilt eine Kombination aus den folgenden allgemeinen Empfehlungen:

  1. Überwachen der Leistungskennzahlen – dazu gehören CPU-Auslastung, Warteschlangentiefe, Antwortzeiten und Nachrichtenverarbeitungs-Latenzzeit.

  2. Dynamisches Erhöhen oder Verringern der Anzahl von Rolleninstanzen, um vorhersagbare oder nicht vorhersagbare Arbeitsspitzenlasten handhaben zu können.

  3. Programmgesteuertes Erweitern und Reduzieren der Anzahl von Verarbeitungsthreads, um eine Anpassung an die jeweilige Auslastung einer bestimmten Rolleninstanz zu ermöglichen.

  4. Gleichzeitiges Partitionieren und Verarbeiten von differenzierten Arbeitslasten unter Verwendung der Task Parallel Library in .NET Framework 4.

  5. Bewahren einer geeigneten Kapazität in Lösungen mit sehr veränderlichen Arbeitslasten, damit plötzlich auftretende Spitzenlasten ohne den Mehraufwand, der durch die Einrichtung zusätzlicher Instanzen verursacht wird, bewältigt werden können.

Mithilfe der Dienstverwaltungs-API kann ein von Windows Azure gehosteter Dienst die Anzahl der ausgeführten Rolleninstanzen durch die Änderung der Bereitstellungskonfiguration während der Laufzeit ändern.

noteHinweis
Die maximale Anzahl von kleinen Windows Azure-Serverinstanzen (oder die entsprechende Anzahl von Serverinstanzen anderer Größe hinsichtlich der Anzahl von Kernen) ist in einem typischen Abonnement standardmäßig auf 20 beschränkt. Alle Anfragen, die eine Erhöhung des Kontingents betreffen, sollten an das Windows Azure-Supportteam gerichtet werden. Weitere Informationen finden Sie unter FAQ für Windows Azure.

Die dynamische Skalierung der Anzahl von Rolleninstanzen ist nicht immer die am besten geeignete Methode für den Umgang mit Spitzenlasten. Beispielsweise kann es einige Sekunden dauern, eine neue Rolleninstanz zu erstellen, und zu diesem Zeitpunkt gibt es keine SLA-Metriken in Bezug auf die Erstellungsdauer. Stattdessen kann eine Lösung einfach die Anzahl der Arbeitsthreads erhöhen, um die veränderliche Zunahme der Arbeitslasten zu bewältigen. Während die Arbeitslasten verarbeitet werden, überwacht die Lösung die relevanten Lastmetriken und ermittelt, ob sie die Anzahl der Arbeitsprozesse dynamisch erhöhen oder reduzieren muss.

ImportantWichtig
Zu diesem Zeitpunkt ist das Skalierbarkeitsziel für eine einzelne Windows Azure-Warteschlange auf 500 Transaktionen/Sekunde "beschränkt". Wenn eine Anwendung versucht, diesen Zielwert zu überschreiten, beispielsweise indem Warteschlangenvorgänge von mehreren Rolleninstanzen mit Hunderten von Threads zum Entfernen von Elementen aus der Warteschlange durchgeführt werden, wird möglicherweise die Antwort "HTTP 503 Server ausgelastet" vom Speicherdienst zurückgegeben. In so einem Fall sollte die Anwendung einen Wiederholungsmechanismus mit einem exponentiellen Backoffverzögerungs-Algorithmus implementieren. Wenn die HTTP 503-Fehler regelmäßig auftreten, wird jedoch empfohlen, mehrere Warteschlangen zu verwenden und eine auf Partitionierung basierende Strategie zu implementieren, um die Skalierung über mehrere Warteschlangen hinweg zu ermöglichen.

In den meisten Fällen liegt die automatische Skalierung im Verantwortungsbereich einer einzelnen Rolleninstanz. Dagegen umfasst die Rolleninstanzskalierung häufig ein zentrales Element der Lösungsarchitektur, das für die Überwachung der Leistungsmetriken und das Ausführen der entsprechenden Skalierungsmaßnahmen zuständig ist. Das Diagramm unten zeigt eine Dienstkomponente mit der Bezeichnung Dynamic Scaling Agent, die die Lastmetriken erfasst und analysiert, um zu ermitteln, ob neue Instanzen bereitgestellt oder Instanzen im Leerlauf beendet werden sollen.

Best Practices für Messaginglösungen Azure4

Beachten Sie, dass der Skalierungsagentdienst als Workerrolle, die auf Windows Azure ausgeführt wird, oder als lokaler Dienst bereitgestellt werden kann. Ungeachtet der Bereitstellungstopologie kann der Dienst auf die Windows Azure-Warteschlangen zugreifen.

Um eine dynamische Skalierungsfunktion zu implementieren, ziehen Sie die Verwendung des Microsoft Enterprise Library-Anwendungsblocks für automatische Skalierung in Erwägung, der automatisches Skalierungsverhalten in Lösungen ermöglicht, die auf Windows Azure ausgeführt werden. Der Anwendungsblock für automatische Skalierung stellt alle Funktionen bereit, die zum Definieren und Überwachen der automatischen Skalierung in einer Windows Azure-Anwendung erforderlich sind.

Nachdem wir die Auswirkungen auf die Latenzzeit, die Speichertransaktionskosten und die Anforderungen der dynamischen Skalierung untersucht haben, sollten wir die Empfehlungen in einer technischen Implementierung konsolidieren.

In den vorherigen Abschnitten wurden die wesentlichen Eigenschaften untersucht, die eine sorgfältig entworfene Messagingarchitektur aufweist, die auf den Warteschlangen des Windows Azure-Warteschlangenspeichers basiert. Dabei haben wir uns mit drei wesentlichen Schwerpunktbereichen befasst, die zur Reduzierung der Verarbeitungslatenzzeit, Optimierung der mit Speichertransaktionen verbundenen Kosten und Verbesserung der Reaktionsfähigkeit auf ständig wechselnde Arbeitslasten beitragen.

Dieser Abschnitt ist als Ausgangspunkt gedacht, um Windows Azure-Entwickler bei der Implementierung einiger der Muster, auf die in diesem Whitepaper verwiesen wird, hinsichtlich der Programmierung zu unterstützen.

noteHinweis
Dieser Abschnitt konzentriert sich auf das Erstellen eines automatisch skalierbaren Warteschlangenlisteners, der sowohl das PULL-basierte als auch das PUSH-basierte Modell unterstützt. Informationen zu fortschrittlicheren Techniken für die dynamische Skalierung auf Rolleninstanzebene finden Sie unter Enterprise Library-Anwendungsblock für automatische Skalierung.

Damit das Thema nicht zu lang wird, konzentrieren wir uns auch nur auf einige zentrale Funktionselemente und vermeiden ungewollte Komplexität, indem wir einen Großteil des unterstützenden Infrastrukturcodes aus den Codebeispielen unten weglassen. Der Deutlichkeit halber sollte auch darauf hingewiesen werden, dass die weiter unten erläuterte technische Implementierung nicht die einzige Lösung für einen bestimmten Problembereich ist. Sie soll als Ausgangspunkt dienen, von dem die Entwickler eigene elegantere Lösungen ableiten können.

Ab hier konzentriert sich dieses Whitepaper auf den Quellcode, der erforderlich ist, um die weiter oben erläuterten Muster zu implementieren.

Zunächst definieren wir einen Vertrag, der von einer Warteschlangenlistener-Komponente implementiert wird, die von einer Workerrolle gehostet wird und eine Windows Azure-Warteschlange überwacht.

/// Defines a contract that must be implemented by an extension responsible for listening on a Windows Azure queue.
public interface ICloudQueueServiceWorkerRoleExtension
{
    /// Starts a multi-threaded queue listener that uses the specified number of dequeue threads.
    void StartListener(int threadCount);

    /// Returns the current state of the queue listener to determine point-in-time load characteristics.
    CloudQueueListenerInfo QueryState();

    /// Gets or sets the batch size when performing dequeue operation against a Windows Azure queue.
    int DequeueBatchSize { get; set; }

    /// Gets or sets the default interval that defines how long a queue listener will be idle for between polling a queue.
    TimeSpan DequeueInterval { get; set; }

    /// Defines a callback delegate which will be invoked whenever the queue is empty.
    event WorkCompletedDelegate QueueEmpty;
}

Das QueueEmpty-Ereignis ist für die Verwendung durch einen Host gedacht. Es stellt den Mechanismus bereit, mit dem der Host das Verhalten des Warteschlangenlisteners steuern kann, wenn die Warteschlange leer ist. Der jeweilige Ereignisdelegat wird wie folgt definiert:

/// <summary>
/// Defines a callback delegate which will be invoked whenever an unit of work has been completed and the worker is
/// requesting further instructions as to next steps.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="idleCount">The value indicating how many times the worker has been idle.</param>
/// <param name="delay">Time interval during which the worker is instructed to sleep before performing next unit of work.</param>
/// <returns>A flag indicating that the worker should stop processing any further units of work and must terminate.</returns>
public delegate bool WorkCompletedDelegate(object sender, int idleCount, out TimeSpan delay);

Die Behandlung von Warteschlangenelementen ist einfacher, wenn ein Listener mit Generika funktioniert, statt "Bare-Metal"-SDK-Klassen wie CloudQueueMessage zu verwenden. Daher wird eine neue Schnittstelle definiert, die von einem Warteschlangenlistener implementiert wird, der Generika-basierten Zugriff auf Warteschlangen unterstützen kann:

/// <summary>
/// Defines a contract that must be supported by an extension that implements a generics-aware queue listener.
/// </summary>
/// <typeparam name="T">The type of queue item data that will be handled by the queue listener.</typeparam>
public interface ICloudQueueListenerExtension<T> : ICloudQueueServiceWorkerRoleExtension, IObservable<T>
{
}

Durch die Verwendung der IObservable<T>-Schnittstelle in .NET Framework 4 und durch die Implementierung des Beobachter-Entwurfsmusters kann der Generika-bewusste Listener Warteschlangenelemente an einen oder mehrere Abonnenten weitergeben.

Es soll eine einzelne Instanz einer Komponente, die die ICloudQueueListenerExtension<T>-Schnittstelle implementiert, beibehalten werden. Es ist jedoch erforderlich, dass mehrere Threads zum Entfernen von Elementen aus der Warteschlange ausgeführt werden können (Arbeitsprozesse oder der Einfachheit halber Tasks). Daher wird Unterstützung für Multithread-Logik für das Entfernen von Elementen aus der Warteschlange in der Warteschlangenlistener-Komponente hinzugefügt. Hierfür wird die Task Parallel Library (TPL) genutzt. Die StartListener-Methode ist für das nachfolgend beschriebene Erstellen der angegebenen Anzahl von Threads zum Entfernen von Elementen aus der Warteschlange zuständig:


/// <summary>
/// Starts the specified number of dequeue tasks.
/// </summary>
/// <param name="threadCount">The number of dequeue tasks.</param>
public void StartListener(int threadCount)
{
    Guard.ArgumentNotZeroOrNegativeValue(threadCount, "threadCount");

    // The collection of dequeue tasks needs to be reset on each call to this method.
    if (this.dequeueTasks.IsAddingCompleted)
    {
        this.dequeueTasks = new BlockingCollection<Task>(this.dequeueTaskList);
    }

    for (int i = 0; i < threadCount; i++)
    {
        CancellationToken cancellationToken = this.cancellationSignal.Token;
        CloudQueueListenerDequeueTaskState<T> workerState = new CloudQueueListenerDequeueTaskState<T>(Subscriptions, cancellationToken, this.queueLocation, this.queueStorage);

        // Start a new dequeue task and register it in the collection of tasks internally managed by this component.
        this.dequeueTasks.Add(Task.Factory.StartNew(DequeueTaskMain, workerState, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default));
    }

    // Mark this collection as not accepting any more additions.
    this.dequeueTasks.CompleteAdding();
}

Die DequeueTaskMain-Methode implementiert den Funktionstext eines Threads zum Entfernen von Elementen aus der Warteschlange. Die Hauptvorgänge sind nachfolgend aufgeführt:

/// <summary>
/// Implements a task performing dequeue operations against a given Windows Azure queue.
/// </summary>
/// <param name="state">An object containing data to be used by the task.</param>
private void DequeueTaskMain(object state)
{
    CloudQueueListenerDequeueTaskState<T> workerState = (CloudQueueListenerDequeueTaskState<T>)state;

    int idleStateCount = 0;
    TimeSpan sleepInterval = DequeueInterval;

    try
    {
        // Run a dequeue task until asked to terminate or until a break condition is encountered.
        while (workerState.CanRun)
        {
            try
            {
                var queueMessages = from msg in workerState.QueueStorage.Get<T>(workerState.QueueLocation.QueueName, DequeueBatchSize, workerState.QueueLocation.VisibilityTimeout).AsParallel() where msg != null select msg;
                int messageCount = 0;

                // Process the dequeued messages concurrently by taking advantage of the above PLINQ query.
                queueMessages.ForAll((message) =>
                {
                    // Reset the count of idle iterations.
                    idleStateCount = 0;

                    // Notify all subscribers that a new message requires processing.
                    workerState.OnNext(message);

                    // Once successful, remove the processed message from the queue.
                    workerState.QueueStorage.Delete<T>(message);

                    // Increment the number of processed messages.
                    messageCount++;
                });

                // Check whether or not we have done any work during this iteration.
                if (0 == messageCount)
                {
                    // Increment the number of iterations when we were not doing any work (e.g. no messages were dequeued).
                    idleStateCount++;

                    // Call the user-defined delegate informing that no more work is available.
                    if (QueueEmpty != null)
                    {
                        // Check if the user-defined delegate has requested a halt to any further work processing.
                        if (QueueEmpty(this, idleStateCount, out sleepInterval))
                        {
                            // Terminate the dequeue loop if user-defined delegate advised us to do so.
                            break;
                        }
                    }

                    // Enter the idle state for the defined interval.
                    Thread.Sleep(sleepInterval);
                }
            }
            catch (Exception ex)
            {
                if (ex is OperationCanceledException)
                {
                    throw;
                }
                else
                {
                    // Offload the responsibility for handling or reporting the error to the external object.
                    workerState.OnError(ex);

                    // Sleep for the specified interval to avoid a flood of errors.
                    Thread.Sleep(sleepInterval);
                }
            }
        }
    }
    finally
    {
        workerState.OnCompleted();
    }
}

In Bezug auf die Implementierung der DequeueTaskMain -Methode sollte Folgendes erwähnt werden.

Erstens wird Parallel LINQ (PLINQ) bei der Verteilung von Nachrichten für die Verarbeitung genutzt. Der Hauptvorteil von PLINQ besteht in einer schnelleren Behandlung von Nachrichten, indem der Abfragedelegat in separaten Arbeitsthreads auf mehreren Prozessoren nach Möglichkeit parallel ausgeführt wird.

noteHinweis
Da die Parallelisierung der Abfragen von PLINQ intern verwaltet wird, gibt es keine Garantie, dass PLINQ mehr als einen Kern für die Arbeitsparallelisierung verwendet. PLINQ kann eine Abfrage sequenziell ausführen, wenn der Aufwand der Parallelisierung die Abfrage verlangsamen würde. Um von PLINQ zu profitieren, muss die Gesamtmenge der Arbeit in der Abfrage so groß sein, dass die Vorteile den Mehraufwand für das Planen der Arbeit im Threadpool überwiegen.

Zweitens wird nicht jedes Mal eine einzelne Nachricht abgerufen. Stattdessen soll die Warteschlangendienst-API eine bestimmte Anzahl von Nachrichten aus einer Warteschlange abrufen. Dieser Vorgang wird durch den DequeueBatchSize-Parameter gesteuert, der an die Get<T>-Methode übergeben wird. Bei der Eingabe der Speicherabstraktionsebene, die als Teil der gesamten Lösung implementiert wird, wird dieser Parameter an die Warteschlangendienst-API-Methode übergeben. Darüber hinaus wird eine Sicherheitsüberprüfung ausgeführt, um sicherzustellen, dass die Batchgröße nicht die maximal von den APIs unterstützte Größe überschreitet. Diese Überprüfung wird wie folgt implementiert:

/// This class provides reliable generics-aware access to the Windows Azure Queue storage.
public sealed class ReliableCloudQueueStorage : ICloudQueueStorage
{
    /// The maximum batch size supported by Queue Service API in a single Get operation.
    private const int MaxDequeueMessageCount = 32;

    /// Gets a collection of messages from the specified queue and applies the specified visibility timeout.
    public IEnumerable<T> Get<T>(string queueName, int count, TimeSpan visibilityTimeout)
    {
        Guard.ArgumentNotNullOrEmptyString(queueName, "queueName");
        Guard.ArgumentNotZeroOrNegativeValue(count, "count");

        try
        {
            var queue = this.queueStorage.GetQueueReference(CloudUtility.GetSafeContainerName(queueName));

            IEnumerable<CloudQueueMessage> queueMessages = this.retryPolicy.ExecuteAction<IEnumerable<CloudQueueMessage>>(() =>
            {
                return queue.GetMessages(Math.Min(count, MaxDequeueMessageCount), visibilityTimeout);
            });

            // ... There is more code after this point ...

Und außerdem wird die Task zum Entfernen von Elementen aus der Warteschlange nicht unbegrenzt ausgeführt. Es wird ein expliziter Prüfpunkt bereitgestellt, der als QueueEmpty-Ereignis implementiert ist, das ausgelöst wird, wenn eine Warteschlange leer ist. An dieser Stelle müssen wir ermitteln, ob der QueueEmpty-Ereignishandler die Fertigstellung der Task zum Entfernen von Elementen aus der Warteschlange zulässt. Eine gut entworfene Implementierung des QueueEmpty-Ereignishandlers lässt zu, dass die "automatische Abwärtsskalierung" unterstützt wird, wie im folgenden Abschnitt beschrieben wird.

Der QueueEmpty-Ereignishandler hat eine zweifache Funktion. Erstens ist er dafür zuständig, Feedback für die Task zum Entfernen von Elementen aus der Warteschlange der Quelle bereitzustellen, das den Übergang in den Energiesparmodus und den Verbleib in diesem Modus für ein bestimmtes Zeitintervall anordnet (wie im Verzögerungs-Ausgabeparameter im Ereignisdelegaten definiert). Zweitens weist er die Task zum Entfernen von Elementen aus der Warteschlange darauf hin, ob sie sich kontrolliert selbst beenden muss (wie vom booleschen Rückgabeparameter vorgeschrieben).

Die folgende Implementierung des QueueEmpty-Ereignishandlers löst die beiden weiter oben in diesem Whitepaper beschriebenen Herausforderungen. Diese Implementierung berechnet ein exponentielles Backoffintervall und weist die Task zum Entfernen von Elementen aus der Warteschlange an, die Verzögerung zwischen Warteschlangen-Abrufanforderungen exponentiell zu erhöhen. Beachten Sie, dass die Backoffverzögerung 1 Sekunde nicht überschreitet – wie in der Lösung konfiguriert –, da eine lange Verzögerung zwischen Abrufvorgängen wirklich nicht erforderlich ist, wenn die automatische Skalierung zufriedenstellend implementiert ist. Darüber hinaus fragt die Implementierung den Status des Warteschlangenlisteners ab, um die Anzahl der aktiven Tasks zum Entfernen von Elementen aus der Warteschlange zu ermitteln. Wenn dieser Wert größer ist als 1, weist der Ereignishandler die Task zum Entfernen von Elementen aus der Warteschlange an, die Abrufschleife fertig zu stellen, vorausgesetzt, dass das Backoffintervall auch den angegebenen Höchstwert erreicht hat. Andernfalls wird die Task zum Entfernen von Elementen aus der Warteschlange nicht beendet, und zu jedem Zeitpunkt wird 1 Abrufthread pro Instanz des Warteschlangenlisteners ausgeführt. Dieser Ansatz trägt dazu bei, die Anzahl der Speichertransaktionen zu reduzieren und infolgedessen die Transaktionskosten zu senken, wie weiter oben erläutert wurde.

private bool HandleQueueEmptyEvent(object sender, int idleCount, out TimeSpan delay)
{
    // The sender is an instance of the ICloudQueueServiceWorkerRoleExtension, we can safely perform type casting.
    ICloudQueueServiceWorkerRoleExtension queueService = sender as ICloudQueueServiceWorkerRoleExtension;

    // Find out which extension is responsible for retrieving the worker role configuration settings.
    IWorkItemProcessorConfigurationExtension config = Extensions.Find<IWorkItemProcessorConfigurationExtension>();

    // Get the current state of the queue listener to determine point-in-time load characteristics.
    CloudQueueListenerInfo queueServiceState = queueService.QueryState();

    // Set up the initial parameters, read configuration settings.
    int deltaBackoffMs = 100;
    int minimumIdleIntervalMs = Convert.ToInt32(config.Settings.MinimumIdleInterval.TotalMilliseconds);
    int maximumIdleIntervalMs = Convert.ToInt32(config.Settings.MaximumIdleInterval.TotalMilliseconds);

    // Calculate a new sleep interval value that will follow a random exponential back-off curve.
    int delta = (int)((Math.Pow(2.0, (double)idleCount) - 1.0) * (new Random()).Next((int)(deltaBackoffMs * 0.8), (int)(deltaBackoffMs * 1.2)));
    int interval = Math.Min(minimumIdleIntervalMs + delta, maximumIdleIntervalMs);

    // Pass the calculated interval to the dequeue task to enable it to enter into a sleep state for the specified duration.
    delay = TimeSpan.FromMilliseconds((double)interval);

    // As soon as interval reaches its maximum, tell the source dequeue task that it must gracefully terminate itself
    // unless this is a last deqeueue task. If so, we are not going to keep it running and continue polling the queue.
    return delay.TotalMilliseconds >= maximumIdleIntervalMs && queueServiceState.ActiveDequeueTasks > 1;
}

Auf einer höheren Ebene kann die oben beschriebene "Abwärtsskalierung der Task zum Entfernen von Elementen aus der Warteschlange" wie folgt beschrieben werden:

  1. Immer, wenn sich Elemente in der Warteschlange befinden, stellen die Tasks zum Entfernen von Elementen aus der Warteschlange sicher, dass die Arbeitslasten so schnell wie möglich verarbeitet werden. Es gibt keine Verzögerung zwischen den Anforderungen zum Entfernen von Nachrichten aus einer Warteschlange.

  2. Sobald die Quellwarteschlange leer ist, löst die Task zum Entfernen von Elementen aus der Warteschlange ein QueueEmpty-Ereignis aus.

  3. Der QueueEmpty-Ereignishandler berechnet eine zufällige exponentielle Backoffverzögerung und weist die Task zum Entfernen von Elementen aus der Warteschlange an, die Aktivitäten über ein bestimmtes Zeitintervall hinweg zu unterbrechen.

  4. Die Tasks zum Entfernen von Elementen aus der Warteschlange setzen das Abrufen der Quellwarteschlange in berechneten Intervallen fort, bis die Leerlaufdauer den zulässigen Höchstwert überschreitet.

  5. Wenn der Höchstwert des Leerlaufintervalls erreicht ist – vorausgesetzt, dass die Quellwarteschlange noch leer ist –, beginnen alle aktiven Tasks zum Entfernen von Elementen aus der Warteschlange damit, sich kontrolliert selbst zu beenden. Die Tasks beenden sich nicht alle gleichzeitig, da die Backoffphase an unterschiedlichen Zeitpunkten im Backoffalgorithmus startet.

  6. Irgendwann gibt es nur eine aktive Task zum Entfernen von Elementen aus der Warteschlange, die auf Arbeit wartet. Infolgedessen gibt es für eine Warteschlange keine Abruftransaktionen im Leerlauf – mit Ausnahme dieser einzelnen Task.

Um den Prozess des Sammelns von Zeitpunktlast-Eigenschaften näher zu erläutern, sollten die relevanten Quellcodeartefakte erwähnt werden. Erstens gibt es eine Struktur mit den relevanten Metriken, die das Ergebnis der Last messen, die auf die Lösung angewendet wird. Der Einfachheit halber wird eine kleine Teilmenge von Metriken bereitgestellt, die im Beispielcode weiterverwendet werden.

/// Implements a structure containing point-in-time load characteristics for a given queue listener.
public struct CloudQueueListenerInfo
{
    /// Returns the approximate number of items in the Windows Azure queue.
    public int CurrentQueueDepth { get; internal set; }

    /// Returns the number of dequeue tasks that are actively performing work or waiting for work.
    public int ActiveDequeueTasks { get; internal set; }

    /// Returns the maximum number of dequeue tasks that were active at a time.
    public int TotalDequeueTasks { get; internal set; }
}

Zweitens gibt es eine von einem Warteschlangenlistener implementierte Methode, die die Lastmetriken wie im folgenden Beispiel gezeigt zurückgibt:

/// Returns the current state of the queue listener to determine point-in-time load characteristics.
public CloudQueueListenerInfo QueryState()
{
    return new CloudQueueListenerInfo()
    {
        CurrentQueueDepth = this.queueStorage.GetCount(this.queueLocation.QueueName),
        ActiveDequeueTasks = (from task in this.dequeueTasks where task.Status != TaskStatus.Canceled && task.Status != TaskStatus.Faulted && task.Status != TaskStatus.RanToCompletion select task).Count(),
        TotalDequeueTasks = this.dequeueTasks.Count
    };
}

Im vorherigen Abschnitt wurde die Funktion vorgestellt, mit der die Anzahl der aktiven Tasks zum Entfernen von Elementen aus der Warteschlange auf eine einzelne Instanz reduziert werden kann, um die Auswirkungen von Transaktionen im Leerlauf auf die Kosten für Speichervorgänge zu minimieren. In diesem Abschnitt wird ein entgegengesetztes Beispiel beschrieben, in dem die Funktion zum "automatischen Skalieren nach oben" implementiert wird, um die Verarbeitungsleistung zu erhöhen, wenn dies erforderlich ist.

Zunächst wird ein Ereignisdelegat definiert, mit dem der Übergang aus einer leeren in eine nicht leere Warteschlange nachverfolgt wird, um relevante Aktionen auszulösen:

/// <summary>
/// Defines a callback delegate which will be invoked whenever new work arrived to a queue while the queue listener was idle.
/// </summary>
/// <param name="sender">The source of the event.</param>
public delegate void WorkDetectedDelegate(object sender);

Als Nächstes wird die Anfangsdefinition der ICloudQueueServiceWorkerRoleExtension-Schnittstelle um ein neues Ereignis erweitert, das jedes Mal ausgelöst wird, wenn der Warteschlangenlistener neue Arbeitselemente erkennt – eine wesentliche Funktion, wenn die Warteschlangentiefe sich von Null (0) zu einem positiven Wert ändert:

public interface ICloudQueueServiceWorkerRoleExtension
{
    // ... The other interface members were omitted for brevity. See the previous code snippets for reference ...

    // Defines a callback delegate to be invoked whenever a new work has arrived to a queue while the queue listener was idle.
    event WorkDetectedDelegate QueueWorkDetected;
}

Außerdem wird die richtige Stelle im Code des Warteschlangenlisteners bestimmt, an der ein solches Ereignis ausgelöst wird. Das QueueWorkDetected-Ereignis wird aus der Schleife zum Entfernen von Elementen aus der Warteschlange ausgelöst, die in der DequeueTaskMain-Methode implementiert wird, die wie folgt erweitert werden muss:

public class CloudQueueListenerExtension<T> : ICloudQueueListenerExtension<T>
{
    // An instance of the delegate to be invoked whenever a new work has arrived to a queue while the queue listener was idle.
    public event WorkDetectedDelegate QueueWorkDetected;

    private void DequeueTaskMain(object state)
    {
        CloudQueueListenerDequeueTaskState<T> workerState = (CloudQueueListenerDequeueTaskState<T>)state;

        int idleStateCount = 0;
        TimeSpan sleepInterval = DequeueInterval;

        try
        {
            // Run a dequeue task until asked to terminate or until a break condition is encountered.
            while (workerState.CanRun)
            {
                try
                {
                    var queueMessages = from msg in workerState.QueueStorage.Get<T>(workerState.QueueLocation.QueueName, DequeueBatchSize, workerState.QueueLocation.VisibilityTimeout).AsParallel() where msg != null select msg;
                    int messageCount = 0;

                    // Check whether or not work items arrived to a queue while the listener was idle.
                    if (idleStateCount > 0 && queueMessages.Count() > 0)
                    {
                        if (QueueWorkDetected != null)
                        {
                            QueueWorkDetected(this);
                        }
                    }

                    // ... The rest of the code was omitted for brevity. See the previous code snippets for reference ...

Im letzten Schritt wird ein Handler für das QueueWorkDetected-Ereignis bereitgestellt. Die Implementierung dieses Ereignishandlers wird von einer Komponente bereitgestellt, die den Warteschlangenlistener instanziiert und hostet. In diesem Fall ist es eine Workerrolle. Der für die Instanziierung und Implementierung des Ereignishandlers zuständige Code besteht aus den folgenden Elementen:

public class WorkItemProcessorWorkerRole : RoleEntryPoint
{
    // Called by Windows Azure to initialize the role instance.
    public override sealed bool OnStart()
    {
        // ... There is some code before this point ...

        // Instantiate a queue listener for the input queue.
        var inputQueueListener = new CloudQueueListenerExtension<XDocument>(inputQueueLocation);

        // Configure the input queue listener.
        inputQueueListener.QueueEmpty += HandleQueueEmptyEvent;
        inputQueueListener.QueueWorkDetected += HandleQueueWorkDetectedEvent;
        inputQueueListener.DequeueBatchSize = configSettingsExtension.Settings.DequeueBatchSize;
        inputQueueListener.DequeueInterval = configSettingsExtension.Settings.MinimumIdleInterval;

        // ... There is more code after this point ...
    }

    // Implements a callback delegate to be invoked whenever a new work has arrived to a queue while the queue listener was idle.
    private void HandleQueueWorkDetectedEvent(object sender)
    {
        // The sender is an instance of the ICloudQueueServiceWorkerRoleExtension, we can safely perform type casting.
        ICloudQueueServiceWorkerRoleExtension queueService = sender as ICloudQueueServiceWorkerRoleExtension;

        // Get the current state of the queue listener to determine point-in-time load characteristics.
        CloudQueueListenerInfo queueServiceState = queueService.QueryState();

        // Determine the number of queue tasks that would be required to handle the workload in a queue given its current depth.
        int dequeueTaskCount = GetOptimalDequeueTaskCount(queueServiceState.CurrentQueueDepth);

        // If the dequeue task count is less than computed above, start as many dequeue tasks as needed.
        if (queueServiceState.ActiveDequeueTasks < dequeueTaskCount)
        {
            // Start the required number of dequeue tasks.
            queueService.StartListener(dequeueTaskCount - queueServiceState.ActiveDequeueTasks);
        }
    }       // ... There is more code after this point ...

Angesichts des oben genannten Beispiels empfiehlt sich eine genauere Betrachtung der GetOptimalDequeueTaskCount-Methode. Diese Methode ist für die Berechnung der Anzahl von Tasks zum Entfernen von Elementen aus der Warteschlange zuständig, die für die Behandlung der Arbeitslasten in einer Warteschlange als optimal angesehen wird. Wenn diese Methode aufgerufen wird, sollte sie (mithilfe aller entsprechenden Entscheidungsmechanismen) bestimmen, wie viel "Pferdestärke" der Warteschlangenlistener erfordert, um die Arbeitsmenge zu verarbeiten, die in einer bestimmten Warteschlange wartet oder eingehen soll.

Der Entwickler kann z. B. einen sehr vereinfachten Ansatz wählen und einen Satz von statischen Regeln direkt in die GetOptimalDequeueTaskCount-Methode einbetten. Unter Verwendung der bekannten Durchsatz- und Skalierbarkeitseigenschaften der Warteschlangeninfrastruktur, der durchschnittlichen Verarbeitungslatenzzeit, der Nutzlastgröße und anderer relevanter Eingaben könnte der Regelsatz optimistisch herangehen und eine optimale Anzahl von Tasks zum Entfernen von Elementen aus der Warteschlange festlegen.

Im Beispiel unten wird eine absichtlich pauschalisierte Technik verwendet, um die Anzahl von Tasks zum Entfernen von Elementen aus der Warteschlange festzulegen:

/// <summary>
/// Returns the number of queue tasks that would be required to handle the workload in a queue given its current depth.
/// </summary>
/// <param name="currentDepth">The approximate number of items in the queue.</param>
/// <returns>The optimal number of dequeue tasks.</returns>
private int GetOptimalDequeueTaskCount(int currentDepth)
{
    if (currentDepth < 100) return 10;
    if (currentDepth >= 100 && currentDepth < 1000) return 50;
    if (currentDepth >= 1000) return 100;

    // Return the minimum acceptable count.
    return 1;
}

Wie bereits erwähnt wurde, ist der oben genannte Beispielcode nicht als Einheitsansatz gedacht, der sich für jedes Szenario eignet. Eine bessere Lösung besteht darin, eine extern konfigurierbare und verwaltbare Regel aufzurufen, die die notwendigen Berechnungen ausführt.

Der vorliegende funktionsfähige Prototyp eines Warteschlangenlisteners kann sich automatisch durch Aufwärts- und Abwärtsskalierung an wechselnde Arbeitslasten anpassen. Um ihm den letzten Feinschliff zu geben, kann er mit der Funktion erweitert werden, sich an variable Lasten anzupassen, während diese verarbeitet werden. Diese Funktion kann hinzugefügt werden, indem das gleiche Muster wie beim Hinzufügen von Unterstützung für das QueueWorkDetected-Ereignis angewendet wird.

Der nächste Schwerpunkt liegt auf einer anderen wichtigen Optimierung, die zur Reduzierung der Latenzzeit in den Warteschlangenlistenern beiträgt.

In diesem Abschnitt wird die oben erwähnte Implementierung eines Warteschlangenlisteners mit einem PUSH-basierten Benachrichtigungsmechanismus erweitert, der auf die unidirektionale Multicastfunktion von Service Bus aufbaut. Der Benachrichtigungsmechanismus ist für das Auslösen eines Ereignisses zuständig, das den Warteschlangenlistener auffordert, mit dem Entfernen von Arbeitsaufgaben aus der Warteschlange zu beginnen. Dieser Ansatz trägt dazu bei, dass das Abrufen der Warteschlage zur Überprüfung auf neue Nachrichten vermieden wird, wodurch die damit verbundene Latenzzeit eliminiert wird.

Zunächst wird ein Triggerereignis definiert, das von diesem Warteschlangenlistener empfangen wird, falls eine neue Arbeitslast in einer Warteschlange abgelegt wird:

/// Implements a trigger event indicating that a new workload was put in a queue.
[DataContract(Namespace = WellKnownNamespace.DataContracts.Infrastructure)]
public class CloudQueueWorkDetectedTriggerEvent
{
    /// Returns the name of the storage account on which the queue is located.
    [DataMember]
    public string StorageAccount { get; private set; }

    /// Returns a name of the queue where the payload was put.
    [DataMember]
    public string QueueName { get; private set; }

    /// Returns a size of the queue's payload (e.g. the size of a message or the number of messages in a batch).
    [DataMember]
    public long PayloadSize { get; private set; }

    // ... The constructor was omitted for brevity ...
}

Als Nächstes werden die Implementierungen des Warteschlangenlisteners so eingerichtet, dass sie als Abonnenten auftreten, um ein Triggerereignis zu erhalten. Der erste Schritt besteht darin, einen Warteschlangenlistener als Beobachter für das CloudQueueWorkDetectedTriggerEvent-Ereignis zu definieren:

/// Defines a contract that must be implemented by an extension responsible for listening on a Windows Azure queue.
public interface ICloudQueueServiceWorkerRoleExtension : IObserver<CloudQueueWorkDetectedTriggerEvent>
{
    // ... The body is omitted as it was supplied in previous examples ...
}

Der zweite Schritt besteht darin, die OnNext-Methode, die in der IObserver<T>-Schnittstelle definiert ist, zu implementieren. Diese Methode wird vom Anbieter aufgerufen, um den Beobachter über ein neues Ereignis zu informieren:

public class CloudQueueListenerExtension<T> : ICloudQueueListenerExtension<T>
{
    // ... There is some code before this point ...

    /// <summary>
    /// Gets called by the provider to notify this queue listener about a new trigger event.
    /// </summary>
    /// <param name="e">The trigger event indicating that a new payload was put in a queue.</param>
    public void OnNext(CloudQueueWorkDetectedTriggerEvent e)
    {
        Guard.ArgumentNotNull(e, "e");

        // Make sure the trigger event is for the queue managed by this listener, otherwise ignore.
        if (this.queueLocation.StorageAccount == e.StorageAccount && this.queueLocation.QueueName == e.QueueName)
        {
            if (QueueWorkDetected != null)
            {
                 QueueWorkDetected(this);
            }
        }
    }

    // ... There is more code after this point ...
}

Wie im Beispiel oben zu sehen ist, wird absichtlich der gleiche Ereignisdelegat aufgerufen, der in den vorherigen Schritten verwendet wird. Der QueueWorkDetected-Ereignishandler stellt bereits die erforderliche Anwendungslogik für das Instanziieren der optimalen Anzahl von Tasks zum Entfernen von Elementen aus der Warteschlange bereit. Infolgedessen wird der gleiche Ereignishandler wiederverwendet, wenn die CloudQueueWorkDetectedTriggerEvent-Benachrichtigung behandelt wird.

Wie in den vorangehenden Abschnitten erwähnt, müssen die Tasks zum Entfernen von Elementen aus der Warteschlange nicht kontinuierlich ausgeführt werden, wenn die PUSH-basierte Benachrichtigung verwendet wird. Daher kann die Anzahl der Warteschlangentasks pro Warteschlangenlistener-Instanz auf Null (0) reduziert werden, und ein Benachrichtigungsmechanismus kann zum Instanziieren von Tasks zum Entfernen von Elementen aus der Warteschlange verwendet werden, wenn in der Warteschlange Arbeitsaufgaben eingehen. Um sicherzustellen, dass keine Tasks zum Entfernen von Elementen aus der Warteschlange im Leerlauf ausgeführt werden, ist die folgende einfache Änderung im QueueEmpty-Ereignishandler erforderlich:

private bool HandleQueueEmptyEvent(object sender, int idleCount, out TimeSpan delay)
{
    // ... There is some code before this point ...

    // As soon as interval reaches its maximum, tell the source dequeue task that it must gracefully terminate itself.
    return delay.TotalMilliseconds >= maximumIdleIntervalMs;
}

Zusammengefasst ausgedrückt wird nicht mehr erkannt, ob eine einzelne aktive Task zum Entfernen von Elementen aus der Warteschlange vorhanden ist. Nach der Überarbeitung des QueueEmpty-Ereignishandlers wird nur eine Überschreitung des maximalen Leerlaufintervalls berücksichtigt, woraufhin alle aktiven Tasks zum Entfernen von Elementen aus der Warteschlange beendet werden.

Um die CloudQueueWorkDetectedTriggerEvent-Benachrichtigungen zu erhalten, wird das Veröffentlichungs-/Abonnementmodell genutzt, das als lose gekoppeltes Messagingsystem zwischen Windows Azure-Rolleninstanzen implementiert ist. Im Wesentlichen wird dieselbe Schicht für die Kommunikation zwischen Rollen verwendet, und eingehende Ereignisse werden wie folgt behandelt:

public class InterRoleEventSubscriberExtension : IInterRoleEventSubscriberExtension
{
    // ... Some code here was omitted for brevity. See the corresponding guidance on Windows Azure CAT team blog for reference ...

    public void OnNext(InterRoleCommunicationEvent e)
    {
        if (this.owner != null && e.Payload != null)
        {
            // ... There is some code before this point ...

            if (e.Payload is CloudQueueWorkDetectedTriggerEvent)
            {
                HandleQueueWorkDetectedTriggerEvent(e.Payload as CloudQueueWorkDetectedTriggerEvent);
                return;
            }

            // ... There is more code after this point ...
        }
    }

    private void HandleQueueWorkDetectedTriggerEvent(CloudQueueWorkDetectedTriggerEvent e)
    {
        Guard.ArgumentNotNull(e, "e");

        // Enumerate through registered queue listeners and relay the trigger event to them.
        foreach (var queueService in this.owner.Extensions.FindAll<ICloudQueueServiceWorkerRoleExtension>())
        {
            // Pass the trigger event to a given queue listener.
            queueService.OnNext(e);
        }
    }
}

Die Durchführung von Multicasting für ein Triggerereignis, das in der CloudQueueWorkDetectedTriggerEvent-Klasse definiert ist, ist die letzte Aufgabe eines Verlegers, d. h. der Komponente, die Arbeitsaufgaben in einer Warteschlange ablegt. Dieses Ereignis kann ausgelöst werden, bevor die erste Arbeitsaufgabe in die Warteschlange eingereiht wird oder nachdem die letzte Aufgabe in die Warteschlange eingereiht wird. Im Beispiel unten wird ein Triggerereignis veröffentlicht, nachdem das Ablegen von Arbeitsaufgaben in der Eingabewarteschlange abgeschlossen ist:

public class ProcessInitiatorWorkerRole : RoleEntryPoint
{
    // The instance of the role extension which provides an interface to the inter-role communication service.
    private volatile IInterRoleCommunicationExtension interRoleCommunicator;

    // ... Some code here was omitted for brevity. See the corresponding guidance on Windows Azure CAT team blog for reference ...

    private void HandleWorkload()
    {
        // Step 1: Receive compute-intensive workload.
        // ... (code was omitted for brevity) ...

        // Step 2: Enqueue work items into the input queue.
        // ... (code was omitted for brevity) ...

        // Step 3: Notify the respective queue listeners that they should expect work to arrive.
        // Create a trigger event referencing the queue into which we have just put work items.
        var trigger = new CloudQueueWorkDetectedTriggerEvent("MyStorageAccount", "InputQueue");

        // Package the trigger into an inter-role communication event.
        var interRoleEvent = new InterRoleCommunicationEvent(CloudEnvironment.CurrentRoleInstanceId, trigger);

        // Publish inter-role communication event via the Service Bus one-way multicast.
        interRoleCommunicator.Publish(interRoleEvent);
    }
}

Nachdem wir einen Warteschlangenlistener erstellt haben, der Benachrichtigungen mit Multithreading und mit automatischer Skalierung sowie PUSH-basierte Benachrichtigungen unterstützen kann, ist es an der Zeit, alle Empfehlungen für den Entwurf von warteschlangenbasierten Messaginglösungen auf der Windows Azure-Plattform zu konsolidieren.

Um die Effizienz und die Wirtschaftlichkeit von warteschlangenbasierten Messaginglösungen zu maximieren, die auf der Windows Azure-Plattform ausgeführt werden, sollten Lösungsarchitekten und -entwickler die folgenden Empfehlungen beachten.

Lösungsarchitekten sollten Folgendes beachten:

  • Stellen Sie eine warteschlangenbasierte Messagingarchitektur bereit, die den Windows Azure-Warteschlangenspeicherdienst für hoch skalierbare, asynchrone Kommunikation zwischen Ebenen und Diensten in Cloud-basierten oder Hybridlösungen verwendet.

  • Empfehlen Sie eine partitionierte Warteschlangenarchitektur, die eine Skalierung auf mehr als 500 Transaktionen/Sekunde unterstützt.

  • Machen Sie sich mit den Grundlagen des Windows Azure-Preismodells vertraut, und optimieren Sie die Lösung zur Reduzierung der Transaktionskosten mithilfe einer Reihe von Best Practices und Entwurfsmustern.

  • Berücksichtigen Sie die Anforderungen an die dynamische Skalierung, indem Sie eine Architektur bereitstellen, die an veränderliche und ständig wechselnde Arbeitslasten angepasst werden kann.

  • Nutzen Sie die richtigen Techniken und Ansätze für die automatische Skalierung, um die Rechenleistung flexibel zu erhöhen und zu reduzieren und die Betriebsausgaben dadurch noch weiter zu optimieren.

  • Beurteilen Sie das Kosten-Nutzen-Verhältnis einer Reduzierung der Latenzzeit durch eine von Windows Azure Service Bus abhängige Verteilung von PUSH-basierten Benachrichtigungen in Echtzeit.

Entwickler sollten Folgendes beachten:

  • Entwerfen Sie eine Messaginglösung, die die Batchverarbeitung beim Speichern und Abrufen von Daten aus Windows Azure-Warteschlangen nutzt.

  • Implementieren Sie einen effizienten Warteschlangenlistener-Dienst, um sicherzustellen, dass bei leeren Warteschlangen maximal ein Thread zum Entfernen von Elementen aus der Warteschlange für das Abrufen verwendet wird.

  • Skalieren Sie die Anzahl der Workerrolleninstanzen dynamisch abwärts, wenn Warteschlangen über einen längeren Zeitraum hinweg leer bleiben.

  • Implementieren Sie einen anwendungsspezifischen zufälligen exponentiellen Backoff-Algorithmus, um die Auswirkungen des Warteschlangenabrufs im Leerlauf auf die Speichertransaktionskosten zu reduzieren.

  • Setzen Sie die richtigen Techniken ein, die das Überschreiten von Skalierbarkeitszielen für eine einzelne Warteschlange verhindern, wenn Sie Warteschlangenverleger und -verbraucher mit vielen Threads und Instanzen implementieren.

  • Verwenden Sie eine stabile Wiederholungsrichtlinie, die eine Vielzahl von Übergangszuständen unterstützen kann, wenn Daten in Windows Azure-Warteschlangen veröffentlicht bzw. genutzt werden.

  • Verwenden Sie die unidirektionale Ereignisfunktion, die von Windows Azure Service Bus zur Unterstützung von PUSH-basierten Benachrichtigungen bereitgestellt wird, um die Latenzzeit zu reduzieren und die Leistung der warteschlangenbasierten Messaginglösung zu verbessern.

  • Untersuchen Sie die neuen Funktionen von .NET Framework 4, wie z. B. TPL, PLINQ und das Beobachter-Muster, um den Grad des Parallelismus zu maximieren, die Parallelität zu verbessern und den Entwurf von Multithreaddiensten zu vereinfachen.

Der zugehörige Beispielcode kann von der MSDN Code Gallery heruntergeladen werden. Der Beispielcode enthält auch alle erforderlichen Infrastrukturkomponenten, wie z. B. die Generika-bewusste Abstraktionsebene für den Windows Azure-Warteschlangendienst, die in den oben aufgeführten Codeausschnitten nicht angegeben sind. Beachten Sie, dass alle Quellcodedateien der Microsoft Public License unterliegen, wie in den entsprechenden rechtlichen Hinweisen erläutert wird.

Weitere Informationen zu dem in diesem Whitepaper erläuterten Thema finden Sie in den folgenden Ressourcen:

Microsoft führt eine Onlineumfrage durch, um Ihre Meinung zur MSDN-Website zu erfahren. Wenn Sie sich zur Teilnahme entscheiden, wird Ihnen die Onlineumfrage angezeigt, sobald Sie die MSDN-Website verlassen.

Möchten Sie an der Umfrage teilnehmen?
Anzeigen:
© 2014 Microsoft