Cloudmuster

Entwerfen von Diensten für Windows Azure

Thomas Erl, Arman Kurtagic und Herbjörn Wilhelmsen

Beispielcode herunterladen.

Windows Azure ist eine neue, bei Microsoft in der Entwicklung befindliche Cloud-Computing-Plattform (microsoft.com/windowsazure). Cloud-Computing ermöglicht es Entwicklern, Anwendungen in einer über das Internet zugänglichen, virtuellen Umgebung zu hosten. Die Umgebung stellt auf transparente Weise Hardware, Software, Netzwerk und Speicher zur Verfügung, die von der Anwendung benötigt werden.

Ebenso wie andere Cloudumgebungen stellt Windows Azure eine gehostete Umgebung für Anwendungen bereit. Zusätzlich bietet Windows Azure den Vorteil, dass zur Bereitstellung von .NET Framework-Anwendungen nur minimale Änderungen an den Desktopversionen dieser Anwendungen erforderlich sind.

Die Anwendung von Mustern der dienstorientierten Architektur (Service-oriented Architecture, SOA) und die Erfahrungen, die bei der Implementierung dienstorientierter Lösungen gesammelt wurden, werden für die Verlagerung unserer Dienste und Anwendungen in den neuen Bereich des Cloud-Computing entscheidend sein. Um besser zu verstehen, wie SOA-Muster auf Windows Azure-Bereitstellungen angewendet werden können, sehen wir uns ein Szenario an, in dem eine fiktive Bank Cloud-Computing für ihre Dienste nutzt.

Cloud-Banking

Die Woodgrove Bank ist ein kleines Finanzinstitut, das beschlossen hat, sich auf die neue Onlinebankinginitiative mit dem Markennamen Woodgrove Bank Online zu konzentrieren. Einer der wichtigsten Kunden der Woodgrove Bank namens Fourth Coffee erklärte sich freiwillig bereit, die neue Lösung zur Verarbeitung von Kartentransaktionen auszuprobieren. Eine Teilmenge der für die Lösung geplanten Dienste ist bereits in Betrieb, und die Verfügbarkeit dieser Dienste hat auch das Interesse anderer Kunden geweckt. Während die Einführung weiterer Lösungselemente geplant wird, bilden sich jedoch neue Herausforderungen heraus.

Das erste Problem betrifft die Skalierbarkeit und Zuverlässigkeit. Die Woodgrove Bank wollte nie die Verantwortung als Host ihrer IT-Lösungen übernehmen. Stattdessen wurde mit einem örtlichen ISP namens Sesame Hosting Company eine Bereitstellungsvereinbarung getroffen. Bislang hat Sesame Hosting die Webhostinganforderungen der Woodgrove Bank erfüllt, aber die neue Kartenverarbeitungslösung stellt Anforderungen an die Skalierbarkeit, auf die Sesame Hosting nicht vorbereitet ist.

Das Technologiearchitekturteam der Woodgrove Bank schlägt eine redundante Bereitstellung der Online-Dienste der Woodgrove Bank nach dem Muster der redundanten Implementierung (Redundant Implementation) vor (Beschreibungen der hier besprochenen Muster finden Sie unter soapatterns.org). Im Grunde genommen legt dieses Muster ein Vorgehen nahe, bei dem die Dienste redundant bereitgestellt werden, um Skalierbarkeit und Ausfallsicherung zu verbessern. Die Firma Sesame Hosting untersucht diese Option, kann es sich aber nicht leisten, ihre Infrastruktur zu erweitern, um redundante Dienstbereitstellungen unterbringen zu können. Sie hat einfach nicht die Ressourcen bzw. das Budget zur Handhabung der zusätzlichen Hardware, operativen Softwarewartung und Netzwerkausstattung.

Auch der Zeitrahmen ist ein Problem. Selbst wenn Sesame Hosting die notwendige Infrastruktur zur Verfügung stellen könnte, könnte die Firma dies nicht rechtzeitig genug tun, um den Rolloutplan der Woodgrove Bank einhalten zu können. Allein das Erfordernis, Personal einstellen und schulen zu müssen, würde die Infrastrukturerweiterung weit über den Zeitplan der Woodgrove Bank hinaus verlängern.

