dinsdag 26 maart 2013

Elastic Computing

Onlangs kreeg ik een mailtje van een hostingprovider die zijn clouddiensten wilde aanbieden. Het was zo'n ronkende tekst over hoe geweldig hun dienst wel niet was en hoe de kosten nog verder gereduceerd konden worden door "elastic computing". Elastic Computing? Geen idee wat ze daarmee bedoelen, maar het klinkt ingewikkeld genoeg om serieus te nemen. Vooral dat kostenreducerende aspect spreekt mij als oerhollander wel aan. Op Wikipedia vind ik het volgende over "elastic computing":

Elastic computing is the use of computer resources which vary dynamically to meet a variable workload. This is a feature of cloud computing such as the Amazon Elastic Compute Cloud.

Aha! Dus als de systeemresources, zoals processorkracht en geheugengebruik, dreigen op te raken, worden die resources geheel automatisch opgeschaald. Bij overvloed wordt de hele zaak weer afgeschaald. 
Dat moet in Windows Azure natuurlijk ook mogelijk zijn en inderdaad, er is zelfs al software op de markt, zoals Enterprise Library en AzureWatch, die dit schalen al voor je doet. Voor een betere begripsvorming ga ik er nu zelf eentje schrijven.

Om te kunnen schalen moeten we eerst weten hoe het met de computerresources gesteld is. Dit kan via het tooltje "perfmon.exe" dat op ieder Windows systeem te vinden is (afbeelding 1). Erg handig programmaatje dat het resourcegebruik tot in de goorste details inzichtelijk maakt.


Afbeelding 1. perfmon.exe met het processorgebruik en het vrije geheugen

Perfmon doet niets anders dan performance counters, die door de softwaremakers zijn geïmplementeerd, uitlezen en in een fraai grafiekje presenteren. Als deze software die performance counters kan uitlezen, dan kunnen wij dat natuurlijk ook. Voor Windows Azure is er een bibliotheek gemaakt waarin alle benodigdheden verzameld zijn (Microsoft.WindowsAzure.Diagnostics.dll). Het probleem doet zich nu voor dat we in een cloud omgeving meerdere windowssystemen hebben met ieder zijn eigen perfomance monitor. Om die data te verzamelen zouden we al die omgevingen moeten bezoeken. Afgezien van toegangsperikelen is dat eigenlijk onbegonnen werk. Een betere benadering is om die instanties hun eigen performance data te laten posten in de table storage. Die table storage kunnen we zelf dan weer uitlezen en zo krijgen we inzicht in het resourcegebruik. Nadeel is wel dat die storage snel vol kan lopen. Nu is er in de cloud storage aan ruimte geen gebrek, maar er moet uiteindelijk wel voor worden betaald.

Wanneer je de data geanalyseerd hebt, zul je eventueel actie moeten ondernemen. Wij doen dit via de Azure Portal, maar als we dit willen automatiseren, zullen we het anders aan moeten pakken. Gelukkig biedt Windows Azure, naast de portal, ook een ReST (Representational State Transfer) API aan waarmee we instructies aan Windows Azure kunnen geven.

Performance data verzamelen en opslaan in de Table Storage

De eerste stap is het verzamelen van de performance data. Deze data wordt lokaal op de instantie verzameld en op gezette tijden naar de table storage geduwd. Dat stuk moeten we eerst configureren. Dat doe je in de WebRole class (in het geval van een Web Role) of in de WorkerRole class (bij een Worker Role). Beide classen erfen van RoleEntryPoint. Wanneer je van een bestaande website uit gaat, zal die WebRole class in het algemeen niet aanwezig zijn. Geen nood, gewoon alsnog zelf toevoegen.
In die class overschrijf je de OnStart() methode waarin je de gewenste performance counters configureert. Zo'n implementatie kan er als volgt uitzien:

public class WebRole : RoleEntryPoint
{
    private string configString =  "Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString";

    public override bool OnStart()
    {
        CloudStorageAccount cloudStorageAccount = CloudStorageAccount.Parse(
                RoleEnvironment.GetConfigurationSettingValue(configString ));

        RoleInstanceDiagnosticManager roleInstanceDiagnosticManager = cloudStorageAccount.CreateRoleInstanceDiagnosticManager(
            RoleEnvironment.DeploymentId,
            RoleEnvironment.CurrentRoleInstance.Role.Name,
            RoleEnvironment.CurrentRoleInstance.Id);

        DiagnosticMonitorConfiguration config = roleInstanceDiagnosticManager.GetCurrentConfiguration() ?? new DiagnosticMonitorConfiguration();

        config.PerformanceCounters.DataSources.Add(
            new PerformanceCounterConfiguration
            {
                CounterSpecifier = @"\Processor(_Total)\% Processor Time",
                SampleRate = TimeSpan.FromSeconds(5)
            });
        config.PerformanceCounters.DataSources.Add(
            new PerformanceCounterConfiguration
            {
                CounterSpecifier = @"\Memory\Available MBytes",
                SampleRate = TimeSpan.FromSeconds(5)
            });
        config.PerformanceCounters.ScheduledTransferPeriod = TimeSpan.FromMinutes(15.0);
        config.PerformanceCounters.BufferQuotaInMB = 512;

        DiagnosticMonitor.Start(configString , config);
        return base.OnStart();
    }
}

