Ein verteiltes System (engl. Distributed System) ist eine Menge unabhängiger Computer, die sich ihren Anwendern als ein einziges zusammenhängendes System präsentieren [TvS16]. In einem verteilten Anwendungssystem werden also Teilprozesse der Anwendung auf unterschiedlichen Computern ausgeführt.

"A distributed system is a collection of independent computers that appears to its users as a single coherent system." (Andrew Tanenbaum)

Ein Computer kann dabei ganz unterschiedlich ausgeprägt sein, z.B. als handelsübliches Notebook oder Smartphone eines Anwenders, als Einplatinencomputer (z.B. Raspberry Pi, Arduino) eingebettet in ein Gerät mit speziellen Sensoren/Aktuatoren oder als dedizierter oder virtueller Server in einem Rechenzentrum. Es wird i.d.R. eine sogenannte Shared Nothing-Architektur angenommen – ein Begriff den Michael Stonebraker bereits 1986 prägte (s. The Case for Shared Nothing). Die Computer in einem verteilten System werden dabei vereinfacht Knoten genannt. Jeder Knoten verfügt über mind. einen Prozessor, über flüchtigen Arbeitsspeicher (RAM) und über nicht-flüchtigen Sekundärspeicher (HDD, Flash-Speicher). Die Prozessoren führen die Teilprozesse (engl. Threads) der verteilten Anwendung aus. Da sich die Knoten keinen gemeinsamen Speicher teilen, kommunizieren sie untereinander über Nachrichten. Die Koordination der Knoten erfolgt also auf Softwareebene und setzt eine Netzwerkverbindung der Knoten voraus. Die Netzwerkverbindung kann u.U. vorübergehend unterbrochen sein.

In der obigen Abbildung ist zu erkennen, dass alle am System beteiligten Computer mit unterschiedlichen Betriebssystemen (engl. Operating System, kurz OS) ausgestattet sein können. Die Komponenten einer verteilten Anwendung können direkt untereinander kommunizieren – wie für Application B in der obigen Abbildung angedeutet. In den folgenden Kapiteln werden wir aktuell geläufige Protokolle für diese Interprozesskommunikation, die die Grenzen eines Computers überwindet, vorstellen, z.B. RMI, SOAP und WebSockets. Alternativ können die Komponenten in einem verteilten System auch indirekt Nachrichten über eine vermittelnde Middleware austauschen. Neben verteilten Datenbanksystemen bieten sich hier insbesondere zuverlässige Message Broker wie Kafka oder RabbitMQ an, die auf einen hohen Durchsatz an vermittelten Nachrichten ausgelegt sind.

Eine Shared Nothing-Architektur lässt sich leicht skalieren, indem weitere Knoten dem System hinzugefügt werden oder wieder entfernt werden. Die maßgeblichen Motivationsfaktoren bei der Skalierung sind eine Verbesserung der Performance und eine Verbesserung der Datensicherheit durch Replikation.

Kommunikation im verteilten System

Es existiert eine Vielzahl von Protokollen zur Kommunikation in einem verteilten System, d.h. zum Austausch von Nachrichten/Daten zwischen Software-Komponenten, die in Prozessen auf verschiedenen Computern ausgeführt werden und über ein Netzwerk miteinander verbunden sind. Entsprechend den Anforderungen eines konkreten Anwendungsfalls ist ein geeignetes Protokoll als Grundlage für diese Interprozesskommunikation auszuwählen. Eine erste Orientierung bietet das TCP/IP-Modell, das die Kommunikationsprotokolle gemäß der folgenden Abbildung in 4 Schichten untergliedert.

Aus der Perspektive eines Anwendungsentwicklers möchten wir uns auf die Protokolle der unteren Schichten (Transport-, Internet-, Netzzugangsschicht im TCP/IP-Modell) verlassen und ein adäquates Protokoll auf Ebene der Anwendungsschicht nutzen. Wir verfolgen grundsätzlich das Ziel, dass eine lokale Client-Komponente eine Kommunikationsverbindung zu einer entfernten (engl. remote) Server-Komponente aufbaut und anschließend anwendungsspezifische Daten austauschen kann. In den nächsten Kapiteln werden dazu folgende alternative Ansätze zur sogenannten Remote-Kommunikation am Beispiel von Java-Anwendungen vorgestellt:

Eine wichtige Entwurfsentscheidung ist, ob die Kommunikation zwischen dem aufrufenden Client und dem Server nach dem Pull-Prinzip oder nach dem Push-Prinzip erfolgen soll. Der Unterschied zwischen beiden Kommunikationsprinzipien ist bereits im Kapitel zum Beobachter-Muster vorgestellt worden. Eine weitere Entwurfsentscheidung ist, ob die Remote-Kommunikation zwischen Client und Server synchron oder asynchron erfolgen soll. Es ist allgemein anzustreben, dass die Remote-Kommunikation (z.B. in Form von HTTP-Requests) nicht das User Interface eines Client blockiert. Insbesondere bei synchroner Remote-Kommunikation ist darauf zu achten, dass die Anfragen an einen Server nicht aus dem Thread der Anwendung abgesetzt werden, in dem auch die Anwenderinteraktion verarbeitet wird. Auf der Serverseite ist zu berücksichtigen, dass i.d.R. mit mehreren Clients parallel kommuniziert werden soll. Daher ist sowohl bei der Implementierung der Client-Komponente als auch bei der Implementierung der Server-Komponente zu empfehlen, die Remote-Kommunikation in nebenläufige Threads zu verlagern.

Performance

Die Performance eines Anwendungssystems beschreibt den Erfüllungsgrad von spezifizierten Zielen bezüglich der Metriken Antwortzeit und Durchsatz [SW01]. Beide Metriken lassen sich einfach beobachten. Die Antwortzeit (engl. Response Time) wird i.d.R. aus Sicht der Anwenders gemessen, der eine Anfrage an das System sendet, und kann wie in der folgenden Abbildung dargestellt in Netzwerkzeit, Wartezeit und Bedienzeit zerlegt werden. Jeder dieser Teilaspekte der Antwortzeit kann für sich optimiert werden.

"Performance is the degree to which a software system or component meets its objectives for timeliness. There are two important dimensions to software performance: responsiveness and scalability. Responsiveness is the ability of a system to meet its objectives for response time or throughput. Scalability is the ability of a system to continue to meet its response time or throughput objectives as the demand for the software functions increases." (Connie Smith, Lloyd Williams)

Es ergeben sich folgende Einflussfaktoren auf die Performance einer Komponente zur Laufzeit:

Skalierbarkeit drückt das Verhältnis zwischen der Steigerung der Problemgröße (z.B. mehr Workload) und den zur Lösung zusätzlich benötigten Ressourcen aus. Antwortzeiten und Durchsatz skalieren bei steigendem Workload und beschränkter Ressourcen-Kapazität bis zum Erreichen der Kapazitätsgrenze etwa linear. Anschließend steigen die Antwortzeiten exponentiell, da eine bestimmte Ressource (z.B. CPU, RAM, I/O) aufgrund von Überlastung zum Engpass wird. Dieser typische Effekt ist in der folgenden Abbildung dargestellt. Gleichermaßen erreicht auch der Durchsatz einen Sättigungspunkt, an dem die Komponente oder das System einfach nicht mehr Anfragen pro Zeitintervall verarbeiten kann. In automatisch skalierenden Cloud-Plattformen kann dieser Sättigungspunkt soweit hinausgezögert werden, das er praktisch nicht erreicht wird. Allgemein ist es aus Gründen der Wirtschaftlichkeit zu empfehlen, die bereitgestellten Hardware-Ressourcen nicht zu stark überzudimensionieren.

