Betrouwbare applicaties met messaging

Gedistribueerde systemen worden gevormd door verschillende applicaties. Dit kan bestaan uit een klassiek client-server systeem waar een Windows of Silverlight applicatie communiceert met een server in de backend. Het kunnen ook verschillende WCF services zijn, zonder enkele vorm van user interface. Wat deze applicaties complex maakt, is het communiceren buiten de grenzen van je eigen proces. In dit artikel gaan we de voordelen van messaging bekijken en zullen we kort ingaan op de verschillen met Remote Procedure Calls.

Wat is messaging

Elke developer is bekend met lokale ‘procedure calls’, dit is namelijk niets meer of minder dan een andere methode aanroepen in je software. Dit gebeurd in-process en is de snelste manier om code aan te roepen welke is opgedeeld in methodes en functies. Als we met andere applicaties communiceren, gebruiken veel ontwikkelaars een variant hierop wat bekend staat als Remote Procedure Calls (RPC). Als voorbeeld kun je hierbij denken aan een webservice. In .NET voorzien we methodes van een attribuut, waardoor het framework deze opneemt in onze webservice contracten. Een voorbeeld hiervan zien we in listing 1 gebouwd met Windows Communication Foundation (WCF).
 
[DataContract]
public class Game
{
 [DataMember]
 public string Name { get; set; }
 [DataMember]
 public int Year { get; set; }
}

[ServiceContract]
public class MyService
{
 [OperationContract]
 public Game GetGameOfTheYear(int year)
 {
    return new Game()
    {
      Name = "Star Wars : The Old Republic",
      Year = 2012
    };
 }
}
Listing 1
 
Messaging verschilt hiervan in enkele bijzondere opzichten. De meest in het oog springende is dat we berichten verzenden, in plaats van het aanroepen van methodes met input parameters. Met het aanroepen van methodes wordt onze service layer al snel erg groot en complex. Stel je voor dat er methodes komen voor verschillende type games, lijsten, zoek mogelijkheden, etc. Welke bestaande methode moet je als ontwikkelaar gebruiken voor jouw specifieke wens, of moet je een nieuwe toevoegen? Daarnaast is het lastig om hier met verschillende ontwikkelaars aan te werken. Ook zal code ten behoeve van zaken als auditing, logging en authorisatie door elke methode in de service layer staan. Dit moet netter kunnen.

Berichten

In listing 1 hebben we een methode gemaakt, welke een parameter ontvangt. Hier zie je al een eerste vorm van messaging; we sturen namelijk niet een standaard .NET type terug, maar een bericht. Als we dit doortrekken, zouden we als binnenkomende parameter dus ook een bericht willen hebben. Daarna vervangen we de methodenaam door ‘Send’, zodat deze algemeen en generiek wordt. Het eindresultaat hiervan zien we in listing 2.
 
[ServiceContract]
public class MyMessagingService
{
   [OperationContract]
   public GameOfTheYearResponse Send(
                     GameOfTheYearRequest message)
   {
      GameRepository repository = new GameRepository();
      var result = repository.GetGameOfTheYear(message.Year);
      return new GameOfTheYearResponse()
            {
                  Name = result.Name,
                  Year = message.Year
            };
   }
}
Listing 2
 
Hoewel de naam van de methode generiek is, is de methode dit zelf nog niet. Laten we de code verplaatsen naar een nieuwe class. Als we de methode uit listing 2 duidelijk kunnen maken, welke class hoort bij welk bericht, kan hij die dynamisch opzoeken en uitvoeren. Dit kunnen we bereiken door classes te maken die een vooraf bepaalde interface implementeren, waar ons bericht ook in verwerkt zit. Een voorbeeld zien we in listing 3 terugkomen.
 
public interface IMessage { }

public interface IHandleMessages<T> where T : IMessage
{
   void Execute(T message);
}

public class GameOfTheYearHandler : IHandleMessages<GameOfTheYearRequest>
{
   public IOriginator Originator { get; set; }
   public void Execute(GameOfTheYearRequest message)
   {
      GameRepository repository = new GameRepository();
      var result = repository.GetGameOfTheYear(message.Year);
      var response = new GameOfTheYearResponse() { Name = result.Name };
      Originator.Reply(response);
   }
}
Listing 3
 
Allereerst zien we de IHandleMessages interface die een generiek bericht <T> afhandelt, en dat het bericht de interface IMessage moet implementeren. Daarna zien we een implementatie van deze interface in de class GameOfTheYearHandler. Nu kunnen we onze ‘Send’ methode in de webservice ombouwen zodat deze met behulp van .NET Reflection de juiste class erbij kan zoeken.
 
