Autoscaling für Windows Azure in 7 Zeilen Code – Schritt für Schritt Anleitung

Das Thema “Autoscaling in Azure” ist beinahe so alt wie die Windows Azure Platform. Verspricht das Cloud Paradigma nicht ohnehin “quasi beliebige” Skalierbarkeit der eingesetzten Ressourcen, die eine Optimierung des Ressourceneinsatzen in Abhängigkeit von der auf dem Service anliegenden Last ermöglicht. Grundsätzlich wird also eine Implementierung für die folgende Logik benötigt:

    1: while(true)
    2: {
    3:     if(loadApproachesLimit)
    4:     {
    5:         adjustResources();
    6:     }
    7:     sleep(interval);
    8: }

Listing 1: abstrakte Darstellung der Skalierung

In regelmäßigen Intervallen (Zeile 7: interval) soll also geprüft werden, ob ein – wie auch immer spezifiziertes – Last-Limit erreicht wurde (Zeile 3). Sofern dies der Fall ist, sollen die eingesetzten Ressourcen angepasst werden (Zeile 5). Ziel ist zu jedem Zeitpunkt, so wenige Ressourcen wie möglich und so viele Ressourcen wie nötig einzusetzen. Nun gibt es bereits eine Reihe von Blog-Posts, die diese Thematik im Zusammenhang mit Azure aufgreifen, nur leider beschreiben diese Blog-Einträge zumeist nur wie dies in Azure konzeptionell möglich ist, d.h.

Leider werden kaum konkrete Implementierungen aufgeführt. Da hierbei auch vielfach REST-API-Aufrufe (zum Auslesen der Diagnoseinformationen, zum Anpassen der Ressourcen etc.) involviert sind, ist die Implementierung auch etwas hakelig. In diesem Artikel sollen nun die aus unterschiedlichen Quellen zusammengetragenen Erkenntnisse zusammengefasst werden und eine Implementierung der folgenden Form hergeleitet werden:

    1: while (true)
    2: {
    3:     double cpuLoad = perfCtr.CurrentAverageValue();
    4:     if (cpuLoad > 0.6)
    5:     {
    6:         sub.HostedService(hostedServiceName).Deployment(deploymentName).Configuration.incrementNumberOfInstances(roleName);
    7:         sub.HostedService(hostedServiceName).Deployment(deploymentName).Change();
    8:         Thread.Sleep(900000);
    9:     }
   10:     Thread.Sleep(10000);
   11: }

Listing 2: Ziel der Implementierung – Autoscaling in 7 Zeilen

Sicherlich ist dieses Code-Fragment stark vereinfacht. Die Skalierung geht nur nach oben, die Wartezyklen zwischen den Abrufen des PerformanceCounters sind zu grobgranular… – für den Hausgebrauch soll dies aber ausreichen. Ziel soll ja nur die Erläuterung der Grundprinzipien und das Aufzeigen von Implementierungsmöglichkeiten sein.

Szenarien für die Ausführung des Scaling-Agents

Grundsätzlich wird für die Autoskalierung eine Agentensoftware benötigt, der die Lastinformationen des zu skalierenden Services in festen Intervallen ausliest und beim Erreichen bestimmter Schwellwerte eine Änderung der Konfiguration veranlasst. Die Funktionsweise ist hierbei wie in Abbildung 1 gezeigt.

Verteilung der Anwendungskomponenten

Abbildung 1: Funktionsweise der Autoskalierung

Um die Diagnoseinformationen verfügbar zu machen, wird Windows Azure Diagnostics eingesetzt. Ein vom Cloud Service zu instanziierender Diagnostics Monitor lädt die benötigten Diagnosedaten in regelmäßigen Abständen in Windows Azure Table Storage (1). Von dort liest der Scaling Agent in entsprechenden Intervallen die Daten aus (2) und ermittelt, ob der Diagnosewert einen Schwellwert erreicht hat, der eine Konfigurationsänderung auslösen soll. Beim Erreichen des Schwellwerts ruft der Agent den Service Management Service (SMS) über die Windows Azure Management API auf (3) und aktualisiert darüber die Konfiguration des Cloud Service. Der SMS veranlasst dann die Umsetzung der Konfigurationsänderung (4), d.h. beispielsweise startet er zusätzliche Instanzen bzw. reduziert die Zahl der Instanzen.

