Kolumne: Olis bunte Welt der IT

Microservices und Aktoren mit Akka.NET im .NET-Umfeld
Kommentare

Aktoren sind eine alte Idee, versprechen jedoch viele moderne Vorteile in ebenso modernen Umgebungen. Mit dem Framework Akka.NET sind Aktoren mittlerweile auch in C# und anderen .NET-Sprachen verwendbar, und es ist Zeit, den Ansatz etwas näher zu betrachten und in den Kontext moderner Entwicklung stark modularisierter und dienstbasierter Anwendungen zu bringen.

In den Kolumnen der letzten Ausgaben habe ich mehrfach das Thema Microservices angesprochen. Dahinter verbergen sich mehrere Prioritäten, die in der modernen Softwareentwicklung als relevant angesehen werden. Wie der Name der Idee vermuten lässt, geht es zentral um die Erzeugung von „kleinen“ Diensten.

Dies ist jedoch nur zum kleinen Teil ein Selbstzweck, interessanter sind die Konsequenzen, die sich allerseits aus dieser Struktur von Anwendung und Code ergeben. Eine solche Konsequenz, die von Anhängern der Microservices besonders gern genannt wird, ist die Freiheit bei der Wahl von Plattformen und Programmierwerkzeugen, die sich aus der Unabhängigkeit zwischen Diensten ergibt. Wenn Dienste eigenverantwortlich arbeiten und mit klaren Schnittstellen definiert werden, bilden sich Module, deren interne Implementation beliebig gestaltet werden kann.

Modularisierung ist natürlich immer ein interessantes Ziel, das Programmierer schon lange verfolgt haben. Möglichst granular zu modularisieren, bietet Vorteile in der Wartbarkeit: Je kleiner ein Modul, desto einfacher ist es zu warten. In der Vergangenheit wurde dieser einfache Zusammenhang jedoch oft nicht als der wichtigste angesehen, sondern vielmehr mit dem Hauptziel der Wiederverwendbarkeit modularisiert. So bietet die Idee von Microservices auch in diesem Bereich eine neue Sichtweise.

In der Praxis ist alles anders

Bei der Erstellung einer Anwendung, die aus zahlreichen einzelnen Diensten bestehen soll, zeigen sich schnell einige Probleme. In der Praxis ist es schwierig und aufwändig, tatsächlich jeden Dienst vollständig unabhängig von jedem anderen anzusehen. Letztlich müssen Dienste gemeinsam an der Lösung von Problemen arbeiten, wozu der Umgang mit gemeinsamen Daten notwendig ist. Entweder muss also eine gemeinsame Datenhaltung implementiert oder ein Schema entworfen werden, mit dem relevante Informationen von Dienst zu Dienst weiter- und zurückgereicht werden können. Schnittstellen für diese Zwecke immer wieder neu und potenziell unterschiedlich zu erarbeiten, ist offensichtlich weder der Effizienz noch der Wartbarkeit zuträglich. So ergibt sich in einem Gesamtsystem Affinität zwischen Diensten oder Gruppen von Diensten.

Ein weiterer Aspekt mit ähnlichen Folgen ist die Publikation von Diensten. Zunächst müssen Dienste irgendwo ausgeführt werden, und manche müssen extern, also über ein Netzwerk, ansprechbar sein. Sicherheit ist immer wichtig, Skalierbarkeit soll her, die Systemadministratoren wollen konsistente Zustandsprotokolle und andere Formen der Systemüberwachung. Es ist verlockend, zu diesen Zwecken eine vorhandene Softwareinfrastruktur einzusetzen, wie sie etwa von den unterschiedlichen Cloud-Anbietern angeboten wird. Auch dieser Schritt erzeugt Affinität für Gruppen von Diensten, wenn nicht gar gesamte Systeme.

Mächtige Plattformen sparen Arbeit

Insgesamt darf also zum Thema Microservices festgehalten werden, dass erzwungene Modularisierung und Kapselung auf Dienstebene wertvolle Ziele darstellen, letztlich dadurch eine Struktur aufgebaut wird, in der die Freiheit besteht, später Änderungen vorzunehmen, falls die Anforderungen sich entsprechend entwickeln. Um allerdings Entscheidungen für einzelne Plattformen zu treffen, müssen triftige Gründe vorliegen, und einzelne Aspekte von Entwicklungs- und Laufzeitumgebungen sollten ebenso sorgfältig ausgewertet werden wie in der Zeit vor den Microservices.

Letztlich kann ich natürlich alles selbst programmieren, aber die Verwendung von bestehenden Plattformen, Libraries und Frameworks zur Arbeitserleichterung ist eine starke Versuchung. Je mehr eine Plattform bietet, desto mehr mag mein Code letztlich an sie gebunden sein. Puristen mögen daher vorschlagen, mehr selbst zu machen – für den Pragmatiker hingegen ist die sorgfältige Auswahl einer oder mehrerer spezifischer Standards nach wie vor von großer Bedeutung.

Und nun zu Akka.NET