Als laatste zie je nog dat er wel een bericht terug gestuurd wordt, maar dat dit bericht niet als return-type is gedefinieerd. Hiermee realiseren we dat we meerdere type antwoorden terug kunnen geven. In ons voorbeeld kan het, naast de gegevens van de game, ook een foutmelding als bericht terug kunnen geven waarin we aangeven dat er voor dat specifieke jaar geen game is. Waar we dit bericht naar moeten terugsturen, bevat de Originator property. Dit kan overigens ook een totaal ander adres zijn dan waar de originele opdracht vandaan kwam. Hiermee bereiken we optimale schaalbaarheid doordat we de aanvragen kunnen verdelen of terug kunnen sturen naar een load-balancer.
 
Een ander voordeel is dat we wijzigingen kunnen maken in zowel de request als response berichten, zonder backwards compatibility direct te breken. Ook kunnen we opvolgende versies van berichten maken, met verbeterde of nieuwe handlers, zodat we meerdere versies van berichten ondersteunen. Stel je een GameOfTheYearRequestV2 voor en een bijbehorende handler. Beide versies van het bericht kunnen door deze handler worden opgepakt.
 
public void ExecuteHandlers(IMessage message)
{
   Assembly assembly = (message.GetType()).Assembly;
   var allMessageHandlers =
      from type in assembly.GetTypes()
      where !type.IsAbstract
      from interfaceType in type.GetInterfaces()
      where interfaceType.IsGenericType
      where interfaceType.GetGenericTypeDefinition() == typeof(IHandleMessages<>)
      select type;

   Type messageInterface = typeof(IHandleMessages<>).MakeGenericType(message.GetType());
   var myMessageHandlers = allMessageHandlers.Where(type => type.GetInterfaces().Any(it => it == messageInterface))

   foreach (var handler in myMessageHandlers)
   {
      object handlerInstance = Activator.CreateInstance(handler);
      MethodInfo methodInfo = handler.GetMethod("Execute");
      methodInfo.Invoke(handlerInstance, new[] { message });
   }
}
Listing 4
 
Zoals eerder gezegd is dit eenvoudiger te maken met behulp van reflection. In listing 4 zie je hoe eerst wordt bepaald in welke assembly het bericht zich bevindt, dan kunnen we daar ook de handler in zoeken. Daarna bepalen we met twee linq queries welke handlers ons bericht kunnen verwerken. Daarna instantiëren we de handler en roepen de ‘Execute’ methode aan. Om het plaatje compleet te maken, roepen we vanuit onze ‘Send’ methode in de WCF service de ExecuteHandlers methode aan, welke onze handlers uit zal voeren.
Zoals je ziet in listing 4 kunnen we meerdere handlers bouwen voor hetzelfde bericht. Ook zou je elk bericht van het type ‘IMessage’ kunnen oppakken, waarmee we authorisatie en auditing generiek kunt implementeren, zonder dat er een regel wijzigt aan onze bestaande handlers. Goede voorbeelden van het Single Responsibility Principle en Open/Closed Principle.

Queuing

Langzamerhand wordt het voor ontwikkelaars steeds belangrijker en eenvoudiger om applicaties te bouwen die gebruik maken van meerdere threads. Onze applicaties voeren asynchroon methodes uit, waardoor we gebruik maken van onze multi-core processors en betere performance verkrijgen. Met WCF is het eenvoudig om services asynchroon aan te roepen en met Silverlight bijvoorbeeld verplicht, om te voorkomen dat je user interface niet meer reageert.
 
Als er veel aanvragen op hetzelfde moment worden gedaan, worden er in de service meerdere threads gestart. Hier moeten database transacties op elkaar wachten, er wordt meer geheugen in gebruik genomen, meer resources gebruikt zoals je processor. Langzaam aan kan de service het niet meer aan en worden nieuwe aanvragen geweigerd, terwijl bestaande falen door timeouts. Die worden allemaal opnieuw geprobeerd, terwijl nieuwe aanvragen binnen blijven komen. De service krijgt het nog zwaarder te verduren tot performance in elkaar stort en je application pool recycled.
 
Het effect hiervan is te zien in afbeelding 1. De andere lijn laat echter het voordeel van messaging zien, waar we queuing gebruiken. Net als bij RPC loopt de verwerking van berichten op naarmate er meer berichten verstuurd worden. Maar zodra we het maximale aantal threads hebben bereikt, blijven we stabiel op het maximum hangen. Alle nieuwe aanvragen worden toegevoegd in de queue, en worden verwerkt zodra je service een thread vrij heeft. Hierdoor wordt de load niet hoger dan een vooraf vastgesteld maximum en kunnen we deze beter onder controle houden.
 
Afbeelding 1
 