Der Scaling Agent kann im Prinzip auf einem beliebigen System ausgeführt werden, von dem aus Windows Azure Table Storage und der SMS über REST-Aufrufe möglich ist. Beispiele für den Ausführungsort sind:

  • Ausführung auf einem lokalen Client-System (z.B. Windows 7, Windows Server)
  • Ausführung auf Windows Azure (z.B. in einer Worker Role in einer Extra Small Instance)

Im Folgenden werden die Schritte zum Aufsetzen bzw. der Implementierung der Aktionen (1-3 aus Abbildung 1) erläutert.

Aktivierung der Azure Diagnostics im zu skalierenden Cloud Service

Damit der Scaling-Agent später die benötigten Diagnosedaten (z.B. Performance Counter) auslesen kann, muss der zu überwachende Cloud Service zunächst das Diagnostics System aktivieren. Dies kann nach [1] auf mehrere Arten geschehen. Die einfachste Möglichkeit ist seit Azure SDK 1.3, in Visual Studio bei dem Eigenschaften der betreffenden Rolle den Haken bei “Enable Diagnostics” zu setzen (siehe Abbildung 2) und die Informationen zum Storage Account, in den die Diagnosedaten geschrieben werden sollen, einzutragen.

Diagnostics

Abbildung 2: Aktivierung der Azure Diagnostics

Prinzipiell ist damit die Konfiguration des Cloud Service abgeschlossen. Alles weitere (wie z.B. die Konfiguration der Diagnosedaten, die geloggt werden sollen) kann theoretisch auch “remote” erfolgen. Z.B. könnte dies auch durch den Scaling Agent erfolgen. Der Einfachheit halber kann dies aber auch im Cloud Service selbst konfiguriert werden. Dies geschieht in der OnStart()-Methode der betreffenden Rolle.

    1: public override bool OnStart()
    2: {
    3:     var config = DiagnosticMonitor.GetDefaultInitialConfiguration();
    4:  
    5:     config.PerformanceCounters.DataSources.Add(
    6:         new PerformanceCounterConfiguration()
    7:         {
    8:             CounterSpecifier = @"\Processor(_Total)\% Processor Time",
    9:             SampleRate = TimeSpan.FromSeconds(5)
   10:         }
   11:     );
   12:  
   13:     config.PerformanceCounters.ScheduledTransferPeriod = System.TimeSpan.FromMinutes(1.0);
   14:     DiagnosticMonitor.Start("Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString", config);
   15:  
   16:     return base.OnStart();
   17: }

Listing 3: Konfiguration der zu exportierenden Diagnosedaten

In Listing 3 wird gezeigt, wie dem Diagnostics Monitor (der ja über das Setzen des Hakens vorhanden ist) so konfiguriert wird, dass in Fünf-Sekunden-Abständen der Wert der Prozessorauslastung bestimmt und lokal zwischengespeichert wird, und die gesammelten Werte ein Mal pro Minute in Windows Azure Table Storage (im Falle der Performance Counter in die Tabelle “WADPerformanceCountersTable”) übertragen werden.

Auslesen der erforderlichen Performance Counter

Der Scaling Agent kann diese Diagnosedaten nun mit Hilfe des im Windows Azure SDK enthaltenen StorageClient auslesen. Um diesen nutzen zu können und auf die Diagnosedaten zugreifen zu können, müssen folgende DLLs als Referenz der Scaling Agent Solution hinzugefügt werden:

  • Microsoft.WindowsAzure.Diagnostics.dll
  • Microsoft.WindowsAzure.ServiceRuntime.dll
  • Microsoft.WindowsAzure.StorageClient.dll

Außerdem müssen folgende Libraries hinzugefügt werden

  • System.Data
  • System.Data.Services.Client
  • System.Web