In welchem Zusammenhang stehen die bisherigen Betrachtungen nun zum eingangs erwähnten Akka.NET? Um dies zu erklären, möchte ich den Nutzen dieses Frameworks etwas näher beschreiben. Auf technischer Ebene geht es um die Verwendung von Aktoren, die bei Verwendung von Akka.NET etwa mit C# oder VB.NET durch Klassen repräsentiert werden. (Es gibt auch ein API für die Verwendung mit F#, das eine funktionale Struktur ermöglicht.)

Aktoren sind Entitäten, die Funktionalität und Zustandsinformationen kapseln, und sie kommunizieren untereinander durch Nachrichten (Messages). Ein System von Aktoren kann vollständig auf einer einzelnen physikalischen Maschine ausgeführt werden, aber einzelne oder alle Aktoren können ebenfalls auf anderen Systemen laufen und über ein Netzwerk angesprochen werden. Mit dieser grundlegenden Zusammenfassung wird schnell offensichtlich, dass ein Vergleich zwischen einem Aktor und einem Dienst angebracht ist. Es gibt viele Parallelen, der Hauptunterschied besteht aber in der Fähigkeit eines Aktors, Zustandsinformationen zu speichern.

Indem ein System auf Basis von Aktoren modelliert wird, ergibt sich automatisch eine strikte Trennung von Zuständigkeiten und eine klare Kapselung von Informationen. Der Begriff „Aktor“ weist darauf hin, dass Aktoren im Wesentlichen Handlungen abbilden sollen. Daher sollten Sie einen Aktor nicht in Zusammenhang mit einer geschäftlichen Entität betrachten, sondern als einen Block von Funktionalität, zum Beispiel von Geschäftslogik. Zur Illustration finden Sie in Listing 1 einen Teil eines Aktors, der eine Berechnung ausführt und im Anschluss Zustandsnachrichten an andere Aktoren sendet.

public class PointCalculator : TypedActor, IHandle {
  public void Handle(CalcPoint message) {
    var iterations = Iterator(
      message.CalcInfo.XStart + message.Point.X * message.CalcInfo.XStep,
      message.CalcInfo.YStart + message.Point.Y * message.CalcInfo.YStep,
      message.CalcInfo.MaxIterations);
    message.ResultReceiver.Tell(
      new PointResult(message.Point, ColorFromIterations(iterations,
        message.CalcInfo.MaxIterations)));
    Sender.Tell(new AreaCalculator.PointDone());
  }

  ...
}

Nachrichten

Die Aufrufe an die Methode Tell zeigen, wie dieser Aktor mit anderen Aktoren durch den Versand von Nachrichten kommunizieren kann. Die Nachrichten sind selbst wiederum einfache Klassen, unveränderbar (immutable) implementiert. Auch der Typ CalcPoint ist eine solche Nachricht, die von außen an diesen Aktor gesendet werden kann (Listing 2).

public class CalcPoint {
  public CalcPoint(IActorRef resultReceiver, Point point, CalcInfo calcInfo) {
    ResultReceiver = resultReceiver;
    Point = point;
    CalcInfo = calcInfo;
  }

  public IActorRef ResultReceiver { get; }
  public Point Point { get; }
  public CalcInfo CalcInfo { get; }
}

Um einen Aktor anzusprechen, benötigt der Code natürlich eine Referenz dazu. Wenn der Aktor noch nicht existiert, kann dieser etwa so erzeugt werden:

pointCalculator = Context.ActorOf("pointCalculator");

Warten? Nein, danke!

Anhand dieser einfachen Beispiele ist schnell ersichtlich, dass die Programmierung mit Aktoren eine ähnliche Struktur erzeugt, wie sie sich in funktionalem Code darstellt, oder bei der Verwendung von Diensten. Informationen, mit denen ein Aktor arbeiten soll, müssen mithilfe der Nachrichten verfügbar gemacht werden. Wenn Resultate erzeugt werden, können diese wiederum als Nachrichten versandt werden. Letzterer Mechanismus ist übrigens eine Abweichung vom typischen funktionalen Modell, denn die Methode, die eine Nachricht in einem Aktor verarbeitet, hat keinen Rückgabewert! Es gibt zwar Hilfsmethoden, mit denen ein Aufrufer direkt auf Antwort warten kann, aber deren Verwendung wird nicht allgemein empfohlen, da sie die lose Kopplung eines Systems, das auf Nachrichtenübertragung basiert, außer Kraft setzen. Nachrichten sind von Natur aus asynchron, auf Rückgabewerte (im technischen Sinne eines return) muss gewartet werden.