Als je .NET ontwikkelaar bent, zit je waarschijnlijk op het Microsoft platform. Een van de grote voordelen van Windows is dat Microsoft Message Queuing (MSMQ) standaard erbij geleverd wordt. Een queuing mechanisme wat dus al op je pc en servers draait! Als je een bericht in MSMQ plaatst, wordt dit allereerst lokaal opgeslagen. Dit noemen we ook wel fire & forget, want je applicatie krijgt de controle meteen terug en kan verder. Ondertussen gaat MSMQ proberen het bericht af te leveren bij de ontvanger. Dit gebeurt allemaal transactioneel, waardoor berichten altijd aankomen. Ook WCF ondersteund MSMQ waardoor veel services waar de operaties als one-way zijn gemarkeerd, via enkel een configuratie wijziging hiervan gebruik kan maken.

Transacties

Stel je voor, je hebt een succesvolle webshop gebouwd en orders komen binnen via de webshop. Als een klant een order uiteindelijk wil betalen, slaan we de order op in de database. We maken een order, order regels, berekenen de prijs en verlagen de voorraad. Na een aantal wijzigingen gaat het echter fout en onze applicatie gooit excepties op. Een bug, een update, een deadlock op de database; het kan van alles zijn. Uiteraard maken we gebruik van een transactie, zodat we alle wijzigingen terugrollen en de database consistent blijft. De grote vraag is echter, waar blijft onze order?
 
Afbeelding 2
 
Maar we loggen de foutmelding toch, zoals je ziet in afbeelding 2. Maar inclusief alle gegevens met betrekking tot die order? En in de zeldzame gevallen dat je de ontwikkelaar bent die dit doet, kun je die order dan ook eenvoudig opnieuw plaatsen en verwerken? Er is wel een plaats waar de order zich nog bevindt, namelijk de browser van de gebruiker. We hebben echter de afgelopen jaren vertelt dat de gebruiker nooit, maar dan ook nooit nogmaals op de opslaan knop mag drukken. Velen van ons zullen deze zelfs uitzetten, zodat dit echt niet meer mogelijk is. Uiteindelijk zullen we veelal tot de conclusie komen dat we de order waarschijnlijk kwijt zijn.
 
Afbeelding 3
 
Messaging brengt ook hier een voordeel met zich mee, zoals je kunt zien in afbeelding 3. Voordat het het bericht uit de queue wordt gehaald, zal een transactie worden gestart. Het resultaat is dat bij een exceptie niet alleen de database wordt terug gerold, maar het bericht in de queue zal blijven staan en nogmaals door je applicatie opgepakt kan worden.

Nadelen van messaging

Uiteindelijk kan nooit een oplossing de silver bullit zijn. Ook messaging heeft nadelen. Zo is het niet eenvoudig om simpel request/reply mechanisme te gebruiken. Dit zal volledig asynchroon gebeuren en is lastiger te bouwen dan wanneer je RPC gebruikt. Ook is onbekend wanneer een antwoord terug komt. De service kan onder stress staan, dan staat je vraag nog in de queue, of de service kan helemaal uit staan.
 
Ook zul je bij het bouwen van applicaties goed in de gaten moeten houden dat bepaalde acties alsnog buiten de transactie vallen. Als je bericht na een rollback opnieuw opgepakt wordt, zullen deze acties voor een tweede maal uitgevoerd worden. Denk hierbij aan het versturen van email. Een oplossing is om een component te bouwen welke de email verstuurd en ook naar dit component een bericht te sturen. Als je transactie faalt, zal dit verstuurde bericht binnen de transactie vallen en verwijderd worden. Dit betekent echter wel een additioneel component waar je hosting, auditing, monitoring, security, etc. voor zult moeten beheren.
 
Doordat messaging vooral stuurt naar task based acties, is het bijna niet te gebruiken om eenvoudig data op te halen om in de gebruikers interface te tonen. Hiervoor zal naast messaging een andere methode ontwikkelt moeten worden om data op te halen, wat extra werk en onderhoud met zich meebrengt. Omdat we onze business rules, validatie en zaken zoals authorisatie veelal binnen onze messaging architectuur oplossen, kunnen we het querying gedeelte veel eenvoudiger maken en wellicht standaard oplossingen zoals WCF Data Services gebruiken.

Conclusie

Messaging bied voordelen als betere controle over performance en schaalbaarheid. En dan hebben we het nog niet gehad over het gebruik publish/subscribe of het gebruik van het distributor pattern waarbij de load verdeeld wordt op aanvraag en over verschillende servers. Nogmaals, messaging is geen vervanger is van RPC, echter wel een heel bruikbare uitbreiding. Hierdoor ontstaan veel alternatieve opties en wellicht nieuwe inzichten voor inzet binnen je eigen architectuur landschap.
Geef feedback:

CAPTCHA image
Vul de bovenstaande code hieronder in
Verzend Commentaar