Domain-driven Design und Microservices

Reactive Microservices mit Scala und Akka

Vaughn Vernon

© Shutterstock.com / Casther

Vaughn Vernon zeigt in diesem Artikel einen Zugangsweg zum Thema Microservices auf, der die Grundlage für die weitere Beschäftigung mit dem Thema bilden kann. Er stellt dazu das reaktive Programmiermodell vor und erklärt, wie Domain-driven Design für die Entwicklung reaktiver Microservices eingesetzt werden kann.

Hatten Sie jemals den Eindruck, dass unsere Branche von Extremen geprägt ist? Ich habe manchmal dieses Gefühl. Viele Entwickler scheinen eine sehr polarisierte Meinung zu vertreten, egal, ob es nun um die Entwicklung von Software oder deren Funktionsweise geht. Ich glaube indes, dass wir ein Gleichgewicht zwischen diesen Positionen erreichen könnten, wenn wir uns auf das Problem konzentrieren, das gerade anliegt.

Dabei denke ich zum Beispiel an die weit verbreitete Meinung, dass Daten per ACID-Transaktionen persistiert werden müssen. Ich möchte nun nicht falsch verstanden werden: Es ist nicht so, dass ich ACID-Transaktionen nicht nützlich finde. Das sind sie – sie werden allerdings viel zu oft benutzt. Könnten wir hier also nicht ein besseres Gleichgewicht finden?

Nehmen wir einmal an, dass über verschiedene Teilsysteme verteilte Daten eine gewisse Übereinstimmung aufweisen müssen. Manche bestehen nun darauf, dass solche abhängigen Daten auf der Transaktionsebene konsistent sein sollten, also überall zur gleichen Zeit auf dem gleichen Stand sein müssen (Abbildung 1).

Figure 1: Global Transaction

Abbildung 1: Globale Transaktion

Würden Sie dieser Sichtweise zustimmen? Verwenden einige Ihrer Software-Systeme solche globalen Transaktionen, um Daten in multiplen Systemen zu synchronisieren? Wenn ja, warum? Haben die Business-Stakeholder darauf bestanden, das so zu machen? Oder haben Sie an der Uni gelernt, warum die Konsistenz globaler Transaktionen wichtig ist? War es der Datenbank-Anbieter, der Ihnen ein derartiges Service-Level-Agreement verkauft hat?

Die wichtigste dieser drei Positionen ist die Business-Perspektive. Echte geschäftskritische Aspekte sollten darüber bestimmen, wann Daten konsistent sein müssen. Wann aber haben Sie zuletzt auf geschäftlicher Ebene nach den Anforderungen an die Daten-Konsistenz gefragt; was hat das Business-Ebene dazu gesagt?

Wenn wir einmal in die Prä-Computer-Ära zurückblicken, als die meisten Geschäftsfelder noch nicht von IT-Aspekten bestimmt waren, sehen wir ein deutlich anderes Bild der Datenkonsistenz. Damals war so gut wie gar nichts konsistent. Es konnte Stunden oder sogar Tage dauern, bis geschäftliche Transaktionen vollständig ausgeführt waren. Mit Papier-basierten Systemen ist es notwendig, Daten physisch vom Tisch eines Mitarbeiters zum nächsten zu tragen. Damals war selbst eine halbwegs vernünftige Datenkonsistenz absolut unerreichbar. Der springende Punkt dabei: Zu Problemen hat dies nicht geführt!

Mal im Ernst: Wenn man heute eine ehrliche Antwort auf das notwendige Maß der Datenkonsistenz auf Geschäftsebene bekäme, müsste diese häufig wohl lauten, dass viele Daten nicht bis auf die Millisekunde konsistent sein müssen. Trotzdem bringen extreme Sichtweisen in unserer Branche viele Entwickler dazu, überall eine absolute Datenkonsistenz anzustreben.

Figure 2: Isolated Transaction

Abbildung 2: Isolierte Transaktion

Wer mit Microservices arbeiten möchte, muss sich mit einer Eventual Consistency anfreunden, einer Konsistenz also, die lediglich garantiert, dass ein Datensatz nach einer gewissen Zeitspanne konsistent sein wird. Er muss verstehen, dass das dem Geschäft meistens nicht im Weg steht, selbst wenn Uni-Professor und Datenbank-Anbieter etwas anderes sagen. Ich erkläre später in diesem Artikel, wie das funktioniert.

Rethink-IT-Survey-300x180Microservices – Wie sieht es in Ihrem Unternehmen aus?

Die Themen Continuous Delivery, Microservices, Container, Cloud und agile Unternehmenskultur sind seit einigen Jahren in aller Munde. Aber wie sieht es in Unternehmen in diesen Bereichen wirklich aus? In unserer großen Umfrage zur DevOps-Kultur haben Sie die Gelegenheit klarzustellen, ob und wie DevOps in Ihrem Unternehmen umgesetzt wird.

An der Umfrage teilnehmen

Lassen Sie unsere Leser wissen, welche Tools wirklich verwendet werden und wie agil es auf dem realen Arbeitsmarkt zugeht. Räumen Sie mit Vorurteilen und Halbwissen auf und teilen Sie unseren Lesern mit, wie bei Ihnen die einzelnen Abteilungen und Teams zusammenarbeiten.

 

Microservices & Domain-driven Design

Es gibt noch ein weiteres Extrem, das sich in letzter Zeit eingeschlichen hat. Es geht um die Servicegröße, vor allem in Hinblick auf die Verwendung von „Microservices“. Was ist ein Microservice überhaupt, und was bedeutet das „Micro“ für seine Größe? Es werden da zum Teil verschiedene Richtlinien ausgegeben: Einige sprechen von 100, 400 oder 1000 Zeilen Code.

Wirklich? Ja, ich habe tatsächlich gesehen und gehört, dass Microservices nicht mehr als 100 Zeilen Code umfassen sollten. Andere sprechen von 400 Zeilen Code, manche sind etwas großzügiger und erlauben 1000 Zeilen. Also, was stimmt denn nun?

Ich glaube nicht, dass irgendeines dieser Extreme als Richtlinie für die Größe eines Microservices dienen kann. Wenn man beispielsweise der Regel zustimmt, dass Microservices nicht mehr als 400 Zeilen Code haben sollten – bedeutet das, dass mein Microservice mit 537 Zeilen Code zu groß ist? Gibt es eine magische Grenze bei 400 Zeilen Code, die dafür sorgt, dass der Service dann genau richtig ist?

Auf der anderen Seite gibt es aber auch diejenigen, die das andere Extrem vertreten und daran glauben, dass Softwaresysteme als Monolithen entwickelt werden sollten. In diesem Fall muss man davon ausgehen, dass so gut wie jedes Teilsystem, das zur Unterstützung des Kernsystems notwendig ist, in einer einzigen Codebasis zu finden ist. Wird dieses System nun deployt, müssen alle Teilsysteme mit deployt werden. Wenn sich nur eine Kleinigkeit in einem Teilsystem ändert, selbst wenn das die anderen Teile nicht beeinflusst, muss das gesamte System neu deployt werden, damit diese eine, isolierte Änderung beim Nutzer ankommt.

Natürlich scheint dieser Ansatz klare Nachteile zu haben; doch sind 400 Zeilen Code die Lösung?

Abbildung 3: Monolith vs. Microservices

Abbildung 3: Monolith vs. Microservices

Ich denke, dass die Betonung beim Wort „Micro“ mehr darauf liegen sollte, dass das Team keinen Monolithen baut, sondern stattdessen kleinere, unabhängige Services entwickelt, die zusammenarbeiten, um wichtige Geschäftsziele zu erreichen. Zu sagen, dass ein Service, der zu den „Micro“-Services gehört, nicht mehr als 400 Zeilen Code haben darf, ist keine ausgeglichene Position.