Nachdem das Woodgrove Bank-Team erkannt hat, dass Sesame Hosting seine Anforderungen nicht würde erfüllen können, beginnt sich das Team mit der Option zu beschäftigen, seine Dienste in einer öffentlichen Cloud zu hosten. Die Plattform Windows Azure bietet eine Möglichkeit, Dienste zu virtualisieren, die naturgemäß das Muster der redundanten Implementierung anwenden. Dieses Feature von Windows Azure wird als bedarfsgesteuerte Anwendungsinstanz bezeichnet (und wurde im Mai 2009 besprochen). Dieses Feature und die Fähigkeit, die Datenzentren von Microsoft zu nutzen, ohne sich langfristig binden zu müssen, erscheint dem Woodgrove Bank-Team vielversprechend. Sehen wir uns genauer an, wie die Woodgrove Bank ihre Lösung zu Windows Azure migriert.

Bereitstellungsgrundlagen

Der erste Tagesordnungspunkt besteht darin, einen Webdienst bereitzustellen, indem ein Contract-First-Ansatz entsprechend dem Prinzip des standardisierten Dienstvertrags verfolgt wird. Das Team generiert mit dem WSCF.blue-Tool WCF-Verträge (Windows Communication Foundation) aus WSDL und XSDs, die auf optimale Interoperabilität ausgelegt sind. Die Dienstverträge sind in Abbildung 1 dargestellt.

Abbildung 1 Die anfänglichen Dienstverträge

image: The Initial Service Contracts

Weil sich die Dienste im Lauf der Zeit ändern und weiterentwickeln müssen, beschließen die Entwickler auch, dass ihre Datenverträge die IExtensibleObject-Schnittstelle zur Unterstützung des Aufwärtskompatibilitätsmusters (Forward Compatibility) implementieren sollen (siehe Abbildung 2).

Abbildung 2 Die anfänglichen Datenverträge

image: The Initial Data Contracts

Zum Speichern der notwendigen Daten wollte das Woodgrove Bank-Team SQL Azure verwenden, weil bereits eine Datenbankstruktur vorhanden war, die das Team beibehalten wollte. Wenn die Entwickler einen nichtrelationalen Datenspeicher verwenden könnten, würden sie möglicherweise stattdessen Windows Azure Storage in Betracht ziehen.

Die Architekten der Woodgrove Bank fahren mit der Erstellung eines Clouddiensts auf der Grundlage einer Visual Studio-Vorlage fort und veröffentlichen ihn mit Visual Studio. Sie melden sich dann beim Windows Azure-Portal an, um ihren neuen Clouddienst zu erstellen (siehe Abbildung 3).

Abbildung 3 Erstellen eines Diensts im Windows Azure-Portal

image: Creating a Service in the Windows Azure Portal

Als Nächstes wird ein Bildschirm angezeigt, der es ihnen ermöglicht, den Dienst bereitzustellen. Sie klicken auf die Schaltfläche "Bereitstellen" und geben ein Anwendungspaket, Konfigurationseinstellungen und einen Bereitstellungsnamen an. Nach einigen weiteren Mausklicks befindet sich der Dienst in der Cloud.

Abbildung 4 zeigt ein Beispiel für die Dienstkonfiguration.

Abbildung 4 Dienstkonfiguration in Windows Azure

<Role name="BankWebRole">
  <Instances count="1" />
  <ConfigurationSettings>
    <Setting 
      name="DataConnectionString" 
      value="DefaultEndpointsProtocol=https;AccountName=YOURACCOUNTNAME;AccountKey=YOURKEY" />
    <Setting 
      name="DiagnosticsConnectionString" 
      value="DefaultEndpointsProtocol=https;AccountName=YOURDIAGNOSTICSACCOUNTNAME;AccountKey=YOURKEY" />

Das folgende Konfigurationselement ist ausschlaggebend dafür, dass die Lösung hinsichtlich der Anforderungen an die Skalierbarkeit, die die Woodgrove Bank stellt, elastisch ist:

<Instances count="1" />

Wenn die Entwickler beispielsweise 10 Instanzen wünschen, dann würde dieses Element wie folgt festgelegt:

<Instances count="10" />

Abbildung 5 zeigt den Bildschirm, der bestätigt, dass gegenwärtig nur eine Instanz ausgeführt wird. Durch Klicken auf die Schaltfläche "Konfigurieren" wird ein Bildschirm geöffnet, in dem die Dienstkonfiguration bearbeitet und die Instanzeneinstellung nach Bedarf geändert werden können.

Abbildung 5 In Windows Azure ausgeführte Instanzen

Leistung und Flexibilität