Het eerste dat hier gebeurt, is het doorgeven van de cloud storage credentials. Deze informatie komt uit het configuratiebestand en staat onder de naam "Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString", die verschijnt wanneer je "Enable Diagnostics" aan hebt staan (afbeelding 1 en 2)


Afbeelding 1 & 2. Diagnose aanzetten.

Op basis van deze storage informatie, die de DiagnosticManager nodig heeft om toegang te krijgen tot de table storage, genereren we de RoleInstanceDiagnosticManager waarmee we een handvat krijgen naar de DiagnosticMonitorConfiguration. Hierop geven we aan welke performance counters we willen hebben ("\Processor(_Total)\% Processor Time" en 
"\Memory\Available MBytes"). Deze kreten kun je zo uit perfmon.exe plukken.
Vervolgens stellen we in na hoeveel minuten de data naar de table storage wordt gestuurd, hoe groot de interne buffers maximaal mogen worden en tenslotte roepen we de Start() methode van de DiagnosticManager aan. De zaak is aan het lopen. Nu wordt om de 15 minuten de performance data in de table storage gezet (tabelnaam: WADPerformanceCountersTable. Zie afbeelding 3)


Afbeelding 3. De performance data vanuit de table storage

De performance data uit de table storage lezen

Om de data uit de storage te lezen maak je eerst een CloudStorageAccount object aan. Hiermee kun je een TableServiceContext object initialiseren (vergelijkbaar met de ObjectContext in Entity Framework) waarmee je queries op de table storage kunt definiëren. Merk op dat we de query omzetten naar een CloudTableQuery object, die je krijgt met de extensiemethode AsTableServiceQuery(). Standaard  zal een query op een storage table maximaal 1000 records teruggeven. De rest  moet je via een continuation token eruit trekken. CloudQueryTable zal dit alles voor  jou doen. 
Nadat je alle data uit de table storage hebt getrokken en verwerkt, is het zaak om de table storage weer leeg te maken. Hierbij moet je wel letten op het feit dat iedere actie geld kost, dus gooi de records niet één voor één weg, maar knikker de hele tabel weg of probeer de te verwijderen records in een batch te stoppen (is aanzienlijk lastiger). Een mogelijke implementatie is hieronder weergegeven.


class Program
{
    static void Main(string[] args)
    {
        StorageCredentialsAccountAndKey account =
            new StorageCredentialsAccountAndKey("storage_naam""storage_key");
        CloudStorageAccount cloudStorageAccount = new CloudStorageAccount(account, true);
        TableServiceContext serviceContext =
            new TableServiceContext(cloudStorageAccount.TableEndpoint.AbsoluteUri, cloudStorageAccount.Credentials);
        var query = serviceContext.CreateQuery<PerformanceCountersEntity>("WADPerformanceCountersTable");

        CloudTableQuery<PerformanceCountersEntity> cpu_query = 
                 query.Where(n => n.CounterName == @"\Processor(_Total)\% Processor Time").AsTableServiceQuery();
        CloudTableQuery<PerformanceCountersEntity> mem_query = 
                  query.Where(n => n.CounterName == @"\Memory\Available MBytes").AsTableServiceQuery();
        
        List<PerformanceCountersEntity> cpuList = cpu_query.ToList();
        List<PerformanceCountersEntity> memList = mem_query.ToList();
        if (cpuList.Count > 0)
        {
            Console.WriteLine(cpuList.Average(e=>double.Parse(e.CounterValue)));
        }
        if (memList.Count > 0)
        {
            Console.WriteLine(memList.Average(e => double.Parse(e.CounterValue)));
        }
        cloudStorageAccount.CreateCloudTableClient().DeleteTable("WADPerformanceCountersTable");
    }
}

public class PerformanceCountersEntity : TableServiceEntity
{
    public long EventTickCount { getset; }
    public string DeploymentId { getset; }
    public string Role { getset; }
    public string RoleInstance { getset; }
    public string CounterName { getset; }
    public string CounterValue { getset; }
}