Jetzt kann im Scaling Agent die Methode CurrentAverageValue() implementiert werden. Zunächst sind zwei Hilfsklassen erforderlich: PerformanceDataEntity und PerformanceDataServiceContext. Die Implementierung der Klasse PerformanceDataEntity ist in Listing 4 zu sehen:

    1: public class PerformanceDataEntity : TableServiceEntity
    2: {
    3:     public Int64 EventTickCount { get; set; }
    4:     public string DeploymentId { get; set; }
    5:     public string Role { get; set; }
    6:     public string RoleInstance { get; set; }
    7:     public string CounterName { get; set; }
    8:     public double CounterValue { get; set; }
    9: }

Listing 4: Klasse PerformanceDataEntity

Diese Klasse bildet die Struktur bzw. das Schema der Windows Azure Table WADPerformanceCountersTable nach und wird im Folgenden benötigt, wenn über ADO.NET Data Services auf den Table Storage zugegriffen werden soll. Voraussetzung hierfür ist ein Kontext, über den die Zugriffe laufen. Dieser ist in der Klasse PerformanceDataServiceContext implementiert (siehe Listing 5).

    1: public class PerformanceDataServiceContext : TableServiceContext
    2: {
    3:     public PerformanceDataServiceContext(string baseAddress, StorageCredentials credentials)
    4:         : base(baseAddress, credentials)
    5:     {
    6:     }
    7:  
    8:     public IQueryable<PerformanceDataEntity> PerformanceData
    9:     {
   10:         get
   11:         {
   12:             return this.CreateQuery<PerformanceDataEntity>("WADPerformanceCountersTable");
   13:         }
   14:     }
   15: }

Listing 5: Klasse PerformanceDataServiceContext

Mit den in Listing 4 und 5 gezeigten Klassen kann nun der Zugriff auf die PerformanceCounter erfolgen. Dies geschieht in der Methode CurrentAverageValue().

    1: public double CurrentAverageValue()
    2: {
    3:     var storageCredentials = new StorageCredentialsAccountAndKey("[YOUR_STORAGE_ACCOUNT]", "[YOUR_STORAGE_ACCOUNT_KEY");
    4:     var account = new CloudStorageAccount(storageCredentials, true);
    5:     var context = new PerformanceDataServiceContext(account.TableEndpoint.ToString(), account.Credentials);
    6:  
    7:     DateTime now = DateTime.UtcNow;
    8:     DateTime nowMinus = now - TimeSpan.FromMinutes(4.0);
    9:  
   10:     string strNow = "0" + now.Ticks.ToString();
   11:     string strNowMinus = "0" + nowMinus.Ticks.ToString();
   12:  
   13:     var data = context.PerformanceData;
   14:  
   15:     List<PerformanceDataEntity> selectedData = (from d in data
   16:                                                 where d.CounterName == this.PerformanceCounterId
   17:                                                 && d.PartitionKey.CompareTo(strNowMinus) >= 0
   18:                                                 && d.PartitionKey.CompareTo(strNow) <= 0
   19:                                                 select d).ToList<PerformanceDataEntity>();
   20:  
   21:     double perfCounter = (from d in selectedData
   22:                           where d.CounterName == this.PerformanceCounterId
   23:                           select d.CounterValue).Average();
   24:  
   25:     return perfCounter;
   26: }

Listing 6: Methode CurrentAverageValue() zum Auslesen der Performance-Daten

Damit liegen nun die Informationen vor, die zur Entscheidung über die Skalierung benötigt werden. Die eigentliche Anpassung der Ressourcen im nächsten Abschnitt.

Dank für diese Codezeilen gebührt meinem Kollegen Yi-Lun Luo, der in seinem Forumsbeitrag auf MSDN wertvollen Input geliefert hat. Ebenso Dank an Joseph Fultz, der über seinen Blog ebenfalls viel Code beigesteuert hat.

Anpassung der Ressourcen

Die Anpassung der Ressourcen (im Beispiel die Erhöhung der Instanzenzahl) erfolgt in zwei Schritten

  1. Anpassung der Service-Konfiguration
  2. Aktualisierung des Service mit der neuen Konfiguration