Nach einigen Belastungstests stellte das Woodgrove Bank-Entwicklungsteam fest, dass die Nutzung lediglich eines zentralen Datenspeichers in SQL Azure mit zunehmendem Datenverkehr zu immer langsameren Antwortzeiten führte. Die Entwickler entschieden, dieses Leistungsproblem durch die Verwendung des Windows Azure-Tabellenspeichers zu lösen, der darauf ausgelegt ist, die Skalierbarkeit durch die Verteilung der Partitionen auf viele Speicherknoten zu verbessern. Der Windows Azure-Tabellenspeicher bietet zudem schnellen Datenzugriff, weil das System die Nutzung der Partitionen überwacht und einen automatischen Lastenausgleich durchführt. Weil der Windows Azure-Tabellenspeicher kein relationaler Datenspeicher ist, musste das Team einige neue Datenspeicherstrukturen entwerfen und eine Kombination von Partitionen und Zeilenschlüsseln auswählen, die gute Antwortzeiten bieten würde.

Sie entscheiden sich schließlich für drei Tabellen, wie in Abbildung 6 gezeigt. UserAccountBalance dient zum Speichern der Benutzerkontostände. AccountTransactionLogg wird zum Speichern aller Transaktionsnachrichten für bestimmte Konten verwendet. Die UserAccountTransaction-Tabelle wird zum Speichern der Kontotransaktionen genutzt. Die Partitionsschlüssel für die Tabellen UserAccountTransaction und AccountTransactionLogg wurden durch eine Verkettung von UserId und AccountId erstellt, weil diese Teil aller Abfragen sind und schnelle Antwortzeiten ermöglichen können. Der Partitionsschlüssel für die UserAccountBalance-Tabelle ist UserId und der Zeilenschlüssel ist AccountId. Diese Kombination ermöglicht die eindeutige Identifizierung eines Benutzers und dessen Kontos.

Abbildung 6 Tabellenspeichermodelle in Windows Azure

image: Windows Azure Table Storage Models

Die Woodgrove Bank betrachtet das Projekt bislang als gelungen und möchte daher, dass mehr Kunden die Lösung zu nutzen beginnen. Bald ist World Wide Importers bereit, mitzumachen, stellt jedoch einige neue Anforderungen an die Funktionalität.

Die Forderung, die am schwerwiegendsten zu sein scheint, beinhaltet die Änderung der Dienstschnittstelle (oder Informationsstruktur). Nach Aussage der Firma World Wide Importers ist die von der Woodgrove Bank verwendete Informationsstruktur nicht mit ihrer kompatibel. Wegen der Bedeutung dieses speziellen Kunden schlägt das Entwicklungsteam der Woodgrove Bank vor, das Datenmodelltransformationsmuster (Data Model Transformation) zu verwenden. Die Entwickler würden mehrere neue Dienste mit den von World Wide Importers geforderten Schnittstellen erstellen, und diese Dienste würden Logik enthalten, um Anforderungen von Datenmodellen von World Wide Importers in die Datenmodelle der Woodgrove Bank und in umgekehrter Richtung übersetzen zu können.

Um diese Anforderung zu erfüllen, wird eine neue Struktur für UserAccount erstellt. Die Entwickler stellen sorgfältig sicher, dass eine klare Zuordnung zwischen den Klassen UserAccountWwi und UserAccount gegeben ist (siehe Abbildung 7).

Abbildung 7 UserAccount-Struktur für die Datenmodelltransformation

image: UserAccount Structure for Data Model Transformation

Die Dienstverträge müssen einen bestimmten Datenvertrag akzeptieren (UserAccountWwi), der an UserAccount gerichtete Anforderungen transformiert, bevor er den Aufruf an andere Teile der Lösung weiterleitet und anschließend die Antwort wieder zurückübersetzt. Die Architekten der Woodgrove Bank erkennen, dass sie bei der Implementierung dieser neuen Anforderungen eine grundlegende Dienstschnittstelle wiederverwenden können. Der fertige Entwurf ist in Abbildung 8 dargestellt.

Abbildung 8 Dienstverträge für World Wide Importers

image: Service Contracts for World Wide Importers

Die Entwickler beschließen, die Datentransformation zu implementieren, indem sie einige Erweiterungsmethoden für die UserAccount-Klasse erstellen, darunter die Methoden TransformToUserAccountWwi und TransformToUserAccount.

Der neue Dienst akzeptiert den UserAccountWwi-Datenvertrag. Bevor Anforderungen an andere Ebenen gesendet werden, werden die Daten in UserAccount durch einen Aufruf der TransformToUserAccount-Erweiterungsmethode transformiert. Bevor eine Antwort an den Consumer gesendet wird, wird der UserAccount-Vertrag durch einen Aufruf von TransformToUserAccountWwi in das Format von UserAccountWwi zurückverwandelt. Einzelheiten zu diesen Elementen können Sie dem Quellcode für UserAccountServiceAdvanced im Codedownload für diesen Artikel entnehmen.

