Software-Architektur

Das Ökosystem der Softwareentwicklung ändert sich ständig und es gibt einen ständigen Strom von Innovationen bei Tools, Frameworks und Techniken. In den letzten Jahren haben inkrementelle Entwicklungen in den Kernentwicklungspraktiken für die Softwareentwicklung den Weg geebnet, um zu überdenken, wie sich die Architektur im Laufe der Zeit ändern kann und wie wichtige architektonische Merkmale im Laufe ihrer Entwicklung geschützt werden können.

Auswahl der richtigen NoSQL-Datenbank

Relationale Datenbanken dominierten lange die Softwareindustrie und sind mit Mechanismen wie Redundanz, Transaktionskontrolle und Standardschnittstellen sehr ausgereift. Sie konnten jedoch zunächst nur mäßig auf höhere Anforderungen an Skalierbarkeit und Performance reagieren. So kam ab Anfang 2010 verstärkt der Begriff NoSQL auf um neuartige Datenbanken zu beschreiben, die diesen Anforderungen besser genügten.

NoSQL-Datenbanken sollten die folgenden Probleme lösen:

  • Überbrücken der internen Datenstruktur der Anwendung und der relationalen Datenstruktur der Datenbank
  • Abkehr von der Integration verschiedenster Datenstrukturen in ein einheitliches Datenmodell
  • Die wachsende Anzahl an Daten erforderte zunehmend Cluster zur Datenspeicherung

Aggregierte Datenmodelle

Die relationale Datenbankmodellierung unterscheidet sich erheblich von den Arten von Datenstrukturen, die Anwendungsentwickler verwenden. Die Verwendung der von Entwickler*innen modellierten Datenstrukturen zur Lösung verschiedener Problemdomänen hat zu einer Abkehr von der relationalen Modellierung hin zu aggregierten Modellen geführt. Der größte Teil davon wird von Domain Driven Design angetrieben. Ein Aggregat ist eine Sammlung von Daten, mit denen wir als Einheit interagieren. Diese Aggregate bilden die Grenzen für ACID-Operationen, wobei die Schlüsselwerte, Dokumente und Column Family als Formen einer aggregatorientierten Datenbank angesehen werden können.

Aggregate erleichtern der Datenbank die Verwaltung der Datenspeicherung auf einem Cluster, da sich die Dateneinheit jetzt auf jedem Computer befinden kann. Aggregatorientierte Datenbanken funktionieren am besten, wenn die meisten Dateninteraktionen mit demselben Aggregat durchgeführt werden, z.B. wenn ein Profil mit allen Details abgerufen werden muss. Es ist besser, das Profil als Aggregationsobjekt zu speichern, und diese Aggregate zu verwenden, um Profildetails abzurufen.

Distributionsmodelle

Aggregatorientierte Datenbanken erleichtern die Verteilung von Daten, da der Verteilungsmechanismus nur das Aggregat verschieben muss und sich nicht um verwandte Daten kümmern muss, da alle verwandten Daten im Aggregat selbst enthalten sind. Dabei gibt es im Wesentlichen zwei Arten der Datenverteilung:

Sharding
Sharding verteilt unterschiedliche Daten auf mehrere Server, sodass jeder Server als einzige Quelle für eine Teilmenge von Daten fungiert.
Replikation

Replikation kopiert Daten über mehrere Server hinweg, sodass jedes Datum an mehreren Stellen gefunden werden kann. Die Replikation erfolgt in zwei Formen:

Die Master-Slave-Replikation macht einen Knoten zur autorisierenden Kopie, die Schreibvorgänge verarbeitet, während Slaves mit dem Master synchronisiert werden und möglicherweise Lesevorgänge verarbeiten.

Die Peer-to-Peer-Replikation ermöglicht Schreibvorgänge auf jeden Knoten. Die Knoten koordinieren sich um ihre Kopien der Daten zu synchronisieren.

Die Master-Slave-Replikation verringert zwar die Wahrscheinlichkeit von Aktualisierungskonflikten, aber die Peer-to-Peer-Replikation vermeidet das Schreiben allerVorgänge auf einen einzelnen Server, wodurch ein einzelner Fehlerpunkt vermieden wird. Ein System kann eine oder beide Techniken verwenden.

CAP-Theorem

In verteilten Systemen sind die folgenden drei Aspekte wichtig:

  • Konsistenz (engl.: Consistency)
  • Verfügbarkeit (engl.: Availability)
  • Ausfalltoleranz (engl.: Partition tolerance)

Eric Brewer hat das CAP-Theorem aufgestellt, das besagt, dass wir in jedem verteilten System nur zwei der drei Optionen auswählen können. Viele NoSQL-Datenbanken versuchen, Optionen bereitzustellen, bei denen ein Setup gewählt werden kann, die Datenbank entsprechend dein Anforderungen aufzusetzen. Wenn ihr beispielsweise Riak als verteilte Schlüssel-Werte-Datenbank betrachtet, gibt es im Wesentlichen die drei Variablen

r
Anzahl der Knoten, die auf eine Leseanforderung antworten sollen, bevor sie als erfolgreich angesehen wird
w
Anzahl der Knoten, die auf eine Schreibanforderung antworten sollen, bevor sie als erfolgreich angesehen wird
n
Anzahl der Knoten, auf denen die Daten repliziert werden, auch Replikationsfaktor genannt

In einem Riak-Cluster mit 5 Knoten können wir die Werte für r, w und n so anpassen, dass das System sehr konsistent ist indem wir r = 5 und w = 5 setzen. Damit haben wir jedoch den Cluster für Netzwerkpartitionen anfällig gemacht, da kein Schreibvorgang möglich ist wenn nur ein Knoten nicht antwortet. Wir können denselben Cluster für Schreib- oder Lesevorgänge hoch verfügbar machen, indem wir r = 1 und w = 1 setzen. Jetzt kann jedoch die Konsistenz beeinträchtigt werden, da einige Knoten möglicherweise nicht über die neueste Kopie der Daten verfügen. Das CAP-Theorem besagt, dass ihr, wenn ihr eine Netzwerkpartition erhaltet, die Verfügbarkeit von Daten gegen die Konsistenz von Daten abwägen müsst. Auch die Haltbarkeit kann gegen die Latenz abgewogen werden, insbesondere wenn ihr Fehler mit replizierten Daten überstehen möchtet.

Häufig benötigtet ihr bei relationalen Datenbanken kaum ein Verständnis für diese Anforderungen; nun werden diese wieder wichtig. So dürftet ihr gewöhnt gewesen sein, in relationalen Datenbanken Transaktionen zu verwenden. In NoSQL-Datenbanken stehen euch diese jedoch nicht mehr zur Verfügung und ihr müsst selbst überlegen, wie diese implementiert werden sollen. Muss das Schreiben transaktionssicher sein? Oder ist es auch akzeptabel, dass mal Daten verlorengehen? Schließlich kann manchmal auch ein externer Transaktionsmanager wie ZooKeeper hilfreich sein.

Verschiedene Arten von NoSQL-Datenbanken

NoSQL-Datenbanken können grob in vier Typen eingeteilt werden:

Schlüssel-Werte-Datenbanken

Schlüssel-Werte-Datenbanken sind aus API-Sicht die einfachsten NoSQL-Datenspeicher. Der Client kann entweder den Wert für den Schlüssel abrufen, einen Wert für einen Schlüssel eingeben oder einen Schlüssel aus dem Datenspeicher löschen. Der Wert ist ein Blob, den der Datenspeicher nur speichert, ohne sich darum zu kümmern oder zu wissen, was sich darin befindet. Es liegt ausschließlich in der Verantwortung der Anwendung, zu verstehen, was gespeichert wurde. Da Schlüssel-Werte-Datenbanken immer den Primärschlüsselzugriff verwenden, weisen sie im Allgemeinen eine hohe Leistung auf und können leicht skaliert werden.

Einige der beliebtesten Schlüsselwertdatenbanken sind

Riak KV
Home | GitHub | Docs
Redis
Home | GitHub | Docs
Memcached
Home | GitHub | Docs
Berkeley DB
Home | GitHub | Docs
Upscaledb
Home | GitHub | C API Docs

Ihr müsst sie sorgfältig auswählen da es große Unterschiede zwischen ihnen gibt. Während Riak z.B. die Daten persistent speichert ist dies bei Memcached üblicherweise nicht der Fall.

Dokumentendatenbanken

Diese Datenbanken speichern und rufen Dokumente ab, bei denen es sich um XML, JSON, BSON usw. handeln kann. Diese Dokumente sind hierarchische Baumdatenstrukturen, die aus Karten, Sammlungen und Skalarwerten bestehen können. Dokumentendatenbanken bieten umfangreiche Abfragesprachen und Konstrukte wie Datenbanken, Indizes usw., die einen einfacheren Übergang von relationalen Datenbanken ermöglichen.

Einige der beliebtesten Dokumentendatenbanken sind

MongoDB
Home | GitHub | Docs
CouchDB
Home | GitHub | Docs
RavenDB
Home | GitHub | Docs
Elasticsearch
Home | GitHub | Docs
eXist
Home | GitHub | Docs

Column Family Stores

Diese Datenbanken speichern Daten in Column Families als Zeilen, die einem Zeilenschlüssel zugeordnet sind. Sie sind hervorragend geeignet für Gruppen verwandter Daten, auf die häufig zusammen zugegriffen wird. Dies können beispielsweise alle Profilinformationen einer Person sein, nicht jedoch deren Aktivitäten.

Zwar kann jede Column Family mit der Zeile in einer RDBMS-Tabelle verglichen werden, in dem der Schlüssel die Zeile identifiziert und die Zeile aus mehreren Spalten besteht; aber in Column Family Stores müssen die verschiedene Zeilen nicht dieselben Spalten haben.

Einige der beliebtesten Column Family Stores sind

Cassandra
Home | GitHub | Docs
HBase
Home | GitHub | Docs
Hypertable
Home | GitHub | Docs

Cassandra kann als schnell und einfach skalierbar beschrieben werden, da Schreibvorgänge über den Cluster verteilt sind. Der Cluster verfügt nicht über einen Masterknoten, sodass Lese- und Schreibvorgänge von jedem Knoten im Cluster ausgeführt werden können.

Graphdatenbank

In Graphdatenbanken könnt ihr Entitäten mit bestimmten Eigenschaften und Beziehungen zwischen diesen Entitäten speichern. Entitäten werden auch als Knoten bezeichnet. Stellt euch einen Knoten als Instanz eines Objekts in einer Anwendung vor; Beziehungen können dann als Kanten bezeichnet werden, die ebenfalls Eigenschaften haben können und gerichtet sind.

Graph-Modelle
Labeled-Property-Graph
In einem Labeled-Property-Graph können sowohl Knoten als auch Kanten Eigenschaften (engl.: Properties) besitzen.
Resource Description Framework (RDF)
Im RDF werden Graphen mit Hilfe von Triplen repräsentiert. Ein Triple besteht aus drei Elementen in der Form Knoten-Kante-Knoten Subjekt --Prädikat-> Objekt, die als Ressourcen in Form einer weltweit eindeutigen URI oder als anonyme Ressource definiert werden. Um innerhalb einer Datenbank verschiedene Graphen verwalten zu können, werden diese als Quads gespeichert wobei ein Quad jedes Triple um eine Referenz auf den zugehörigen Graphen erweitert. Aufbauend auf RDF wurde mit RDF-Schema ein Vokabular entwickelt, um schwache Ontologien zu formalisieren und darüber hinaus mit der Web Ontology Language auch vollständig entscheidbare Ontologien beschreiben.
Algorithmen

Wichtige Algorithmen zur Abfrage von Knoten und Kanten sind:

Breitensuche, Tiefensuche
Breitensuche (engl.: breadth-first search, BFS) ist ein Verfahren zum Durchlaufen der Knoten eines Graphen verwendet. Im Gegensatz zur Tiefensuche (engl.: depth-first search, DFS) werden zunächst alle Knoten beschritten, die vom Ausgangsknoten direkt erreichbar sind. Erst danach werden Folgeknoten beschritten.
Kürzester Pfad
Pfad zwischen zwei unterschiedlichen Knoten eines Graphen, welcher minimale Länge bezüglich einer Kantengewichtsfunktion hat.
Eigenvektor
In der linearen Algebra ein vom Nullvektor verschiedener Vektor, dessen Richtung durch die Abbildung nicht verändert wird. Ein Eigenvektor wird also nur skaliert und der Skalierungsfaktor wird als Eigenwert der Abbildung bezeichnet.
Abfragesprachen
Blueprints
eine Java-API für Eigenschaftsgraphen, die zusammen mit verschiedenen Graphdatenbanken verwendet werden kann.
Cypher
eine Abfragesprache entwickelt von Neo4j.
GraphQL
eine SQL-artige Abfragesprache
Gremlin
eine Open-Source-Graphen-Programmiersprache, die mit verschiedenen Graphdatenbanken (Neo4j, OrientDB) genutzt werden kann.
SPARQL
vom W3C spezifizierte Abfragesprache für RDF-Datenmodelle.
Abgrenzung zu relationalen Datenbanken

Wenn wir Graphen in relationalen Datenbanken speichern wollen, geschieht dies meist nur für spezifische Bedingungen, z.B. für die Beziehungen zwischen Menschen. Das Hinzufügen weiterer Beziehungsarten ist dann normalerweise mit vielen Schemaänderungen verbunden.

In Graphdatenbanken ist das Durchlaufen der Verknüpfungen oder Beziehungen sehr schnell, da die Beziehung zwischen Knoten nicht erst zur Abfragezeit berechnet werden muss.

Einige der beliebtesten Graphdatenbanken sind

Neo4j
Home | GitHub | Docs
InfiniteGraph
Home

Auswahl der NoSQL-Datenbank

Allen NoSQL-Datenbanken ist gemein, dass sie kein bestimmtes Schema erzwingen. Im Gegensatz zu relationalen Datenbanken mit starkem Schema müssen Schemaänderungen nicht zusammen mit dem Quellcode, der auf diese Änderungen zugreift, gespeichert werden. Schemalose Datenbanken können Änderungen im implizierten Schema tolerieren, sodass sie keine Downtime zur Migration benötigen; sie sind daher vor allem bei Systemen, die 24/7 zur Verfügung stehen sollen, sehr beliebt.

Wie wählen wir nun aber unter so vielen NoSQL-Datenbanken die richtige aus? Im Folgenden können wir Ihnen nur einige allgemeine Kriterien nennen:

Schlüssel-Werte-Datenbanken
sind im Allgemeinen nützlich zum Speichern von Sessions, Benutzerprofilen und Einstellungen. Wenn jedoch Beziehungen zwischen den gespeicherten Daten abgefragt oder mehrere Schlüssel gleichzeitig bearbeitet werden sollen, würden wir Schlüsselwertdatenbanken vermeiden.
Dokumentendatenbanken
sind im Allgemeinen nützlich für Content-Management-Systeme und E-Commerce-Anwendungen. Wir würden die Verwendung von Dokumentdatenbanken jedoch vermeiden, wenn komplexe Transaktionen benötigt odermehrere Vorgänge oder Abfragen für unterschiedliche Aggregatstrukturen gestellt werden sollen.
Column Family Stores
sind im Allgemeinen nützlich für Content-Management-Systeme und für hohe Schreibvolumen wie z.B. bei der Protokollaggregation. Wir würden die Verwendung von Datenbanken für Column Family Stores vermeiden, die sich in der frühen Entwicklung befinden und deren Abfragemuster sich noch ändern können.
Graphdatenbanken
eignen sich sehr gut für Problembereiche, in denen wir Daten wie soziale Netzwerke, Geodaten, Routing-Informationen sowie Empfehlungssystem miteinander verbunden werden sollen.

Fazit

Der Aufstieg der NoSQL-Datenbanken führte nicht zum Niedergang der relationalen Datenbanken. Sie können gut nebeneinander existieren. Häufig werden unterschiedliche Datenspeichertechnologien verwendet um die Daten passend zu ihrer Struktur und erforderlichen Abfrage zu speichern.

Domain-Driven Datenarchitektur

Viele Unternehmen investieren weiterhin in Data-Lake, in der Hoffnung, Daten in großem Maßstab demokratisieren zu können. Es ist jedoch bereits jetzt absehbar, dass darauf basierende Plattformen die Versprechungen nicht erfüllen werden können. Um die Probleme anzugehen, müssen wir die bisherigen Paradigmen des Data-Lake und seines Vorgängers Data-Warehouse überwinden und die Vorteile beider nutzen. Diese Lösung geht von einer verteilten Architektur aus, die den verschiedene Fachdomänen eine Self-Service-Dateninfrastruktur bereitstellt und die Daten als Produkte behandelt.

Wir arbeiten seit zwei Jahren am Aufbau eines Computercluster für das Fraunhofer ISE.

Dabei wurde von uns häufig erwartet, dass wir diesen zentralisiert, monolithisch und domänenunabhängig aufbauen, also als sog. Data Lake.

Die größte Herausforderung besteht nicht darin, einen Datensee zu erstellen, sondern die sich daraus ergebenden Chancen zu nutzen.

– Sean Martin, CTO Cambridge Semantics

Wir konnten jedoch bereits früher zusammen mit anderen Kunden erkennen, dass diese Chancen kaum genutzt werden konnten. Hier deswegen zunächst ein kurzer Überblick über frühere Architektur-Generationen:

Historie

  1. Generation: Enterprise Data Warehouse (EDW)

    Vor einem Jahrzehnt waren Daten meist ein Synonym für relationale Datenbanken. Meist führte dies jedoch zu hohen technischen Schulden in Form von tausenden nicht zu überarbeitenden ETL (extract, transform, load)-Jobs, die nur eine kleine Gruppe von Fachleuten versteht. Damit blieben jedoch die erhofften positiven Auswirkungen unerfüllt.

  2. Generation: Big-Data-Ökosystem mit einem Data Lake

    Jetzt können Daten eine erstaunliche Vielfalt von Formen annehmen einschließlich NoSQL, Zeitreihen und SQL. Dies ist auf den Wunsch zurückzuführen, immer schneller auf immer größere, vielfältigere und schnellere Datenquellen zu reagieren. Forscher werden jedoch mit dem Verstehen unterschiedlicher Arten der Datenhaltung vor zusätzliche Herausforderungen gestellt. Aktuell bekommen wir also zusätzliche Möglichkeiten, ohne dass jedoch die grundlegenden Probleme der vorgehenden Generation gelöst worden wären. Diese Ökosysteme mit lange laufenden Batch-Jobs werden mit all ihren Auswirkungen meist nur von hochspezialisierten Fachleuten verstanden. Zwar führte dies theoretisch zu einer Fülle von möglichen Analysen und Simulationen, diese konnten aber wegen ihres Umfangs und ihrer Komplexität praktisch nur selten realisiert werden.

    CockRoachDB und Spanner bieten darüberhinaus globale Konsistenz und Event stream processing (ESP) mit Abfragemöglichkeiten für aggregierte Log-Dateien.

    Ein Nachteil dieses Ansatzes besteht jedoch darin, dass er eher an low-level Database Events als an Geschäftsereignisse auf höherer Ebene gebunden ist, die für uns die Grundlage einer ereignisorientierten Architektur sein sollte – Ereignisse sollten in ihren Domänen explizit erfasst werden oder Statusübergänge sollten nicht impizit erschlossen werden müssen.

Beiden Generationen sind jedoch die folgenden Architekturfehler gemeinsam:

  1. zentral und monolithisch
  2. sequenzierte Pipeline
  3. spezialisiertes Eigentum

Ad 1. Zentral und monolithisch

Alle zugänglichen Daten wurden erfasst und dem ETL (extract, transform, load)-Dreischritt unterworfen:

  1. Extract

    Möglichst alle Daten wurden erfasst, unabhängig davon, ob sie für eine spezifische Fachdomäne benötigt wurden oder nicht.

  2. Transform

    Die Transformation der Daten erfolgte üblicherweise ebenfalls nicht in Hinblick auf die konkreten Erfordernisse sondern vielmehr in Hinblick auf eine möglichst universelle Verwendbarkeit. Häufig wurden die Daten auch noch mit Informationen aus anderen Datenquellen angereichert um prinzipiell noch umfassendere Analysen machen zu können.

  3. Load

    In diesem Schritt wurde darauf geschaut, dass die Daten für eine Vielzahl von Fachdomänen über die gewünschten Protokolle bereitgestellt werden können.

Während wir bei der Anwendungsentwicklung in den letzten Jahren darauf achteten, dass das Design spezifisch für die jeweilige Domäne ist und auch nur in diesem spezifischen Kontext funktionieren muss, wurde Domain Driven Design bei den Datenplattformen weitgehend ignoriert. Dies führte dann zu den bekannten Problemen:

  • Die Aufwände, diese domänenunspezifischen Daten zu harmonisieren, nahmen zu.
  • In vielen Fällen war zu sehen, dass die Integration neuer Datenquellen verzögert oder blockiert wurde, weil sie nicht in das universelle Schema passen wollen.

Die von uns vorgeschlagene Alternative wird nicht zu fragmentierten, isolierten und domänenorientierten Daten führen, die schwer zu entdecken sind sowie spezifisches Wissen zum Verstehen und verarbeiten benötigen. Dies wären dann auch nur wieder fragmentierte Data Warehouses, wie sie sich häufig zur Umgehung der oben genannten Probleme angestaut haben und die nun als technische Schulden weitere Innovationen behindern.

Ad 2: Sequenzierte Pipeline

Der ETL (extract, transform, load)-Dreischritt offenbart jedoch noch weitere Schwierigkeiten, die sich aus der sequentiellen Aufteilung der Funktionen ergeben. Die Motivation für das Sequenzieren der Datenverarbeitung lag in der bestehenden Arbeitsteilung der Forschungsteams.

Organisationen, die Systeme entwerfen, … sind auf Entwürfe festgelegt, welche die Kommunikationsstrukturen dieser Organisationen abbilden.

– Melvyn E. Conway: Conway’s Law, 1967

Obwohl diese Gliederung ein gewisses Maß an Skalierung zu bietet scheint, da Teams verschiedenen Phasen der Verarbeitungspipeline zugewiesen werden, kommt dieser Effekt jedoch meist nicht zum Tragen, da bei neuen Features meist alle Sequenzen der Pipeline überarbeitet werden müssen.

So müssten z.B. für die Ergänzung der Strom-Ex- und Import-Daten um geplante und ungeplante Kraftwerksausfälle alle drei ETL-Bereiche angepasst werden.

Ad 3: Spezialisiertes Eigentum

Das dritte Problem hängt damit zusammen, wie die Teams strukturiert sind, die die Plattform erstellen und betreiben. Meist sind diese Teams isoliert von denjenigen, die diese Plattform für ihre Analysen und Simulationen nutzen. Sie erlangen also meist keine Kenntnis von den Datenquellen und deren Verwendung; sie sollen nur betriebliche und analytische Anforderungen erfüllen, ohne jedoch jemals ein klares Verständnis für eine konkrete Anwendung der Daten zu erhalten.

Die nächste Generation von Datenplattformen

Aus den oben angesprochenen Problemen wird deutlich, dass ein Paradigmenwechsel erforderlich ist. Ein Paradigmenwechsel, der in anderen IT-Bereichen bereits stattgefunden hat und dort bereits die Erprobungsphase verlassen hat: Domain-Driven Architecture, Self-Service-Plattforms und Product Thinking führen zu ubiquitous computing in einem vermaschten Netz (engl. Mesh).

Domain-Driven Architektur

Domänenorientierte Aufteilung und Eigentum der Datensätze

Eric Evans Buch Domain-Driven Design hat nicht nur die moderne Software-Architektur tiefgreifend beeinflusst, sondern auch die Organisationsentwicklung: Nicht nur wurden monolithische Systeme in Microservices zerlegt, auch die um Technologieschichten (Datenbank, Anwendungslogik, UI) organisierten Teams wurden umgewandelt in domänenspezifische Teams.

Bei einem solchen dezentralen Datenmanagement ist dann die Herausforderung, wie diese domänenspezifischen Daten gemeinsam genutzt werden können. Dies ist gut in Ben Stopfords Data Dichotomy-Artikel beschrieben.

Um nun eine monolithische Datenplattform zu dezentralisieren, müssen wir auch unsere Einstellung zu Ort und Eigentum dieser Daten überdenken: statt fleißig Daten in unseren eigenen, zentral gelegenen Data Lake zu übernehmen, müssen nun die Fachdomänen selbst für das Hosting dieser Daten sorgen und sie in einfach zu verarbeitender Weise zur Verfügung stellen. Dies kann dann zur Folge haben, dass wir dieselben Daten, jedoch für jede der Fachdomänen unterschiedlich aufbereitet, so auch in verschiedenen Domänen speichern.

Damit verlagert sich der Datenfluss von einem sequenzierten Pipeline-Prozess hin zu einer domänenspezifischen Aufbereitung.

Quelldomän-Datensätze

Einige Domänen richten sich natürlich nach der Herkunft der Datensätze. Meist entstehen diese Datensätze sehr nahe an der Quelle, an der die Daten entstehen. Idealerweise übernehmen die Lieferanten dieser Datensätze auch die Gewähr, dass diese hinreichend integer sind.

Note

Diese Datensätze enthalten meist nur Rohdaten, die zum Zeitpunkt der Erstellung möglichst genau wiedergeben und nicht für einen bestimmten Konsumenten angepasst oder modelliert sind.

Verbraucherorientierte und gemeinsam genutzte Datensätze

Einige Domänen bestimmen sich nicht aus der Herkunft sondern dem Ziel, das erreicht werden soll. Auch ansonsten unterscheiden sich diese Datensätze erheblich von denjenigen der Quelldomäne: Sie werden jedoch transformiert, um einen möglichst einfachen Zugriff darauf zu erlauben. Hier entsteht also eine Pipeline zur Bereinigung, Aufbereitung, Aggregation und Bereitstellung der Daten.

Self-Service-Platforms

Es gibt jedoch nicht nur innerhalb der Domänen von verbraucherorientierten Datensätzen Verarbeitungspipelines, sondern auch bereits bei Quelldomän-Datensätzen: Dort müssen die Daten auf Vollständigkeit überprüft werden und frei von Dubletten sein.

Um die Komplexität solcher Pipelines jedoch so gering wie möglich zu halten, werden sie immer nur innerhalb ihrer eigenen Domäne behandelt. Dort können dann auch Service-Levels für die Daten dieser Domäne definiert werden wie Aktualität, Fehlerraten etc.

Product Thinking

In anderen Bereichen wurden Produkte schon viel früher über Projekte gestellt, s.a. Products over Projects. Zukünftig werden die Teams einer Datendomäne den Forschungsteams ihre Funktionen als APIs zur Verfügung stellen und liefern so einen Baustein für die Schaffung höherer Funktionalitäten und Auftragswerte. Damit ein solches Produkt erfolgreich wird, müssen die Daten folgende Eigenschaften aufweisen:

  • Ein Datenprodukt muss leicht auffindbar sein

    Eine übliche Implementierung besteht in einer Registrierung und einem Datenkatalog aller Datenprodukte mit ihren Metainformationen. Diese zentrale Anlaufstelle ermöglicht Datenkonsumenten, die Datensätze ihres Interesses leicht zu finden.

  • Jedes Datenprodukt muss eine eindeutige Adresse haben

    Diese Adresse muss einer globalen Konvention folgen und anderen Benutzern erleichtern, programmatisch auf diese Daten zuzugreifen. Und auch wenn die zugrundeliegenden Speicher und Datenformate unterschiedlich sind, z.B. Events als Stream von Kafka Topics oder Kolumnen in Apache Parquet, so müssen diese Daten doch einheitlich adressierbar sein um Reibungsverluste beim Auffinden und Zugreifen auf diese Daten gering zu halten.

  • Die Daten müssen vertrauenswürdig und wahrheitsgemäß sein

    Hierfür werden Service-level Objectives (SLOs) definiert und überwacht. Hierfür werden die Daten bereinigt, Dubletten entfernt und automatisiert Integritätstests durchgeführt.

    Ebenso sollte die Datenherkunft (engl.: Data Lineage) als Metadaten bereitgestellt werden um die Eignung für spezielle Bedürfnisse ermitteln zu können.

    Die SLOs können jedoch zwischen den einzelnen Datenprodukten variieren.

  • Die Semantik und Syntax sollte leicht zugänglich sein

    Datenschemata sind häufig der Ausgangspunkt für die Bereitstellung von Self-Service-Datenbeständen. Daher werden sie üblicherweise zusammen mit Beispieldatensätzen leicht zugänglich veröffentlicht.

  • Die Daten sollten interoperabel sein

    Auch wenn die Daten in einzelnen Fachdomänen verwaltet werden, so ist jedoch erklärtes Ziel, die Daten domänenübergreifend korrelieren zu können. Voraussetzung für effektive Korrelationen sind das Einhalten bestimmter Standards und Harmonisierungsregeln. Dies wird z.B. über die Definition von Feldtypen, die Identifizierung bestimmter Polyseme in verschiedenen Domänen, Adresskonventionen für Datensätze, gemeinsame Metadatenfelder, Event-Formate wie z.B. in der CloudEvents Specification) festgelegt etc.

  • Der Zugriff auf Produktdatensätze muss sicher sein und sollte einer globalen Zugriffskontrolle unterliegen

    Hierfür können Single Sign On und Role Based Access Control verwendet werden.

Vermaschtes Netz

Domain-Driven Architecture, Self-Service-Plattforms und Product Thinking kommen zusammen in einem vermaschten Netz. Einerseits besteht dieses aus verteilten Datenprodukten, die sich an Domänen orientieren und die im Besitz unabhängiger und funktionsübergreifender Teams sind; andererseits nutzen sie eine gemeinsame Dateninfrastruktur mit einer Hosting-Plattform und ETL-Bibliotheken. Darüberhinaus wird die Vereinheitlichung von batch- und streaming-Jobs, z.B. mit Apache Beam, die problemlose Verarbeitung adressierbarer polyglotter Datensätze ermöglichen. Das Beam-Modell basiert auf dem Dataflow-Modell, mit dem sich die Logik elegant ausdrücken lässt. Zudem werden von Beam auch verschiedenste Runner unterstützt, u.a. Apache Flink, Apache Spark und Apache Samza, s.a. Beam Capability Matrix. Es ist jedoch zu beachten, dass die Runner unterschiedlichste Fähigkeiten haben und die Bereitstellung einer übergreifenden API ein schwieriges Unterfangen bleibt.

Beam hat darüberhinaus ein umfangreiches Set an eingebauten I/O-Transformationen, die einen großen Teil der Daten-Pipeline-Anforderungen abdecken dürfte. Und falls diese nicht ausreichen sollten, können auch benutzerdefinierte Transformationen definiert werden.

Zum Weiterlesen

Eric Evans: Domain-Driven Design

ein nicht einfach zu lesendes Buch, aber die kanonische Quelle für Domain-Driven Design.

In Kapitel IV wird das Ermitteln der Domänengrenzen beschrieben.

Vaughn Vernons: Implementing Domain-Driven Design
konzentriert sich auf strategisches Design und erläutert in Kapitel 2 ausführlich, wie eine Domäne in begrenzte Kontexte untergliedert wird.
William Kent: Data and Reality
und auch wenn das Buch etwas in die Jahre gekommen ist, so ist es doch weiterhin relevant für das Datenmanagement.

Microservices – Definition

Microservices, wie wir sie verstehen, unterteilt eine einzelne Anwendung in eine Reihe von kleinen Diensten, die jeweils als eigenständiger Prozess laufen und leichtgewichtig miteinander kommunizieren, oft über eine HTTP-Resource-API.

Diese Dienste implementieren Geschäftsprozesse, die unabhängig und automatisiert einsetzbar sind. Die zentrale Verwaltung dieser Dienste wird auf ein Minimum reduziert und kann in unterschiedlichen Programmiersprachen geschrieben sein und unterschiedliche Speichertechnologien nutzen.

Abgrenzung gegen Monolithen

Die Idee der Microservices ist entstanden aus der Abgrenzung zu monolithischer Software, die verschiedene Geschäftsprozesse in einer Anwendung zusammenfasst. Genauer gesagt werden diese Monolithen üblicherweise in einer Schichtenarchitektur mit drei oder vier Schichten konzipiert:

  1. eine Benutzeroberfläche bestehend aus HTML-Seiten und Javascript, die in einem Browser dargestellt wird
  2. einer meist relationalen Datenbank bestehend aus vielen Tabellen
  3. und einer serverseitigen Anwendung die HTTP-Anfragen beantwortet, dazu fachdomänspezifische Logik ausführt und Daten aus der Datenbank aufruft und aktualisiert.

Bei einer solchen monolithischen Anwendung läuft die Logik für die Bearbeitung einer Anforderung meist in einem einzigen Prozess und für die grundlegenden Funktionen wird nur eine Programmiersprache verwendet. Die Anwendung wird dann durch Klassen, Funktionen und Namespaces aufgeteilt. Meist kann eine solche Anwendung auf dem Laptop des Entwicklers ausgeführt und getestet und eine Deployment-Pipeline verwendet werden, um sicherzustellen, dass Änderungen ordnungsgemäß getestet und in die Produktion überführt wurden. Sie können die Monolithen horizontal skalieren indem Sie viele Instanzen hinter einem Load Balancer laufen lassen.

Solche monolithischen Anwendungen wurden jahrelang von uns erfolgreich betrieben, aber sie fingen zunehmend an, uns zu frustrieren, und zwar bei den folgenden Szenarien:

  • Änderungszyklen müssen aufwändig koordiniert werden, d.h. selbst wenn nur ein kleiner Teil der Anwendung geändert werden soll, muss meist der gesamte Monolith neu gebaut und in Betrieb genommen werden.
  • Zudem wird es im Laufe der Zeit immer schwerer, eine gute modulare Struktur aufrechtzuerhalten, so dass bei Änderungen wirklich nur ein Modul betroffen ist.
  • Schließlich führt Skalierung immer dazu, dass die gesamte Anwendung repliziert werden muss, anstatt nur diejenigen Teile zu replizieren, die tatsächlich unter Last stehen und mehr Ressourcen erfordern.
Microservices

Eine monolithische Anwendung liefert alle Funktionalität in einem Prozess …

Skalierung von Microservices

… und skaliert durch Replikation des Monolithen auf mehreren Maschinen.

Um solche Frustrationen zu vermeiden, setzen wir seit vielen Jahren Microservices-Architekturen ein, die Anwendungen in einzelne Dienste unterteilt. Diese Services sind nicht nur unabhängig voneinander implementierbar und skalierbar, sondern bieten auch feste Kontextgrenzen, so dass unterschiedliche Dienste auch in der jeweils geeigneten Programmiersprache geschrieben und von verschiedenen Teams entwickelt und betreut werden können.

Monolith

Bei einer Microservices-Architektur wird jedes Funktionselement in einem separaten Service ausgeliefert …

Skalierung von Monolithen

… und skaliert durch die Verteilung dieser Dienste über Server, repliziert nach Bedarf.

Dabei erschienen uns Microservices gar nicht besonders neuartig oder innovativ – sie erinnerten uns vielmehr an eines der Designprinzipien von Unix:

Mache nur eine Sache und mache sie gut.

Dieses Designprinzip ist jedoch in monolithischen Anwendungen nicht berücksichtigt worden.

Microservices – Architektureigenschaften

Auch wenn Microservices nicht klar definiert sind, so weist ihre Architektur doch meist gemeinsame Merkmale auf.

Aufteilen der Anwendung in Servicekomponenten

Dabei haben für uns Komponenten die folgenden Eigenschaften:

  • unabhängig
  • austauschbar
  • erweiterbar

Die wesentliche Bedeutung von Microservices ist also die Aufteilung einer Anwendung in einzelne Dienste. Diese werden nicht mit anderen über In-memory-Function-Calls sprechen und unterscheiden sich damit deutlich von Serviceobjekten aus dem Domain-driven Design, die gemeinsam in nur einem Prozess ausgeführt werden.

Wesentlich für die Nutzung von Servicekomponenten und nicht als Bibliotheken ist, dass sie unabhängig voneinander eingesetzt werden können. Wenn eine Anwendung aus mehreren Bibliotheken in einem einzigen Prozess besteht, führt eine Änderung an einer einzelnen Komponente dazu, dass die gesamte Anwendung neu geordnet werden muss. Bei einer Zerlegung dieser Anwendung in mehrere Servicekomponenten jedoch sollte nur ein Service neu implementiert werden müssen. In seltenen Fällen könnte es jedoch auch hier vorkommen, dass Schnittstellen neu definiert werden müssten. Dies sollte durch die Architektur jedoch so weit wie möglich unterbunden werden.

Eine weitere Konsequenz der Verwendung von Serviceskomponenten ist eine explizite Komponentenschnittstelle. Dabei haben jedoch die meisten Sprachen leider keinen guten Mechanismus, um eine explizite Schnittstelle zu definieren. Oft ist es nur Dokumentation oder Disziplin, die verhindert, dass eine Komponentenschnittstelle geändert wird. Dies erhöht dann das Risiko einer unnötig engen Kopplung der Komponenten. Servicekomponenten vermeiden dies einfach durch explizite Remote-Call-Mechanismen.

Damit werden jedoch auch die Nachteile solcher Services offenbar:

  • Remote-Calls sind deutlich teurer als In-Process-Calls.
  • Auch sind die Remote-APIs allgemeiner gefasst und scheinen häufig schwerer bedienbar.
  • Und auch wenn die Zuordnung von Verantwortlichkeiten zwischen Komponenten geändert werden soll, so ist dies meist deutlich aufwändiger da auch Prozessgrenzen überschritten werden müssen.

Zwar werden Services häufig um einen Laufzeitprozess modularisiert, ein Service kann jedoch auch aus mehreren Prozessen bestehen, wie beispielsweise einem Prozess für die Anwendungslogik und einem für die Datenbank, die nur von diesem Service verwendet wird.

Organisation um Geschäftsprozesse

Wenn eine große Anwendung aufgeteilt werden soll, erfolgt diese häufig anhand der Technologie-Schichten, also z.B.

  • UI
  • Anwendungslogik
  • Datenbank

Wenn die einzelnen Teams jedoch genau diesen Schichten entsprechend zusammengesetzt werden, werden sie bei Änderungen meist versuchen, diese innerhalb ihres Zuständigkeitsbereichs umzusetzen. In der Folge wird sich in allen Schichten Logik wiederfinden. Dies ist nur ein Beispiel für Conways Gesetz.

Organisationen, die Systeme entwerfen, … sind auf Entwürfe festgelegt, welche die Kommunikationsstrukturen dieser Organisationen abbilden.

– Melvyn E. Conway, 1967

Funktional getrennte Teams führen zu funktional getrennter Architektur

Bei einer Microservices-Architektur wird die Anwendung nicht in unterschiedliche Schichten sondern in unterschiedliche Services unterteilt. Dabei reicht die Implementierung eines solchen Services von der persistenten Speicherung über Schnittstellen zu anderen Diensten bis hin zur Benutzeroberfläche. Infolgedessen sind die Teams funktionsübergreifend, einschließlich Expertise zur jeweiligen Datenbank, UI und Projektmanagement.

Funktionsübergreifende Teams führen zu funktionsübergreifenden Services

Produkte, nicht Projekte

Meist wird Software in einem Projekt entwickelt, bei dem die Software zu einem bestimmten Zeitpunkt ausgeliefert werden soll. Demnach wird mit der Fertigstellung die Software an ein Wartungsteam übergeben und das Projektteam aufgelöst.

Wir bevorzugen jedoch ein anderes Modell: ein Team nennt die Software über den gesamten Lebenszyklus hinweg sein eigen. Diese Verantwortung des Entwicklungsteams für die Software auch in der Produktion sehen wir auch in der DevOps-Kultur. Dadurch erhalten die Entwickler unseres Erachtens deutlich bessere Einblicke, wie sich ihre Software in der Produktion verhält und von den Anwendern angenommen wird. Durch diese Produkt-Mentatlität erhalten die Entwickler nicht nur besseren Einblick in die gewünschte Funktionalität sondern können zunehmend auch erkennen, wie der Geschäftsprozess optimiert werden könnte.

Wir betreuen unsere Anwendungen schon seit sehr langer Zeit über den gesamten Lebenszyklus hinweg, auch wenn wir sie früher häufig monolithisch, z.B. auf Basis des Web-Frameworks Zope gebaut haben, so erscheint und doch die feinere Granularität von Microservices förderlich für die Produktorientierung zu sein.

Intelligente Endpunkte und dumme Verbindungen

Bei der Erstellung von Kommunikationsstrukturen zwischen verschiedenen Diensten gibt es viele Produkte und Ansätze, die die Kommunikation deutlich erschweren können indem sie beachtliche Anforderungen an die beteiligten Komponenten stellen. Ein gutes Beispiel hierfür ist der Enterprise Service Bus (ESB), der oft anspruchsvolle Aufgaben übernehmen muss wie Message Routing, Choreographie etc.

Microservices hingegen fördern eher intelligente Endpunkte und dumme Verbindungen. Aus Microservices aufgebaute Anwendungen sind meist entkoppelt und so unzusammenhängend wie möglich. Sie besitzen ihre eigene Geschäftslogik und funktionieren wie Filter im Unix-Sinne: sie erhalten eine Anfrage, wenden ihre Logik darauf an und liefern eine Antwort zurück. Die Kommunikation erfolgt meist über einfache REST-Protokolle und nicht über komplexe Protokolle wie Service choreography, BPEL oder Orchestrierung durch zentrale Werkzeuge. Dabei sind die häufigsten Protokolle HTTP-Request-Response, ldap und Lightweight Messaging, z.B. mit RabbitMQ oder ZeroMQ. Sie übernehmen zuverlässig den asynchronen Nachrichtenaustausch ohne überhaupt nur eine Ahnung von der Geschäftslogik zu haben.

Bei einer monolithischen Anwendung sprechen die einzelnen Komponenten mit den anderen meist über einen Methoden- oder Funktionsaufruf. Und dies dürfte dann auch eines der größten Probleme sein um Änderungen bei einem solchen Monolithen vorzunehmen.

Dezentrale Organisation

Zentralisierte Organisationen neigen dazu, sich auf nur eine einzige Technologieplattform zu reduzieren. Dies kann jedoch dazu führen, dass gegebenenfalls auch ein unpassendes Werkzeug zur Lösung des Problems verwendet werden soll.

Wir wählen das passende Werkzeug für die jeweilige Aufgabe!

Wir versuchen schon lange nicht mehr alles in eine monolithischen Anwendung integrieren zu wollen. Stattdessen entwickeln wir nützliche Services, die häufig auch bei anderen Projekten wieder eingesetzt werden können.

Dezentralisierte Organisation

Wenn wir die Komponenten des Monolithen in Services aufteilen, haben wir die Wahl, wie wir sie bauen. Da verwenden wir dann z.B. ReactJS, um eine einfache Berichtseite zu erstellen und C++ für einen Real-Time-Service. Auch wählen wir die passende Datenbank für die jeweilige Datenstruktur.

Dezentrales Datenmanagement

Wenn wir über das Datenmanagement einer Anwendung nachdenken, machen wir dies häufig anhand der Kontextgrenzen (bounded context) aus dem Domain-driven Design: DDD teilt eine komplexe Fachdomäne in mehrere, möglichst klar umrissene Zusammenhänge auf und bildet daraus die Beziehungen zwischen ihnen ab. Üblicherweise lassen sich diese Kontextgrenzen sehr einfach auf Microservices abbilden wohingegen sie bei Komponenten von Monolithen sehr leicht verwischt werden können.

Neben der Teamzuordnung trennen die unterschiedlichen Kontexte auch das jeweils dahinterliegende Datenbankschemata. Microservices tendieren daher auch dazu, ihre eigenen Datenbanken zu verwalten.

Dezentralisierte Datenbanken

Die dezentrale Organisation der Teams rund um ihren Microservice führt auch zu einer verteilten Verantwortung für die Daten z.B. bei Updates. Während bei monolithischen Anwendungen meist auf Transaktionssicherheit geachtet wird, um die Konsistenz der Daten zu erhalten, so erfordert dies jedoch auch eine zeitliche Kopplung, die kaum über mehrere Services hinweg aufrechterhalten werden kann. Daher setzen die meisten Microservice-Architekturen auf eine transaktionslose Koordination zwischen den Diensten und auf eventual consistency, d.h. ein Datensatz wird irgendwann konsistent sein, sofern nur eine hinreichend lange Zeit ohne Schreibvorgänge und Fehler vorausgegangen ist.

Evolutionäres Design

Eine wesentliche Frustration bei Monolithen ist, dass sie mit der Zeit immer schwerer weiterzuentwickeln sind da eine modulare Struktur nur mühsam aufrechtzuerhalten ist und dadurch Änderungen aufwändiger und langwieriger sind. Daher erschien uns die Zerlegung in einzelne Services eine angenehme Möglichkeit zu bieten um eine Anwendung leichtgewichtig weiterentwickeln zu können.

Wir haben viele Anwendungen übernommen, die monolithisch entworfen und gebaut wurden. Meist dauert es mehrere Jahre bis wir einen solchen Monolithen vollständig ablösen können. Aber neue Features fügen wir meist als Microservices ein, die die API des Monolithen verwenden. Dieses Vorgehen ist nicht nur praktisch für nur vorübergehende Änderungen wie Kampagnen etc., sondern auch in anderen Bereichen, die sich häufig ändern. So wird die Funktionalität des ursprünglichen Monolithen immer geringer bis er dann schließlich ganz abgelöst werden kann.

Evolutionäres Design

Microservices – Nutzen und Kosten

Nutzen Kosten
Modularität
Microservices fördern die Modularität, was vor allem für große Teams hilfreich ist.
Verteilte Systeme
Verteilte Systeme sind schwieriger zu administrieren, da sie deutlich komplexer sind.
Unabhängiges Deployment
Einfache Services sind einfacher und mit geringerem Risiko bereitzustellen.
Eventual consistency
Die Aufrechterhaltung starker Konsistenzu ist in verteilten Systemen nur sehr schwierig aufrechtzuerhalten. Dies bedeutet, dass jeder Service eventuell consistency selbst managen muss.
Technologische Vielfalt
Ihr könnt die zu Eurem Service passenden Sprachen, Frameworks und Datenspeicher frei wählen.
Operationale Komplexität
Ihr benötigt qualifizierte System-Ingenieure, um die verschiedenen Dienste managen zu können.
Einfachere Skalierbarkeit
Ihr müsst nicht die gesamte Anwendung skalieren sondern nur den Teil, der zuviel Last erhält.
 
Bessere Trennungskontrolle
Mit Microservices können vertrauliche Daten getrennt werden und damit eine höhere Sicherheit erlangt werden.
 

Modularität

Microservices fördern üblicherweise die Modularität von Anwendungen, da ungenügende Modularität schnell zu sehr viel höheren Aufwänden und Risiken führt. Kleine, überschaubare Moduleinheiten lassen sich viel einfacher ändern da die Konsequenzen überschaubarer bleiben.

Ein weiterer Vorteil von Microservices sind das dezentrale Datenmanagement, bei dem jeder Service seine eigene Datenbank verwaltet und ggf. die API anderer Services durchläuft.

Verteilte Systeme

Microservices sind verteilte Systeme, wodurch die Komplexität deutlich erhöht wird.

Zunächst wirkt sich dies auf die Performance aus, da Remote-Calls deutlich langsamer sind. Wenn jeder Service jedoch viele andere Services befragen muss wird die Latenz schnell unerträglich. Dem kann zwar dadurch begegnet werden, dass die Anfragen asynchron und feingranularer gestellt werden sowie die Antworten ggf. zwischengespeichert werden.Dies verkompliziert jedoch den Programmcode erheblich.

Fehlertoleranz

Die Komplexität wird noch weiter erhöht, da Ihr Euch nicht auf eine gültige Antwort in einer bestimmten Zeit verlassen könnt. So solltet Ihr schon beim Design Eures Services darauf achten, dass dieser fehlertolerant ist. Wie das geschehen kann, ist z.B. in Principles of chaos engineering oder in Fault Tolerance in a High Volume, Distributed System. So entwickelte z.B. Netflix Chaos Monkey, der zufällig Prozesse von Instanzen und Containern beendet, um sowohl die Ausfallsicherheit als auch die Zuverlässigkeit der Gesamtanwendung zu testen. Wenn Services aber jederzeit ausfallen können, wird es umso wichtiger, den Fehler schnell zu erkennen und den Service möglichst automatisiert wieder bereitstellen zu können. Daher wird die Echtzeitüberwachung der betriebs- und geschäftsrelevanten Metriken immer wichtiger.

Dies sind nur einige der Probleme von verteilten Systemen; weitere findet Ihr in Fallacies of Distributed Computing Explained).

Unabhängiges Deployment

Ein Schlüsselprinzip von Microservices ist, dass Services Komponenten sind und daher unabhängig voneinander bereitgestellt werden können. Dies hat den Vorteil, dass Ihr bei Änderungen an einem Service nur diesen Testen und neu bereitstellen müsst. Zudem sollte selbst ein vollständiger Ausfall dieser einen Komponente nicht zum Ausfall anderer Teile des Systems führen sondern nur zu einem geringeren Funktionsumfang führen. Dieser Vorteil kommt jedoch erst richtig mit Continuous Delivery zum Tragen.

Eventual consistency

Ihr kennt sicher alle das Problem, dass Ihr ein Update durchgeführt habt und dennoch könnt Ihr die Änderungen nicht sofort sehen. Dies kann einfach damit zusammenhängen, dass gerade der eine Knoten aktualisiert wird während Eure Anfrage von einen noch nicht aktualiserten Knoten beantwortet wird. Solche Inkonsistenzen können auch zu schwerwiegenderen Problemen führen. Und auch wenn bei Monolithen leichter Transaktionssicherheit hergestellt werden kann, so sind sie keineswegs frei davon, z.B. wenn die Cache-Invalidation nicht zuverlässig ist.

Technologische Vielfalt

Da jeder Microservice eine unabhängige Einheit ist, habt Ihr auch erhebliche Freiheit bei der Auswahl Eurer Technologie. Microservices können in verschiedenen Sprachen geschrieben werden, verschiedene Bibliotheken und Datenspeicher verwenden. Auf diese Weise können Teams ein geeignetes Tool für den Job auswählen. Ihr könnt also profitieren davon, dass sich manche Sprachen und Bibliotheken besser für bestimmte Arten von Problemen eignen.

Dies ist jedoch nicht der einzige Vorteil der technischen Vielfalt von Microservices. Auch die Verwendung unterschiedlicher Versionen einer Bibliothek kann Upgrades deutlich vereinfachen. Bei einem Monolithen könnte ein Teil des Systems eine neue Funktion einer Sprache oder Bibliothek nutzen wollen, das Upgrade kann jedohch nicht verwendet werden, wenn dadurch ein anderer Teil des Monolithen kaputt geht.

Umgekehrt besteht jedoch die Gefahr, dass die technologische Vielfalt zu viel wird, sodass das Team überfordert werden kann. Die meisten mir bekannten Organisationen lassen nur eine begrenzte Anzahl von Technologien zu. Dies erleichtert dann z.B. auch die Überwachung der vielfältigen Tools da sich die Anzahl der Environments reduziert.

Operationale Komplexität

Die Möglichkeit, kleine unabhängige Module schnell bereitzustellen, ist ein großer Vortiel für die Entwicklung, stellt jedoch gleichzeitig eine zusätzliche Belastung für den Betrieb dar, da aus einem halben Dutzend Anwendungen jetzt Hunderte kleiner Mikrodienste werden können. Viele Organisationen werden die Schwierigkeit, mit einem solchen Schwarm sich schnell ändernder Tools umzugehen, als unmöglich empfinden. Tatsächlich wird dies auch nur mit Continuous Delivery möglich sein. Die Komplexität verlagert sich also von den einfacher werdenden Microservices zum gemeinsamen Betrieb dieser zunehmenden Anzahl dieser Services.

Einfachere Skalierbarkeit

Es liegt auf der Hand, dass Microservices besser den Service skalieren können, der unter erhöhter Last steht. Dies ist jedoch noch nicht alles: bei Microservices könnt Ihr auch schneller und präziser erkennen, welcher Teil unter Last steht.

Bessere Trennungskontrolle

Mit Microservices können Sie vertrauliche Daten trennen und diesen Daten mehr Sicherheit verleihen. Durch die kontrolliertere Steuerung des gesamten Datenverkehrs zwischen Microservices dürfte dies das Risiko, bei einem Einbruch umfassend Zugriff auf alle Daten zu erhalten, deutlich erschweren. Mit der zunehmenden Bedeutung von Sicherheitsproblemen ist dies eine wichtige Überlegung bei der Verwendung von Microservices.

Resümee

In diesem Artikel werden nur allgemein die Kosten und Nutzen von Microservices dargelegt. Er kann nicht die Entscheidung für ein konkretes Projekt vorwegnehmen, sondern nur dazu beitragen, dass Ihr die verschiedenen Faktoren berücksichtigt. Jeder Kosten- und Nutzenfaktor hat für verschiedene Systeme ein unterschiedliches Gewicht. Beurteilt, welche Faktoren für Euer System am wichtigsten sind und wie sie sich auf Euren speziellen Kontext auswirken. Erschwerend kommt hinzu, dass Architekturentscheidungen normalerweise erst sinnvoll beurteilt werden können, nachdem ein System ausgereift ist.

Microservices – Hype oder Zukunft?

Auch wenn wir bei vielen Anwendungen auf einer Microservices-Architektur aufgebaut haben, so sind wir doch nicht überzeugt, dass Microservices eine universelle Lösung sind.

So haben wir zwar durchaus schon Erfahrungen gemacht mit evolutionärem Design, bei dem eine einzelne Komponente ausgetauscht wurde, uns fehlt jedoch noch die Erfahrung beim Refactoring über Services hinweg: hier wird nicht nur das Verschieben von Code deutlich schwieriger als bei monolithischen Anwendungen. Auch die Koordinierung der Schnittstellenänderungen mit allen Teilnehmern, zusätzliche Schichten von Rückwärtskompatibilität und Testen verkomplizieren den Umbau.

Um die Servicegrenzen besser erkennen zu können scheint es zunächst eine gute Idee zu sein, nicht mit einer Microservices-Architektur zu beginnen sondern mit einer monolithischen Architektur, die modular aufgebaut ist (s.a. Martin Fowler: Monolith First). Wenn sich die passenden Komponenten im Lauf der Zeit bewährt haben, sollen sie dann in Microservices zerlegt haben. Diese Idee scheint uns jedoch problematisch da In-Process-Interfaces selten gut auf Service-Interfaces abbilden lassen.

Dennoch bleiben wir optimistisch, dass Microservices mit ihren Eigenschaften zu deutlich besserer Software führen, vor allem wenn es sich um komplexe Anwendungen handelt.

Produktivität und Komplexität

aus Martin Fowler: Microservice Premium