Werken met de Azure ReST api.

Tot dusver hebben we alles via de Azure Portal kunnen afregelen, maar als we zaken willen automatiseren, hebben we weinig aan die grafische interface. Gelukkig biedt Microsoft hiervoor een ReST API aan. ReST is een op http gebaseerde manier om te kunnen communiceren met andere computers. ReST is bijzonder eenvoudig en licht in gebruik en kan in combinatie met xml, json of wat dan ook gebruikt worden. Ideaal dus voor communicatie over internet. Windows Azure gebruikt ReST in combinatie met xml, dus de data wordt in xml-formaat aangeleverd. Natuurlijk willen we niet dat Jan en alleman zomaar aan onze clouddienst gaat lopen morrelen, vandaar dat Windows Azure eerst wil weten of jij wel mag wijzigen. Hiervoor gebruiken ze certificaten. Aaahhhh certificaten! Toch niet die enge dingen? Helaas wel. 
Zo'n certificaat bestaat in de basis uit een public en private key die bij elkaar horen (schijnt iets ingewikkelds met priemgetallen te zijn). Daarnaast heb je nog wat extra herkenningsinformatie. Meestal zul je van zo'n certificaat nog een variant gebruiken die alleen de public key bevat. Dit certificaat lever je uit aan derden. In dit geval dus Windows Azure (in de Azure portal bij Settings->Managment Certificates kun je hem uploaden).
Certificaten kun je kopen, bijvoorbeeld bij  Verisign of Thawte, en zijn knetterduur. Gelukkig hebben we bij Visual Studio de beschikking over het makecert.exe tooltje dat voor ons certificaten genereert. Deze zijn bedoeld voor testdoeleinden en missen dus het autoriteitstoken van Verisign of Thawte (vergelijk het maar met een vals paspoort. Is ook niet uitgegeven door een officiële instantie). Het commando ziet er als volgt uit (uitvoeren met administrator rechten)

makecert -sky exchange -r -n "CN=<CertificateName>" -pe -a sha1 -len 2048 -ss My "<CertificateName>.cer"

Onthoud het gedeelte "CN=<CertificateName>". Dit is de subject name die je kunt gebruiken om het certificaat op te vragen. (als <CertificateName> kies ik BananenRepubliek)
Wanneer dit gelukt is, kun je met "certmgr.msc" kijken of hij in de certificate store van de Current User is opgenomen (afbeelding 4)


Afbeelding 4. Het certificaat met de public en private key is geregistreerd.
Het resulterende .cer bestand kun je nu uploaden naar de Azure Portal.
In de code hieronder kun je zien hoe je zo'n certificaat uitleest in. Het "certificate" object wordt uiteindelijk gebruikt voor de ReST aanroepen.

class Program
{
    static void Main(string[] args)
    {
        X509Certificate2 certificate = ReadCurrentUserMyCertificate("Bananenrepubliek");
        if (certificate == nullreturn;
    }

    private static X509Certificate2 ReadCurrentUserMyCertificate(string subjectName)
    {
        X509Store myStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        myStore.Open(OpenFlags.ReadOnly);
        X509Certificate2Collection certificates =
               myStore.Certificates.Find(X509FindType.FindBySubjectName, subjectName, false);
        myStore.Close();
        if (certificates.Count > 0)
        {
            return certificates[0];
        }
        return null;
    }
}

De volgende stap is het in elkaar knutselen van de ReST aanroepen. Hiervoor 
kun je het best het HttpWebRequest object gebruiken. Hieronder zie je dat uitgewerkt in een base class. Let hierbij op wat je wilt doen (method). Dit kan zijn:
  1. GET om data op te vragen
  2. PUT voor update acties
  3. POST voor inserts
  4. DELETE voor het verwijderen van data.
Daarnaast is de version van belang. De Windows Azure ReST API wordt voortdurend uitgebreid en nieuwe toevoegingen werken dan ook alleen vanaf een bepaalde datum. Die datum geeft je op bij version. Let ook op het BaseAddress dat er als volgt uit ziet, "https://management.core.windows.net/<subscriptionID>/". SubscriptionID kun je weer in de Azure Portal vinden.
Een ReST aanroep kan er als volgt uitzien:

https://management.core.windows.net/<subscriberID>/services/hostedservices

Deze aanroep geeft een lijst van alle services, behorend bij een subscriptionID, terug. De volgende ReST aanroep vraagt informatie op van een specifieke service. De querystring "embed-detail=true" geeft daarbij ook een lijst van alle releases (deployments) terug. Die laatste heb je nodig om op te vragen hoeveel instanties er momenteel actief zijn.


https://management.core.windows.net/<subscriberID>/services/hostedservices/<hostedservice>?embed-detail=true

waarbij <hostedservice> de naam van de service is. Een volledige beschrijving van de ReST API vind je hier.

public class AzureClient
{
    private X509Certificate2 _certificate = null;
    protected readonly string BaseAddress;