Messaging und Queuing

Obwohl die Woodgrove Bank jetzt gut aufgestellt und in der Lage ist, eine Vielzahl eingehender Anforderungen zu verarbeiten, haben die Analysten deutliche Spitzen in der Dienstnutzung bemerkt. Einige dieser Spitzen treten regelmäßig auf (speziell montagmorgens und donnerstagnachmittags). Einige Schwankungen sind allerdings nicht vorherzusagen.

Eine einfache Lösung bestünde darin, über die Windows Azure-Konfiguration mehr Ressourcen online zur Verfügung zu stellen. Nachdem jetzt aber einige große Kunden wie World Wide Importers an den neuen Diensten interessiert sind, werden die Schwankungen in der gleichzeitigen Nutzung wahrscheinlich noch größer werden.

Die Entwickler der Woodgrove Bank haben sich das Leistungsspektrum von Windows Azure genauer angesehen und Features entdeckt, die die Anwendung des Musters für zuverlässiges Messaging und asynchrones Queuing (Reliable Messaging and Asynchronous Queuing) erlaubten. Sie entschieden, dass zuverlässiges Messaging nicht die optimale Wahl darstellte, da die technischen Möglichkeiten der Kunden dadurch eingeschränkt wurden. Asynchrones Queuing erfordert keine spezielle Technologie seitens der Kunden, und daher wollten sie sich darauf konzentrieren. In der Windows Azure-Cloud war zuverlässiges Messaging jedoch ideal, da die gesamte verwendete Technologie von Microsoft bereitgestellt wurde.

Das Ziel besteht darin, dass keine Nachricht verloren gehen soll, selbst wenn die Dienste wegen Fehlerbedingungen oder geplanter Wartungsmaßnahmen offline sind. Das Muster für asynchrones Queuing ermöglicht dies, allerdings passen einige Angebote nicht zu diesem Muster. Beispielsweise sind bei der Abwicklung von Onlinekartentransaktionen sofortige Antworten zur Bestätigung oder Verweigerung von Geldüberweisungen erforderlich. In anderen Situationen wäre das Muster jedoch passend.

Die Kommunikation zwischen den Rollen Web und Worker (unter msdn.microsoft.com/magazine/dd727504 werden diese Rollen erläutert) erfolgt mithilfe von Windows Azure-Warteschlangen (seit der November CTP-Version ist eine direkte Kommunikation zwischen Rolleninstanzen möglich), die standardmäßig sowohl asynchron als auch zuverlässig sind. Das heißt aber nicht, dass die Kommunikation zwischen den Endbenutzern und den Diensten der Woodgrove Bank automatisch zuverlässig ist. In der Tat sind einige Kommunikationsstränge zwischen dem Client und den Diensten, die der Webrolle eigen sind, eindeutig unzuverlässig. Das Team der Woodgrove Bank beschloss, nicht darauf einzugehen, weil eine bis zum Kunden durchgängige Implementierung von Mechanismen zur Gewährleistung der Zuverlässigkeit in der Praxis bedeuten würde, dass die Kunden dieselbe Technologie wie die Woodgrove Bank einsetzen müssten. Dies wurde als unrealistisch und unerwünscht erachtet.

Einsetzen von Warteschlangen

Sobald ein Kunde eine Nachricht an UserAccountService sendet, wird diese Nachricht in eine Windows Azure-Warteschlange eingefügt, und der Kunde erhält eine Bestätigungsnachricht. UserAccountWorker kann dann die Nachricht aus der Warteschlange abrufen. Falls UserAccountWorker offline ist, geht die Nachricht nicht verloren, da sie in der Warteschlange sicher gespeichert wird.

Wenn während der Verarbeitung in UserAccountWorker ein Fehler auftritt, wird die Nachricht nicht aus der Warteschlange entfernt. Um dies sicherzustellen, wird die DeleteMessage-Methode der Warteschlange erst aufgerufen, nachdem die Arbeit abgeschlossen wurde. Wenn UserAccountWorker die Nachrichtenverarbeitung nicht vor Ablauf des Zeitlimits abschließen kann (das Zeitlimit ist hartcodiert und beträgt 20 Sekunden), dann wird die Nachricht wieder in der Warteschlange angezeigt, sodass eine andere Instanz von UserAccountWorker versuchen kann, sie zu verarbeiten.

