Terug naar blog
    Development24 juni 2021

    De boeiende, maar onbekende wereld van actors

    Om bij te blijven met de nieuwste ontwikkelingen in .NET development, stuurden we onze .NET-developer Gaëtan op pad naar Antwerpen voor een training georganiseerd door Azug. Een Belgische gebruikersgroep van Microsoft Azure die zich focust op ontwikkeling en architectuur van het platform. Die dag op het programma: Actor Based Services. Een term die onze developer nog nooit hoorde. En hij was blijkbaar niet alleen. Bijna niemand in de zaal had er al van gehoord. Maar na een boeiende uitleg was alles duidelijk. Actors zijn krachtig. Heel krachtig. En hier is waarom.

    De boeiende, maar onbekende wereld van actors

    Schaalbaarheid in applicaties: waarom actors het verschil kunnen maken

    Wanneer je een applicatie ontwikkelt, is schaalbaarheid vaak niet het eerste waar je aan denkt. In de beginfase wil je vooral dat alles werkt. Maar zodra je applicatie groeit, meer gebruikers krijgt of over meerdere processen en servers moet draaien, wordt schaalbaarheid ineens een cruciale factor.

    Het probleem? Een applicatie achteraf schaalbaar maken is vaak complex. Je komt dan al snel terecht bij oplossingen zoals multithreading, caching en externe API’s. Elk van die technieken heeft zijn nut, maar brengt ook extra uitdagingen met zich mee.

    Multithreading kan bijvoorbeeld performantie opleveren, maar introduceert ook risico’s zoals locking en uiteindelijk zelfs deadlocks. Caching helpt om state tijdelijk bij te houden, maar zodra meerdere componenten dezelfde cache moeten delen, stijgt de complexiteit snel. En API’s zijn handig om systemen los van elkaar te houden, maar ze zijn vaak trager omdat ze stateless werken en gegevens telkens in een database moeten opslaan en weer ophalen.

    Maar wat als je applicatie moet draaien over meerdere processen? Of als je state wil delen tussen verschillende servers? In dat soort scenario’s is het misschien tijd om een andere aanpak te overwegen: actors.

    Maak kennis met Actors

    Een actor is een primitieve die berichten kan ontvangen, berichten kan versturen en zijn eigen state beheert. In plaats van rechtstreeks gedeelde data te manipuleren, communiceren actors met elkaar via berichten. Een actor ontvangt een boodschap, voert logica uit op basis van die boodschap en past zijn eigen state aan waar nodig.

    Het grote voordeel hiervan is dat complexiteit rond gedeelde state en synchronisatie veel beter beheersbaar wordt. Je werkt niet langer met allerlei threads die dezelfde data proberen te benaderen, maar met geïsoleerde eenheden die elk verantwoordelijk zijn voor hun eigen gedrag en toestand.

    Om met actors te werken, gebruik je doorgaans een framework. Dat framework neemt veel technische zorgen uit handen, zoals threading, scheduling en state management.

    Binnen het .NET-ecosysteem zijn er twee bekende actor-frameworks:

    • Akka.NET – een krachtige .NET-implementatie van het Akka-model
    • Orleans – een actor framework van Microsoft, gericht op schaalbare en gedistribueerde applicaties

    Don’t kill Kenny: een voorbeeld met Orleans

    Om concreet te tonen wat actors doen, nemen we een eenvoudig voorbeeld in Orleans. Orleans is een actor framework van Microsoft dat draait op .NET Core en gebruikmaakt van interfaces om met actors te communiceren.

    Stel je voor: je bouwt een programma dat aan Kenny kan vragen of hij nog leeft. Kenny moet daarop antwoorden dat hij nog leeft, én bijhouden hoe vaak die vraag al gesteld werd.

    Hoe pak je dat aan met actors? Grains en silo’s

    In Orleans heet een actor een Grain. Een Grain bevat dus zowel logica als state. Die Grains worden gehost en uitgevoerd binnen een Silo, wat je kan zien als de serveromgeving van Orleans.

    Onze opstelling bestaat uit drie delen:

    • een Client
    • een Silo
    • een gedeelde Interface library

    Die interface library wordt zowel door de Client als de Silo gebruikt, zodat beide kanten weten hoe ze met elkaar moeten communiceren.

    Voor deze opstelling installeer je volgende NuGet packages:

    • Microsoft.Orleans.Client – in het project SouthParkClient
    • Microsoft.Orleans.Server – in SouthParkSilo
    • Microsoft.Orleans.Core.Abstractions – in SouthParkSilo en SouthParkInterfaces
    • Microsoft.Orleans.OrleansCodeGenerator.Build – in SouthParkClient, SouthParkSilo en SouthParkInterfaces

    In de Silo zit onze CharacterGrain. Die erft van Grain, een Orleans-klasse die instaat voor zaken zoals references, lifecycle en statebeheer. Daarnaast implementeert deze Grain ook de ICharacterGrain interface, die gedeeld wordt tussen de Client en de Silo.

    De interface bevat in dit voorbeeld slechts één methode: AreYouAlive. Die methode geeft terug hoe vaak Kenny al bevraagd werd. ** Hoe de communicatie werkt**

    In de Program.cs van de Silo bouwen we de host op. In de interface library definiëren we een interface die erft van IGrainWithStringKey. Dankzij die string key kan de client een specifieke Grain aanspreken op basis van een unieke sleutel.

    In de client configureren we vervolgens de Orleans-client met dezelfde ClusterId en ServiceId als de host. Zo weet Orleans met welke cluster de client moet verbinden.

    Daarna vragen we een Grain op via GetGrain, waarbij we het type ICharacterGrain meegeven én een key. Die key bepaalt naar welke specifieke actor we verwijzen — in dit geval: Kenny.

    Vanaf dan kunnen we AreYouAlive oproepen en wachten op antwoord van de Silo. ** Wat gebeurt er in de praktijk?**

    Stel dat we de Silo opstarten en daarna drie Clients laten verbinden. Elk van die Clients vraagt aan Kenny of hij nog leeft. Telkens stijgt de teller. Sluit je daarna één client af en start je later een nieuwe op, dan loopt de teller gewoon verder.

    Dat betekent dat de state van Kenny niet in de client zit, maar centraal wordt bijgehouden door de Silo. Omdat de Silo in een apart proces draait, blijven de clients en de server mooi losgekoppeld van elkaar.

    Tot hier klinkt dat al behoorlijk sterk.

    Maar er is nog een probleem.

    Don’t kill Kenny… tenzij de Silo stopt

    Zodra je de Silo afsluit, ben je in deze eenvoudige opstelling ook de state kwijt. De teller van Kenny verdwijnt dus samen met het proces.

    Dat lossen we op door een cluster op te zetten.

    Stateful clustering met Orleans

    Om meerdere Silo’s samen te laten werken, maakt Orleans gebruik van clustering. Daarvoor is een gedeelde opslag nodig, meestal via een database.

    In dit voorbeeld gebruiken we Microsoft SQL Server. Daarvoor installeren we de NuGet package Microsoft.Orleans.Clustering.AdoNet in zowel het Client- als het Silo-project.

    Na installatie krijg je ook een map OrleansAdoNetContent, waarin scripts zitten voor verschillende databaseproviders. Voor SQL Server gebruik je de juiste scripts om de nodige tabellen aan te maken.

    Vervolgens voeg je in zowel de Client als de Silo UseAdoNetClustering toe aan de builder, samen met je eigen connection string.

    Aan de Silo-kant voeg je daarnaast ook AddAdoNetGrainStorage toe. Daarmee kan Orleans de state van je Grains opslaan in de database.

    Omdat we nu met meerdere Silo’s werken, moet elke Silo ook zijn eigen IP-adres, poort en gateway hebben. Dat stel je in via de EndpointOptions.

    State apart definiëren

    Om de state correct te kunnen opslaan, splitsen we CharacterState uit in een aparte class. Die geven we vervolgens mee als type voor onze Grain. Zo weet Orleans exact welke properties deel uitmaken van de state en dus gepersisteerd moeten worden in de database.

    Het mooie hieraan is dat Orleans veel werk voor je automatiseert. De state wordt opgeslagen en opnieuw opgehaald zonder dat je zelf allerlei complexe synchronisatielogica moet schrijven.

    Meerdere Silo’s, gedeelde state

    Zodra de cluster draait, kan je meerdere Silo’s opstarten. Elke Silo heeft toegang tot dezelfde Grain state, die centraal wordt opgeslagen in de database.

    Sluit je één Silo af, dan blijft de applicatie gewoon werken. De andere Silo’s nemen het over en halen de state opnieuw op waar nodig. Op die manier blijft Kenny dus leven, zelfs als een server uitvalt.

    De enige echte manier om Kenny te doden, is door alle servers tegelijk af te sluiten.

    En zelfs dan is hij niet helemaal weg.

    Want zodra je daarna opnieuw een Silo opstart, wordt de state weer uit de database geladen en kan je gewoon verdergaan waar je gebleven was.

    Conclusie: Kenny never dies?

    Niet helemaal. Kenny kan nog steeds sterven, maar actors maken het wel een stuk moeilijker.

    Actor frameworks zijn een bijzonder efficiënte manier om schaalbare applicaties te bouwen. Ze helpen je om state en logica op een gecontroleerde manier te verdelen, zonder meteen te vervallen in complexe threading-problemen of moeilijk beheersbare gedeelde state.

    Dat betekent niet dat actor-systemen altijd eenvoudig zijn. Zodra je met veel actoren werkt, of services over het netwerk via TCP/IP wil aanspreken, komt daar uiteraard extra complexiteit bij kijken. Je hebt dus wel degelijk wat kennis nodig van gedistribueerde systemen en netwerken.

    Maar eens alles opgezet is, biedt deze aanpak enorme voordelen. Nieuwe Silo’s toevoegen aan een cluster gaat vlot, de state blijft behouden en je applicatie wordt veel robuuster en schaalbaarder.

    En dat is nog maar het begin.

    Frameworks zoals Orleans en Akka.NET bieden ook ondersteuning voor zaken zoals timers, observers, persistence en nog veel meer. Wat we hier besproken hebben, is dus slechts een eerste stap in wat mogelijk is met actors.

    Code samples

    CodeSamples: GitHub

    Met dank aan

    AZUG – https://www.azug.be/

    Wim Van Houts – http://www.xqting.com en http://www.splitvice.com

    Start vandaag & daag onze experts uit!

    We bekijken graag je digitale mogelijkheden.

    Contacteer ons