Die Anpassung der Service-Konfiguration erfolgt in der Methode incrementNumberOfInstances(), deren Code in Listing 7 zu sehen ist.

    1: public int incrementNumberOfInstances(string roleName)
    2: {
    3:     int retValue = 0;
    4:     XNamespace ns = "https://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration";
    5:  
    6:     List<XElement> xRoles = configuration.Descendants(ns + "Role").ToList();
    7:  
    8:     foreach (var xRole in xRoles)
    9:     {
   10:         string name = xRole.Attribute("name").Value;
   11:         if (name.Equals(roleName))
   12:         {
   13:             int currentNumberOfInstances =
   14:                 int.Parse(xRole.Descendants(ns + "Instances").First().Attribute("count").Value);
   15:             int newNumberOfInstances = currentNumberOfInstances + 1;
   16:             xRole.Descendants(ns + "Instances").First().Attribute("count").Value = newNumberOfInstances.ToString();
   17:             return newNumberOfInstances;
   18:         }
   19:     }
   20:  
   21:     return retValue;
   22: }

Listing 7: Erhöhung der Instanzenzahl

Die Methode erfordert, dass im Feld configuration (vom Typ XDocument) das XML-Dokument der Service Konfiguration (also im Prinzip die *.cscfg-Datei des Deployments) vorliegt. Aus dieser wird zunächst der bisherige Wert für die Instanzenzahl der betreffenden Rolle ermittelt, um 1 erhöht und ins Konfigurationsdokument wieder abgelegt.

Im letzten Schritt muss dieses Konfigurationsdokument über die Azure Management API in den betreffenden Service geladen werden. Windows Azure übernimmt dann den Rest, d.h. Anpassung der tatsächlichen Instanzenzahl an die in der Konfiguration festgelegte Zahl. Der Upload geschieht in der Methode Change(), die in Listing 8 zu sehen ist.

    1: public void Change()
    2: {
    3:     string responseFromServer;
    4:     Uri uri = new Uri(String.Format(@"https://management.core.windows.net/{0}/services/hostedservices/{1}/deployments/{2}/?comp=config",
    5:         this.Subscription.SubscriptionId,
    6:         this.HostedService.ServiceName,
    7:         this.Name));
    8:     string httpMethod = "POST";
    9:  
   10:     HttpWebRequest request = HttpHelper.CreateHttpRequest(uri, httpMethod, this.Subscription.Certificate.AccessCertificate);
   11:  
   12:     using (Stream requestStream = request.GetRequestStream())
   13:     {
   14:         XNamespace ns = "https://schemas.microsoft.com/windowsazure";
   15:         XDocument xDocument = new XDocument(
   16:             new XDeclaration("1.0", "utf-8", "yes"),
   17:             new XElement(ns + "ChangeConfiguration",
   18:                 new XElement(ns + "Configuration", HttpHelper.GetBase64EncodedString(this.Configuration.ConfigurationString))
   19:             )
   20:         );
   21:  
   22:         xDocument.Save(requestStream);
   23:     }
   24:  
   25:     using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
   26:     {
   27:         Stream dataStream = response.GetResponseStream();
   28:         using (StreamReader reader = new StreamReader(dataStream))
   29:         {
   30:             responseFromServer = reader.ReadToEnd();
   31:         }
   32:     }
   33: }

Listing 8: Ablage der neuen Konfiguration in den Cloud Service

Ich verwende in meinem Code eine Klassenbibliothek (eine Managed Code Library), die ich mir selbst auf Basis der Windows Azure Management API implementiert habe. Ich denke aber, dass die betreffenden Codezeilen auch entsprechend ohne diese Bibliothek implementiert werden können. In der Dokumentation der Management API sind die REST-Aufrufe des Service Management Service beschrieben.

Ausblick

Die von mir verwendete Klassenbibliothek, die eine Managed Library für die Windows Azure Management API implementiert, ist derzeit noch im Entwicklungsstadium. Deshalb möchte ich sie noch nicht veröffentlichen. Bei Interesse bitte einfach eine E-Mail über den Blog an mich.

Weitere Informationen

[1] How to Initialize the Windows Azure Diagnostic Monitor

[2] Querying Azure Perf Counter Data with LINQ

[3] Windows Azure Management API - Change Deployment Configuration