Sobald ein Kunde eine Nachricht an UserAccountService sendet, wird diese Nachricht in eine Warteschlange eingefügt, und der Kunde erhält eine Bestätigungsnachricht vom Typ TransactionResponse. Aus der Sicht der Kunden wird asynchrones Queuing verwendet. Zuverlässiges Messaging wird in der Kommunikation zwischen UserAccountStorageAction und AccountStorageWorker eingesetzt, die in der Rolle Web bzw. Worker angesiedelt sind. Nachfolgend wird gezeigt, wie die Nachricht in die Warteschlange eingefügt wird:

public TransactionResponse ReliableInsertMoney(
  AccountTransactionRequest accountTransactionrequest) {

//last parameter (true) means that we want to serialize
//message to the queue as XML (serializeAsXml=true)
  return UserAccountHandler.ReliableInsertMoney(
    accounttransactionRequest.UserId, 
    accounttransactionRequest.AccountId, 
    accounttransactionRequest.Amount, true);
}

UserAccountHandler ist eine Eigenschaft, die eine IUserAccountAction-Instanz zurückgibt, die zur Laufzeit eingefügt wird. Dadurch lässt sich die Implementierung leichter vom Vertrag trennen und zu einem späteren Zeitpunkt ändern:

public IUserAccountAction<Models.UserAccount> UserAccountHandler
  {get;set;}

public UserAccountService(
  IUserAccountAction<Models.UserAccount> action) {

  UserAccountHandler = action;
}

Nachdem die Nachricht an eine der zuständigen Aktionen gesendet wurde, wird sie in die Warteschlange eingefügt. Die erste Methode in Abbildung 9 zeigt, wie Daten als serialisierbares XML gespeichert werden können, und die zweite Methode zeigt, wie Daten als Zeichenfolge in der Warteschlange gespeichert werden können. Beachten Sie, dass für Windows Azure-Warteschlangen eine Beschränkung der Nachrichtengröße auf 8 KB gilt.

Abbildung 9 Datenspeicherung

public TransactionResponse ReliableHandleMoneyInQueueAsXml( 
  UserAccountTransaction accountTransaction){ 

  using (MemoryStream m = new MemoryStream()){ 
    XmlSerializer xs = 
      new XmlSerializer(typeof(UserAccountTransaction)); 
    xs.Serialize(m, accountTransaction); 

    try 
    { 
      QueueManager.AccountTransactionsQueue.AddMessage( 
        new CloudQueueMessage(m.ToArray())); 
      response.StatusForTransaction = TransactionStatus.Succeded; 
    } 
    catch(StorageClientException) 
    { 
      response.StatusForTransaction = TransactionStatus.Failed; 
      response.Message = 
        String.Format("Unable to insert message in the account transaction queue userId|AccountId={0}, messageId={1}", 
        accountTransaction.PartitionKey, accountTransaction.RowKey); 
    } 
  } 
  return response; 
} 

public TransactionResponse ReliableHandleMoneyInQueue( 
  UserAccountTransaction accountTransaction){ 

  TransactionResponse response = this.CheckIfTransactionExists( 
    accountTransaction.PartitionKey, accountTransaction.RowKey); 
       
  if (response.StatusForTransaction == TransactionStatus.Proceed) 
  { 
    //userid|accountid is partkey 
    //userid|accountid|transactionid|amount 
    string msg = string.Format("{0}|{1}|{2}", 
      accountTransaction.PartitionKey, 
      accountTransaction.RowKey, 
      accountTransaction.Amount); 

    try 
    { 
      QueueManager.AccountTransactionsQueue.AddMessage( 
        new CloudQueueMessage(msg)); 
      response.StatusForTransaction = TransactionStatus.Succeded; 
    } 
    catch(StorageClientException) 
    { 
      response.StatusForTransaction = TransactionStatus.Failed; 
      response.Message = 
        String.Format("Unable to insert message in the account transaction queue userId|AccountId={0}, messageId={1}", 
        accountTransaction.PartitionKey, accountTransaction.RowKey); 
    } 
  } 
  return response; 
}

Die QueueManager-Klasse initialisiert Warteschlangen mithilfe der Definitionen aus der Konfiguration:

CloudQueueClient queueClient = 
  CloudStorageAccount.FromConfigurationSetting(
    "DataConnectionString").CreateCloudQueueClient();

accountTransQueue = queueClient.GetQueueReference(
  Helpers.Queues.AccountTransactionsQueue);
accountTransQueue.CreateIfNotExist();

loggQueue = queueClient.GetQueueReference(
  Helpers.Queues.AccountTransactionLoggQueue);
loggQueue.CreateIfNotExist();

AccountStorageWorker überprüft die AccountTransactionQueue-Warteschlange auf Nachrichten und ruft die Nachrichten aus der Warteschlange ab. Um dies leisten zu können, muss AccountStorageWorker die richtige Warteschlange öffnen:

var storageAccount = CloudStorageAccount.FromConfigurationSetting(
  "DataConnectionString");
// initialize queue storage 
CloudQueueClient queueStorage = storageAccount.CreateCloudQueueClient();
accountTransactionQueue = queueStorage.GetQueueReference(
  Helpers.Queues.AccountTransactionsQueue);

Nachdem die Warteschlange geöffnet wurde und AccountStorageWorker die Nachricht gelesen hat, ist die Nachricht 20 Sekunden lang (das Sichtbarkeitszeitlimit wurde auf 20 Sekunden festgelegt) nicht in der Warteschlange sichtbar. Innerhalb dieses Zeitraums versucht der Worker, die Nachricht zu verarbeiten.

Wenn die Nachricht erfolgreich verarbeitet werden kann, wird sie aus der Warteschlange gelöscht. Schlägt die Nachrichtenverarbeitung fehl, wird sie wieder in die Warteschlange eingefügt.

Nachrichtenverarbeitung

Die ProcessMessage-Methode muss zuerst den Inhalt der Nachricht erhalten. Dies kann auf zweierlei Weise geschehen: Erstens kann die Nachricht als Zeichenfolge in der Warteschlange gespeichert werden:

//userid|accountid|transactionid|amount
var str = msg.AsString.Split('|');...

Zweitens kann die Nachricht serialisiertes XML enthalten:

using (MemoryStream m = 
  new MemoryStream(msg.AsBytes)) {

  if (m != null) {
    XmlSerializer xs = new XmlSerializer(
      typeof(Core.TableStorage.UserAccountTransaction));
    var t = xs.Deserialize(m) as 
      Core.TableStorage.UserAccountTransaction;

    if (t != null) { ....... }
  }
}

Falls AccountStorageWorker aus irgendeinem Grund offline oder nicht in der Lage ist, die Nachricht zu verarbeiten, geht keine Nachricht verloren, da sie in der Warteschlange gespeichert wird. Wenn die Verarbeitung in AccountStorageWorker fehlschlägt, wird die Nachricht nicht aus der Warteschlange entfernt, sondern wird nach 20 Sekunden wieder in der Warteschlange sichtbar.

Um dieses Verhalten sicherzustellen, wird die DeleteMessage-Methode der Warteschlange nur aufgerufen, nachdem die Arbeit abgeschlossen wurde. Wenn AccountStorageWorker die Nachrichtenverarbeitung nicht vor Ablauf des Zeitlimits abschließen kann, wird die Nachricht erneut in der Warteschlange angezeigt, damit eine andere Instanz von AccountStorageWorker versuchen kann, sie zu verarbeiten. Abbildung 10 zeigt, wie eine als Zeichenfolge gespeicherte Nachricht verarbeitet wird.

Abbildung 10 Verarbeiten von Nachrichten, die sich in einer Warteschlange befinden

if (str.Length == 4){
  //userid|accountid|transactionid|amount
  UserAccountSqlAzureAction ds = new UserAccountSqlAzureAction(
    new Core.DataAccess.UserAccountDB("ConnStr"));
  try
  {
    Trace.WriteLine(String.Format("About to insert data to DB:{0}", str),      
      "Information");
    ds.UpdateUserAccountBalance(new Guid(str[0]), new Guid(str[1]), 
      double.Parse(str[3]));
    Trace.WriteLine(msg.AsString, "Information");
    accountTransactionLoggQueue.DeleteMessage(msg);
    Trace.WriteLine(String.Format("Deleted:{0}", str), "Information");
  }
  catch (Exception ex)
  {
    Trace.WriteLine(String.Format(
      "fail to insert:{0}", str, ex.Message), "Error");
  }
}

Idempotenz

Was passiert, wenn ein Kunde der Woodgrove Bank Geld von einem Konto auf ein anderes Konto überweisen möchte und die Nachricht mit dieser Anforderung verloren geht? Wenn der Kunde die Nachricht erneut sendet, kommen möglicherweise mehrere Kopien dieser Anforderung beim Dienst an, die separat gehandhabt werden.