Um eine Nachricht an einen Aktor zu senden, verwendet Akka.NET das Interface IActorRef. Dieses Interface kann einen anderen lokal laufenden Aktor beschreiben, aber auch einen, der auf einer anderen Maschine läuft und über das Netzwerk angesprochen werden muss. Für den Aufrufer ist dies transparent, und die Infrastruktur sorgt automatisch für die Übertragung der Nachrichten an das richtige Ziel. Es ist sogar möglich, dass ein IActorRef eine Gruppe von Aktoren bezeichnet. Nachrichten an dieses IActorRef werden von einem Router verteilt, und es gibt einige Standardrouter im Paket Akka.NET. Mit folgender Initialisierung etwa werden mehrere Instanzen des Aktors PointCalculator erstellt, die reihum Nachrichten zur Verarbeitung zugeteilt bekommen.

pointCalculator = Context.ActorOf(
  Props.Create().WithRouter(new RoundRobinPool(4)),
  "pointCalculator");

Infrastruktur

In ähnlicher Weise können Aktoren mit verschiedenen Dispatchern arbeiten, etwa zur Unterstützung des Threadpools oder zur Verwendung von Tasks. Ein besonderer Dispatcher kann auch automatisch einen bestimmten Synchronisationskontext aktivieren, was bei der Aktualisierung von Benutzerschnittstellen in WPF oder Windows Forms Arbeit spart. All diese Vorgänge sind für den Versender von Nachrichten unsichtbar, und selbst die Initialisierung von Aktoren kann mithilfe von externen Konfigurationen oder Dependency Injection automatisiert werden, sodass der Code eines Aktors sehr stabil und ohne Betrachtung externer Umstände geschrieben werden kann.

Die Stabilität eines Aktorensystems ist tatsächlich eine Grundidee der aktorenbasierten Programmierung, und Akka.NET bietet Werkzeuge, um Hierarchien von Aktoren zu erzeugen und zu überwachen. Es bietet sich an, Aktoren wie eine Gruppe von Mitarbeitern zu betrachten, die gemeinsam an der Lösung eines Problems arbeiten, gruppiert in Arbeitsgruppen oder Teams. In solch einer Umgebung werden Aufgaben und Teilaufgaben erzeugt und zur Bearbeitung an verantwortliche Gruppenleiter übertragen, die wiederum aufteilen, delegieren und zuweisen und letztlich erarbeitete Resultate liefern. Wenn ein Mitarbeiter bei der Bearbeitung seiner Teilaufgabe ein Problem findet, einen Umstand, der die Bearbeitung unmöglich macht, stellt er die Arbeit vorläufig ein und informiert seine Chefin, die wiederum entweder einen anderen Weg findet oder ihrerseits nach „oben“ Bescheid gibt.

Dieselben Zusammenhänge werden auch in Aktorensystemen abgebildet: Aktoren können in ihrem jeweiligen Kontext weitere Aktoren aufnehmen und so eine Hierarchie aufbauen, um eine Problemlösung umzusetzen. Fehlerzustände werden entsprechend der Hierarchie überwacht und weitergeleitet, und es gibt Strategien, wie Aktoren behandelt werden, die einen Fehler erzeugt haben, zum Beispiel durch Neustart (der übrigens für den Halter einer entsprechenden IActorRef unsichtbar sein kann) mit oder ohne Haltung des aktuellen Zustands.

Eine der bekanntesten Plattformen, auf der das Aktorenmodell eingesetzt wird, ist Erlang. Besonders bekannt ist Erlang für seine erstaunliche Laufzeitstabilität, aber auch für die Leistungsfähigkeit des Aktorenmodells zur parallelen Verarbeitung, die sich etwa Facebook (für die Umsetzung einer Generation von Facebook Chat) und WhatsApp zunutze gemacht haben. Akka.NET setzt dieselben Ideen um und bietet Programmierern auf der .NET-Plattform alle wesentlichen Werkzeuge, um Aktoren zum Baustein einer Anwendungsarchitektur zu machen.

Fazit

Akka.NET ist ein großartiges Framework, das Lösungen für viele Belange der modernen Entwicklung bietet. Es basiert auf einem stabilen Konzept, das zwar in .NET neu ist, aber anderswo seit vielen Jahren erfolgreich eingesetzt wird. Interessant finde ich persönlich auch, dass ein Aktorensystem beliebig groß oder klein sein kann. Somit lassen sich damit algorithmische Zusammenhänge darstellen und gleichzeitig Modularisierung und saubere Kapselung vorantreiben oder gar erzwingen.

Aktoren lassen sich als Dienste betrachten und Akka.NET als Plattform für Dienstgruppen oder gar komplette verteilte Systeme. Viele Standarddienste, die direkt in Akka.NET enthalten oder separat bei NuGet verfügbar sind, bilden eine ausgereifte Infrastrukturlandschaft. Der Zusammenhang und die Parallelen mit der Idee der Microservices sind deutlich erkennbar, und Akka.NET-basierter Code lässt sich in beliebiger Einfachheit oder Komplexität mit anderen Elementen kombinieren.

Windows Developer

Windows DeveloperDieser Artikel ist im Windows Developer erschienen. Windows Developer informiert umfassend und herstellerneutral über neue Trends und Möglichkeiten der Software- und Systementwicklung rund um Microsoft-Technologien.

Natürlich können Sie den Windows Developer über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist der Windows Developer ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -