Communicatie tussen roles in Windows Azure applicaties verloopt normaal gesproken via queues. Dit is een diepgeworteld feit van de architectuur van dit soort applicaties. Zo wordt ook voorgeschreven om items van deze queues af te halen met behulp van polling, met andere woorden, een role controleert de queue en gaat in slaap, wordt weer wakker en herhaalt het proces. Afgezien van de vraag of dit mooi programmeerwerk oplevert, zijn de kosten ervan ook nog eens nodeloos hoog en is het proces inefficiënt in zijn gebruik. In het hieropvolgende artikel zal ik een alternatief bieden voor dit mechanisme door een event af te vuren op het vullen van de queue.
Stel je voor dat je een worker role hebt die berichten van de queue haalt en deze verwerkt. Je stelt het pollingmechanisme zo in dat de role elke minuut de queue controleert op de aanwezigheid van berichten. Dat betekent in het ergste geval dat iedere keer als er een bericht verwerkt moet worden, het proces een minuut lang stil ligt.
Om dit probleem op te lossen zou je uiteraard de slaaptijd naar één seconde kunnen terugzetten. De reactietijd van je proces benadert dan meer realtime, maar de kosten zullen met een factor 60 groeien. 31.536.000 Transacties per jaar voor elke seconde pollen, kost ongeveer $ 31,54 ($ 0,01 per 10k transacties) per role instance. Op het eerste gezicht lijkt dat niet een schrikbarend hoog bedrag, maar wanneer de applicatie uitschaalt, kunnen de kosten snel oplopen.
Een veel genoemde oplossing voor dit probleem, is het opstellen van een sleeptime met een logaritmische schaal: elke keer dat de role de queue controleert en de queue leeg is, wordt de slaaptijd vergroot. Dat drukt weliswaar de kosten, maar het vergroot de inefficiëntie; immers, elke keer dat je de sleeptime vergroot, verlaag je de reactiesnelheid.
Een alternatieve oplossing hiervoor is om roles te notificeren dat de queue gevuld is door een event af te laten gaan bij het vullen van deze queue. Hierdoor reageert de worker role direct op nieuwe berichten, zonder dat er nodeloze pollingacties tegenover staan.
Het mooiste zou zijn als eventhandlers rechtstreeks geabonneerd konden worden op het vullen van een queue, maar omdat de queues niet observable zijn, is die weg helaas afgesloten. We moeten dus een list bedenken om de notificatie-event te triggeren en om dat te faciliteren, moet je een aantal stappen doorlopen.
Figure 1: Queue notification Service architectuur
Allereerst moet de standaard code van de worker role zo worden aangepast dat Thread.Sleep wordt vervangen met Monitor.Wait. We willen immers een role die wakker gemaakt wordt als er echt iets te doen is en niet één die uit zichzelf wakker wordt om vervolgens niets te doen. Vervolgens maak je op de worker role een eventhandler die Monitor.Pulse uitvoert, zodat de role ook daadwerkelijk wakker wordt.
private object _lock = new object();
public override bool OnStart() {
QueueManager.Instance.NotifyRoleHandler +=
new NotifyRoleDelegate(Instance_NotifyRoleHandler);
...
return base.OnStart();
}
public override void Run()
{
while (true)
{
try { Wait(); ProcessQueue(); } catch (StorageClientException ex) { ... } }
}
void Instance_NotifyRoleHandler()
{
lock(_lock)
{
Monitor.Pulse(_lock); }
}
protected void Wait()
{
lock(_lock)
{
Monitor.Wait(_lock); }
}
Listing 1: worker role zonder Thread.Sleep
Hierna moeten we een mechanisme ontwikkelen dat de event triggert. Hiervoor moet er een Input WCF service gemaakt worden op de worker role, die wordt opgestart bij de initialisatie van de role (zie Listing 2) en als doel heeft om de hierboven genoemde event triggeren. Elke instantie van de role krijgt een eigen instantie van de service en door middel van het loadbalancingmechanisme in Windows Azure wordt bepaald welke role instance de items van queue haalt.
Uri uri =
new Uri(
string.Format(http://{0}/{1}, RoleEnvironment .CurrentRoleInstance .InstanceEndpoints["NotifyQueueService"] .IPEndpoint.ToString(), "NotifyQueueService"));
BasicHttpBinding binding = new BasicHttpBinding();
_serviceHost = new ServiceHost(typeof(NotifyQueueService), uri);
_serviceHost.AddServiceEndpoint(typeof(INotifyQueueService), binding, "");
try
{
_serviceHost.Open();
}
catch (Exception ex)
{
...
}
Listing 2: Service binding NotifyQueueService
Een singleton klasse, QueueManager, komt tussen de worker role en de service in te staan waarin de event code, de betreffende queue en de Dequeue en Enqueue functies staan, waardoor de service de juiste event kan triggeren (Listing 3). Vervolgens kan de WCF service QueueManager.Instance.NotifyRole() aanroepen om de worker role uit de wachtstand te halen en het verwerken van de queue items te laten beginnen.
public event NotifyRoleDelegate NotifyRoleHandler;
public void NotifyRole()
{
NotifyRoleHandler();
}
public void Enqueue(string message)
{
_queue.AddMessage(new CloudQueueMessage(message));
SignalService();
}
private void SignalService()
{
BasicHttpBinding binding =
new BasicHttpBinding(BasicHttpSecurityMode.None); var factory = new ChannelFactory<INotifyQueueService>(binding);
INotifyQueueService instance =
factory.CreateChannel( new EndpointAddress( ConfigurationManager.AppSettings["NotificationServiceUri"]));
instance.SignalQueueEvent();
}
public List<string> Dequeue()
{
List<string> result = new List<string>();
if (_queue.PeekMessage() != null)
{
IEnumerable<CloudQueueMessage> messageList = _queue.GetMessages( _queue.RetrieveApproximateMessageCount()); if (messageList != null) { foreach (var message in messageList) { _queue.DeleteMessage(message); result.Add(message.AsString); }
} }
return result;
}
Listing 3: QueueManager
Zoals Listing 3 aangeeft, wordt de QueueManager ook gebruikt om items op de queue te plaatsen door de functie Enqueue aan te roepen. Hierna wordt direct de WCF service aangeroepen, zodat een worker role instantie kan beginnen aan het verwerken van de berichten op de queue.
Nadelen
Net als bij het pollingmechanisme is ook deze oplossing niet vrijgesteld van nadelen. Hoewel het helemaal in de Cloud-gedachte past van “je betaalt voor wat je gebruikt” en het het kostenprobleem van het pollen opvangt, kun je vraagtekens zetten bij de veiligheid. Het gebruik van een publieke service die onherroepelijk de queue benadert kan security issues geven. Immers, wanneer iemand met kwade bedoelingen achter het adres van deze service komt, zouden de kosten aardig opgedreven kunnen worden door de service eindeloos vaak aan te roepen.
Voor dit risico zijn verschillende oplossingen beschikbaar. Je zou bijvoorbeeld de services kunnen beveiligen met authenticatie of de input services kunnen vervangen met Internal services. Bij de laatste oplossing moet je wel zelf voor loadbalancing zorgen. De loadbalancer van Windows Azure verdeelt de load namelijk alleen voor publieke services.
Een andere oplossing is gebruik te maken van de service bus van Windows Azure AppFabric, die je services o.a. voor DOS-attacks beschermt. Het gebruik van deze dienst brengt echter weer extra kosten met zich mee ($ 3,99), waardoor het van de toepassing afhangt of het rendabel is.
Conclusie
Het pollingmechanisme dat gebruikt wordt om queues in Windows Azure uit te lezen brengt nodeloze kosten met zich mee. Zoals ik hierboven heb aangegeven, is er een alternatief voor, namelijk het optuigen van een WCF service in combinatie met een event.
Om de veiligheid van je service te garanderen, zul je ofwel de publieke service moeten omzetten naar een interne service, of de service moeten aansluiten op de service bus; beide oplossingen hebben hun eigen voor- en nadelen.
Nog mooier zou zijn als Microsoft met een eigen oplossing komt om events op de queue te definiëren. Totdat zij dat doet, zullen wij, ontwikkelaars, gebruik moeten maken van de middelen die wél voorhanden zijn om zo tot creatieve oplossingen te komen.
Download hier de sources