Ein Mitglied des Teams der Woodgrove Bank identifizierte dieses Szenario sofort als eines der Szenarien, in denen das Idempotenzmuster (Idempotent Capability) erforderlich war. Dieses Muster fordert, dass Funktionen oder Vorgänge derart implementiert werden, dass sie auf sichere Weise wiederholt werden können. Kurz gesagt, die Lösung, die die Woodgrove Bank implementieren möchte, erfordert Clients, die jeder Anforderung eine eindeutige ID zuordnen und garantieren, dass sie im Wiederholungsfall die genau gleiche Nachricht erneut senden, einschließlich derselben eindeutigen ID. Hierzu wird die eindeutige ID im Windows Azure-Tabellenspeicher gespeichert. Bevor eine Anforderung verarbeitet wird, muss überprüft werden, ob bereits eine Nachricht mit dieser ID verarbeitet wurde. Falls eine solche Nachricht verarbeitet wurde, wird die entsprechende Antwort erstellt, die erneut gesendete Anforderung wird jedoch nicht nochmals verarbeitet.

Obwohl dies bedeutet, dass der zentrale Datenspeicher mit zusätzlichen Abfragen belastet wird, wurde es als notwendig erachtet. Die Leistung wird dadurch etwas beeinträchtigt, da einige Abfragen an den zentralen Datenspeicher gesendet werden, bevor weitere Verarbeitungsschritte ausgeführt werden können. Es ist jedoch vernünftig, diesen zusätzlichen Aufwand an Zeit und Ressourcen zuzulassen, um die Anforderungen der Woodgrove Bank erfüllen zu können.

Das Team der Woodgrove Bank aktualisierte die Methoden ReliableInsertMoney und ReliableWithDrawMoney in der IUserAccountAction und deren Implementierungen, indem es eine Transaktions-ID hinzufügte:

TransactionResponse ReliableInsertMoney(
  Guid userId, Guid accountId, Guid transactionId, 
  double amount, bool serializeToQueue);

TransactionResponse ReliableWithDrawMoney(
  Guid userId, Guid accountId, Guid transactionId, 
  double amount, bool serializeToQueue);

Die UserAccountTransaction-Tabelle (Windows Azure-Speicher) wurde aktualisiert, indem TransactionId als RowKey hinzugefügt wurde, damit jede Einfügung in die Tabelle über eine eindeutige Transaktions-ID verfügte.

Die Verantwortung für das Senden der Nachrichten-ID für jede eindeutige Transaktion wird dem Client übertragen:

WcfClient.Using(new AccountServiceClient(), client =>{ 
  using (new OperationContextScope(client.InnerChannel)) 
  { 
    OperationContext.Current.OutgoingMessageHeaders.MessageId = 
      messageId; 
    client.ReliableInsertMoney(new AccountTransactionRequest { 
      UserId = userId, AccountId = accountId, Amount = 1000 }); 
  } 
});

Sie finden die hier verwendete Hilfsklasse unter soamag.com/I32/0909-4.asp.

Die IUserAccountService-Definition wurde nicht verändert. Zur Implementierung dieser Funktionalität muss lediglich die vom Client gesendete MessageId aus den eingehenden Nachrichtenheadern eingelesen und in der Verarbeitung im Hintergrund verwendet werden (siehe Abbildung 11).

Abbildung 11 Erfassen der MessageId-Werte

public TransactionResponse ReliableInsertMoney(
  AccountTransactionRequest accountTransactionrequest) {
  var messageId = 
    OperationContext.Current.IncomingMessageHeaders.MessageId;
  Guid messageGuid = Guid.Empty;
  if (messageId.TryGetGuid(out messageGuid))
    //last parameter (true) means that we want to serialize
    //message to the queue as XML (serializeAsXml=true)
    return UserAccountHandler.ReliableInsertMoney(
      accounttransactionRequest.UserId, 
      accounttransactionRequest.AccountId, messageId, 
      accounttransactionRequest.Amount, true);
  else 
    return new TransactionResponse { StatusForTransaction = 
      Core.Types.TransactionStatus.Failed, 
      Message = "MessageId invalid" };      
}

Die aktualisierte IUserAccountAction-Schnittstelle erhält jetzt für jeden idempotenten Vorgang eine Transaktions-ID. Wenn der Dienst einen idempotenten Vorgang abzuschließen versucht, überprüft er, ob die Transaktion bereits im Tabellenspeicher vorhanden ist. Wenn die Transaktion bereits vorhanden ist, gibt der Dienst die Nachricht der Transaktion zurück, die in der AccountTransactionLogg-Tabelle gespeichert wurde. Die Transaktions-ID wird als RowKey in der UserAccountTransaction-Tabelle gespeichert. Um den richtigen Benutzer und das richtige Konto zu finden, sendet der Dienst den Partitionsschlüssel (userid|accountid). Wird die Transaktions-ID nicht gefunden, dann wird die Nachricht zur weiteren Verarbeitung in die AccountTransactionsQueue-Warteschlange eingefügt:

public TransactionResponse ReliableHandleMoneyInQueueAsXml(
  UserAccountTransaction accountTransaction) {
  TransactionResponse response = this.CheckIfTransactionExists(
    accountTransaction.PartitionKey, accountTransaction.RowKey);
  if(response.StatusForTransaction == TransactionStatus.Proceed) {
    ...
  }
  return response;
}

Mithilfe der CheckIfTransactionExists-Methode (siehe Abbildung 12) wird sichergestellt, dass die Transaktion noch nicht verarbeitet wurde. Die Methode versucht, die Transaktions-ID für ein bestimmtes Benutzerkonto zu suchen. Wird die Transaktions-ID gefunden, dann erhält der Client eine Antwortnachricht mit den Details der bereits abgeschlossenen Transaktion:

Abbildung 12 Überprüfen von Transaktionsstatus und -ID

private TransactionResponse CheckIfTransactionExists(
  string userIdAccountId, string transId) {

  TransactionResponse transactionResponse = 
    new Core.Models.TransactionResponse();

  var transaction = this.TransactionExists(userIdAccountId, transId);
  if (transaction != null) {
    transactionResponse.Message = 
      String.Format("messageId:{0}, Message={1}, ", 
      transaction.RowKey, transaction.Message);
    transactionResponse.StatusForTransaction = 
      TransactionStatus.Completed;
  }
  else
    transactionResponse.StatusForTransaction = 
      TransactionStatus.Proceed;
  return transactionResponse;
}

private UserAccountTransaction TransactionExists(
  string userIdAccountId, string transId) {
  UserAccountTransaction userAccountTransaction = null;
  using (var db = new UserAccountDataContext()) {
    try {
      userAccountTransaction = 
        db.UserAccountTransactionTable.Where(
        uac => uac.PartitionKey == userIdAccountId && 
        uac.RowKey == transId).FirstOrDefault();
      userAccountTransaction.Message = "Transaction Exists";
    }
    catch (DataServiceQueryException e) {
      HttpStatusCode s;
      if (TableStorageHelpers.EvaluateException(e, out s) && 
        s == HttpStatusCode.NotFound) {
        // this would mean the entity was not found
        userAccountTransaction = null;
      }
    }
  }
  return userAccountTransaction;
}

Eine interessante Eigenschaft von CheckIfTransactionExists ist, dass Windows Azure Storage den HTTP-Statuscode 404 zurückgibt, wenn die gewünschten Daten nicht gefunden werden (weil eine REST-Schnittstelle verwendet wird). Wenn die Daten nicht gefunden werden, wird von den ADO.NET-Clientdiensten (System.Data.Services.Client) außerdem eine Ausnahme ausgelöst.

Weitere Informationen

Weitere Informationen zur Implementierung dieser Lösung zum Machbarkeitsnachweis finden Sie in dem online verfügbaren Quellcode. Eine Beschreibung der SOA-Muster wurde veröffentlicht unter soapatterns.org. Falls Sie Fragen haben, wenden Sie sich an herbjorn@wilhelmsen.se.

Arman Kurtagić* ist Unternehmensberater und konzentriert sich auf die neuen Microsoft-Technologien. Er ist Mitarbeiter von Omegapoint, einem Unternehmen, das geschäftsorientierte, sichere IT-Lösungen anbietet. Er hat verschiedene Funktionen bekleidet, einschließlich Entwickler, Architekt, Mentor und Unternehmer, und in verschiedenen Branchen, darunter Finanzen, Gaming, Medien, Erfahrungen gesammelt.*

Herbjörn Wilhelmsen* ist Unternehmensberater und für die in Stockholm ansässige Forefront Consulting Group tätig. Seine Schwerpunktbereiche sind dienstorientierte Architekturen und Geschäftsarchitekturen. Wilhelmsen ist Vorsitzender der SOA Patterns Review-Kommission und leitet gegenwärtig auch die Business 2 IT-Gruppe im schwedischen Ortsverband der IASA. Er ist Mitverfasser des Buchs* SOA with .NET and Azure*, das im Rahmen der Reihe *Prentice Hall Service-Oriented Computing from Thomas Erl erschienen ist.

Thomas Erl* ist Bestsellerautor zum Thema SOA, Autor der Buchreihe*  Prentice Hall Service-Oriented Computing from Thomas Erl und Herausgeber der Zeitschrift SOA Magazine. Erl ist Gründer von SOA Systems Inc. und des SOASchool.com SOA Certified Professional-Programms. Erl ist Gründer der Arbeitsgruppe SOA Manifesto und Referent und Kursleiter bei privaten und öffentlichen Veranstaltungen. Weitere Informationen finden Sie unter thomaserl.com.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Steve Marx