Nehmen wir einmal an, dass wir ein bereits bestehendes monolithisches System in Komponenten von jeweils nicht mehr als 400 Zeilen segmentieren wollen, um diese dann zu deployen. Bei 400 Zeilen Code sprechen wir dann wahrscheinlich davon, dass jeder Microservice nur einen Entity-Typ umfassen soll. Dann hätte man am Ende hunderte bis tausende von sehr kleinen Microservice-Komponenten. Welchen Problemen könnte sich ein Team nun also gegenübersehen, wenn es um die Administration all dieser Microservices geht? Allein schon die Hardware und die Netzwerk-Infrastruktur wären sehr komplex. Es könnte gelinde gesagt ziemlich schwierig werden, ein solches System zu erstellen und zu pflegen.

Wenn wir nun aber nach einem Gleichgewicht in Sachen „Micro“ suchen, könnte sich ein „Maßstab“ für die Service-Größe anbieten, der nicht nur logisch, sondern sogar auf Geschäftsprozesse ausgerichtet ist. Da ich ein großer Befürworter des Domain-Driven Design (DDD) in der Softwareentwicklung bin, schlage ich dazu die Anwendung der zwei wichtigsten Konzepte des DDD vor, um die Größe von Microservices zu bestimmen: Bounded Context und Ubiquitous Language.

Abbildung 4: Bounded Context

Abbildung 4: Bounded Context

Wer das Buch „Building Microservices“ von Sam Newmann gelesen hat, weiß bereits, dass Sam sagt, dass Microservices vom Bounded Context definiert werden sollten. Sam und ich stimmen darin überein. Die Aussage, dass ein Microservice ein Bounded Context ist, umfasst allerdings noch keine Information zur Größe des Microservice. Das liegt daran, dass die Größe des Bounded Context von seiner Ubiquitous Language bestimmt wird. Wenn man also die Größe des Bounded Context (und somit des Microservice) wissen möchte, muss man die Ubiquitous Language fragen.

Die Ubiquitous Language des Bounded Context wiederum wird gemeinsam von Experten für die Branche und Entwicklern bestimmt. Man muss verstehen, dass die Verwendung der Sprache der Business-Treiber bedeutet, dass alle linguistisch zusammenhängenden Komponenten im selben Bounded Context erscheinen. Was linguistisch nicht zu einem bestimmten Bounded Context gehört, gehört somit zu einem anderen.

Abbildung 5: Verschiedene Bounded Contexts

Abbildung 5: Verschiedene Bounded Contexts

Ich habe festgestellt, dass ein Bounded Context, der wirklich von einer Business-getriebenen Ubiquitous Language begrenzt wird, recht klein ist. Er ist typischerweise größer als eine Entity (obwohl auch ein so kleiner Bounded Context möglich ist), aber gleichzeitig viel, viel kleiner als ein Monolith. Es ist unmöglich, eine exakte Zahl anzugeben, weil die Ubiquitous Language komplett vom jeweiligen Geschäftsbereich bestimmt wird. Viele Bounded Contexts umfassen aber, um dennoch einmal eine Zahl in den Raum zu werfen, insgesamt zwischen 5 und 10 Entity-Typen. 20 Entity-Typen könnten hingegen schon einen großen Bounded Context darstellen.

Insofern ist ein Bounded Context also von einem kleinen, ja, von einem „Micro“-Format, vor allem wenn man ihn mit einem Monolithen vergleicht. Wenn man jetzt darüber nachdenkt, einen Monolithen in eine Reihe von Bounded Contexts zu unterteilen, wird die Depolyment-Topologie nicht ganz so unübersichtlich.