    protected void DoRestCall(string url, 
                                         Action
<XmlReaderHttpStatusCode> returnAction, 
                                         string
 method = "GET"
                                         string
 version = "2012-03-01"
                                         byte
[] data = null)
    {
        HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
        request.Method = method;
        request.Headers.Add("x-ms-version", version);
        request.ClientCertificates.Add(_certificate);
        request.ContentType = "application/xml";

        if (data != null)
        {
            request.ContentLength = data.Length;
            using (Stream stream = request.GetRequestStream())
            {
                stream.Write(data, 0, data.Length);
            }
        }

        HttpStatusCode statusCode;
        HttpWebResponse response = null;
        XmlReader reader;
        try
        {
            response = (HttpWebResponse)request.GetResponse();
        }
        catch (WebException ex)
        {
            response = (HttpWebResponse)ex.Response;
        }

        statusCode = response.StatusCode;
        if (response.ContentLength > 0)
        {
            reader = XmlReader.Create(response.GetResponseStream());
            if (returnAction != null)
            {
                returnAction(reader, statusCode);
            }
        }
        if (response != null)
        {
            response.Dispose();
        }
    }
    public AzureClient(X509Certificate2 certificate, string subscriptionID)
    {
        _certificate = certificate;
        BaseAddress = string.Format("https://management.core.windows.net/{0}/", subscriptionID);
    }
}


Voor het managen van onze cloud service leiden we een nieuwe class van de hierboven beschreven base class af. Hierin programmeren we de echte ReST calls. een voorbeeld van zo'n afgeleide class is hieronder uitgewerkt. Hij bestaat uit drie aanroepen:
  1. GetHostedService, om informatie over een cloud service op te vragen
  2. GetHostedServiceDeployment, om informatie over een specifieke release op te vragen
  3. UpdateHostedServiceDeployments, om een release op of af te schalen.

public class AzureManagementClient : AzureClient
{
    private const string SERVICES = "services";
    private const string HOSTEDSERVICES = "hostedservices";
    private const string DEPLOYMENTS = "deployments";

    private T Deserialize<T>(XmlReader reader) where T : class
    {
        XmlSerializer serializer = new XmlSerializer(typeof(T));
        return serializer.Deserialize(reader) as T;
    }

    public HostedService GetHostedService(string serviceName)
    {
        string url = string.Format("{0}/{1}/{2}/{3}?embed-detail=true", BaseAddress, SERVICES, HOSTEDSERVICES, serviceName);
        HostedService service = null;
        DoRestCall(url, (reader, statusCode) =>
        {
            if (statusCode == HttpStatusCode.OK)
            {
                service = Deserialize<HostedService>(reader);
            }
        });

        return service;
    }

    public Deployment GetHostedServiceDeployment(string serviceName, string deploymentUniqueName)
    {
        string url = string.Format("{0}/{1}/{2}/{3}/{4}/{5}", BaseAddress, SERVICES, HOSTEDSERVICES, serviceName, DEPLOYMENTS, deploymentUniqueName);
        Deployment deployment = null;
        DoRestCall(url, (reader, statusCode) =>
        {
            if (statusCode == HttpStatusCode.OK)
            {
                deployment = Deserialize<Deployment>(reader);
            }
        });

        return deployment;
    }

    public bool UpdateHostedServiceDeployments(string serviceName, string deploymentUniqueName, ChangeConfiguration configuration)
    {
        XmlSerializer serializer = new XmlSerializer(typeof(ChangeConfiguration));
        byte[] bytes;
        using (MemoryStream stream = new MemoryStream())
        {
            serializer.Serialize(stream, configuration);
            bytes = stream.ToArray();
        }

        bool isSuccess = false;
        string url = string.Format("{0}/{1}/{2}/{3}/{4}/{5}/?comp=config", BaseAddress, SERVICES, HOSTEDSERVICES, serviceName, DEPLOYMENTS, deploymentUniqueName);
        DoRestCall(url, (reader, statusCode) =>
        {
            if (statusCode == HttpStatusCode.BadRequest)
            {
                Error error = Deserialize<Error>(reader);
                if (error.Message == "No change in settings specified")
                {
                    // Met deze foutmelding is de opschaling geslaagd.
                    isSuccess = true;
                }
            }
        }, "POST", data: bytes);

        return isSuccess;
    }

