Microservices Blog - Architektur, Skalierung und Best Practices https://jax.de/blog/microservices/ Java, Architecture & Software Innovation Fri, 18 Oct 2024 13:30:25 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Exactly Once in verteilten Systemen: Realität oder Utopie? https://jax.de/blog/exactly-once-idempotenz-java/ Mon, 10 Jun 2024 13:24:45 +0000 https://jax.de/?p=89825 Sind verteilte Systeme im Einsatz, wie es beispielsweise bei Microservices der Fall ist, ist eine verteilte Datenverarbeitung – häufig asynchron über Message Queues – an der Tagesordnung. Werden Nachrichten ausgetauscht, sollen diese häufig genau einmal verarbeitet werden – gar nicht so einfach, wie sich herausstellt.

The post Exactly Once in verteilten Systemen: Realität oder Utopie? appeared first on JAX.

]]>
Bei verteilten Systemen wird eine asynchrone Kommunikation häufig über einen Message Broker abgedeckt. Dadurch soll eine Entkopplung zwischen zwei Diensten erreicht werden, die unter Umständen separat skaliert werden können. Eine Kommunikation über einen Message Broker ist inhärent immer mindestens zweigeteilt, nämlich in Producer und Consumer.

Ein Producer erstellt dabei Nachrichten, wie in Abbildung 1 gezeigt, während ein Consumer sie verarbeitet.

Abb.1: Einfacher Nachrichtenaustausch

Der Message Broker ist häufig ein zustandsbehaftetes System – eine Art Datenbank – und vermittelt Nachrichten zwischen Producer und Consumer. Als zustandsbehaftetes System hat ein Message Broker die Aufgabe, Nachrichten vorzuhalten und abrufbar zu machen. Ein Producer schreibt also Nachrichten in den Broker, während ein Consumer sie zu einer beliebigen Zeit lesen kann. Exactly once, also einmaliges Ausliefern, bedeutet, dass der Producer genau eine Nachricht produziert und der Consumer diese genau einmal verarbeitet. Also ganz einfach, oder?

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Es kann so einfach sein

Da eine Kommunikation zwischen den Systemen über eine Netzwerkebene stattfindet, ist nicht gewährleistet, dass die Systeme über den gleichen Wissensstand verfügen. Es muss also einen Rückkanal geben, der einzelne Operationen bestätigt, um einen Zustand zu teilen. Nur so wird sichergestellt, dass Nachrichten, die erstellt wurden, auch korrekt angekommen und verarbeitet worden sind. Aktualisiert sieht der Fluss also eher so aus, wie es Abbildung 2 zeigt.

Abb. 2: Durch Bestätigungen wird der Zustand zwischen Systemen geteilt

Erstellt ein Producer eine Nachricht, die vom Consumer gelesen werden soll, benötigt dieser eine Bestätigung (Abb. 2, Schritt 2). Nur dadurch weiß der Producer, dass die Nachricht korrekt im Broker persistiert vorliegt und er sie nicht erneut übertragen muss.

Der Consumer wiederum liest die Nachricht und verarbeitet sie. Ist die Verarbeitung fehlerfrei abgeschlossen, bestätigt der Consumer das. Der Broker muss die Nachricht deshalb nicht noch einmal ausliefern.

Immer Ärger mit der Kommunikation

Bei einer verteilten Kommunikation über ein Netzwerk kann es leider immer passieren, dass diverse Kommunikationskanäle abbrechen oder Fehler entstehen – und zwar an vielen Stellen: beim Erstellen, vor dem Konsumieren und danach. Genau diese Eigenschaft macht es so schwer oder gar unmöglich, eine Exactly-once-Semantik zu erreichen.

Angenommen, ein Producer produziert eine Nachricht (Abb. 3). Um sicherzustellen, dass diese auch vom Broker gespeichert wurde, wartet der Producer auf eine Bestätigung. Bleibt sie aus, ist nicht garantiert, dass der Broker diese Nachricht wirklich ausliefern wird.

Abb. 3: Der Producer erhält keine Bestätigung und sendet die Nachricht erneut

Der Producer muss diese Nachricht folglich erneut ausliefern. Genau das ist in Schritt 2 der Abbildung 3 auch geschehen, weshalb der Producer in Schritt 3 eine weitere Nachricht mit demselben Inhalt sendet. Da jetzt zwei Nachrichten vorliegen, verarbeitet der Consumer in den Schritt 4 und 5 beide Nachrichten – wohl eher nicht „exactly once“. Die Nachricht wird durch den Retry-Mechanismus „at least once“ – mindestens einmal, nicht genau einmal – übertragen. Denn wie im Bild zu erkennen ist, überträgt der Producer dieselbe Nachricht zweimal, um sicherzustellen, dass sie mindestens einmal vom Broker bestätigt wurde. Nur so ist sichergestellt, dass die Nachricht nicht verloren geht.

ALL ABOUT MICROSERVICES

Microservices-Track entdecken

 

Natürlich kann die Bestätigung auch ignoriert werden. Schritt 2 kann also ausbleiben. Ein Retry-System würde folglich fehlen. Der Producer überträgt also eine Nachricht, ohne auf eine Bestätigung des Brokers zu warten. Kann der Broker die Nachricht selbst nicht verarbeiten oder wegspeichern, hat er keine Möglichkeit, das Fehlschlagen oder eine erfolgreiche Operation zu quittieren. Die Nachricht würde „at most once“ – maximal einmal oder eben keinmal – übertragen werden. Exactly once ist also grundsätzlich ein Problem verteilter Anwendungen, die mittels Bestätigungen funktionieren.

Leider ist das noch nicht das Ende der Fahnenstange, wenn die Nachricht Ende zu Ende, also vom Producer bis zum Consumer, betrachtet wird. Denn es existiert in einem solchen System zusätzlich ein Consumer, der die Nachrichten wiederum einmalig verarbeiten muss. Selbst wenn garantiert wird, dass der Producer eine Nachricht einmalig erzeugt, ist ein einmaliges Verarbeiten nicht garantiert.

Abb. 4: Ein Consumer verarbeitet die Nachricht und versucht diese danach zu bestätigen

Es kann passieren, dass der Consumer wie in Abbildung 4 gezeigt die Nachricht in Schritt 3 liest und in Schritt 4 korrekt verarbeitet. In Schritt 5 geht die Bestätigung verloren. Das führt dazu, dass die Nachricht mehrmals, aber mindestens einmal – at least once – verarbeitet wird.

Abb. 5: Ein Consumer bestätigt die Nachricht vor dem Verarbeiten

Es ist natürlich umgekehrt auch möglich, die Nachricht vor dem Verarbeiten zu bestätigen. Der Consumer lädt also die Nachricht und bestätigt sie direkt. Erst dann wird in Schritt 5 von Abbildung 5 die Bearbeitung der Nachricht erfolgen. Schlägt jetzt die Bearbeitung fehl, ist die Nachricht in Schritt 4 bereits bestätigt worden und wird nicht erneut eingelesen. Die Nachricht wurde wieder maximal einmal oder keinmal – at most once – verarbeitet.

Wie also zu erkennen, ist es leicht, At-most-once- und At-least-onceSemantiken in den verschiedenen Konstellationen sowohl auf Producer- als auch auf der Consumer-Seite herzustellen. Exactly once ist aber aufgrund der verteilten Systematik ein schwieriges Problem – oder gar unmöglich?

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Lösungen müssen her

Für eine Möglichkeit, eine Exactly-once-Semantik zu erreichen, muss die Verarbeitung der Nachrichten einer Applikation eine bestimmte Eigenschaft unterstützen: Idempotenz. Idempotenz bedeutet, dass eine Operation, egal wie oft sie verarbeitet wird, immer dasselbe Ergebnis zur Folge hat. Ein Beispiel dieses Prinzips könnte das Setzen einer Variablen im Programmcode sein. Hier gibt es etwa die Möglichkeit, dies über Setter oder eben relative Mutationen zu implementieren.

Zum Beispiel setAge oder incrementAge. Die Operation person.setAge(14); kann beliebig oft nacheinander ausgeführt werden, das Ergebnis bleibt immer dasselbe, nämlich 14. Hingegen wäre person.incrementAge(1) nicht idempotent. Wird diese Methode unterschiedlich oft hintereinander ausgeführt, gibt es verschiedene Ergebnisse, nämlich nach jeder Ausführung ein Jahr mehr. Genau diese Eigenschaft der Idempotenz ist der Schlüssel, um eine Exactly-once-Semantik zu etablieren.

Angewandt auf die Systeme von zuvor bedeutet das, dass eine At-least-once-Semantik mit der Eigenschaft der Idempotenz zu einer Exactly-once-Verarbeitung führen kann. Wie eine At-least-once-Semantik umgesetzt werden kann, zeigt das zuvor beschriebene Bestätigungssystem. Was fehlt, ist also ein System von Idempotenz in der Verarbeitung. Aber wie kann eine Verarbeitung von Nachrichten idempotent gemacht werden?

Um das zu erreichen, muss der Consumer die Möglichkeit haben, einen lokalen, synchronisierten Zustand zu erhalten. Um den Zustand einer Nachricht zu erhalten, muss diese eindeutig identifizierbar sein. Nur so werden das Aufsuchen und eine Deduplizierung der Nachricht ermöglicht.

Abb. 6: Eine idempotente Verarbeitung

Anders als zuvor speichert der Consumer mit jedem Aufruf in Schritt 4 der Abbildung 6 die Nachricht zunächst in einer lokalen Zustandshaltung. An dieser Stelle kann, sofern die Nachricht bereits lokal vorhanden ist, ein erneutes Speichern vernachlässigt werden. In Schritt 5 wird die Nachricht bestätigt. Schlägt die Bestätigung fehl und wird die Nachricht folglich erneut übertragen, ist das kein Problem, da in Schritt 4 das erneute Speichern der Nachricht verhindert werden kann. An dieser Stelle lebt also die Idempotenz. Beim Bearbeiten kann der Consumer nun selbst entscheiden, ob eine Verarbeitung notwendig ist, z. B. indem zu einer Nachricht ein Status eingeführt und dieser in Schritt 6 lokal abgefragt wird. Steht dieser bereits auf Processed, muss nichts getan werden. Umgekehrt muss eine verarbeitete Nachricht den Status korrekt aktualisieren.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Fazit

Verteilte Systeme haben ein grundsätzliches Problem, eine Exactly-once-Semantik herzustellen. Es kann auf Infrastrukturebene entweder zwischen at least once oder at most once gewählt werden. Erst durch die Eigenschaft der Idempotenz kann auf dem Applikationslevel sichergestellt werden, dass Nachrichten genau einmal von Ende zu Ende verarbeitet werden.

Natürlich ist das nicht kostenlos. Es bedeutet, dass die Applikation selbst eine Verwaltung von Nachrichten übernehmen muss und deren Zustand verwaltet – wirklich exactly once ist das natürlich auch nicht, es kommt diesem durch die Eigenschaft der Idempotenz jedoch im Ergebnis sehr nahe.

The post Exactly Once in verteilten Systemen: Realität oder Utopie? appeared first on JAX.

]]>
Resiliente Kafka Consumer bauen https://jax.de/blog/kafka-fehlerbehandlung-guide/ Mon, 04 Mar 2024 08:55:02 +0000 https://jax.de/?p=89539 Asynchrone Kommunikation ist der Königsweg für den Datenaustausch zwischen lose gekoppelten Systemen. Laut einer Erhebung von 6sense [1] ist der Marktführer für den Austausch asynchroner Nachrichten Apache Kafka. Wir beschreiben in diesem Artikel unseren Weg als agiles Team zu einer einfachen Catch-all-Fehlerbehandlung für Nachrichten, die wir via Kafka empfangen. Und so viel sei vorab verraten: Am Ende unserer Reise finden wir sogar die richtige Strategie zur Selbstheilung.

The post Resiliente Kafka Consumer bauen appeared first on JAX.

]]>
Wir berichten hier über die Erfahrungen, die wir in einem gemeinsamen Projekt im öffentlichen Sektor gesammelt haben. Dort haben wir verschiedene Strategien für die Fehlerbehandlung bei der Verarbeitung asynchroner Nachrichten in unseren Consumern erarbeitet und verglichen. Neben der Minimierung unserer Betriebsaufwände mussten wir dabei wegen notorisch enger Zeitpläne auch unseren Entwicklungsaufwand minimieren. Deswegen haben wir zunächst nach einer Strategie gesucht, die in einem ersten Schritt als einzige „Catch all“-Strategie für alle Fehlerszenarien anwendbar ist: eine „Minimum Viable“-Fehlerstrategie.

Bei der Bewertung der Strategien haben wir die möglichen Fehlerszenarien nach zwei Dimensionen unterschieden, der Fehlerquelle und der Fehlerhäufigkeit.

Fehlerquelle:

  • Der Producer erzeugt Nachrichten, die nicht dem Schnittstellenvertrag entsprechen, entweder syntaktisch oder semantisch.
  • Der Consumer ist nicht in der Lage, alle Nachrichten, die dem Schnittstellenvertrag entsprechen, zu verarbeiten, z. B., wirft er unerwartete Ausnahmen.
  • Die Infrastruktur arbeitet nicht erwartungsgemäß, z. B. fällt die Verbindung des Consumers zu seiner Datenbank aus.

Fehlerhäufigkeit:

  • Der Fehler tritt nur selten und vereinzelt auf, z. B. als sporadischer Edge Case.
  • Der Fehler ist ein generelles und länger andauerndes Problem, das viele Nachrichten betrifft.

Bei der schlussendlichen Bewertung der Strategien haben sich die Vor- und Nachteile dann oft als unabhängig von Fehlerquelle oder Fehlerhäufigkeit herausgestellt.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Kafka-Grundlagen

Um die verschiedenen Strategien der Fehlerbehandlung zu verstehen, ist es notwendig, die grundlegenden Konzepte von Kafka zu kennen. Diese erläutern wir hier kurz. Für Details verweisen wir z. B. auf den Artikel „Kafka 101: Massive Datenströme mit Apache Kafka“ [2].

Broker

Kafka bezeichnet sich als Event-Streaming-Plattform, die als Ersatz für traditionelle Message-Broker benutzt werden kann. Der Message-Broker dient dazu, asynchrone Nachrichten in Topics zu veröffentlichen. Nach einer konfigurierbaren Zeit, der Retention Time, löscht der Broker die Nachrichten.

Producer

Der Producer veröffentlicht Nachrichten mit einem Schlüssel in einem Kafka Topic. Der Broker teilt ein Topic in mehrere Partitionen auf und speichert eine Nachricht in genau einer Partition des Topics. Dabei berücksichtigt der Broker den Schlüssel der Nachricht und schreibt alle Nachrichten mit demselben Schlüssel auch in dieselbe Partition. Je Partition garantiert der Broker, dass die Reihenfolge der Nachrichten erhalten bleibt. Der Producer muss die Schlüssel der Nachrichten also so wählen, dass alle Nachrichten, deren Reihenfolge untereinander erhalten werden muss, denselben Schlüssel haben.

Consumer

Die an den Nachrichten interessierten Consumer registrieren sich für das Topic und können so die Nachrichten vom Broker lesen und verarbeiten. Dabei geben die Consumer die Consumer Group an, zu der sie gehören. Der Broker stellt dann sicher, dass jede Partition genau einem Consumer einer Consumer Group zugewiesen ist.

Problembeschreibung

In unserem Projekt nutzen wir Kafka (Kasten: „Kafka-Grundlagen“), um Änderungen an den Daten, die ein System verantwortet, als Events dieses Systems zu veröffentlichen. Im Domain-Driven Design spricht man hier von den Zustandsänderungen eines Aggregats [3]. Wir erzeugen und verarbeiten die Events im Publish-Subscribe-Modus. Da die Retention Time mit fünf Tagen als endlich vorgegeben ist, enthalten die Events nicht nur die geänderten Daten, sondern immer den kompletten neuen Zustand des Aggregats.

Bezüglich der Performance haben wir recht entspannte Anforderungen: Aus fachlicher Sicht ist es ausreichend, wenn die Events vor Ablauf der Retention Time verarbeitet werden. Dabei ist es jedoch wichtig, dass die Reihenfolge der Events mit demselben Schlüssel erhalten bleibt und kein Event übersprungen oder anderweitig nicht vollständig verarbeitet wird. Events mit demselben Schlüssel nennen wir abhängig.

Nach Mathias Verraes [4] gibt es zwei schwierige Probleme in verteilten Systemen. Die Einhaltung der Reihenfolge ist das erste. Dieses Problem haben wir gelöst, indem die Nachrichten, deren Reihenfolge untereinander erhalten werden muss, denselben Schlüssel bekommen. Das zweite schwere Problem ist die Exactly-once Delivery. Wir können es umgehen, indem wir unsere Consumer idempotent implementieren.

Codebeispiele

Im Folgenden illustrieren wir jede betrachtete Strategie mit einem Codebeispiel. Diese nutzen die MicroProfiles Reactive Messaging und Fault Tolerance. Als Implementierung für beide MicroProfiles benutzen wir die für Quarkus üblichen Bibliotheken von SmallRye. Die Codebeispiele sind auch auf GitHub [5] zu finden.

Bei der Nutzung der Codebeispiele sind zwei Aspekte zu berücksichtigen: die Commit Strategy [6] und die Failure Strategy [7]. Als Commit Strategy haben wir latest gewählt, da beim Default throttled der Timeout throttled.unprocessed-record-max-age.ms [8] während länger dauernder Retries einer fehlerhaften Nachricht erreicht werden kann. Als Failure Strategy haben wir eine eigene Strategie implementiert, die davon ausgeht, dass der Code, der die Nachricht empfängt, die Fehler direkt behandelt und daher keine separate Failure Strategy mehr notwendig ist (Listing 1).