Ich glaube, dass das ein guter Ausgangspunkt ist, wenn ein Team sich vornimmt, einen Legacy-Monolithen in eine Reihe von Microservices zu unterteilen. Wer mit Bounded Context und Ubiquitous Languages seine ersten Schritte in die Welt der Microservices tut, wird die Monolithen schnell hinter sich lassen.

scala-iconDossier Scala
Im Themen-Dossier „Scala“ dreht sich alles um die JVM-Sprache Scala.

 

Was ist Reaktive Software?

Reactive Software wird anhand dieser vier zentralen Merkmale definiert:

  • Responsiv
  • Resilient
  • Elastisch
  • Message Driven

Responsiv ist ein System, wenn es auf herausragende Weise auf User Requests und Integrationen im Hintergrund reagiert. Wer die Lightbend Platform verwendet, wird beeindruckt davon sein, wie viel Responsiveness sich erreichen lässt. Ein gut designter Microservice kann Write-basierte Requests in 20 Millisekunden oder weniger verarbeiten, sogar die Hälfte dieser Zeit ist nicht selten.

Systeme, die unter Verwendung von Akka auf dem Aktoren-Model basieren, können unglaublich resilient sein. Supervisor-Hierarchien sorgen dafür, dass die Eltern-Kette von Komponenten für das Auffinden und Beheben von Fehlern zuständig ist, sodass die Clients nur noch mit den Services beschäftigt sind, die sie benötigen. Anders als bei Code, der in Java geschrieben ist und Exceptions auswirft, müssen sich Clients von Aktor-basierten Services nie selbst mit Fehlern des Aktoren befassen, von dem sie einen Service anfordern. Clients müssen stattdessen nur den Request-Response-Contract verstehen, den sie mit einem bestimmten Service eingehen, und Requests gegebenenfalls wiederholen, wenn keine Antwort in einem bestimmten Zeitraum erfolgt.

Elastisch ist eine Microservice-Plattform dann, wenn sie je nach Bedarf hoch und herunter skalierbar ist. Ein Beispiel dafür ist ein Akka Cluster, das ohne Funktionsverlust auf 2400 Nodes skalieren kann. Elastisch bedeutet aber auch, dass nur so viele Nodes zugewiesen werden, wie gerade notwendig sind – also keine 2400 Nodes, wenn sie nicht gebraucht werden. Wenn man Akka und andere Komponenten der Reactive Platform ausführt, gewöhnt man sich daran, deutlich weniger Server zu verwenden als mit anderen Plattformen (z.B. JEE). Das liegt daran, dass Akka Fähigkeiten im Bereich der Nebenläufigkeit mitbringt, die es Microservices erlauben, die Ressourcen aller Server jederzeit zu nutzen, ohne sie dafür zu blockieren.

Das Aktoren-Modell in Akka ist bis in den Kern Ereignis-getrieben. Um einen Service von einem Aktoren abzurufen, wird eine Nachricht abgeschickt, die asynchron ausgeliefert wird. Um auf einen Request von einem Client zu antworten, schickt der Service-Aktor eine Nachricht an den Client, die ebenfalls asynchron ausgeliefert wird. Mit der Reactive Platform arbeiten auch Web Components asynchron. Der Persistenz-Mechanismus ist grundsätzlich als asynchrone Komponente angelegt, wenn er einen Persistent State eines Aktoren speichert. Das bedeutet, dass Interaktionen mit der Datenbank entweder komplett asynchron oder nur minimal blockierend ablaufen. Der zentrale Punkt ist tatsächlich, dass der Aktor, der die Persistenz angefragt hat, den Thread nicht blockiert, solange er darauf wartet, dass die Datenbank die Anfrage bearbeitet. Das alles ist aufgrund des asynchronen Messagings möglich.

Reaktive Komponenten

Auf jeder logischen Schicht der Architektur finden sich die folgenden Lightbend Plattform- und Microservice-Komponenten, die Ihr Team verwenden oder weiterentwickeln kann (Abbildung 6).

Abbildung 6: Lightbend Platform Components

Abbildung 6: Lightbend Platform Components

Wie ein Persistenz-Aktor auf Basis von Akka aussieht, zeigt Listing 1.

 
class Product(productId: String) extends PersistentActor { override def persistenceId = productId
var state: Option[ProductState] = None
override def receiveCommand: Receive = {
case command: CreateProduct =>
  val event = ProductCreated(productId, command.name, command.description)
  persist(event) { persistedEvent =>
    updateWith(persistedEvent)
    sender ! CreateProductResult(
}
... }
productId,
command.name,
command.description,
command.requestDiscussion)
override def receiveRecover: Receive = {
case event: ProductCreated => updateWith(event) ...
}
  def updateWith(event: ProductCreated) {
    state = Some(ProductState(event.name, event.description, false, None))
} }

Diese Komponente basiert auf Event Sourcing. Die Product Entity erhält Commands und gibt Events aus. Die Events werden verwendet, um den Status der Entity festzulegen, sowohl während die Commands verarbeitet werden als auch wenn die Entity angehalten, aus dem Speicher entfernt und dann aus alten Events wiederhergestellt wird.

Aus diesen Events können wir nun weiteren Nutzen ziehen. Zuerst können wir Events in Views projizieren, die die Nutzer zu sehen bekommen, indem wir auf bestimmte Anwendungsfälle zugeschnittene Queries erstellen. Zweitens können die Events einer größeren Anzahl von Microservices zugänglich  gemacht werden, die darauf reagieren und mit dem Microservice interagieren müssen, der das Event ausgegeben hat.

Abbildung 7: Event-driven Architecture

Abbildung 7: Event-driven Architecture

Was ich hier beschreibe, ist eine Ereignis-getriebene Architektur, die komplett reaktiv ist. Aktoren innerhalb aller Microservices machen sie reaktiv; Microservices, die Events aus anderen Microservices konsumieren, sind ebenfalls reaktiv. An dieser Stelle kommt die Eventual Consistency in verschiedenen Transaktionen ins Spiel. Wenn andere Microservices die Events sehen, erzeugen und/oder modifizieren sie den Zustand, den sie in ihrem Kontext besitzen, und sorgen so nach und nach für die Übereinstimmung des gesamten Systems.

Ich habe drei Bücher über die Entwicklung dieser Art von Microservices auf Basis des Domain-Driven Designs geschrieben und gebe Workshops zu diesen Themen. Für Feedback und weitere Informationen darüber, wie man diese Microservice-Architektur umsetzen kann, kontaktieren Sie mich gerne.

W-JAX
Mike Wiesner

Data- und Event-driven Microservices mit Apache Kafka

mit Mike Wiesner (MHP – A Porsche Company)

Niko Köbler

Digitization Solutions – a new Breed of Software

with Uwe Friedrichsen (codecentric AG)

Software Architecture Summit 2017
Dr. Carola Lilienthal

The Core of Domain-Driven Design

mit Dr. Carola Lilienthal (Workplace Solutions)

Sascha Möllering

Reaktive Architekturen mit Microservices

mit Sascha Möllering (Amazon Web Services Germany)

Geschrieben von
Vaughn Vernon
Vaughn Vernon
Vaughn is a veteran software craftsman, with more than 30 years of experience in software design, development, and architecture. He is a thought leader in simplifying software design and implementation using innovative methods. Vaughn is the author of the books “Implementing Domain-Driven Design”, “Reactive Messaging Patterns with the Actor Model”, and “Domain-Driven Design Distilled”, all published by Addison-Wesley. Vaughn speaks and presents at conferences internationally, he consults, and has taught his “IDDD” Workshop and “Go Reactive with Akka” workshop around the globe to hundreds of developers. He is also the founder of “For Comprehension”, a training and consulting company, found at ForComprehension.com You may follow him on Twitter: @VaughnVernon
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.