Scala: Microservices mit dem Actor-Modell serialisieren

Seite 3: Vorteile von CQRS gegenüber dem Read-Modify-Write Pattern

Inhaltsverzeichnis

Da stets nur ein einziger Actor für einen Vorgang zuständig ist, der Messages nacheinander verarbeitet, treten keine Race Conditions auf, wie das folgende Beispiel aus der Praxis verdeutlicht. Im Rahmen eines Projekts einer Online-Klickstrecke für einen Antragsvorgang werden nach jeder Seite die Daten übermittelt. In dieser Konstellation treten – in seltenen Fällen – Datenverluste auf, wenn zwei Requests kurz nacheinander erfolgen.

Während der erste Request die Daten aus der Datenbank lädt, einen externen Service anfragt und anschließend alle Daten zurück in die Datenbank schreibt und der zweite Request ebenfalls die Daten liest, die übermittelten Daten einfügt und dann den kompletten Datensatz zurückschreibt, kann es zum Verlust von Daten kommen. Das passiert genau dann, wenn die Anfrage des externen Services zu lange dauert (mehrere Sekunden). Der zweite Request überholt in diesem Fall den ersten, sodass die von ihm gespeicherten Daten vom ersten Request überschrieben werden – eine klassische Race Condition.

Bei korrekter Implementierung mit Actors kann der Fall nicht auftreten. Der Actor bearbeitet einen Request nach dem anderen. Dabei ändert sich jeweils nur sein interner Zustand. Da für jeden Vorgang ein eigener Actor zuständig ist, entsteht auch kein Bottleneck. Es lassen sich beliebig viele Vorgänge gleichzeitig bearbeiten. Das Sichern des internen Zustands übernimmt die Actor-Persistenz.

Ein weiterer Vorteil gegenüber dem Read-Modify-Write Pattern liegt in der geringeren Anzahl der Datenbankzugriffe. Vor allem Lesezugriffe sind praktisch nur beim Wiederherstellen alter Vorgänge aus der Datenbank notwendig, und die lassen sich als eine einzige Query ausführen, die alle benötigten Datensätze der Reihe nach zurückgibt. Entwicklerinnen und Entwickler können die Datenbank daher stark auf Schreibzugriffe optimieren, beziehungsweise auf für diese Zwecke ausgelegte Datenbanken wie Cassandra, aber auch auf MariaDB, MySQL oder PostgreSQL zurückgreifen.

Neben den offensichtlichen Vorteilen gilt es allerdings auch Einschränkungen zu berücksichtigen. Im Fall von Cluster-Sharding beispielsweise, durch eine Unterbrechung in der Netzwerkverbindung innerhalb eines Clusters, müssen Entwickler verhindern, dass zwei Singletons eines Cluster-Coordinator entstehen. Auch wenn der Node, der ein Singleton erstellt hat, plötzlich offline ist, kann das ein Problem darstellen. Bis ein Node aus dem Cluster entfernt und anschließend ein neues Singleton erstellt ist, dauert es ein paar Sekunden. Die Anwendung muss mit dieser Unterbrechung umgehen können.

Um Microservices performant skalieren zu können, greifen Entwicklerinnen und Entwickler häufig auf die containerisierte Bereitstellung mit Kubernetes zurück. Wichtig dabei sind nicht nur das Hinzufügen neuer Ressourcen, sondern auch das Entfernen nicht mehr benötigter. Akka-Cluster und Kubernetes ergänzen sich bei der Skalierung. Fügt Kubernetes neue Instanzen hinzu, nimmt Akka diese in den Cluster auf. Kubernetes leitet alle Requests über eine Round-Robin-Verteilung gleichmäßig an alle Knoten weiter. Da sämtliche Vorgänge über Actors abgebildet sind, kann der Cluster intern die Requests zum passenden Actor routen. Neue Vorgänge werden an den Node Coordinators mit den wenigsten Vorgängen erstellt. Der Cluster balanciert sich selbst aus.

Beendet Kubernetes eine Instanz wegen geringer Auslastung, sollte zuvor idealerweise der Knoten aus dem Cluster entfernt werden, um dessen Restrukturieren zu beschleunigen. Die Actors auf den entfernten Knoten fallen dabei zwar einfach weg, lassen sich aber leicht auf einem anderen Node des Clusters wiederherstellen. Das sollte vorzugsweise auf einem Node geschehen, der aktuell weniger Actors verwaltet. Der Cluster balanciert sich auf diese Weise wieder aus.