    public AzureManagementClient(X509Certificate2 certificate, string subscriptionID)
        : base(certificate, subscriptionID)
    {

    }
}

de ReST aanroepen geven xml-data terug die we zelf kunnen uitvlooien, maar wellicht is het handiger om die data meteen om te zetten naar .NET objecten. Dat kun je het beste met de class XmlSerializer doen zoals hierboven gebeurt in de methode Deserialize<T>(). Hiervoor heb je wel een aantal class definities nodig waarnaar XmlSerializer de xml-data vertaalt. Hieronder zie je een aantal van die classen uitgewerkt (Let op dat je de propertienamen gelijk houdt aan de tag-namen in de xml-structuur. Verder zijn alleen properties opgenomen die ik relevant vond. Er komt nog veel meer data terug).


[XmlRoot("HostedService", Namespace = "http://schemas.microsoft.com/windowsazure")]
public class HostedService
{
    public string Url { getset; }
    public string ServiceName { getset; }
    public Deployment[] Deployments{ getset; }
}

[XmlRoot("Deployment", Namespace = "http://schemas.microsoft.com/windowsazure")]
public class Deployment
{
    public string Name { getset; }
    public string DeploymentSlot { getset; }
    public string PrivateID { getset; }
    public string Running { getset; }
    public string Label { getset; }
    public string Configuration { getset; }
    [XmlIgnore]
    public XDocument ConfigurationXml
    {
        get
        {
            return XDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(Configuration)));
        }
        set
        {
            Configuration = Convert.ToBase64String(Encoding.UTF8.GetBytes(value.ToString()));
        }
    }
}

[XmlRoot("ChangeConfiguration", Namespace = "http://schemas.microsoft.com/windowsazure")]
public class ChangeConfiguration
{
    public string Configuration { getset; }
    public string Mode { getset; }
    public string ExtendedProperties { getset; }

    [XmlIgnore]
    public XDocument ConfigurationXml
    {
        get
        {
            return XDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(Configuration)));
        }
        set
        {
            Configuration = Convert.ToBase64String(Encoding.UTF8.GetBytes(value.ToString()));
        }
    }
}

[XmlRoot("Error", Namespace = "http://schemas.microsoft.com/windowsazure")]
public class Error
{
    public string Code { getset; }
    public string Message { getset; }
}

Al het voorbereidende werk is nu achter de rug. Nu kunnen we de zaak aan elkaar knopen. Hieronder zie je een voorbeeld waarbij een specifieke release wordt geschaald naar acht instanties. We halen hiervoor eerst de actuele configuratie van de deployment op. Daarin wijzigen we het aantal instanties en de gewijzigde configuratie sturen we vervolgens weer naar Windows Azure.


class Program
{
    static void Main(string[] args)
    {
        X509Certificate2 certificate = ReadCurrentUserMyCertificate("Bananenrepubliek");
        if (certificate == nullreturn;

        AzureManagementClient client = new AzureManagementClient(certificate, "subscriberid");
        HostedService service = client.GetHostedService("myservice");
        XDocument configXml = service.Deployments[0].ConfigurationXml;   
        XNamespace wa = "http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration";
        XElement serviceConf = configXml.Element(wa + "ServiceConfiguration");
        var instances = serviceConf.Descendants(wa + "Instances").SingleOrDefault();
        if (instances != null)   
        {
            instances.SetAttributeValue("count", 8);
        }
        ChangeConfiguration changeConfig = new ChangeConfiguration();
        changeConfig.ConfigurationXml = configXml;
        changeConfig.Mode = "auto";
        client.UpdateHostedServiceDeployments("myservice""deployment_unique_name", changeConfig);
    }
 
    private static X509Certificate2 ReadCurrentUserMyCertificate(string subjectName)
    {
        X509Store myStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        myStore.Open(OpenFlags.ReadOnly);
        X509Certificate2Collection certificates = myStore.Certificates.Find(X509FindType.FindBySubjectName, subjectName, false);
        myStore.Close();
        if (certificates.Count > 0)
        {
            return certificates[0];
        }
        return null;
    }
}

Nu je weet hoe je de performancedata kunt opvragen en hoe je ReST calls stuurt naar Windows Azure, kun je een Windows service in elkaar draaien die op gezette tijden de storage uitleest, bepaalt of er op- of afgeschaald moet worden en die de uiteindelijke ReST aanroepen naar Windows Azure verzorgt. Het bouwen van zo'n dienst laat ik aan jouw eigen fantasie over.

Geen opmerkingen:

Een reactie posten