public class CustomKafkaFailureHandler implements KafkaFailureHandler {

  @ApplicationScoped
  @Identifier("custom")
  public static class Factory implements KafkaFailureHandler.Factory {

    @Override
    public KafkaFailureHandler create(
      KafkaConnectorIncomingConfiguration config,
      Vertx vertx,
      KafkaConsumer<?, ?> consumer,
      BiConsumer<Throwable, Boolean> reportFailure) {
        return new CustomKafkaFailureHandler();
    }
  }

  @Override
  public <K, V> Uni<Void> handle(IncomingKafkaRecord<K, V> record,
    Throwable reason, Metadata metadata) {
      return Uni.createFrom()
                .<Void>failure(reason)
                .emitOn(record::runOnMessageContext);
  }

Consumer herunterfahren

Der erste Halt auf unserer Reise führt uns zu einer äußerst einfach umzusetzenden Strategie: Consumer herunterfahren. Hierbei stoppt der Consumer die Verarbeitung und fährt den Prozess, in dem er läuft, herunter. Das macht er in dem Codebeispiel (Listing 2), indem er System.exit() aufruft. Alternativ könnte er z. B. in einer Kubernetes-Umgebung die Liveness-Probe [9] fehlschlagen lassen und so den Containermanager dazu veranlassen, den Prozess herunterzufahren. Diese Strategie, eingeschränkt auf den Kafka-Client des betroffenen Topics, bietet auch SmallRye Reactive Messaging an und nennt sie fail [10], [11].

@Incoming("aggregate-in")
public CompletionStage<Void> consume(Message<String> record) {
  LOGGER.trace("Aggregate received: {}", record.getPayload());
  try {
    controller.process(record.getPayload());
    return record.ack();
  } catch (Exception e) {
    LOGGER.error("Oops, something went terribly wrong with "
      + record.getPayload(), e);
    System.exit(1);
    return null; // never reached
  }
}

Um diese Strategie universell einsetzen zu können, ist es hilfreich, einen Containermanager zu haben, der den heruntergefahrenen Prozess neu startet. So bekommen wir den Retry in Form eines Restart Loops [7] geschenkt.

Der Weg zur Korrektur ist je nach Fehlerursache unterschiedlich: Bei einem Fehler im Producer sendet dieser die korrigierte Nachricht sowie alle abhängigen Nachrichten erneut. Der Consumer muss dann die Offsets der fehlerhaften Nachricht und aller ursprünglichen abhängigen Nachrichten überspringen.

Bei einem Fehler im Consumer ist lediglich der Bugfix zu deployen, dann läuft der Consumer einfach weiter. Bei einem Fehler in der Infrastruktur ist nichts zu tun, der Consumer läuft einfach weiter, sobald der Fehler behoben wurde.

Vorteile:

  • Die Strategie erhält die Reihenfolge.
  • Der manuelle Aufwand für die Fehlerbehebung ist beim Team des Consumers unabhängig von der Anzahl fehlerhafter Nachrichten.

Nachteile:

  • Der Microservice blockiert auch fehlerfreie Nachrichten.
  • Die Strategie führt in überwachten Umgebungen wie Kubernetes zu einer Restart-Schleife. Die wiederum führt je nach Timeout, Restart Backoff, Restart Cutoff und Start-up-Zeit dazu, dass der Broker die Partitionen auf die restlichen Consumer umverteilt, sodass am Ende jeder Consumer versuchen wird, die fehlerhafte Nachricht zu verarbeiten, und dann herunterfährt. Das wiederum verursacht vermutlich einen erhöhten Ressourcenverbrauch. Durch das Herunterfahren weiterer Consumer werden auch immer mehr Nachrichten in anderen Partitionen blockiert.
  • Die Strategie erfordert in nicht überwachten Umgebungen einen Mechanismus, um den Consumer nach der Fehlerbehebung wieder zu starten.
  • Die Strategie erfordert einen zusätzlichen Mechanismus zum Überspringen von fehlerhaften und ggf. auch abhängigen Nachrichten.
  • Bei Fehlern in der Nachricht müssen alle abhängigen Nachrichten vom Producer nochmals geschickt werden.

Dead Letter Queue

Die nächste Station unserer Reise ist ein Klassiker der Fehlerbehandlung von asynchronen Nachrichten: die Dead Letter Queue (DLQ). Im Zusammenhang mit Kafka wird diese Strategie oft auch als Dead Letter Topic bezeichnet, bei der Umsetzung mittels Datenbank auch als Dead Letter Table. Tritt ein Fehler beim Verarbeiten einer Nachricht im Consumer auf, sortiert der Consumer die betroffene Nachricht in die namensgebende Warteschlage für „tote“ Nachrichten aus. Anschließend macht er mit der Verarbeitung der nächsten Nachricht weiter (Listing 3). Als Alternative zu einer eigens implementierten Fehlerbehandlung bietet SmallRye die Dead Letter Queue auch als Failure Strategy an [10], [12].

@Incoming("aggregate-in")
public CompletionStage<Void> consume(Message<String> record) {
  LOGGER.trace("Aggregate received: {}", record.getPayload());
  try {
    controller.process(record.getPayload());
  } catch (Exception e) {
    LOGGER.error("Oops, something went terribly wrong with "
      + record.getPayload(), e);
    deadLetterEmitter.send(record);
  }
  return record.ack();
}

Der Weg zur Korrektur ist wieder von der Fehlerursache abhängig. Bei einer semantisch oder syntaktisch fehlerhaften Nachricht schickt der Producer eine korrigierte Nachricht. Die fehlerhafte Nachricht in der DLQ muss daraufhin übersprungen werden. Bei einem Fehler des Consumers muss der Bugfix deployt werden, anschließend müssen die Nachrichten der DLQ wieder eingelesen werden. Auch nach Behebung eines Infrastrukturproblems muss die DLQ wieder eingelesen werden.

Vorteile:

  • Blockiert nur Nachrichten, bei denen ein Fehler auftritt, d. h. keine Verzögerung nicht betroffener Nachrichten und maximaler Durchsatz.
  • Gute Skalierung auch bei vielen fehlerhaften Nachrichten.
  • Falls der Fehler nicht in der Nachricht lag, kann die DLQ nach dem Bugfix erneut eingelesen werden.
  • Der manuelle Aufwand für die Fehlerbehebung ist beim Team des Consumers unabhängig von der Anzahl fehlerhafter Nachrichten.
  • Bei einer Implementierung der DLQ mittels Datenbank kann man die Retention Time von Kafka umgehen, wodurch man mehr Zeit für die Fehlerbehebung hat.

Nachteile:

  • Erfordert einen separaten (u. U. manuellen) Mechanismus zum Einlesen der DLQ nach dem Bugfix.
  • Wirft die Reihenfolge der Nachrichten durcheinander.
  • DLQ muss eine Kapazität wie das ursprüngliche Topic haben, falls der Fehler einen Großteil der Nachrichten betrifft.
  • Die Strategie erfordert einen zusätzlichen Mechanismus zum Überspringen von fehlerhaften und ggf. auch abhängigen Nachrichten.
  • Bei Fehlern in der Nachricht müssen alle abhängigen Nachrichten vom Producer nochmals geschickt werden.

DLQ Advanced

Mit Blick auf unsere Problemstellung hat die einfache Dead Letter Queue ein entscheidendes Problem: Sie kann die Reihenfolge von Nachrichten nicht erhalten. Unsere Reise führt deswegen tiefer in die Welt der DLQs. Die Idee: Eine erweiterte Dead Letter Queue nimmt nicht nur Nachrichten auf, die bei der Verarbeitung Probleme machen, sondern auch alle von ihr abhängigen Nachrichten. Dazu muss lediglich der Schlüssel der fehlerhaften Nachricht gespeichert werden. Anschließend können alle abhängigen Nachrichten durch den gleichen Schlüssel identifiziert und ebenfalls in die DLQ geleitet werden (Listing 4).

@Incoming("aggregate-in")
public CompletionStage<Void> consume(KafkaRecord<String, String> record) {
  LOGGER.trace("Aggregate received: {}", record.getPayload());
  try {
    if(controller.shouldSkip(record.getKey())) {
      LOGGER.warn("Record skipped: " + record);
      deadLetterEmitter.send(record);
    } else {
      controller.process(record.getPayload());
    }
  } catch (Exception e) {
    LOGGER.error("Oops, something went terribly wrong with "
      + record.getPayload(), e);
    controller.addKeyToSkip(record.getKey());
    deadLetterEmitter.send(record);
  }
  return record.ack();
}

Der Weg zur Korrektur ist zunächst identisch mit dem der einfachen DLQ. Im Detail wird es jedoch kompliziert. Um die Reihenfolge beim Wiedereinlesen der DLQ weiterhin zu erhalten, muss ihr Einlesen mit dem Einlesen des regulären Topics koordiniert werden. Benutzt man ein Kafka Topic als DLQ, stellen sich weitere Probleme, wie die Verteilung von Schlüsseln in zwei Topics auf unterschiedliche Consumer und die Vermischung von korrigierten und nicht korrigierten Nachrichten in der DLQ. Die Realisierung als Dead Letter Table in der Datenbank löst manche dieser Probleme, verursacht aber auch neue. Wegen dieser steigenden Komplexität haben wir zu diesem Zeitpunkt ein Zwischenfazit gezogen, um nach weiteren Alternativen zu suchen.

Vorteile:

  • Die Strategie erhält die Reihenfolge.
  • Blockiert nur fehlerhafte und davon abhängige Nachrichten, d. h. keine Verzögerung nicht betroffener Nachrichten und maximaler Durchsatz.
  • Bei einer Implementierung der DLQ mittels Datenbank kann man die Retention Time von Kafka umgehen, womit man mehr Zeit für die Fehlerbehebung hat.

Nachteile:

  • Erfordert einen zusätzlichen Mechanismus zum Überspringen von fehlerhaften und ggf. auch abhängigen Nachrichten.
  • Erfordert komplexen (u. U. manuellen) Mechanismus zum Einlesen und Synchronisieren der DLQ nach dem Bugfix mit dem ursprünglichen Topic.
  • Die DLQ muss eine Kapazität wie das ursprüngliche Topic haben, falls der Fehler einen Großteil der Nachrichten betrifft.
  • Der manuelle Aufwand für die Fehlerbehebung ist beim Team des Consumers unabhängig von der Anzahl fehlerhafter Nachrichten.

Pausieren und Wiederholen

Die Dead Letter Queue ist eine naheliegende Lösung, wenn man von Fehlern ausgeht, die einzelne Nachrichten betreffen. Je mehr Nachrichten betroffen sind – egal, ob durch die Reihenfolge oder die gemeinsame Fehlerursache –, desto komplexer und aufwendiger wird die Lösung. Man kann sich dem Problem einer allgemeinen Fehlerbehandlung aber auch von einem anderen Szenario her nähern: dem Ausfall der Infrastruktur. Dabei spielt es keine Rolle, ob dieser Fehler den Ausfall einer notwendigen Datenbank, des Kafka-Clusters oder der Netzwerkverbindung betrifft. Entscheidend ist: Der Fehler betrifft vermutlich alle Nachrichten. Er lässt sich also nicht durch eine Quarantäne einzelner Nachrichten begrenzen. Die klassische Lösung für diesen Fall kennt jeder von automatischen Telefonansagen: „Bitte versuchen Sie es später noch einmal!“

Der erste Teil dieser Taktik besteht im Pausieren der Arbeit. Beim kompletten Herunterfahren des Consumers provozieren wir jedoch ein kostspieliges Rebalancing durch den Broker (siehe oben). Man kann aber auch das Konsumieren pausieren, solange man nicht den gesamten Consumer anhält. Das verhindert, dass der Broker die Pause als Ausfall des Consumers wertet und ein Rebalancing einleitet.

Der zweite Teil der Taktik ist das Wiederholen (engl. Retry) der Aktion, bei der ein Fehler aufgetreten ist. In unserem Fall bedeutet das auf oberster Abstraktionsebene immer das Konsumieren einer Nachricht. Wenn man außerdem die fehlerhafte Nachricht nicht committet, wird sie der Consumer nach der Pause einfach erneut einlesen und versuchen, sie zu verarbeiten.

Listing 5 zeigt, wie man beide Schritte mit der Retry-Annotation des MicroProfile Fault Tolerance umsetzt. Hiermit pausiert der Consumer für alle Partitionen, die ihm der Broker zugewiesen hat. Alternativ kann man dem Broker auch das Pausieren [13] einer einzelnen Partition direkt mitteilen und später die Verarbeitung mittels resume [14] wieder aufnehmen.

@Incoming("aggregate-in")
@Retry(delay = 2000L, jitter = 400L, maxRetries = -1, maxDuration = 0)
@ExponentialBackoff(maxDelay = 2, maxDelayUnit = ChronoUnit.HOURS)
public CompletionStage<Void> consume(Message<String> record) {
  LOGGER.trace("Aggregate received: {}", record.getPayload());
  try {
    controller.process(record.getPayload());
    return record.ack();
  } catch (Exception e) {
    LOGGER.error("Oops, something went terribly wrong with "
      + record.getPayload(), e);
    return record.nack€;
  }
}

Der Weg zur Korrektur ist wie immer abhängig von der tatsächlichen Fehlerursache. Da Infrastrukturprobleme meist externe Probleme sind, muss das Entwicklerteam bei der Lösung der Fehlerursache oft gar nicht aktiv werden. Tritt ein Infrastrukturproblem auf, können alle Consumer in einer Consumer Group unabhängig voneinander die Arbeit einstellen und in den Wiederholungsmodus wechseln. Sobald das Problem gelöst ist, sorgt das automatische Wiederholen für den Wiederanlauf aller Consumer.

Falls der Fehler beim Producer liegt, muss dieser wie bei allen anderen Strategien auch eine korrigierte Nachricht sowie eventuell weitere abhängige Nachrichten erneut schicken. Der Consumer muss alle vom Fehler betroffenen Nachrichten konsumieren und ignorieren. Bis dahin werden je nach gewählter Umsetzungsvariante entweder alle Nachrichten blockiert, die in der Partition mit der fehlerhaften Nachricht landen oder die in den Partitionen landen, die dem Consumer zugewiesen sind. Für abhängige Nachrichten wird die Partition quasi zu einer erweiterten Dead Letter Queue, die beim Wiedereinlesen allerdings keine Synchronisation benötigt. Ob und wie viele unabhängige Nachrichten vom Pausieren betroffen sind, hängt vom Verhältnis der Anzahl der Kafka-Schlüssel zur Anzahl der Partitionen und ggf. auch von der Anzahl der Consumer ab.

Falls der Fehler im Consumer liegt, blockieren die Partitionen, die von dem Fehler aktuell betroffen sind. Wie stark das Pausieren den Durchsatz bremst, hängt von der Schwere des Fehlers, möglichen Zusammenhängen mit den Kafka-Schlüsseln sowie vom Zufall ab. Sobald ein Bugfix deployt wurde, laufen die pausierten Partitionen durch das automatische Wiederholen ohne operativen Eingriff selbstständig wieder an.

Vorteile:

  • Pausiert nur betroffene Partitionen eines Consumers bzw. nur einen Consumer. Die Granularität hängt von der Anzahl der Consumer und der Anzahl der Partitionen ab.
  • Die Umsetzungsvariante mit der Retry-Annotation ist einfacher als das Pausieren einzelner Partitionen.
  • Verhindert Ressourcenverschwendung durch unnötiges Rebalancing.
  • Bei notwendigem Rebalancing (z. B. Consumer stürzt ab, neue Consumer kommen zwecks Skalierung hinzu) wird der neue Consumer automatisch die fehlerhafte Partition wieder pausieren.
  • Das System macht automatisch weiter, nachdem das Problem behoben ist, z. B. durch Deployment eines Bugfixes. Auch bei sporadischen Problemen, die sich selbst nach kurzer Zeit lösen, macht das System automatisch weiter. Das Team profitiert dabei vom Prinzip der Selbstheilung.
  • Der manuelle Aufwand für die Fehlerbehebung ist beim Team des Consumers unabhängig von der Anzahl fehlerhafter Nachrichten.

Nachteile:

  • Falls es mehr Kafka-Schlüssel als Partitionen gibt (was der Normalfall sein dürfte), werden auch Schlüssel angehalten, die sich zufällig in derselben Partition befinden, aber nicht betroffen sind.
  • Bei der Umsetzungsvariante mit der Retry-Annotation werden auch Nachrichten in Partitionen angehalten, die nicht die fehlerhafte Nachricht enthalten, falls es weniger Consumer als Partitionen gibt.
  • Erfordert zusätzlichen Mechanismus zum Überspringen von fehlerhaften und ggf. auch abhängigen Nachrichten.
  • Bei Fehlern in der Nachricht müssen alle abhängigen Nachrichten vom Producer nochmals geschickt werden.

Einfach loggen

Zunächst dachten wir nach dem produktiven Einsatz unseres Systems einige Wochen lang, wir könnten es uns auf der Insel „Pausieren und Wiederholen“ gemütlich machen. Dann mussten wir jedoch einen Abstecher zu einer weiteren Strategie unternehmen, die wir ursprünglich als absurd angesehen und daher direkt aussortiert hatten. Mittlerweile haben wir sie jedoch für spezielle Fälle zu schätzen gelernt. Diese Fälle zeichnen sich dadurch aus, dass ein Sachbearbeiter im ursprünglichen System Daten manuell korrigieren muss. Das verringerte den Durchsatz bei der Strategie „Pausieren und Wiederholen“ so deutlich, dass wir die Nachricht nun mittels einfachem Loggen behandeln. Dabei haben wir sichergestellt, dass nur einzelne Nachrichten so behandelt werden, und setzen diese Strategie somit nicht als Catch-all-Strategie ein.

Diese Strategie loggt die fehlerhafte Nachricht und bzw. oder ihre Metadaten, zählt gegebenenfalls eine Metrik hoch und verarbeitet dann die nächste Nachricht (Listing 6). Auch diese Strategie bietet SmallRye an und nennt sie ignore [10], [15].

@Incoming("aggregate-in")
public CompletionStage<Void> consume(Message<String> record) {
  LOGGER.trace("Aggregate received: {}", record.getPayload());
  try {
    controller.process(record.getPayload());
  } catch (Exception e) {
    LOGGER.error("Oops, something went terribly wrong with "
      + record.getPayload(), e);
  }
  return record.ack();
}

Der Weg zur Korrektur ist hier unabhängig von der Fehlerursache manuell: Die Logmeldung oder die erhöhte Metrik stößt einen manuellen Prozess an, der die Fehlerursache beheben und dann die geloggten und damit nicht verarbeiteten Nachrichten reproduzieren muss. Aufgrund des hohen manuellen Aufwands sollte diese Strategie nur für Fehlerursachen angewandt werden, die einzelne Nachrichten betreffen. Insbesondere scheidet sie damit für Infrastrukturprobleme aus.

In unserem Projekt setzen wir diese Strategie für Nachrichten ein, die der Producer fehlerhaft erstellt hat. Nachdem der Fehler im Producer behoben wurde, lässt der manuelle Prozess die Nachricht und alle davon abhängigen Nachrichten erneut erzeugen.

Vorteil:

  • Überspringt nur Nachrichten, bei denen ein Fehler auftritt, und vermeidet damit eine Verzögerung nicht betroffener Nachrichten. Die Strategie maximiert also den Durchsatz.

Nachteile:

  • Wirft die Reihenfolge der Nachrichten durcheinander.
  • Verliert Nachrichten, falls die Fehlerursache im Consumer oder in der Infrastruktur liegt.

Fazit

Unsere Reise durch die Untiefen der asynchronen Fehlerbehandlung zeigt, dass je nach konkreter Problemstellung verschiedene Optionen infrage kommen, die auf den ersten Blick absurd erscheinen. Ganz prinzipiell kann man die von uns gefundenen Optionen nach der Granularität des Eingriffs und der Komplexität ihrer Umsetzung sortieren (Abb. 1).

Abb. 1: Strategien im Überblick

Einfach loggen bedeutet de facto keinen Eingriff und ermöglicht so einen hohen Durchsatz. Zudem ist es bestechend simpel. Der Preis ist das Risiko des Verlusts der korrekten Reihenfolge und kompletter Nachrichten. Die klassische Dead Letter Queue verhindert den Datenverlust durch einen minimalen Eingriff, kann aber die Reihenfolge auch nicht sicherstellen. Sie ist dafür auch etwas komplexer, als nur zu loggen. Die erweiterte Dead Letter Queue greift exakt so viel ein wie notwendig, um Datenverlust zu verhindern und die Reihenfolge zu erhalten. Dafür muss man sich aber um eine Reihe von Detailproblemen kümmern. Diese Option ist deshalb die komplexeste. Bei Pausieren und Wiederholen werden u. U. auch unabhängige Nachrichten angehalten, allerdings selten das gesamte Topic. Datenverlust und Reihenfolge sind kein Thema. Die Komplexität ist moderat und beschränkt sich größtenteils auf das Wissen über die Konfiguration von Kafka selbst. Die einfachste Option, die Datenverlust vermeidet und die Reihenfolge einhält, ist das komplette Herunterfahren des Consumers. Das bedeutet aber in den meisten Fällen auch den niedrigsten Durchsatz während eines Fehlers.

Selbstverständlich kann man all diese Lösungen durch weitere kleinere Lösungen ergänzen. Auch wenn wir auf der Suche nach einer Catch-all-Strategie waren: Oft ist es besser, Probleme dort zu lösen, wo sie auftreten. Das heißt, dass z. B. eine Datenbankausnahme auch einfach das Wiederholen des Datenbankaufrufs auslösen kann, statt die Verarbeitung der Nachricht sofort abzubrechen. Für bestimmte fachliche Spezialfälle mögen sogar einfaches Loggen und eine manuelle Lösung durch den Fachbereich akzeptabel sein.

Für unser Projekt haben wir uns am Ende für Pausieren und Wiederholen entschieden. Die erweiterte Dead Letter Queue wurde uns zu komplex und wir waren uns nicht sicher, ob wir trotz dieser Komplexität nicht immer noch manuell in die Produktion würden eingreifen müssen. Pausieren und Wiederholen waren dagegen vergleichsweise simpel zu verstehen und dabei nicht zu restriktiv. Vor allem aber faszinierte uns, dass wir damit effektiv manuelle Eingriffe in der Produktion vermeiden konnten. Das automatische Wiederholen bedeutete letztlich den Beginn einer geradezu magischen neuen Eigenschaft unseres Systems: der Selbstheilung.


Links & Literatur

[1] https://6sense.com/tech/queueing-messaging-and-background-processing/apache-kafka-market-share

[2] https://entwickler.de/software-architektur/kafka-101/

[3] https://www.dddcommunity.org/library/vernon_2011/

[4] https://twitter.com/mathiasverraes/status/632260618599403520

[5] https://github.com/HilmarTuneke/kafka-fehlerbehandlung

[6] https://smallrye.io/smallrye-reactive-messaging/4.16.0/kafka/receiving-kafka-records/#acknowledgement

[7] https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy

[8] https://smallrye.io/smallrye-reactive-messaging/4.16.0/kafka/receiving-kafka-records/#configuration-reference

[9] https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/

[10] https://smallrye.io/smallrye-reactive-messaging/4.16.0/kafka/receiving-kafka-records/#failure-management

[11] https://quarkus.io/blog/kafka-failure-strategy/#the-fail-fast-strategy

[12] https://quarkus.io/blog/kafka-failure-strategy/#the-dead-letter-topic-strategy

[13] https://kafka.apache.org/37/javadoc/org/apache/kafka/clients/consumer/KafkaConsumer.html#pause(java.util.Collection)

[14] https://kafka.apache.org/37/javadoc/org/apache/kafka/clients/consumer/KafkaConsumer.html#resume(java.util.Collection)

[15] https://quarkus.io/blog/kafka-failure-strategy/#the-ignore-strategy

The post Resiliente Kafka Consumer bauen appeared first on JAX.

]]>
Und jetzt? Microservices nach dem Hype https://jax.de/blog/microservices-nach-dem-hype/ Tue, 09 Jun 2020 16:29:04 +0000 https://jax.de/?p=77928 Microservices haben jahrelang im Mittelpunkt des Interesses gestanden. Zeit, ein Fazit zu ziehen und aufzuzeigen, wie Microservices die Softwareentwicklung beeinflusst haben.

The post Und jetzt? Microservices nach dem Hype appeared first on JAX.

]]>
Natürlich sollten sich IT-Experten mit Hypes wie Microservices [1] [2] [3] [4] kritisch auseinandersetzen. Am Ende gibt es nur vorteilhafte oder weniger vorteilhafte Entscheidungen für ein bestimmtes Projekt. Die individuellen Vor- und Nachteile in dem jeweiligen Projekt sollten im Mittelpunkt stehen. Es ist schließlich kaum sinnvoll, coole Ansätze zu wählen, die dem Projekt nicht helfen. Genauso wenig sinnvoll ist es, uncoole Ansätze von vornherein auszuschließen, obwohl sie vielleicht ein konkretes Problem lösen.

Aber ein Hype entsteht nicht einfach so. Um die Vorteile besser zu verstehen, ist es sinnvoll, den Ursprung des Hypes nachzuvollziehen. Dazu zunächst ein Blick auf die Situation vor der Zeit der Microservices: Damals gab es nur Deployment-Monolithen. Einige dieser Monolithen hatten so große Mengen Code, dass allein das Kompilieren teilweise 10 oder 20 Minuten dauerte und nochmal so lange, bis die Anwendung gestartet war. Dazu kommen noch die Zeiten für Unit- und andere Tests. Und für die Tests muss die Anwendung möglicherweise mehrmals gestartet werden. Irgendwann ist dann eine produktive Arbeit kaum noch möglich, weil Programmierer so lange warten müssen, bis die Ergebnisse ihrer Arbeit sichtbar sind.

Wenn man den Code tatsächlich weiterentwickelt und testet, sind solche Zeiten kaum akzeptabel. Eine Änderung dauert einfach viel zu lange. Dazu kommen dann noch komplexe Integrationen mit anderen Systemen oder komplizierte Laufzeitumgebungen, wie beispielsweise Application Server. Sie gilt es, zu konfigurieren, was die Zyklen weiter verlängert und den Weg in die Produktion noch schwieriger macht.

Ich habe in meinen ersten Vorträgen über Microservices das Publikum gefragt, ob es Projekte mit zu großen Deployment-Einheiten kennen würde oder welche mit zu kleinen. Damals gab es scheinbar viele Projekte mit zu großen, aber praktisch kaum Projekte mit zu kleinen Deployment-Einheiten. Zumindest damals war eine Verkleinerung der Deployment-Einheiten sicher sinnvoll.

 

Der Ursprung

Eine gute Quelle zum Ursprung des Hypes und der Geschichte der Microservices ist Wikipedia [5]. Demnach gab es 2011 einen Softwarearchitekturworkshop, bei dem mehrere Teilnehmer einen neuen Architekturstil beschrieben. 2012 wurde dann im nächsten Workshop aus dieser Serie der Begriff „Microservice“ erfunden. Beteiligt waren dabei beispielsweise Adrian Cockcroft (Netflix), James Lewis (Thoughtworks) oder Fred George (Freelancer). Alle drei hatten bereits sehr viel Erfahrung und arbeiteten an wichtigen Projekten. Sie haben den Microservices-Ansatz sicher nicht gewählt, um einen neuen Hype zu erzeugen, sondern um reale Probleme zu lösen.

Die konkreten Probleme in ihren Projekten glichen sich, aber es gab auch Unterschiede: Während beispielsweise für Netflix eine Cloud-Strategie sehr wichtig war, ist das bei den anderen Projekten nicht so stark der Fall gewesen. Einige der Projekte müssen skalieren, aber teilweise in unterschiedlichen Bereichen: Skalierung der Teamgröße oder der Software auf mehrere Server.

Die Lösungen unterscheiden sich daher: Netflix setzt auf synchrone Kommunikation und Services, die so groß sind, dass ein Team mit der Entwicklung ausgelastet ist. Fred George hingegen empfiehlt asynchrone Kommunikation und Microservices, die höchstens einige hundert Zeilen groß sind und sehr einfach neu geschrieben werden können.

Es gab also nie das eine wahre Microservices-Konzept, sondern schon von Anfang an verschiedene Ansätze, die unterschiedliche Probleme lösen. Auch heute ist es noch so, dass der Microservices-Ansatz sehr unterschiedlich interpretiert und umgesetzt wird.

 

Module

Aber worum geht es bei Microservices überhaupt? Im Kern sind Microservices eine andere Art von Modulen und stehen damit in Konkurrenz zu Modularisierungsansätzen wie JARs, Maven-Projekten oder Java Namespaces. Eine Aufteilung wie in Abbildung 1 kann also als Microservices umgesetzt werden – oder mit einem anderen Modularisierungsansatz.

Abb. 1: Module einer Anwendung können Microservices sein – oder andere Arten von Modulen

 

Zwischen Microservices und anderen Optionen gibt es wesentliche Unterschiede:

  • In einem Deployment-Monolith kann es schnell passieren, dass man in neuem Code irgendeine vorhandene Klasse nutzt, die in irgendeinem Package abgelegt ist. Dadurch kann eine Abhängigkeit zwischen Packages entstehen, die eigentlich nicht gewünscht ist. Diese Abhängigkeit ist nicht beabsichtigt. Sie fällt vielleicht noch nicht einmal auf, weil sie im Code versteckt ist und Abhängigkeiten zwischen Packages nicht offensichtlich sind. Das führt dazu, dass ein Deployment-Monolith nach einiger Zeit eine Vielzahl von unerwünschten Abhängigkeiten hat. Natürlich kann z. B. ein Architekturmanagementwerkzeug das vermeiden. Wenn aber keine Vorkehrungen getroffen worden sind, ist das Abhängigkeitschaos in der Architektur unaufhaltsam. Sind die Module hingegen Microservices, kommunizieren sie untereinander durch eine explizite Schnittstelle z. B. per REST. Also muss man eine Abhängigkeit von einem Microservice zu einem anderen explizit einführen, indem man die Schnittstelle nutzt. Dadurch können sich Abhängigkeiten nicht einfach so einschleichen, weil man irgendeine Klasse aus Versehen nutzt.
  • Für den Betrieb sind die einzelnen Module sichtbar, weil sie getrennte Prozesse sind. Bei einem Deployment-Monolithen sind hingegen alle Module in einem einzigen Prozess untergebracht. Das bedeutet, dass neben Deployment auch andere Betriebsaspekte wie Metriken und Sicherheit sich an einzelnen Modulen orientieren können.

Weil Microservices die Frage nach einer sinnvollen Aufteilung eines Systems stellen, sind durch Microservices Module wieder in den Kern der Diskussion zurückgekehrt. Daraus ist die Idee des modularen Monolithen entstanden, denn ein Deployment-Monolith kann natürlich auch in Module aufgeteilt sein. Er sollte sogar modularisiert sein, schließlich wäre ein System ohne Module kaum zu warten. Wenn der Microservices-Hype dazu beigetragen hat, dass Module als grundlegendes Architekturkonzept wieder mehr beachtet werden, ist das schon viel wert. Und wenn es dann noch eine Abwägung zwischen verschiedenen Modularisierungsansätzen gibt, ist das super.

Damit ergibt sich eine wichtige Frage: Wenn so viele Deployment-Monolithen schlecht strukturiert sind – warum sollte dann der nächste Deployment-Monolith besser strukturiert sein? Es geht dabei nicht darum, Deployment-Monolithen generell als schlecht abzuqualifizieren. Schließlich gibt es gut strukturierte Deployment-Monolithen. Aber bei der Vielzahl schlecht strukturierter Deployment-Monolithen muss man die Frage stellen und auch beantworten – beispielsweise durch den Einsatz eines Architekturmanagementwerkzeugs. Microservices haben zumindest den Vorteil, dass die Aufteilung erzwungen wird.

 

Domain-driven Design

Natürlich stellt sich die Frage, wie eine vernünftige Aufteilung in Module erreicht werden kann. Merkmale einer guten Aufteilung wie lose Kopplung sind jedem bekannt – aber sie zu erreichen, ist gar nicht so einfach. Eine Möglichkeit ist DDD.

Domain-driven Design (DDD) [6] hat seine erste Blüte circa 2005 erlebt. Damals ist es als eine Anleitung zum Entwurf objektorientierter Systeme wahrgenommen worden. Klassen wurden Repositories, Services, Entitys oder Aggregates. DDD hat so beim feingranularen Entwurf der Systeme auf Klassenebene geholfen.

Dieser Bereich ist aber nur ein Teil von DDD, das taktische Design. Heutzutage steht das strategische Design viel mehr im Mittelpunkt. Dabei geht es um die Aufteilung von Systemen in Bounded Contexts. Ein Bounded Context hat ein eigenes Domänenmodell, das von den anderen Domänenmodellen getrennt ist. So könnte es ein Domänenmodell für die Lieferung von Bestellungen geben und ein anderes für die Bezahlung. Diese beiden Domänenmodelle sind völlig unterschiedlich: So geht es bei der Bezahlung um Bezahlmöglichkeiten, die Bonität eines Kunden oder die Preise und Steuersätze von Waren. Bei der Lieferung stehen Logistikdienstleister, die Lieferadresse des Kunden oder die Größe oder das Gewicht der Waren im Mittelpunkt. Am Beispiel der Ware wird klar, dass die Domänenmodelle zwar Domänenobjekte mit derselben Bezeichnung haben, aber unterschiedliche Facetten dieser Domänenobjekte modellieren – Preise und Steuersätze in dem einen Bounded Context, Größe und Gewicht in dem anderen.

Diese grobgranulare Modularisierung und Entkopplung ist vielleicht der wichtigste Trend, den Microservices in Gang gesetzt haben. Datenbanken mit Hunderten von Tabellen, die jede auch noch eine Vielzahl von Spalten haben, ist ein Hinweis, dass ein Domänenmodell viel zu komplex geworden ist und aufgeteilt werden muss. Da können Bounded Contexts helfen. Natürlich gilt das auch, wenn gar keine Microservices genutzt werden. Also haben auch in diesem Bereich Microservices eine Diskussion in Gang gesetzt, die über Microservices hinaus relevant ist.

 

Langfristige Architektur

Viele Systeme überleben länger als ursprünglich geplant. Systeme müssen daher so aufgebaut werden, dass sie auch langfristig wartbar und erweiterbar bleiben. Üblicherweise versuchen Teams daher, eine „saubere“ Architektur zu definieren und durchzusetzen. Sehr viele Projekte fangen mit solchen Überlegungen an. Die Anzahl von Projekten, die am Ende mit den Konzepten auch erfolgreich ein langfristig wartbares System erstellt haben, ist allerdings gering. Daher wäre es vielleicht sinnvoll, einen anderen Ansatz auszuprobieren.

Manchmal versuchen Architekten daher, Änderungsschwerpunkte zu identifizieren, die dann besonders flexibel umgesetzt werden. Aber eine Abschätzung der Änderungshäufigkeiten kann nur auf historischen Daten beruhen. Die Zukunft ist jedoch prinzipiell schwer vorhersagbar. Oft haben Architekturen daher genau an der falschen Stelle Flexibilitäten, die dann nur die Komplexität des Systems unnötig erhöhen. Und an den Stellen, die tatsächlich geändert werden, fehlt die Flexibilität dann. Am Ende ist das System sogar noch schwieriger zu ändern.

Mit Microservices kann ein anderer Ansatz umgesetzt werden: Die fachliche Aufteilung nach Bounded Contexts ist fundamental. Bezahlung und Lieferung von Waren wird immer ein Teil eines E-Commerce-Systems sein. Und wenn das nicht mehr der Fall sein sollte, ist die Umstellung der Software vermutlich noch das kleinste Problem. Fachlich kann diese Aufteilung also auch langfristig stabil bleiben. DDD zielt zwar gar nicht auf langfristige Stabilität, sondern auf eine fachlich korrekte Aufteilung. Aber genau das ist vielleicht der beste Weg, um langfristig eine gute fachliche Architektur zu erreichen.

Für Technologien muss es aber auch einen Weg geben, das System langfristig anpassbar zu halten. Schließlich wird jede Technologie, für die sich ein Projekt entscheidet, früher oder später veraltet sein. Und irgendwann gibt es keine Sicherheitsupdates mehr. Spätestens dann ist eine Migration auf eine neue Technologie zwingend. Das System sollte in kleine Einheiten aufgeteilt werden, die unabhängig voneinander auf eine neue Version einer Technologie oder eine neue Technologie migriert werden können. So können große und risikoreiche Technology-Updates vermieden werden.

In anderen Bereichen gibt es erfolgreiche Ansätze, um alte und neue Technologien miteinander zu kombinieren. Modulare Synthesizer bestehen aus Modulen, die kombiniert werden können, um Töne zu erzeugen. Ein Standard für die Module ist Eurorack [7]. Er definiert die Kommunikation zwischen den Modulen zum Beispiel zur Kontrolle der Module und für das Timing sowie natürlich für das Audiosignal. Außerdem definiert der Standard Aspekte für den Betrieb wie die Größe der Module und die Versorgungsspannungen. Den Standard gibt es seit 1996. Mittlerweile gibt es 5 000 teilweise radikal unterschiedliche Module, die alle miteinander kombiniert werden können. Natürlich werden in den Modulen teilweise Technologien eingesetzt, die es 1996 – vor fast 25 Jahren – noch nicht gab. So können also moderne und alte Technologien problemlos kombiniert werden.

Microservices erlauben einen ähnlichen Ansatz: Ein Microservices-System muss lediglich die Kommunikation standardisieren – beispielsweise mit REST oder mit einem Messagingsystem. Außerdem muss der Betrieb durch entsprechende Regeln sichergestellt werden: So können die Microservices als Docker-Container umgesetzt werden und standardisierte Schnittstellen für Metriken oder Logging haben.

So ermöglicht ein Microservices-System heterogene Technology-Stacks, denn in den Docker-Containern können beliebige Technologien genutzt werden. Das unterstützt das Update auf eine neue Technologie: Das Update kann schrittweise für jeden einzelnen Microservice erfolgen. Das Vorgehen verringert das Risiko: Wenn es Schwierigkeiten mit der neuen Technologie gibt, tritt sie nur bei den bereits aktualisierten Microservices auf und man kann zunächst mit einem einzigen Microservice beginnen. Microservices, bei denen ein Update gar nicht lohnt, müssen nicht auf die aktuelle Technologie migriert werden, was Aufwand spart.

Eine solche schrittweise Migration kann nur funktionieren, wenn heterogene Technologiestacks möglich sind. Das ist aber eigentlich nur bei Microservices der Fall, sodass die bessere Unterstützung von Technologieupdates ein entscheidender Unterschied ist.

 

Continuous Delivery

Die kontinuierliche Auslieferung von Software (Continuous Delivery) [8] hat offensichtliche Vorteile. So ist eine Änderung viel schneller in Produktion, wenn die Software regelmäßig ausgeliefert wird. Daher verbessert sich die Time to Market. Mittlerweile belegt aber eine Studie [9], dass es noch viele weitere Vorteile gibt. So können Teams, die oft deployen, den Ausfall eines Service schneller beheben. Da ein Deployment auch einen erneuten Aufbau eines Service darstellt, ist dieses Ergebnis nicht so überraschend. Ebenso wenig überraschend ist es, dass Deployments weniger häufig fehlschlagen, wenn man oft deployt. Schließlich sind die deployten Änderungen nicht nur kleiner, sondern die Teams haben auch mehr Übung beim Deployment.

Aber Continuous Delivery hat weitere Vorteile: Teams, die oft deployen, investieren 50 Prozent ihrer Zeit in die Arbeit an neuen Dingen, während das bei anderen Teams nur 30 Prozent sind. Dafür arbeiten sie weniger an Sicherheitsproblemen, Fehlern oder der Unterstützung von Endnutzern. Continuous Delivery verbessert also die Produktivität der Teams. Die Studie belegt sogar, dass Unternehmen mit Continuous Delivery erfolgreicher am Markt sind und weniger mit Burn-out zu kämpfen haben. Der Grund für diese Vorteile ist vermutlich, dass durch die Erhöhung der Deployment-Geschwindigkeit die aktuellen Probleme im Softwareentwicklungsprozess offensichtlich werden und dann wegoptimiert werden können. Außerdem muss bei einem häufigen Deployment klar sein, unter welchen Bedingungen Änderungen in Produktion kommen. Zusätzlich muss der Prozess einfach ausführbar und zuverlässig sein. Eine solche Umgebung ist sicher für Mitarbeiter ebenfalls angenehmer.

Die Studie zeigt auch, wie oft Teams deployen sollten: Low Performer deployen zwischen einmal im Monat und einmal alle sechs Monate, während Elite Performer mehrmals pro Tag deployen.

Ein Projekt, das einmal im Quartal deployt, sollte also so umgestellt werden, dass es mehrfach täglich deployt, um so die vielen positiven Effekte von Continuous Delivery auszunutzen. In einem beispielhaften Szenario folgten auf die Entwicklungsphase zehn Wochen lang Tests und dann ein Release über das Wochenende. Wenn man in diesem Szenario die Tests beispielsweise durch Automatisierung um den Faktor 100 beschleunigt, dauern sie noch vier Stunden. Das Deployment würde nach einer Beschleunigung um den Faktor vier noch zwei Stunden dauern. Dann wäre man bei sechs Stunden für ein Deployment. Um mehrmals pro Tag zu deployen, müsste man noch einen Faktor von zwei oder drei erreichen. Abbildung 2 zeigt die benötigte Zeit in Relation.

Abb. 2: Die Releasegeschwindigkeit muss massiv erhöht werden

 

Wegen der erforderlichen extremen Beschleunigungsfaktoren ist es kaum vorstellbar, dass eine Strategie, die nur auf die Optimierung und Automatisierung der vorhandenen Prozesse setzt, zum gewünschten Erfolg führt. Tatsächlich zeigt die schon zitierte Studie, dass Verbesserungen in unterschiedlichen Bereichen notwendig sind, um die Geschwindigkeit der Deployments zu erhöhen. Zu den Maßnahmen zählt auch eine entkoppelte Architektur. Ein Ansatz dafür sind Microservices. Ohne eine Aufteilung in getrennt deploybare Module scheint das Ziel von mehreren Deployments pro Tag kaum erreichbar, obwohl es derart viele positive Auswirkungen hat. Durch Automatisierung und Optimierung allein kann man Tests kaum so stark beschleunigen. Wenn man aber die Architektur so ändert, dass ein Teil des Systems getrennt getestet und deployt werden kann, wird das Problem einfacher lösbar.

Dieses Szenario zeigt außerdem, dass die komplette Deployment-Pipeline der Microservices unabhängig sein muss – also auch und insbesondere die Tests. Ersetzt man in dem Szenario die Architektur durch Microservices, bleibt aber beim monolithischen Testansatz, ändert sich nichts.

Auf der einen Seite zeigt Continuous Delivery, dass Microservices in Isolation nicht alle Probleme lösen, sondern viele Optimierungen beispielsweise durch Automatisierungen möglich sind. Auf der anderen Seite sind hohe Deployment-Geschwindigkeiten bei komplexen Systemen wohl nur erreichbar, wenn man das System in getrennt deploybare Einheiten wie Microservices aufteilt.

 

Organisation

Microservices sind zwar ein Architekturansatz, aber sie können auch Auswirkungen auf die Organisation haben. Klassische Organisationen bilden oft Teams nach technischer Qualifikation, also beispielsweise ein UI-Team und ein Backend-Team. Das Gesetz von Conway besagt allerdings, dass die Architektur des Systems die Kommunikationsstrukturen kopiert. Demnach würde diese Aufteilung zur Folge haben, dass es eine UI-Komponente und eine Backend-Komponente gibt. Das passt nicht zu der fachlichen Aufteilung, die DDD predigt. Daher gibt es in der Microservices-Welt das Inverse Conway Maneuver (etwa: umgekehrtes Conway-Manöver). Fachliche Komponenten wie ein Bounded Context werden dann einem Team zugewiesen. So folgt die Organisation in Teams der angestrebten fachlichen Architektur. Natürlich wäre ein solches Vorgehen auch ohne Microservices denkbar. Aber durch Microservices kommt zu der fachlichen Unabhängigkeit der Bounded Contexts eine technische Unabhängigkeit hinzu: Jeder Microservice kann getrennt von den anderen Microservices deployt werden und andere Technologien nutzen (Abb. 3).

Abb. 3: Microservices ergänzen die fachliche Unabhängigkeit von Bounded Context mit technischer Unabhängigkeit

 

Es gibt neben Microservices und dem Inverse Conway Maneuver mehrere Ansätze, die im Kern alle dasselbe aussagen:

  • DDD fordert, dass ein Bounded Context von einem Team entwickelt werden soll. Strategic Design beschreibt nicht nur, wie Bounded Contexts zusammenhängen, sondern auch mögliche Teambeziehungen.
  • Agilität fordert crossfunktionale Teams. Die Teams sollen möglichst viele Fähigkeiten haben, um so möglichst unabhängig voneinander arbeiten zu können. Außerdem sollen sie sich selbst organisieren, also möglichst viele Entscheidungen selbst treffen.
  • Das auf der DevOps-Studie basierende Buch „Accelerate“ [10] empfiehlt eine lose gekoppelte Architektur, um die Teams zu skalieren. Die Teams sollten die Werkzeuge wählen. Der Fokus muss auf Entwicklern und Ergebnissen liegen, nicht auf Technologien oder Werkzeugen. Microservices können das unterstützen, weil sie lose gekoppelt sind und in jedem Microservice andere Technologien genutzt werden können. Andere Ansätze wären aber auch denkbar.
    Microservices haben also die Idee, dass Organisation und Architektur zusammengehören, nicht eingeführt, sondern diese Idee wird von unterschiedlichen Bereichen ins Spiel gebracht.

Microservices können unabhängige und selbstorganisierte Teams ermöglichen. Die Teams können trotz Microservices in ihrer Handlungsfähigkeit eingeschränkt sein. Wichtig ist, den Teams zu vertrauen. Nur dann wird man ihnen zugestehen, Entscheidungen selbst zu treffen. Dieser Aspekt ist vielleicht sogar wichtiger als die Umstellung der Architektur auf Microservices.

Natürlich gibt es neben der Skalierung der Organisation genügend andere Gründe für die Nutzung von Microservices. Es ist also auf keinen Fall so, dass Microservices nur für große, komplexe Systeme sinnvoll sind, sondern sie können, beispielsweise wegen der Vorteile beim Continuous Delivery, auch in kleineren Projekten sinnvoll sein.

 

Fazit

Microservices sind ein Hype, der mittlerweile aber abgeklungen ist und sich eher ins Gegenteil verkehrt hat. Unabhängig davon, ob man Microservices nutzt oder nicht, hat sich durch Microservices die Diskussion über Architektur geändert:

  • Module und Domain-driven Design sind wieder zu wichtigen Themen geworden. Das ist sicher gut, weil eine vernünftige Modularisierung zentral für eine gute Wartbarkeit eines Systems ist.
  • Für eine langlebige Architektur bieten Microservices wegen der heterogenen Technologiestacks eine gute Alternative.
  • Continuous Delivery ist eine wichtige Möglichkeit, um Softwareentwicklung zu optimieren. Zumindest in einigen Fällen kann die notwendige Deployment-Geschwindigkeit nur erreicht werden, wenn man Microservices als Architekturkonzept nutzt; aber Microservices sind nur eine von vielen Optimierungen, die man ergreifen sollte.
  • Die Beziehung zwischen Organisation und Architektur wird nicht nur durch Microservices in den Mittelpunkt gestellt.

So geben Microservices einige interessante Denkanstöße, die auch ohne eine vollständige Microservices-Architektur sinnvoll sein können. Denn am Ende geht es nie darum, einem Hype hinterherzulaufen, sondern immer nur um sinnvolle Architekturentscheidungen.

————-

Links & Literatur

[1] Wolff, Eberhard: „Microservices: Grundlagen flexibler Softwarearchitekturen“, dpunkt, 2015.
[2] Wolff, Eberhard: „Microservices. Ein Überblick“, https://microservices-buch.de/ueberblick.html
[3] Wolff, Eberhard: „Das Microservices-Praxisbuch: Grundlagen, Konzepte und Rezepte“, dpunkt, 2018.
[4] Wolff, Eberhard: „Microservices Rezepte – Technologien im Überblick“, https://microservices-praxisbuch.de/rezepte.html
[5] https://en.wikipedia.org/wiki/Microservices#History
[6] Evan, Eric: „Domain-Driven Design Referenz“, https://ddd-referenz.de
[7] https://en.wikipedia.org/wiki/Eurorack
[8] Wolff, Eberhard: „Continuous Delivery: Der pragmatische Einstieg“, 2. Aufl., dpunkt, 2016.
[9] https://cloud.google.com/devops/state-of-devops/
[10] Forsgren, Nicole; Humble, Jez; Kim, Gene: „Das Mindset von DevOps. Accelerate: 24 Schlüsselkompetenzen, um leistungsstarke Technologieunternehmen zu entwickeln und zu skalieren“, Vahlen, 2019.

 

The post Und jetzt? Microservices nach dem Hype appeared first on JAX.

]]>
Microservices: „Jedes größere Projekt wird ohne ein passendes Service-Mesh-Werkzeug das Geflecht an Services nicht mehr beherrschen“ https://jax.de/blog/microservices-services-mesh-interview-hofmann/ Thu, 31 Oct 2019 11:00:52 +0000 http://new.jax.de/?p=73289 Je größer und verflechteter eine Microservice-Architektur wird, desto unübersichtlicher wird es. In der Entwickler-Welt kommt hier oftmals ein "Service Mesh" zum Einsatz. Während Istio lange als Platzhirsch unter den Service Mesh Tools galt, machen ihm Mitbewerber wie MicroProfile und Linkerd den Platz streitig. Doch worin liegen eigentlich die Unterschiede und welches ist das passende Services Mesh Tool für meine Anwendung? Diesen Fragen widmet sich Michael Hofmann in seinem Talk auf der W-JAX 2019.

The post Microservices: „Jedes größere Projekt wird ohne ein passendes Service-Mesh-Werkzeug das Geflecht an Services nicht mehr beherrschen“ appeared first on JAX.

]]>
Wer Michael Hofmann einmal live erleben möchte, der hat bei der diesjährigen W-JAX 2019 in München wieder Gelegenheit dazu. Dort wird er mit gleich zwei Talks vertreten sein. Michael wird dort zum einen eine Session zum Thema, „Service Mesh mit Istio und MicroProfile“ halten und zeigen, wie diese beiden Welten in einer Cloud-native-Anwendung am besten miteinander kombiniert werden können.

Wie Service Mesh Tools dabei helfen können, die Komplexität von Services Meshes in den Griff zu bekommen, vermittelt der zweite Vortrag von Michael zum Thema “Service Mesh — Kilometer 30 im Microservices-Marathon“.

Redaktion: Hallo Michael und danke, dass du dir die Zeit für das Interview genommen hast. In deinen Sessions auf der W-JAX 2019 sprichst unter anderem über Microservices. Haben Microservices eigentlich tatsächlich einen Vorteil gegenüber großen Monolithen?

Michael Hofmann: Will man im Projekt eine höhere Release-Frequenz erreichen, so bieten die Microservices durch ihre Größe einen klaren Vorteil. Es werden wiederholt nur kleine Teile des gesamten Systems in Produktion genommen, was mit einem Monolithen nur schwer möglich ist. Auch der Wechsel auf einen neuen Technologie-Stack ist in den einzelnen Microservices einfacher zu realisieren, da jeder Service für sich allein, ohne große Auswirkungen auf andere Services, auf den neuen Stack umgestellt werden kann.

Redaktion: Istio ist relativ neu, aber gewinnt zunehmend an Aufwind. Was ist der Grund für das Wachstum?

Hinter Istio steht mit Google und IBM eine mächtige Open-Source-Community, was Nachhaltigkeit und Fortschritt zugleich verspricht.

Michael Hofmann: Zum einen steht hinter Istio mit Google und IBM eine mächtige Open-Source-Community (ca. 430 Contributors laut CNCF Landscape) was Nachhaltigkeit und Fortschritt zugleich verspricht. Damit werden mehr Features pro Release veröffentlicht aber auch die Reaktion der Istio-Community auf Bugs erfolgt sehr zeitnah. Darüber hinaus beginnen nun verschiedene Hersteller oder Cloud-Betreiber, Istio teilweise oder sogar komplett in Ihre Produkte zu integrieren. Das Spektrum reicht von CloudFoundry, über Google und IBM bis hin zu OpenShift. Weitere Cloud-Betreiber (wie beispielsweise Azure) beschreiben zumindest sehr ausführlich, wie Istio in ihre Cloud-Umgebungen integriert werden kann.

Redaktion: Wo von Istio die Rede ist, ist Kubernetes meist nicht weit: Hat Istio das Zeug dazu, der de facto Service Mesh für Kubernetes zu werden?

Michael Hofmann: Fairerweise muss man sagen, dass es auch noch weitere Mitbewerber im Service-Mesh-Markt gibt. Von daher ist es schwer zu sagen, wer am Ende das Rennen machen wird. Bei Istio sind meiner Meinung nach viele Vorraussetzungen gegeben, die dazu notwendig sind, um im Markt einen gewichtigen Anteil zu erreichen. Zur Zeit sehe ich neben Istio und Linkerd auch noch kleinere Mitbewerber, die eine Alternative bieten. Dies erfolgt jedoch meist mit einem reduzierten und somit spezialisierteren Funktionsumfang.

Redaktion: Was ist Dein Lieblingsfeature in Istio?

Michael Hofmann: Das ist schwer zu sagen, da Istio eine Menge nützlicher Features bietet. Aktuell bin ich davon begeistert, wie einfach es ist, mit Istio die Resilienz in der Service-Kommunkikation zu testen. Die Simulierung einer verzögerten Antwortzeit oder der sporadische Ausfall von Kommunikations-Partnern ist ohne Istio, nur sehr schwer zu automatisieren. Meist sind Code-Anpassungen in den Services notwendig, was aber in meinen Augen unbedingt vermieden werden sollte. Mit Istio habe ich hierfür eine sehr elegante Möglichkeit: Durch Aktivieren einer entsprechenden Istio-Regel kann ich jedes beliebige Fehlerverhalten hervorrufen, ohne irgendeine Zeile Code im Service zu ändern. Nach dem Test kann diese Istio-Regel einfach wieder gelöscht werden.

Redaktion: Wie spielen Istio und MicroProfile zusammen?

Michael Hofmann: Grundsätzlich ist Istio mit seiner Architektur technologie-neutral und somit unabhängig von der eingesetzten Programmiersprache der Services. Aber wie so oft liegt der Teufel im Detail. Es gibt mehrere Anforderungen, wie zum Beispiel Tracing oder Resilienz, wo Istio und die Services eng miteinander verzahnt sind. Auch durch die Laufzeit-Umgebung, in welcher Istio eingesetzt wird, wie beispielsweise Kubernetes, ergeben sich Anforderungen an die Services, die ein Entwickler beachten muss. Anfängliche Abstimmungs-Probleme wurden von der MicroProfile-Community erkannt und nach und nach in den jeweiligen Spezifikationen behoben.

Die derzeitige Skepsis, “jetzt schon” Istio zu verwenden, wird durch den Druck der Notwendigkeit verschwinden.

Redaktion: Wie sieht, Deiner Meinung nach, die Zukunft des Service Meshs aus bzw. in welche Richtung wird sich das ganze entwickeln?

Michael Hofmann: Meiner Meinung nach wird jedes größere Projekt ohne ein passendes Service-Mesh-Werkzeug das Geflecht an Services nicht mehr beherrschen und somit betreiben können. Die derzeitige Skepsis, “jetzt schon” Istio zu verwenden, wird durch den Druck der Notwendigkeit verschwinden. Erste Projekte in meinem Umfeld beginnen damit, Teile von Istio zu verwenden und ich denke, der eingesetzte Funktionsumfang wird sich stetig erweitern. Von seitens der Tool-Hersteller wird intensiv daran gearbeitet, die Komplexität, welche in den Tools selbst steckt, zu reduzieren. Auch die Fehlermöglichkeiten, welche durch den falschen Einsatz der Service Mesh Tools entstehen können, wird werkzeug-gestützt verringert. Damit wird insgesamt die Einstiegshürde für die Projekte reduziert.

Redaktion: Vielen Dank für das Interview!

The post Microservices: „Jedes größere Projekt wird ohne ein passendes Service-Mesh-Werkzeug das Geflecht an Services nicht mehr beherrschen“ appeared first on JAX.

]]>
Einführung in Quarkus: Heiliger Gral der Entwicklerproduktivität? https://jax.de/blog/heiliger-gral-der-entwicklerproduktivitaet/ Tue, 22 Oct 2019 11:00:20 +0000 http://new.jax.de/?p=73284 Anfang dieses Jahres ist Red Hat mit Quarkus in die Arena eingetreten, in der Spring Boot und Micronaut bereits darum kämpfen, das beliebteste Full-Stack Framework für die Erstellung von Microservice- und Serverless-Apps zu werden. Was genau unterscheidet Quarkus von seinen Mitbewerbern?

The post Einführung in Quarkus: Heiliger Gral der Entwicklerproduktivität? appeared first on JAX.

]]>
In diesem Artikel lernen Sie die Grundlagen zum Erstellen von Anwendungen mit Quarkus kennen, indem Sie Code aus Spring PetClinic konvertieren, um eine cloud-native Quarkus-Anwendung mit den besten Java-Bibliotheken und -Standards wie Hibernate Panache, RESTEasy und GraalVM zu erstellen.

Was ist Quarkus?

Container First und Cloud Native: Bei Quarkus handelt es sich um ein Kubernetes-natives Java Framework, das Java in der neuen Welt von Serverless-Apps, Microservices, Containern und Cloud zu einer führenden Plattform machen soll. Indem Quarkus Oracles GraalVM nutzt, um native Apps zu erstellen, kann es überaus schnelle Startzeiten in der Größenordnung von Millisekunden sowie eine geringe Speichernutzung für Apps erreichen. Diese Eigenschaften ermöglichen automatisches Scale up und Scale down für Microservices in Containern sowie Function-as-a-Service(FaaS-)-Apps.

Imperative und Reactive: Obwohl Java-Entwickler schnell ein Cloud-natives, ereignisgesteuertes, asynchrones und reaktives Modell einführen können, um Geschäftsanforderungen für die Erstellung von hochkonkurrierenden und reaktionsschnellen Anwendungen zu erfüllen, sind die meisten unter ihnen eher mit dem imperativen Programmiermodell vertraut und möchten diese Erfahrung nutzen, um eine neue Plattform wie Quarkus einzuführen. Quarkus unterstützt sowohl imperative als auch reaktive Programmierparadigmen für Microservices, indem MicroProfile 2.2, die Reactive-Streams-Operators-Spezifikation und sogar Reactive Messaging für die Interaktion mit Apache Kafka vollständig unterstützt werden.

Optimiert auf die Freude des Entwicklers

Die Vision hinter Quarkus strebt mehr als nur Produktivität an: Die Nutzung soll Spaß machen! Deshalb hat das Team dahinter viel Aufmerksamkeit darauf verwendet, dass Livecodierung, Extensions und Unified Configuration gut funktionieren.

  • Im Entwicklungsmodus, den Sie mit mvn compile quarkus: devstarten können, unterstützt Quarkus Livecodierung, indem geänderte Dateien transparent kompiliert werden, wenn eine HTTP-Anfrage eingeht.
  • Das Extension-System soll dazu beitragen, ein lebendiges Ökosystem rund um Quarkus zu schaffen. Extensions, die im Grunde nichts anderes als Projektabhängigkeiten sind, konfigurieren, booten und integrieren ein Framework oder eine Technologie in eine Quarkus-App. Dazu stellen sie GraalVM die richtigen Informationen zur Verfügung, damit Ihre App nativ kompiliert werden kann.
  • Eine einzige Konfigurationsdatei (application.properties) genügt, um Quarkus sowie alle Extensions zu konfigurieren. Um die Größe dieser Datei zu verringern, sollte jede Extension sinnvolle Standardkonfigurationseigenschaften bereitstellen.

Wie teste ich meine Apps mit Quarkus?

Bevor wir uns näher mit dem Code befassen, ist es sinnvoll, einen ersten Blick auf den Testansatz von Quarkus zu werfen. Mit Quarkus können Sie Tests in zwei verschiedenen Modi ausführen, nämlich JVM Mode und Native Mode.

Gemeinhin erweitern die Testklassen im Native Mode die Tests im JVM Mode und werden in einem Docker-Container unter Verwendung der von GraalVM erstellten Native App ausgeführt. Der Vorteil der Wiederverwendung derselben Testklasse für JVM- und native Tests besteht darin, dass wir direkt zu Beginn eines Projekts Tests schreiben können.

Es hat sich als nützlich erwiesen, mit HTTPie die Integrität neuer REST Services zu überprüfen, obwohl Sie auch curloder wgetverwenden können, wenn Sie sich damit besser auskennen. HTTPie (http) ist ein mächtiges Kommandozeilenprogramm mit JSON-Unterstützung, Plug-ins und vielem mehr.

Erste Schritte mit Quarkus

Zum Zeitpunkt der Niederschrift dieses Artikels (August 2019) ist die neueste Version von Quarkus Version 0.21.1. Aus dem Versionsnummernschema können Sie ableiten, dass Quarkus derzeit als Beta eingestuft wird. Es ist wichtig zu wissen, dass zum jetzigen Zeitpunkt jede neue Quarkus-Version wahrscheinlich ihre Abhängigkeiten und Bibliotheken auf die neuesten Versionen aktualisieren wird. Daher habe ich für diesen Artikel die neuesten verfügbaren Versionen von Java, Maven und GraalVM verwendet: GraalVM Community Edition Version 19.2.0, AdoptOpenJDK 8u222 und Maven 3.6.1.

Erstellen einer Quarkus-App aus der Spring-PetClinic-Demo-App

Um ein Gefühl für Quarkus zu entwickeln, konvertieren wir den Code aus der Spring-PetClinic-Demo-App, um eine Cloud-native Quarkus-App zu erstellen. Beginnen wir mit der Ausführung der Spring-PetClinic-App:

git clone https://github.com/spring-projects/spring-petclinic.git
cd spring-petclinic

mvn clean package
mvn spring-boot:run

Abschließend stellen wir sicher, dass die Spring-PetClinic-App ordnungsgemäß funktioniert, indem wir entweder http://localhost:8080in unserem bevorzugten Browser öffnen oder den REST Service mit HTTPie anrufen: http :8080/vets.

Wie zu erwarten war, wird eine JSON-ähnliche Liste aller Tierärzte in der PetClinic-App zurückgegeben. Nun erstellen wir diesen REST Service in Quarkus neu.

Ähnlich wie bei Spring Boot und Micronaut können wir das Quarkus-Maven-Plug-in verwenden, um unsere erste „Hallo-Welt“-App mit Quarkus zu erstellen und auszuführen, wie in Listing 1 gezeigt.

Listing 1

mvn io.quarkus:quarkus-maven-plugin:0.21.1:create \
  -DprojectGroupId=com.github.acme \
  -DprojectArtifactId=quarkus-petclinic \
  -DclassName="com.github.acme.quarkus.petclinic.web.resource.VetResource" \
  -Dpath="/vets"

cd quarkus-petclinic
./mvnw compile quarkus:dev

Wenn wir den resultierenden Dienst testen, indem wir http :8080/vetsaufrufen, wird Hallo im Klartext zurückgegeben. Nicht wirklich spannend, aber was kann man von einer „Hallo-Welt“-App noch erwarten? Wir fahren fort, indem wir das Domänenmodell hinzufügen.

Implementierung des Domänenmodels

JPA, der De-facto-Standard im Object Relational Mapping, wird in Quarkus mit Hibernate ORM vollständig unterstützt. Um Hibernate zu konfigurieren, benötigen wir auch eine Datenquelle, um die Verbindungen zu einer Datenbank herzustellen. In Quarkus ist Agroal die Standardimplementierung für Datenquellen und Connection Pools. Wir können Unterstützung für Hibernate und Agroal hinzufügen, indem wir ihre Quarkus-Erweiterungen zu unserem Projekt hinzufügen:

./mvnw quarkus:add-extension -Dextensions="agroal, hibernate-orm, jdbc-h2, jdbc-mariadb"

Da diese Aktion die Maven-pom.xml-Datei ändert, müssen wir den quarkus:dev-Build stoppen. Gleichzeitig können wir das Modell (Entity Classes) aus der Spring PetClinic in unsere neu erstellte Quarkus PetClinic kopieren.

Da die Domänenklassen Spring-spezifischen Code verwenden, müssen wir etwas mehr als die Import- und Paketnamen korrigieren. Glücklicherweise können wir von Lambdamethoden in Java 8 profitieren, was diese Änderungen fast trivial macht.

Nachdem die Kompilierungsfehler behoben wurden, schlägt die Ausführung von mvn compile quarkus:devleider noch fehl, wie in Listing 2 zu sehen.

Listing 2

  ERROR [io.qua.dev.DevModeMain] Failed to start quarkus: 
java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
     [error]: Build step 
io.quarkus.hibernate.orm.deployment.HibernateOrmProcessor#build threw an exception: 
io.quarkus.deployment.configuration.ConfigurationError: Hibernate extension cannot
 guess the dialect as no JDBC driver is specified by 'quarkus.datasource.driver'

Betrachtet man die Fehlermeldung, so ist der Grund, warum Quarkus nicht startet, offensichtlich. Wir haben noch keine Datenbank konfiguriert.

Konfiguration der Datenbanken in Quarkus

Derzeit bietet Quarkus Erweiterungen für die folgenden Datenbanken an:

  • H2
  • PostgreSQL
  • MariaDB (und MySQL)
  • Microsoft SQL Server

Um die Konsistenz mit Spring PetClinic zu gewährleisten, benötigen wir H2-In-Memory-Datenbanken für die Entwicklung und MariaDB (MySQL) für die Produktion. Ähnlich wie bei Spring und Micronaut können Sie Quarkus-Konfigurationsprofile verwenden, um zwischen verschiedenen Laufzeitumgebungen zu unterscheiden. Voreingestellt versteht Quarkus Entwickler(dev)- und Produktionsprofile (prof). Im Moment müssen wir in der Konfigurationsdatei zumindest H2 konfigurieren (Listing 3).

Listing 3: Kofigurationsdatei: „src/main/resources/application.properties“

%dev.quarkus.datasource.url=jdbc:h2:mem:default
%dev.quarkus.datasource.driver=org.h2.Driver
%dev.quarkus.datasource.username=username-default
%dev.quarkus.datasource.min-size=3
%dev.quarkus.datasource.max-size=13
%dev.quarkus.hibernate-orm.database.generation=drop-and-create
%dev.quarkus.hibernate-orm.sql-load-script=db/hsqldb/data-and-schema.sql

%prod.quarkus.hibernate-orm.database.generation=none
%prod.quarkus.datasource.driver=org.mariadb.jdbc.Driver

Jetzt müssen wir nur noch die SQL-Datei für HSQLDB aus der Spring PetClinic in unsere neue Quarkus PetClinic kopieren. Wenn wir jetzt versuchen, unsere Quarkus-PetClinic-App erneut mit mvn compile quarkus:devauszuführen, sollte sie fehlerfrei starten. Beim Testen sehen wir, dass der Aufruf von http :8080/vetsimmer noch Hallo im Klartext zurückgibt. Wir beheben das, indem wir die Klasse VetResource implementieren.

Implementierung der VetResource-Klasse

Um die Klasse VetResource ähnlich wie Spring PetClinic zu implementieren, werden wir das Repository-Pattern verwenden und die Klasse VetRepository mit der Quarkus-Erweiterung Hibernate Panache erneut implementieren. Bei Hibernate Panache handelt es sich um eine Quarkus-Erweiterung, die das Schreiben von Entitäten zugleich simpel und unterhaltsam machen soll. Sie müssen Ihre Entitäten lediglich die Klasse PanacheEntity erweitern lassen, die Spalten von privaten Feldern in öffentliche Felder ändern und können dann alle Getter und Setter, automatisch generierte IDs usw. entfernen.

Außerdem muss unsere Quarkus-App natürlich auch noch in der Lage sein, JSON zu konsumieren und zu produzieren. Dazu stehen zwei Erweiterungen zur Verfügung, die die JSON-Bindung unterstützen: Jackson und RESTEasy. Da RESTEasy die ausgereifteste zu sein scheint, werden wir die RESTEasy-JSON-Bindungserweiterung verwenden.

Die beiden Abhängigkeiten können wir in einer einzigen Anweisung hinzuzufügen: ./mvnw quarkus:add-extension -Dextensions=”hibernate-orm-panache, resteasy-jsonb”. Standardmäßig wird eine listAll()-Methode schon von der PanacheRepository-Klasse bereitgestellt. Wir werden auch die Methode find by last name in unsere VetRepository-Klasse aufnehmen und dabei die von der PanacheRepository-Superklasse bereitgestellte find()-Methode nutzen (Listing 4).

Listing 4: „VetRepository“

@ApplicationScoped
public class VetRepository implements PanacheRepository<Vet> {
  public Vet findByName(String name) {
    return find("lastName", name).firstResult();
  }
}

Jetzt können wir die VetResource-Klasse wie in Listing 5 implementieren.

Listing 5: „VetResource“

@Path("/vets")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class VetResource {
  @Inject
  VetRepository vetRepository;

  @GET
  public List<Vet> list() {
    return vetRepository.listAll();
  }

  @GET
  @Path("/name/{name}")
  public Vet findByName(@PathParam("name") String name) {
    return vetRepository.findByName(name);
  }
}

Wenn wir jetzt versuchen, unsere Quarkus-PetClinic-App erneut mit mvn compile quarkus:dev auszuführen und den Aufruf von http :8080/vets zu testen, erhalten wir die gleiche JSON-Antwort wie mit der Spring-PetClinic-App!

Erstellen einer nativen App

Eine der besten Sachen an Quarkus ist die hervorragende Unterstützung für die Erstellung nativer Images. Um sie einzurichten, müssen wir die Datei application.properties mit einem Produktionsprofil (prod) aktualisieren, um MariaDB zu unterstützen (ähnlich wie bei Spring PetClinic). Vorausgesetzt, dass GraalVM bereits installiert ist, können wir die native App auf folgende Weise testen und ausführen:

gu install native-image
./mvnw clean package -Pnative
./target/quarkus-petclinic-1.0-SNAPSHOT-runner

Wenn ich auf meinem Computer (MacBook Pro 2017) das Timing von Spring Boot mit Quarkus-Dev- und Quarkus-Native-Modus vergleiche, erhalte ich die in Tabelle 1 aufgeführten Ergebnisse.

Anlaufen time http :8080/vets
Spring Boot 5.654 s – real 0m0.270s
– user 0m0.215s
– sys 0m0.045s
Quarkus Dev 3.430 s – real 0m0.278s
– user 0m0.218s
– sys 0m0.047s
Quarkus Native 0.019 s – real 0m0.265s
– user 0m0.210s
– sys 0m0.045s
Tabelle: Vergleich von Spring Boot mit Quarkus-Dev-Modus und Quarkus-Native-Modus

Wir sehen, dass der Start unserer nativen Quarkus-App viel schneller läuft als Spring Boot und Quarkus-Dev-Modus. In diesem Fall dauert die Bearbeitung der Anfrage in allen Varianten ungefähr gleich lang.

Zusammenfassung

Wie wir gesehen haben, ist die Konvertierung vorhandener JSON REST Services von Spring Boot zu Quarkus ein Kinderspiel. Wir erhalten eine Anwendung mit weniger Code, die viel schneller startet, und mit dem zusätzlichen Bonus einer nativ aktivierten, ausführbaren Kubernetes-Datei!
Der Quellcode ist bei GitHub verfügbar. Dort finden Sie auch Testklassen, mit denen Sie die Quarkus-App sowohl im JVM- als auch im nativen Modus testen können.

The post Einführung in Quarkus: Heiliger Gral der Entwicklerproduktivität? appeared first on JAX.

]]>
Microservices-Architektur: ein Fazit https://jax.de/blog/microservices-architektur-ein-fazit/ Wed, 11 Sep 2019 07:51:09 +0000 https://jax.de/?p=72782 Die Diskussion um Microservices-Architekturen ist keine theoretische mehr. Mittlerweile wurden zahlreiche Projekte in die Praxis umgesetzt. Zeit für eine Bilanz, meint Eberhard Wolff, Software-Architekt bei INNOQ und Sprecher auf der W-JAX 2019. Wir haben ihn nach seinen Erfahrungen mit Microservices-Projekten gefragt. Was haben wir bisher gelernt?

The post Microservices-Architektur: ein Fazit appeared first on JAX.

]]>
Redaktion: “Microservices: Ein Fazit” – so lautet der Titel einer deiner Talks auf der W-JAX. Das klingt ja fast nach einem Nachruf. Ist das Microservices-Konzept gescheitert?

Der Hype rund um Microservices geht  zurück.

Eberhard Wolff: Mittlerweile beschäftige ich mich mehr als 5 Jahre mit Microservices und finde daher ein Fazit angemessen. Es gibt zahlreiche erfolgreiche Microservices-Projekte und sicher auch Fehlschläge, so wie mit jeder anderen Architektur auch.
Der Hype rund um Microservices geht allerdings zurück. Da ist es sicher sinnvoll, inne zu halten und zu reflektieren.

Redaktion: Kannst du einmal ein Beispiel aus deiner Praxis-Erfahrung beschreiben, wo (und v.a. warum) ein Microservices-Projekt den Bach runter gegangen ist?

Eberhard Wolff: Die Faktoren, die Projekte typischerweise zum Scheitern bringen, sind vor allem die mangelnde Einbeziehung der Fachbereiche, andere soziale Faktoren oder unklare Ziele. Das gilt für Microservices-Projekte wie auch für andere Projekte. Sicher muss man sich bei Microservices mit anderen Architektur-Ansätzen beschäftigen und gegebenenfalls andere Technologien einführen. Daran scheitert aber ein Projekt eher selten.

Und wenn es daran scheitert, finde ich es wichtiger, in Zukunft bessere Entscheidungen zu treffen anstatt auf eine bestimmte Architektur zu schimpfen.

Redaktion: Momentan reden einige vom Modulith als (bessere?) Alternative zu einer Microservices-Architektur. Was ist damit eigentlich genau gemeint? In welchen Szenarien ist der Modulith-Ansatz sinnvoll?

Der Begriff “Modulith” zeigt, dass Microservices die Software-Architektur-Diskussion auf jeden Fall bereichert haben.

Eberhard Wolff: Das ist eine gute Frage. Modularisierung ist ein wesentliches Konzept, um große Systeme überhaupt implementieren zu können. Module gibt es seit 50 Jahren. Ein Modulith ist ein System, das modular ist, aber nur als Ganzes deployt werden kann. Wenn Microservices das Konzept der Module wieder in den Fokus gebracht haben und man sich auch bei Monolithen darauf besinnt, dann ist das auf jeden Fall super!

Sicher haben Microservices auch dazu geführt, dass Domain-driven Design wieder im Fokus steht, weil so grobgranulare Module gebildet werden können. So zeigt der Begriff „Modulith“, dass Microservices die Software-Architektur-Diskussion auf jeden Fall bereichert haben.

Redaktion: Du sagst ja, dass sich das Microservices-Konzept vor allem auch für die Modernisierung von Legacy-Systemen eignet. Wie können Legacy-Anwendungen von Microservices profitieren?

Eberhard Wolff: Legacy-Systeme zeichnen sich meistens dadurch aus, dass die Technologien, die Code-Qualität und die Architektur des Systems suboptimal sind. Microservices ermöglichen es, neben den Monolithen neue Microservices zu implementieren und mit dem Legacy-System zu integrieren. Die Microservices können andere Technologien und eine andere Architektur nutzen. Sie müssen den vorhandenen Code auch nicht wiederverwenden. So gibt es die Möglichkeit, das Legacy-System mit Greenfield-Microservices schrittweise abzulösen. Welche Funktionalitäten man ablöst, kann man je nach aktuellen Prioritäten entscheiden und so nur die Teile zu Microservices migrieren, bei denen das sinnvoll ist.

Redaktion: Und nochmal zurück zum Titel – Microservices: Ein Fazit. Was können wir denn aus den bisherigen Erfolgen und Misserfolgen mit Microservices-Architekturen lernen? Wie kann Softwarearchitektur jenseits von Microservices weitergedacht werden?

Ich würde mir wünschen, dass man dem Hype nicht traut.

Eberhard Wolff: Ich würde mir wünschen, dass man dem Hype nicht traut. Gleichzeitig wäre es schön, wenn man sich mit neuen Ideen unvoreingenommen beschäftigt und den Kern des Neuen erkennt. Denn meiner Meinung nach nutzen zu viele Projekte
Microservices einfach unreflektiert, und zu viele Projekte schließen sie von vorneherein aus.

Redaktion: Vielen Dank für dieses Interview!

Die Fragen stellte Hartmut Schlosser.

 

Quarkus-Spickzettel


Quarkus – das Supersonic Subatomic Java Framework. Wollen Sie zeitgemäße Anwendungen mit Quarkus entwickeln? In unserem brandaktuellen Quarkus-Spickzettel finden Sie alles, was Sie zum Loslegen brauchen.

 

Jetzt herunterladen!

The post Microservices-Architektur: ein Fazit appeared first on JAX.

]]>
Micronaut – effiziente und performante Microservices für die Cloud https://jax.de/blog/micronaut-framework/ Wed, 21 Aug 2019 08:23:27 +0000 https://jax.de/?p=72438 Den Chancen, die der Microservices-Ansatz bietet, stehen auch einige Herausforderungen gegenüber, die man aber gut mit Frameworks handhaben kann. Mit Micronaut hat nun ein ganz neuer Vertreter die Bühne mit dem Versprechen betreten, modulare, leicht testbare und sehr performante Anwendungen in Java, Kotlin oder Groovy entwickeln zu können. Ob dem wirklich so ist, zeigt Falk Sippach in seiner Session von der JAX 2019.

The post Micronaut – effiziente und performante Microservices für die Cloud appeared first on JAX.

]]>
Auch wenn Micronaut dem Platzhirsch Spring Boot ähnlich sieht, wurde es von Grund auf explizit für die Erstellung von Microservices im Cloud-Computing-Umfeld erstellt. Dank extrem kurzer Startzeiten, einem enorm niedrigen Speicherverbrauch und sehr kleinen JAR-Größen wird es die Microservices-Welt umkrempeln.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Ermöglicht wird das neuartige Programmiermodell mittels Compile-Zeit-Metaprogrammierung, wodurch die Metadaten für beispielsweise Dependency Injection und die aspektorientierte Programmierung bereits beim Kompilieren erzeugt werden. Reflection, Proxy-Generierung und Data Caching zur Laufzeit entfallen dadurch. Zur Verwendung in der Cloud oder Serverless-Umgebungen gibt es zudem bereits zahlreiche fertig gestellte oder geplante Anbindungen an Service-Discovery-Dienste, Configuration Sharing, Load Balancing und Serverless Computing.

Im Rahmen seiner Session von der JAX 2019 bringt uns Falk Sippach (OIO GmbH) die Funktionsweise von Micronaut näher und stellt sie anhand von realistischen Codebeispielen und Performancemessungen im Vergleich zu anderen JVM-basierten Microservices-Frameworks auf den Prüfstand.

The post Micronaut – effiziente und performante Microservices für die Cloud appeared first on JAX.

]]>
Microframeworks unter der Lupe: Javalin vs. Ktor vs. Spring Fu vs. Micronaut https://jax.de/blog/core-java-jvm-languages/microframeworks-unter-der-lupe-javalin-vs-ktor-vs-spring-fu-vs-micronaut/ Wed, 17 Apr 2019 16:11:09 +0000 https://jax.de/?p=67665 In letzter Zeit gewinnen in der Java-Welt Microframeworks wie Javalin, Ktor, Spring Fu oder Micronaut an Bedeutung. Christian Schwörer (Novatec Consulting GmbH) stellt die Frameworks in seiner kommenden JAX-Session eingesetzt vor. Wir haben im Vorfeld um eine kurze Einschätzung gebeten.

The post Microframeworks unter der Lupe: Javalin vs. Ktor vs. Spring Fu vs. Micronaut appeared first on JAX.

]]>

Was sind Microframeworks eigentlich?

JAX: In der letzten Zeit kommen Microframeworks wieder in Mode. Beispiele in der Java-Welt sind Javalin, Ktor, Spring Fu und Micronaut. Worauf zielen diese Frameworks ab?

Christian Schwörer: Unter „Microframeworks“ versteht man minimalistische Web-Frameworks zum Bau von modularen Anwendungen. Wesentlicher Bestandteil ist die Möglichkeit, einen Webserver wie zum Beispiel Netty zu konfigurieren und zu starten. Darüber werden dann für gewöhnlich REST-Endpunkte bereitgestellt oder Webinhalte ausgeliefert.

Das Besondere bei Microframeworks ist, dass sie sich auf die zentralen Konzepte bei der Anwendungsentwicklung fokussieren. Durch diese Vereinfachung steht die Developer Experience klar im Vordergrund: Es ist möglich, sehr schnell eine Web-Anwendung zu erstellen.

Ebenso zeichnen sich alle genannten Frameworks durch ihre klare Cloud-Ausrichtung und die Eignung für die leichtgewichtige Erstellung von Microservices aus.

JAX: Für die Java-Plattform haben wir mit Spring Boot und dem Eclipse MicroProfile zwei prominente Frameworks für Microservices. Wie unterscheiden sich die Microframeworks von diesen beiden?

Christian Schwörer: Wie erwähnt, konzentrieren sich Microframeworks auf die wesentlichen Bestandteile zur Erstellung von Microservices. Daher haben sie üblicherweise einen geringeren Funktionsumfang als Fullstack-Frameworks wie Spring, MicroProfile oder Grails. Allerdings gibt es auch bei Microframeworks eine große Bandbreite: von Frameworks, die sich wirklich auf das Elementare beschränken, bis hin zu welchen, die so gut wie alle von anderen Frameworks bekannten Features bieten.

Verschaffen Sie sich den Zugang zur Java-Welt mit unserem kostenlosen Newsletter!

Unabhängig vom Funktionsumfang zeichnen sich jedoch alle Microframeworks durch einen schnellen Applikationsstart und einen kleinen Memory Footprint aus. Das ist vor allem beim Einsatz in geclusterten Docker-Containern oder in Serverless-Architekturen ein entscheidender Vorteil.

Das Besondere bei Microframeworks ist, dass sie sich auf die zentralen Konzepte bei der Entwicklung fokussieren.

 

Javalin, Ktor, Spring Fu, Micronaut

JAX: Bleiben wir einmal bei den vier genannten Microframeworks und beginnen bei Javalin. Wie kann man Javalin einordnen – wo liegen die Stärken?

Christian Schwörer: Die Stärke von Javalin liegt ganz klar in seiner Einfachheit. Es ist sehr leicht zu verstehen und handzuhaben, und dementsprechend einfach ist der Code zu lesen. Dies erreicht das Framework, indem es sich auf wenige wesentliche Konzepte beschränkt, die erlernt werden müssen. Daher eignet es sich auch am ehesten für die schnelle Erstellung von überschaubar kleinen (Micro-)Services.

JAX: Ktor beschreibt sich selbst als asynchrones Web Framework für Kotlin. Für welchen Einsatzzweck ist Ktor aus deiner Sicht besonders interessant?

Christian Schwörer: Hinter Ktor steht maßgeblich JetBrains, das Unternehmen, das die meisten vermutlich von der Entwicklungsumgebung IntelliJ und der JVM-Sprache Kotlin kennen.

Es verwundert daher wenig, dass Ktor – anders als die anderen genannten Microframeworks – ausschließlich Kotlin unterstützt. Diese Einschränkung ermöglicht es allerdings, die Kotlin-Sprachfeatures ideal zu nutzen. So setzt Ktor beispielsweise intensiv auf Coroutines, der leichtgewichtigen Kotlin-Lösung für nebenläufige Programmierung. Dadurch ergibt sich ein asynchrones Framework, das sich etwa für den Bau von API-Gateways eignet.

Nebenbei erwähnt, arbeite ich persönlich seit Längerem in der Backend-Entwicklung ausschließlich mit Kotlin, so dass ich eine gewisse Affinität zu verwandten Technologien habe.

Die Stärke von Javalin liegt ganz klar in seiner Einfachheit.

 

JAX: Relativ neu ist auch das Projekt Spring Fu. Damit lassen sich Spring Boot-Anwendungen mittels einer Kotlin DSL oder einer Java DSL konfigurieren. Kannst du das einmal anhand eines Beispiels demonstrieren?

Christian Schwörer: Spring Fu bietet eine funktionale Alternative zur Annotation-basierten, deklarativen Spring Boot-Konfiguration. In folgendem Kotlin-Beispiel wird anhand der @Bean-Annotation eine Spring-Bean definiert:

@Configuration
class MyConfiguration() {
    @Bean
    fun mySpringBean() = MySpringBean()
}

Mit der Kotlin-DSL von Spring Fu sieht dies wie folgt aus:

configuration {
    beans {
        bean< MySpringBean >()
    }
}

Durch die explizite, funktionale Konfiguration wird der Overhead auf ein Minimum reduziert, der sich bei der deklarativen Nutzung von Spring Boot durch Reflection und Classpath Scanning ergibt. Dies führt zu einem schnelleren Applikationsstart und weniger Speicherverbrauch.

Allerdings gibt es Spring Fu erst als Incubator in der Version 0.0.5. Das heißt, auch wenn es sich lohnt, das Projekt im Blick zu behalten, ist es für einen Einsatz in einem produktiven Szenario meines Erachtens noch zu früh.

JAX: Und schließlich Micronaut: Was sind die Eckdaten dieses Frameworks?

Christian Schwörer: Micronaut ist sicherlich das Feature-kompletteste der erwähnten Frameworks. Neben Dependency Injection, Anbindung unterschiedlichster Datenbanken und zahlreicher Security Features bietet es Cloud-native Module wie etwa Service Discovery, Circuit Breakers und Distributed Tracing. Es positioniert sich somit am klarsten als Alternative zu Spring Boot oder Eclipse MicroProfile.

Dennoch weist es Eigenschaften von Microframeworks auf, insbesondere kurze Startzeiten und geringer Speicherverbrauch. Dies wird erreicht, da weitestgehend auf Reflection, Proxies und Classpath Scanning zur Start- und Laufzeit verzichtet wird. Ermöglicht wird dies, indem die benötigten Informationen mittels Annotation Processing und Ahead-of-Time-Compilation bereits zur Compilezeit ermittelt werden.

Ich sollte mir im Vorfeld über den groben Scope meiner Anwendung bewusst sein.

 

Microframeworks in der Praxis

JAX: Du kennst die Microframeworks ja aus der Praxis. Wie kann man am besten loslegen? Hast du einen besonderen Tipp für die Leser, der für dich persönlich gut funktioniert hat?

Christian Schwörer: Einfach selbst ausprobieren! Für alle genannten Frameworks gibt es gute Tutorials, anhand derer man sehr schnell ein Gefühl für die Entwicklung damit bekommt und einen ersten Eindruck, ob das Framework für die angedachte Aufgabe geeignet ist.

Ich sollte mir im Vorfeld aber auch über den groben Scope meiner Anwendung bewusst sein. Gehe ich davon, dass sie in Zukunft stark erweitert werden muss? Brauche ich deshalb beispielweise Features wie Dependency Injection oder explizites Transaktionsmanagement? Dann eignen sich nicht alle der angesprochenen Frameworks, da sich einige – wie bereits erwähnt – zugunsten der schnellen Erlernbarkeit und Einfachheit bewusst auf Kernfunktionen beschränken.

JAX: Vielen Dank für dieses Interview!

Die Fragen stellte Hartmut Schlosser.

 

Quarkus-Spickzettel


Quarkus – das Supersonic Subatomic Java Framework. Wollen Sie zeitgemäße Anwendungen mit Quarkus entwickeln? In unserem brandaktuellen Quarkus-Spickzettel finden Sie alles, was Sie zum Loslegen brauchen.

 

Jetzt herunterladen!

The post Microframeworks unter der Lupe: Javalin vs. Ktor vs. Spring Fu vs. Micronaut appeared first on JAX.

]]>
Spring Boot vs. Eclipse MicroProfile: Microservices-Frameworks im Vergleich https://jax.de/blog/microservices/spring-boot-vs-eclipse-microprofile-microservices-frameworks-im-vergleich/ Mon, 08 Apr 2019 10:51:54 +0000 https://jax.de/?p=67592 Microservices werden im Java-Umfeld immer öfter mit Spring Boot gebaut. Wer aus dem Java-EE- bzw. Jakarta-EE-Lager kommt, hat mit dem Eclipse MicroProfile eine Alternative zur Hand. Wo liegen die Gemeinsamkeiten, wo die Unterschiede?

The post Spring Boot vs. Eclipse MicroProfile: Microservices-Frameworks im Vergleich appeared first on JAX.

]]>

Wir haben mit Tim Zöller, Entwickler bei der ilum:e informatik ag und Sprecher auf der JAX 2019, über den alten Gegensatz „Spring versus Java EE“ gesprochen. Kommt es im Zeichen der Microservices zu einer Neuauflage unter dem Banner „MicroProfile versus Spring Boot“?

Spring Boot oder Eclipse MicroProfile: Microservices-Frameworks im Vergleich

JAX: Hallo Tim, vielen Dank, dass du dir die Zeit für dieses Interview genommen hast! Im Enterprise-Bereich war die Java-Welt ja viele Jahre lang von zwei Frameworks dominiert: Spring und Java EE. Beide hatten den möglichst kompletten Java-Application-Server als zentrale Metapher. Hat diese Metapher in Zeiten von Microservices & Serverless ausgedient?

Tim Zöller: Ich glaube nicht, dass wir die Application Server in näherer Zukunft verschwinden sehen werden. Sie vereinen eine Vielzahl erprobter APIs und Implementierungen in sich, die man aus sehr schlank gebauten WARs heraus benutzen kann. Das ermöglicht beispielsweise auf effiziente Art und Weise, Docker Images aufzubauen und zu pushen, was einigen Unternehmen wichtig ist.

JAX: Wie bewertest du die jüngsten Entwicklungen um Java EE, d.h. den Umzug zur Eclipse Foundation unter dem neuen Namen Jakarta EE? Wird der Java-Enterprise-Standard dadurch beflügelt? Oder handelt es sich eher um ein Auslaufmodell?

Tim Zöller: Ich sehe es als einen positiven Schritt, und die meisten Menschen, mit denen ich bisher darüber gesprochen habe, ebenfalls. Es ist momentan sehr spannend zu sehen, wie die Spezifikation in die Hände der Community übergeht. Die rechtlichen Probleme, die mit der Übertragung von Oracle an die Eclipse Foundation einhergehen, werfen momentan einen Schatten auf den Umzug.

Es gibt beispielsweise immer noch Unklarheiten, wenn es um Namensrechte geht. Offensichtlich wurde das durch die notwendige Umbenennung von Java EE zu Jakarta EE. Im Detail stellt es aber auch ein Problem dar, dass Klassen im Package javax.* von der Übergabe an die Eclipse Foundation betroffen sind. Diese Unsicherheiten sorgen dafür, dass sich einige Mitglieder der Community noch nicht trauen, ihre Mitarbeit zu starten.

Ich glaube nicht, dass die Application Server in näherer Zukunft verschwinden werden.

 

Microservices: Spring Boot versus Eclipse MicroProfile

JAX: Wer neue Java-Anwendungen im Cloud- und Microservices-Kontext baut, greift heute immer mehr zu Spring Boot. Weshalb eigentlich? Wo liegen die Stärken von Spring Boot im Vergleich zum Ansatz des klassischen Spring Frameworks und Java EE?

Tim Zöller: Mit dem Konzept der Spring Boot Starter und dem einfachen Aufsetzen einer neuen Anwendung wurde zunächst einmal ein gewaltiger Konfigurations-Overhead abgeschafft. Die Möglichkeit, ein einzelnes, ausführbares JAR zu erzeugen und die Applikation extern zu konfigurieren, erhöht die Flexibilität in der Entwicklung und im Deployment. Eingebaute Health-Checks und Metriken machen den Betrieb leichter.

Verschaffen Sie sich den Zugang zur Java-Welt mit unserem kostenlosen Newsletter!

JAX: Mit dem Eclipse MicroProfile hat sich ein eher wieder an Java EE bzw. Jakarta EE angelehnter Konkurrent zu Spring Boot herausgebildet. Was haben die beiden Projekte gemeinsam?

Tim Zöller: Enorm viel. Zum einen versucht MicroProfile, genau die Konzepte abzudecken, die Spring Boot erfolgreich machen: geringe Konfiguration, Health Checks, Metriken, Resilience-Maßnahmen – viele Features, die den Betrieb von mehreren, vernetzten Services in der Cloud vereinfachen. Wenn man die gleichen Dinge mit beiden Frameworks umsetzt, fällt sehr schnell auf, dass auch die Konzepte ähnlich umgesetzt sind: REST Clients in MicroProfile sehen beispielsweise OpenFeign REST Clients aus Spring Boot sehr ähnlich, Spring REST Controller werden fast gleich aufgebaut wie REST Services mit JAX-RS. Das setzt sich in den meisten Konzepten fort.

Eclipse MicroProfile und Spring Boot haben enorm viel gemeinsam.

 

JAX: Und wo liegen die Unterschiede?

Tim Zöller: Technisch liegen die Unterschiede darin, dass MicroProfile lediglich das API definiert, welches von den verschiedenen Herstellern umgesetzt wird, etwa von OpenLiberty, Thorntail oder Payara Micro. Das API versucht, hierbei sämtliche Themen zu erfassen: REST Clients, REST Services, Fault Tolerance, Configuration, Health, Metrics, usw. Bei Spring Boot existieren diese Konzepte auch, aber als Bibliotheken im Spring-Boot- oder Spring-Cloud-Umfeld, die Entwickler bei Bedarf in die Anwendung einbinden können. Ein weiterer Unterschied ist, dass MicroProfile komplett im Besitz einer offenen Community ist, welche den Standard vorantreibt.

Spring oder Java EE – der alte Kampf?

JAX: Zurück zum alten Gegensatz Spring versus Java EE: Läuft es aus deiner Sicht nun auf eine Neuauflage unter dem Banner Spring Boot versus Eclipse MicroProfile hinaus?

Tim Zöller: Ich glaube nicht, dass diese Diskussion wieder so grundsätzlich aufkommt. Anwendungslandschaften sind heute homogener als damals. Wenn man 25 Services in Produktion laufen hat, spricht nichts dagegen, die einen in Spring Boot und die anderen mit MicroProfile umzusetzen. Die Kommunikation erfolgt ohnehin über definierte Protokolle.

Spring Boot hat bereits ein gigantisches Ökosystem, welches fertige Lösungen für viele Anwendungsfälle, gerade in der Cloud, bereitstellt. Diese haben sich oft schon in ganz großen Maßstäben bewiesen, z.B. im Einsatz bei Netflix. Wenn Service Discovery, clientseitiges Loadbalancing und eine Einbindung in spezielle Monitoring-Systeme gefragt sind, ist Spring Boot nahezu konkurrenzlos.

Allerdings verlagern sich ebendiese Funktionen zunehmend in die Infrastruktur und werden, z.B. bei einem Einsatz auf Kubernetes, seltener in die Applikation eingebaut. Die Stärke von MicroProfile liegt darin, dass es nur einen Standard beschreibt, keine Implementierung. Neben den „üblichen Verdächtigen“ wie OpenLiberty, Thorntail oder Payara Micro ist es auch möglich, eine Applikation, die gegen diesen Standard entwickelt wurde, auf Implementierungen ausführen zu lassen, die speziell für kleine und schnelle Programme entwickelt wurden. Beispiele sind hier KumuluzEE oder Quarkus.

Sowohl Spring Boot als auch Eclipse MicroProfile stellen großartige Tools bereit.

 

JAX: In deiner JAX-Session Eclipse MicroProfile vs. Spring Boot: getting back in the Ring! nimmst du beide Frameworks genauer unter die Lupe. Was ist die zentrale Botschaft, die du den Teilnehmern mit auf den Weg geben möchtest?

Tim Zöller: Sowohl Spring Boot als auch Eclipse MicroProfile stellen großartige Tools bereit, um die Entwicklung von schlanken, leicht konfigurier- und wartbaren Anwendungen zu ermöglichen. Es ist eher eine Geschmacksfrage als ein „richtig“ oder „falsch“.

JAX: Vielen Dank für dieses Interview!

Die Fragen stellte Hartmut Schlosser

Java-Dossier für Software-Architekten 2019


Mit diesem Dossier sind Sie auf alle Neuerungen in der Java-Community vorbereitet. Die Artikel liefern Ihnen Wissenswertes zu Java Microservices, Req4Arcs, Geschichten des DevOps, Angular-Abenteuer und die neuen Valuetypen in Java 12.

Java-Wissen sichern!

The post Spring Boot vs. Eclipse MicroProfile: Microservices-Frameworks im Vergleich appeared first on JAX.

]]>
Von Monolithen über modulare Architekturen zu Microservices mit DDD https://jax.de/blog/microservices/von-monolithen-ueber-modulare-architekturen-zu-microservices-mit-ddd/ Mon, 25 Mar 2019 16:06:38 +0000 https://jax.de/?p=67483 In jedem Unternehmen gibt es große Softwaresysteme, die über viele Jahre weiterentwickelt wurden und deren Wartung Jahr für Jahr immer zäher und teurer wird. Vor dem Hintergrund neuer Architekturparadigmen wie Microservices sollen diese Systeme nun modern, skalierbar und flexibel werden. Dabei ist die Hoffnung, dass man sich der großen, schwerfälligen Monolithen entledigen kann, indem man sie in kleinere, besser zu beherrschende Microservices zerlegt.

The post Von Monolithen über modulare Architekturen zu Microservices mit DDD appeared first on JAX.

]]>

von Dr. Carola Lilienthal

Dieses Heilsversprechen klingt gut, beinhaltet aber viele Fallstricke, Missverständnisse und Herausforderungen. Ein Umbau hin zu Microservices ist aufwendig und kann, falsch eingeleitet, zu einem schlechteren Ergebnis führen als es die ursprüngliche Architektur einmal war. Auf Basis der Erfahrungen aus Kundenprojekten der letzten Jahre werde ich in diesem Artikel sinnvolle von unsinnigen Maßnahmen trennen und pragmatische Lösungen vorstellen.

Microservices: Warum?

Microservices sind in den letzten Jahren als neues Architekturparadigma aufgekommen. Viele Entwickler und Architekten dachten zuerst, es ginge bei Microservices nur darum, Softwaresysteme in voneinander unabhängig deploybare Services aufzuteilen. Aber eigentlich haben Microservices einen anderen Sinn: Bei Microservices geht es darum, Software so auf Entwicklungsteams aufzuteilen, dass die Teams unabhängig und eigenständig schneller entwickeln können als vorher. Bei Microservices geht es also zuerst einmal nicht um Technik, sondern um den Menschen.

Ein schlagkräftiges Entwicklungsteam hat eine Größe, bei der Urlaub und Krankheit nicht zu Stillstand führen (also ab drei Personen) und bei der die Kommunikation beherrschbar bleibt (also nicht mehr als sieben bis acht Personen, die sich gegenseitig auf dem Stand der Dinge halten müssen). Ein solches Team soll nun ein Stück Software, ein Modul zum (Weiter-)Entwickeln bekommen, das unabhängig vom Rest des Systems ist. Denn nur, wenn das Modul unabhängig ist, kann das Team eigenständig Entscheidungen treffen und sein Modul weiterentwickeln, ohne auf andere Teams und deren Zulieferungen zu warten.

Diese Unabhängigkeit von anderen Teams ist nur dann möglich, wenn das Softwaresystem nach fachlichen Kriterien zerlegt wird. Technische Kriterien führen dazu, dass es irgendwelche Arten von Frontend- und Backend-Teams gibt (Abb. 1). In so einer technischen Teamaufteilung ist mindestens das Frontend-Team davon abhängig, dass das Backend-Team die Frontend-Schnittstelle um die benötigten Features erweitert. Gibt es noch ein Datenbankteam, so hat das Backend-Team auch keine Freiheit und muss seinerseits auf Anpassungen durch das Datenbankteam warten. Neue Features betreffen in einer solchen Teamstruktur fast immer mehrere Teams, die bei der Umsetzung voneinander abhängig sind und viel miteinander kommunizieren müssen, damit die Schnittstellen stimmen.


Abbildung 1: Technische Aufteilung von Teams

Eine Aufteilung von Teams nach fachlichen Kriterien macht es im Gegensatz dazu möglich, dass ein Team für ein fachliches Modul in der Software zuständig ist, das sich durch alle technischen Schichten von der Oberfläche bis zur Datenbank zieht (Abb. 2).


Abbildung 2: Fachliche Aufteilung von Teams

Neue Features sollten, wenn der Schnitt in fachliche Module gut gelungen ist, jeweils einem Team und seinem Modul zugeordnet werden können. Natürlich ist das erst einmal eine Idealvorstellung – in der Praxis können neue Features dazu führen, dass der Modulschnitt überdacht werden muss, weil das neue Feature die aktuelle fachliche Zerlegung über den Haufen wirft. Oder sie führen dazu, dass das wahrscheinlich etwas zu große Feature so geschickt in kleinere Features zerlegt werden muss, dass verschiedene Teams ihren jeweiligen fachlichen Anteil an dem großen Feature unabhängig von den anderen Teams umsetzen können. Die jeweiligen Teilfeatures sind dann hoffentlich auch allein sinnvoll und können unabhängig voneinander ausgeliefert werden. Der Mehrwert des großen Features wird dem Anwender allerdings erst am Ende zur Verfügung stehen, wenn alle beteiligten Teams fertig sind.

Fachliche Zerlegung: Wie geht das?

Strebt man eine fachliche Aufteilung seines großen Monolithen an, stellt sich die Frage: Wie findet man stabile, unabhängige fachliche Schnitte, entlang derer eine Zerlegung möglich wird? Das relativ vage beschriebene Konzept von Microservices gibt darauf keine Antwort. Deshalb hat in den letzten Jahren Domain-driven Design (DDD) von Eric Evans an Bedeutung gewonnen. DDD bietet neben vielen anderen Best Practices eine Anleitung, wie Domänen fachlich aufgeteilt werden können. Diese fachliche Aufteilung in Subdomänen überträgt Eric Evans auf Softwaresysteme. Das Äquivalent zu Subdomänen ist in der Software der Bounded Context. Sind die Subdomänen gut gewählt und die Bounded Contexts entsprechend in der Software umgesetzt, dann entsteht eine gute fachliche Zerlegung.

Um eine gute fachliche Zerlegung zu finden, hat es sich in unseren Projekten als sinnvoll erwiesen, den Monolithen und die in ihm möglicherweise vorhandene Struktur erst einmal beiseite zu legen und sich noch einmal grundlegend mit der Fachlichkeit, also der Aufteilung der Domäne in Subdomänen, zu beschäftigen. Wir fangen in der Regel damit an, uns zusammen mit den Anwendern und Fachexperten einen Überblick über unsere Domäne zu verschaffen. Das kann entweder mittels Event Storming oder mittels Domain Storytelling geschehen – zwei Methoden, die für Anwender und Entwickler gleichermaßen gut verständlich sind.

In Abbildung 3 ist eine Domain Story zu sehen, die mit den Anwendern und Entwicklern eines kleinen Programmkinos erstellt wurde. Die grundsätzliche Frage, die wir uns bei der Modellierung gestellt haben, ist: Wer macht was womit wozu? Nimmt man diese Frage als Ausgangspunkt, so lässt sich in der Regel sehr schnell ein gemeinsames Verständnis der Domäne erarbeiten.


Abbildung 3: Überblicks-Domain-Story für ein Programmkino

Als Personen bzw. Rollen oder Gruppen sind in dieser Domain Story erkennbar: die Werbeagentur, der Kinomanager, der Verleiher, der Kassenmitarbeiter und der Kinobesucher. Die einzelnen Rollen tauschen Dokumente und Informationen aus, wie den Buchungsplan der Werbung, die Vorgaben für Filme und die Verfügbarkeit von Filmen. Sie arbeiten aber auch mit „Gegenständen“ aus ihrer Domäne, die in einem Softwaresystem abgebildet sind: dem Wochenplan und dem Saalplan. Diese computergestützten Gegenstände sind mit einem gelben Blitz in der Domain Story markiert. Die Überblicks-Domain-Story beginnt links oben mit der Ziffer 1, wo die Werbeagentur dem Kinomanager den Buchungsplan mit der Werbung mitteilt, und endet bei der 16, wenn der Kassenmitarbeiter den Saalplan schließt.

An diesem Überblick lassen sich verschiedene Indikatoren erklären, die beim Schneiden einer Domäne helfen:

Abteilungsgrenzen oder verschiedene Gruppen von Domänenexperten deuten darauf hin, dass die Domain Story mehrere Subdomänen enthält. In unserem Beispiel könnte man sich eine Abteilung Kinomanagement und eine Abteilung Kartenverkauf vorstellen (Abb. 4).

Werden Schlüsselkonzepte der Domäne von den verschiedenen Rollen unterschiedlich verwendet oder definiert, deutet das auf mehrere Subdomänen hin. In unserem Beispiel wird das Schlüsselkonzept „Wochenplan“ vom Kinomanager deutlich umfangreicher definiert als der ausgedruckte Wochenplan, den der Kinobesucher zu Gesicht bekommt. Für den Kinomanager enthält der Wochenplan neben den Vorstellungen in den einzelnen Sälen auch die geplante Werbung, den Eisverkauf und die Reinigungskräfte. Diese Informationen sind für den Kinobesucher irrelevant (gestrichelte Kreise in Abb. 4).

Enthält die Überblicks-Domain-Story Teilprozesse, die von verschiedenen Triggern ausgelöst werden und in unterschiedlichen Rhythmen ablaufen, dann könnten diese Teilprozesse eigene Subdomänen bilden (durchgezogene Kreise in Abb. 4).

Gibt es im Überblick Prozessschritte, an denen Information nur in eine Richtung läuft, könnte diese Stelle ein guter Ansatzpunkt für einen Schnitt zwischen zwei Subdomänen sein (hellblauer Pfeil in Abb. 4).


Abbildung 4: Überblicks-Domain Story mit Subdomänengrenzen

Für echte große Anwendungen in Unternehmen sind die Überblicks-Domain-Stories in der Regel deutlich größer und umfassen mehr Schritte. Sogar bei unserem kleinen Programmkino fehlen im Überblick die Eisverkäufer und das Reinigungspersonal, die sicherlich auch mit der Software interagieren werden. Die Indikatoren, nach denen man in seiner Überblicks-Domain-Story suchen muss, gelten allerdings sowohl für kleine als auch für größere Domänen.

Übertragung auf den Monolithen

Mit der fachlichen Aufteilung in Subdomänen im Rücken können wir uns nun wieder dem Monolithen und seinen Strukturen zuwenden. Bei dieser Zerlegung setzen wir Architekturanalysetools ein, die es uns erlauben, die Architektur im Tool umzubauen und Refactorings zu definieren, die für den echten Umbau des Sourcecodes notwendig sind. Hier eignen sich unterschiedliche Tools: der Sotograph, der Sonargraph, Structure101, Lattix, Teamscale, Axivion Bauhaus Suite und andere.


Abbildung 5: Mob Architecting mit dem Team

In Abbildung 5 sieht man, wie die Zerlegung des Monolithen mit einem Analysetool durchgeführt wird. Die Analyse wird von einem Tool-Pilot, der sich mit dem jeweiligen Tool und der bzw. den eingesetzten Programmiersprachen auskennt, gemeinsam mit allen Architekten und Entwicklern des Systems in einem Workshop durchgeführt. Zu Beginn des Workshops wird der Sourcecode des Systems mit dem Analysewerkzeug geparst (Abb. 5, 1) und so werden die vorhandenen Strukturen erfasst (zum Beispiel Build Units, Eclipse-/VisualStudio-Projekte, Maven-Module, Package-/Namespace-/Directory-Bäume, Klassen). Auf diese vorhandenen Strukturen werden nun fachliche Module modelliert (Abb. 5, 2), die der fachlichen Zerlegung entsprechen, die mit den Fachexperten entwickelt wurde. Dabei kann das ganze Team sehen, wo die aktuelle Struktur nahe an der fachlichen Zerlegung ist und wo es deutliche Abweichungen gibt. Nun macht sich der Tool-Pilot gemeinsam mit dem Entwicklungsteam auf die Suche nach einfachen Lösungen, wie vorhandene Struktur durch Refactorings an die fachliche Zerlegung angeglichen werden kann (Abb. 5, 3). Diese Refactorings werden gesammelt und priorisiert (Abb. 5, 4). Manchmal stellen der Tool-Pilot und das Entwicklungsteam in der Diskussion fest, dass die im Sourcecode gewählte Lösung besser oder weitergehender ist als die fachliche Zerlegung aus den Workshops mit den Anwendern. Manchmal ist aber auch weder die vorhandene Struktur noch die gewünschte fachliche Zerlegung die beste Lösung, und beides muss noch einmal grundsätzlich überdacht werden.

Erst modular, dann Micro

Mit den so gefundenen Refactorings kann die Arbeit am Monolithen beginnen. Endlich können wir ihn in Microservices zerlegen. Doch halt! Spätestens hier sollte man sich die Frage stellen, ob man sein System tatsächlich in einzelne deploybare Einheiten zerlegen will oder ob nicht ein gut strukturierter Monolith ausreicht. Durch das Aufsplitten des Monolithen in einzelne Deployables kauft man sich eine weitere Stufe von Komplexität ein, nämlich die Verteilung. Braucht man Verteilung, weil der Monolith nicht mehr performant genug ist, dann muss man diesen Schritt gehen. Unabhängige Teams kann man mit den heute sehr weit entwickelten Build-Pipelines aber auch in einem wohlstrukturierten Monolithen bekommen.

Ein wohlstrukturierter Monolith, also ein modularer Monolith oder (wie Dr. Gernot Starke einmal sagte) „ein Modulith“, besteht aus einzelnen fachlichen Modulen, die in einem Deployable existieren (Abb. 6). In manchen Architekturen sind die User Interfaces der einzelnen fachlichen Module hochintegriert. Details zu dieser Variante sprengen diesen Artikel und finden sich in [1].


Abbildung 6: Der Modulith, ein wohlstrukturierter Monolith

 

Ein solcher Modulith setzt durch seine Aufteilung in möglichst unabhängige Module ein grundlegendes softwaretechnisches Prinzip guter Softwarearchitektur um: hohe Kohäsion und lose Kopplung. Die Klassen in den einzelnen fachlichen Modulen gehören jeweils zu einer Subdomäne und sind als Bounded Context in der Software zu finden. Das heißt, diese Klassen setzen gemeinsam eine fachliche Aufgabe um und arbeiten dafür umfassend zusammen. Innerhalb eines fachlichen Moduls herrscht also hohe Kohäsion. Um ihre fachliche Aufgabe zu erledigen, sollten die Klassen in einem Modul nichts von Klassen aus anderen fachlichen Modulen brauchen.

Maximal sollten fachliche Updates, zum Beispiel als Events, über für andere fachliche Module möglicherweise interessante Änderungen zwischen den Modulen ausgetauscht werden und Arbeitsaufträge, die in einen anderen Bounded Context gehören, als Command an andere Module weitergegeben werden. Allerdings sollte man hier immer darauf achten, dass diese Benachrichtigungen nicht überhandnehmen oder als verkappte direkte Aufrufe an andere fachliche Module missbraucht werden. Denn lose Kopplung zwischen fachlichen Modulen bedeutet, dass es so wenig Beziehungen wie möglich gibt. Lose Kopplung lässt sich niemals durch den Einsatz eines technischen Event-Mechanismus erreichen. Denn technische Lösungen schaffen eine technische Entkopplung, aber keine fachlich lose Kopplung.

So ein wohlstrukturierter Modulith ist hervorragend auf eine möglicherweise später notwendige Zerlegung in Microservices vorbereitet, weil er bereits aus fachlich möglichst unabhängigen Modulen besteht. Wir haben also alle Vorteile auf der Hand: Unabhängige Teams, die in den Grenzen ihres Bounded Contexts schnell arbeiten können, und eine Architektur, die auf Zerlegung in mehrere Deployables vorbereitet ist.

Der Knackpunkt: Das Domänenmodell

Wenn ich mir große Monolithen anschaue, dann finde ich dort in der Regel ein kanonisches Domänenmodell. Dieses Domänenmodell wird aus allen Teilen der Software verwendet, und die Klassen im Domänenmodell haben im Vergleich zum Rest des Systems sehr viele Methoden und viele Attribute. Die zentralen Domänenklassen, wie zum Beispiel Produkt, Vertrag, Kunde etc., sind dann meist auch die größten Klassen im System. Was ist passiert?

Jeder Entwickler, der neue Funktionalität in das System eingebaut hat, hat dafür die zentralen Klassen des Domänenmodells gebraucht. Allerdings musste er diese Klassen auch ein bisschen erweitern, damit seine neue Funktionalität umgesetzt werden konnte. So bekamen die zentralen Klassen mit jeder neuen Funktionalität ein bis zwei neue Methoden und Attribute hinzu. Genau! So macht man das! Wenn es schon eine Klasse Produkt im System gibt und ich Funktionalität entwickle, die das Produkt braucht, dann verwende ich die eine Klasse Produkt im System und erweitere sie so, dass es passt. Ich will nämlich die vorhandene Klasse wiederverwenden und nur an einer Stelle suchen müssen, wenn beim Produkt ein Fehler auftritt. Schade ist nur, dass diese neuen Methoden im Rest des Systems gar nicht benötigt, sondern nur für die neue Funktionalität eingebaut werden.

Domain-driven Design und Microservices gehen an dieser Stelle den entgegengesetzten Weg. In einem Modulithen, der fachlich zerlegt ist, oder in einer verteilten Microservices-Architektur gibt es in jedem Bounded Context, der die Klasse Produkt braucht, eine eigene Klasse Produkt. Diese kontextspezifische Klasse Produkt ist auf ihren Bounded Context zugeschnitten und bietet nur die Methoden an, die in diesem Kontext benötigt werden. Den Wochenplan aus unserem Kinobeispiel in Abbildung 3 und 4 gibt es im fachlich zerlegten System zweimal. Einmal im Bounded Context Kinomanagement mit einer sehr reichhaltigen Schnittstelle, über die man Werbung zu Vorstellungen einplanen und den Eisverkauf und die Reinigungskräfte einteilen kann. Und zum anderen im Bounded Context Kartenverkauf, wo die Schnittstelle lediglich das Suchen von Filmen und die Abfrage des Filmangebots zu bestimmten Zeiten ermöglicht. Werbung, Eisverkauf und Reinigungskräfte sind für den Kartenverkauf irrelevant und werden in diesem Bounded Context in der Klasse Wochenplan also auch nicht benötigt.

Will man einen Monolithen fachlich zerlegen, so muss man das kanonische Domänenmodell zerschlagen. Das ist in den meisten großen Monolithen eine Herkulesaufgabe. Zu verwoben sind die auf dem Domänenmodell aufsetzenden Teile des Systems mit den Klassen des Domänenmodells. Um hier weiterzukommen, kopieren wir zuerst die Domänenklassen in jeden Bounded Context. Wir duplizieren also Code und bauen diese Domänenklassen dann jeweils für ihren Bounded Context zurück. So bekommen wir kontextspezifische Domänenklassen, die von ihrem jeweiligen Team unabhängig vom Rest des Systems erweitert und angepasst werden können.

Selbstverständlich müssen bestimmt Eigenschaften von Produkt, beispielsweise die Produkt-ID und der Produktname, in allen Bounded Contexts gleich gehalten werden. Außerdem müssen neue Produkte in allen Bounded Contexts bekanntgemacht werden, wenn in einem Bounded Context ein neues Produkt angelegt wird. Diese Informationen werden über Updates von einem Bounded Context an alle anderen Bounded Contexts gemeldet, die mit Produkten arbeiten.

SOA ist keine Microservices-Architektur

Wenn ein System auf diese Weise zerlegt wird, entsteht eine Struktur, die einer IT-Landschaft aus dem Anfang der 2000er Jahre überraschend ähnelt. Verschiedene Systeme (möglicherweise von verschiedenen Herstellern) werden über Schnittstellen miteinander verbunden, arbeiten aber weiterhin autark auf ihrem eigenen Domänenmodell. In Abbildung 7 sieht man, dass es den Kunden in jedem der vier dargestellten Systeme gibt und die Kundendaten über Schnittstellen zwischen den Systemen ausgetauscht werden.


Abbildung 7: IT-Landschaft mit dem Kunden in allen Systemen

Anfang der 2000er wollte man dieser Verteilung der Kundendaten entgegenwirken, indem man Service-orientierte Architekturen (SOA) als Architekturstil verfolgt hat. Das Ergebnis waren IT-Landschaften, in denen es einen zentralen Kundenservice gibt, den alle anderen Systeme über einen Service Bus nutzen. Abbildung 8 stellt eine solche SOA mit einem Kundenservice schematisch dar.

So eine Service-orientierte Architektur mit zentralen Services hat im Lichte der Diskussion um Microservices den entscheidenden Nachteil, dass alle Entwicklungsteams, die den Kundenservice brauchen, nicht mehr unabhängig arbeiten können und damit an individueller Schlagkraft verlieren. Gleichzeitig ist es vermessen zu erwarten oder zu fordern, dass alle Großrechnersysteme, in denen in vielen Unternehmen die zentrale Datenhaltung sichergestellt wird und die als SOA-Services ansprechbar sind, auf Microservices-Architekturen umgebaut werden. In solchen Fällen treffe ich häufig eine Mischung aus SOA- und Microservices-Architektur an, was durchaus gut funktionieren kann.


Abbildung 8: IT-Landschaft mit SOA und Kundenservice

Standortbestimmung

Um für Monolithen eine Bewertungsmöglichkeit zu schaffen, wie gut der Sourcecode auf eine fachliche Zerlegung vorbereitet ist, haben wir in den vergangenen Jahren den Modularity Maturity Index (MMI) entwickelt. In Abbildung 9 ist eine Auswahl von 21 Softwaresystemen dargestellt, die in einem Zeitraum von fünf Jahren analysiert wurden (X-Achse). Für jedes System ist die Größe in Lines of Code dargestellt (Größe des Punktes) und die Modularität auf einer Skala von 0 bis 10 (Y-Achse).


Abbildung 9: Modularity Maturity Index (MMI)

Liegt ein System in der Bewertung zwischen 8 und 10, so ist es im Inneren bereits modular aufgebaut und wird sich mit wenig Aufwand fachlich zerlegen lassen. Systeme mit einer Bewertung zwischen 4 und 8 haben gute Ansätze, jedoch sind hier einige Refactorings notwendig, um die Modularität zu verbessern. Systeme unterhalb der Marke 4 würde man nach Domain-driven Design als Big Ball of Mud bezeichnen. Hier ist kaum fachliche Struktur zu erkennen, und alles ist mit allem verbunden. Solche Systeme sind nur mit sehr viel Aufwand in fachliche Module zerlegbar.

Fazit

Microservices sind als Architekturstil noch immer in aller Munde. Inzwischen haben verschiedene Organisationen Erfahrungen mit dieser Art von Architekturen gemacht und die Herausforderungen und Irrwege sind deutlich geworden.

Um im eigenen Unternehmen einen Monolithen zu zerlegen, muss zuerst die fachliche Domäne in Subdomänen zerlegt und diese Struktur im Anschluss auf den Sourcecode übertragen werden. Dabei sind insbesondere das kanonische Domänenmodell, unsere Liebe zur Wiederverwendung und das Streben nach Service-orientierten Architekturen Hindernisse, die überwunden werden müssen.

Links & Literatur

[1] Lilienthal, Carola: „Langlebige Softwarearchitekturen – Technische Schulden analysieren, begrenzen und abbauen“; dpunkt.verlag, 2017.

Java-Dossier für Software-Architekten 2019


Mit diesem Dossier sind Sie auf alle Neuerungen in der Java-Community vorbereitet. Die Artikel liefern Ihnen Wissenswertes zu Java Microservices, Req4Arcs, Geschichten des DevOps, Angular-Abenteuer und die neuen Valuetypen in Java 12.

Java-Wissen sichern!

The post Von Monolithen über modulare Architekturen zu Microservices mit DDD appeared first on JAX.

]]>