Um die Ressourcen-Kapazität eines Systems auszubauen, gibt es zwei grundlegende Ansätze: die vertikale Skalierung (Scale Up) und die horizontale Skalierung (Scale Out). Bei der vertikalen Skalierung wird ein leistungsstarker Großrechner angestrebt, der sukzessiv ausgebaut wird. Dieser Ansatz gilt als vergleichsweise teuer. Außerdem stellt der Großrechner einen möglichen Single Point of Failure dar. Daher wird heute horizontale Skalierung durch ein verteiltes System von vielen gleichartigen Knoten favorisiert. In einem derartigen Cluster wird der Ausfall von wenigen Knoten als Normalzustand angenommen, der die Verfügbarkeit und die Konsistenz der Daten nicht gefährden darf. Zwar ist ein Cluster komplexer zu beherrschen als ein einzelner Großrechner, kann dafür aber skalierbare Performance und Datensicherheit gleichermaßen gewährleisten.

Datensicherheit durch Replikation

Nehmen wir an, dass wie oben dargestellt ein Cluster von gleichartigen Knoten aufgebaut worden ist, um auch bei hohem Workload die geforderte Performance des Systems zu gewährleisten. Eine Komponente wird innerhalb des Clusters nun mehrfach instantiiert, damit der Dienst, den sie anbietet, ebenso skaliert. Jede Instanz dieser Komponente wird nur einen Ausschnitt der gesamten Datenbasis verarbeiten und auf dem Knoten, auf dem sie ausgeführt wird, speichern. Die Daten werden in einem verteilten Datei- oder Datenbanksystem in sogenannte Partitionen (oder auch Shards) zerlegt. Einzelne Datenobjekte/Datensätze werden i.d.R. über eine Hashfunktion einer Partition zugeordnet. Die folgende Abbildung zeigt ein Cluster mit 4 Knoten, auf denen jeweils eine Instanz der gleichen Komponente ausgeführt wird. Die Datenbasis wurde in 6 Partitionen (A-F) unterteilt. Da das System keine Daten verlieren darf, wenn ein Knoten ausfällt, werden die Partitionen repliziert. Im dargestellten Beispiel ist der Replikationsfaktor 3, d.h. von jeder Partition gibt 3 identische Replikate, so dass das abgebildete System den Ausfall beliebiger 2 Knoten tolerieren kann. Die Anzahl der Knoten und insbesondere der Partitionen ist in der Praxis häufig deutlich größer. Eine größere Anzahl an Partitionen als Knoten vereinfacht die Umverteilung der Partitionen, wenn ein neuer Knoten in den Cluster aufgenommen wird oder ein Knoten ausscheidet.

Ein Kompromiss zwischen Verfügbarkeit und Konsistenz der Daten ergibt sich dadurch, dass die Replikate nach einem Schreibzugriff synchronisiert werden müssen. Es stellt sich in einem verteilten System die Frage, ob während der Synchronisation bedingt durch einen Schreibzugriff entweder ein veralteter Zustand gelesen werden darf (d.h. Verfügbarkeit nicht eingeschränkt, aber Konsistenz eingeschränkt) oder das Lesen bis zum Ende der Synchronisation verzögert wird (d.h. Konsistenz nicht eingeschränkt, aber Verfügbarkeit eingeschränkt). In einem verteilten System ist grundsätzlich mit dem Ausfall einzelner Knoten oder der vorübergehenden Unterbrechung der Netzwerkverbindungen zu rechnen, was zu Verzögerungen in der Synchronisation führt. Der Begriff Partitionstoleranz beschreibt, dass das System bis zu einem gewissen Grad tolerant gegenüber einem kurzzeitigen Verfall des Systems in unverbundene Teile sein muss. Der Kompromiss zwischen der Konsistenz (engl. Consistency), der Verfügbarkeit (engl. Availability) und der Partitionstoleranz (engl. Partition Tolerance) in einem verteilten System ist als CAP-Theorem bekannt. Verteilte Datenbanken werden in diesem Model nicht weiter behandelt. Einen fundierten Überblick liefert das populäre Buch "Designing Data-Intensive Applications" von Martin Kleppmann [Kle17].