Datenintensive Anwendungen designen - Martin Kleppmann - E-Book

Datenintensive Anwendungen designen E-Book

Martin Kleppmann

0,0

Beschreibung

Daten stehen heute im Mittelpunkt vieler Herausforderungen im Systemdesign. Dabei sind komplexe Fragen wie Skalierbarkeit, Konsistenz, Zuverlässigkeit, Effizienz und Wartbarkeit zu klären. Darüber hinaus verfügen wir über eine überwältigende Vielfalt an Tools, einschließlich relationaler Datenbanken, NoSQL-Datenspeicher, Stream-und Batchprocessing und Message Broker. Aber was verbirgt sich hinter diesen Schlagworten? Und was ist die richtige Wahl für Ihre Anwendung? In diesem praktischen und umfassenden Leitfaden unterstützt Sie der Autor Martin Kleppmann bei der Navigation durch dieses schwierige Terrain, indem er die Vor-und Nachteile verschiedener Technologien zur Verarbeitung und Speicherung von Daten aufzeigt. Software verändert sich ständig, die Grundprinzipien bleiben aber gleich. Mit diesem Buch lernen Softwareentwickler und -architekten, wie sie die Konzepte in der Praxis umsetzen und wie sie Daten in modernen Anwendungen optimal nutzen können. - Inspizieren Sie die Systeme, die Sie bereits verwenden, und erfahren Sie, wie Sie sie effektiver nutzen können - Treffen Sie fundierte Entscheidungen, indem Sie die Stärken und Schwächen verschiedener Tools kennenlernen - Steuern Sie die notwenigen Kompromisse in Bezug auf Konsistenz, Skalierbarkeit, Fehlertoleranz und Komplexität - Machen Sie sich vertraut mit dem Stand der Forschung zu verteilten Systemen, auf denen moderne Datenbanken aufbauen - Werfen Sie einen Blick hinter die Kulissen der wichtigsten Onlinedienste und lernen Sie von deren Architekturen

Sie lesen das E-Book in den Legimi-Apps auf:

Android
iOS
von Legimi
zertifizierten E-Readern

Seitenzahl: 1086

Das E-Book (TTS) können Sie hören im Abo „Legimi Premium” in Legimi-Apps auf:

Android
iOS
Bewertungen
0,0
0
0
0
0
0
Mehr Informationen
Mehr Informationen
Legimi prüft nicht, ob Rezensionen von Nutzern stammen, die den betreffenden Titel tatsächlich gekauft oder gelesen/gehört haben. Wir entfernen aber gefälschte Rezensionen.



Zu diesem Buch – sowie zu vielen weiteren O’Reilly-Büchern – können Sie auch das entsprechende E-Book im PDF-Format herunterladen. Werden Sie dazu einfach Mitglied bei oreilly.plus+:

www.oreilly.plus

Datenintensive Anwendungendesignen

Konzepte für zuverlässige,skalierbare und wartbare Systeme

Martin Kleppmann

Übersetzung aus dem Englischenvon Frank Langenau

Martin Kleppmann

Lektorat: Alexandra Follenius

Übersetzung: Frank Langenau

Korrektorat: Claudia Lötschert, www.richtiger-text.de

Satz: III-satz, www.drei-satz.de

Herstellung: Stefanie Weidner

Umschlaggestaltung: Karen Montgomery, Michael Oréal, www.oreal.de

Bibliografische Information Der Deutschen NationalbibliothekDie Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.

ISBN:

Print    978-3-96009-075-5

PDF     978-3-96010-183-3

ePub    978-3-96010-184-0

mobi   978-3-96010-185-7

Dieses Buch erscheint in Kooperation mit O’Reilly Media, Inc. unter dem Imprint »O’REILLY«. O’REILLY ist ein Markenzeichen und eine eingetragene Marke von O’Reilly Media, Inc. und wird mit Einwilligung des Eigentümers verwendet.

1. Auflage 2019

Copyright © 2019 dpunkt.verlag GmbH

Wieblinger Weg 17

69123 Heidelberg

Authorized German translation of the English edition of Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems, ISBN 978-1-449-37332-0 © 2017 Martin Kleppmann. This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish and sell the same.

Die vorliegende Publikation ist urheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und daher strafbar. Dies gilt insbesondere für die Vervielfältigung, Übersetzung oder die Verwendung in elektronischen Systemen.

Es wird darauf hingewiesen, dass die im Buch verwendeten Soft- und Hardware-Bezeichnungen sowie Markennamen und Produktbezeichnungen der jeweiligen Firmen im Allgemeinen warenzeichen-, marken- oder patentrechtlichem Schutz unterliegen.

Die Informationen in diesem Buch wurden mit größter Sorgfalt erarbeitet. Dennoch können Fehler nicht vollständig ausgeschlossen werden. Verlag, Autoren und Übersetzer übernehmen keine juristische Verantwortung oder irgendeine Haftung für eventuell verbliebene Fehler und deren Folgen.

5 4 3 2 1 0

Technologie übt in unserer Gesellschaft eine große Macht aus. Daten, Software und Kommunikation können missbraucht werden: um ungerechte Machtstrukturen tiefer zu verankern, Menschenrechte auszuhöhlen und Eigeninteressen zu schützen. Aber sie können auch für gute Zwecke genutzt werden: um Minderheiten Gehör zu verschaffen, Chancen für alle zu eröffnen und Katastrophen abzuwenden. Dieses Buch ist jedem gewidmet, der sich für das Gute einsetzt.

Inhalt

Einleitung

Teil I: Grundlagen von Datensystemen

1Zuverlässige, skalierbare und wartbare Anwendungen

Gedanken zu Datensystemen

Zuverlässigkeit

Hardwarefehler

Softwarefehler

Menschliche Fehler

Wie wichtig ist Zuverlässigkeit?

Skalierbarkeit

Lasten beschreiben

Performance beschreiben

Konzepte zur Bewältigung von Belastungen

Wartbarkeit

Betriebsfähigkeit: Den Betrieb erleichtern

Einfachheit: Komplexität im Griff

Evolvierbarkeit: Änderungen erleichtern

Zusammenfassung

2Datenmodelle und Abfragesprachen

Relationales Modell vs. Dokumentmodell

Die Geburt von NoSQL

Die objektrelationale Unverträglichkeit

n:1- und n:n-Beziehungen

Wiederholen Dokumentdatenbanken die Geschichte?

Heutige relationale Datenbanken vs. Dokumentdatenbanken

Abfragesprachen für Daten

Deklarative Abfragen im Web

MapReduce-Abfragen

Graphen-ähnliche Datenmodelle

Property-Graphen

Die Abfragesprache Cypher

Graph-Abfragen in SQL

Triple-Stores und SPARQL

Das Fundament: Datalog

Zusammenfassung

3Speichern und Abrufen

Datenstrukturen, auf denen Ihre Datenbank beruht

Hash-Indizes

SSTables und LSM-Bäume

B-Bäume

B-Bäume und LSM-Bäume im Vergleich

Andere Indizierungsstrukturen

Transaktionsverarbeitung oder Datenanalyse?

Data-Warehousing

Sterne und Schneeflocken: Schemas für die Analytik

Spaltenorientierte Speicherung

Spaltenkomprimierung

Sortierreihenfolge in spaltenorientierten Datenbanken

In spaltenorientierte Datenbanken schreiben

Aggregation: Datenwürfel und materialisierte Sichten

Zusammenfassung

4Codierung und Evolution

Formate für das Codieren von Daten

Sprachspezifische Formate

JSON, XML und binäre Varianten

Thrift und Protocol Buffers

Avro

Die Vorzüge von Schemas

Datenflussmodi

Datenfluss über Datenbanken

Datenfluss über Dienste: REST und RPC

Datenfluss beim Nachrichtenaustausch

Zusammenfassung

Teil II: Verteilte Daten

5Replikation

Leader und Follower

Synchrone und asynchrone Replikation

Neue Follower einrichten

Knotenausfälle behandeln

Implementierung von Replikationsprotokollen

Probleme mit der Replikationsverzögerung

Die eigenen Schreiboperationen lesen

Monotones Lesen

Präfixkonsistenz

Lösungen für Replikationsverzögerung

Multi-Leader-Replikation

Einsatzfälle für Multi-Leader-Replikation

Schreibkonflikte behandeln

Topologien für Multi-Leader-Replikation

Replikation ohne Leader

In die Datenbank schreiben, wenn ein Knoten ausgefallen ist

Grenzen der Quorumkonsistenz

Sloppy Quoren und Hinted Handoff

Parallele Schreibvorgänge erkennen

Zusammenfassung

6Partitionierung

Partitionierung und Replikation

Partitionierung von Schlüssel-Wert-Daten

Partitionierung nach Schlüsselbereich

Nach dem Hashwert des Schlüssels partitionieren

Schiefe Arbeitslasten und Entlastung von Hotspots

Partitionierung und Sekundärindizes

Sekundärindizes nach Dokument partitionieren

Sekundärindizes nach Begriff partitionieren

Rebalancing – Partitionen gleichmäßig belasten

Strategien für Rebalancing

Operationen: Automatisches oder manuelles Rebalancing

Anfragen weiterleiten

Parallele Abfrageausführung

Zusammenfassung

7Transaktionen

Das schwammige Konzept einer Transaktion

Die Bedeutung von ACID

Einzelobjekt- und Multiobjektoperationen

Schwache Isolationsstufen

Read Committed

Snapshot-Isolation und Repeatable Read

Verlorene Updates verhindern

Schreibversatz und Phantome

Serialisierbarkeit

Tatsächliche serielle Ausführung

Zwei-Phasen-Sperrverfahren (2PL)

Serialisierbare Snapshot-Isolation (SSI)

Zusammenfassung

8Die Probleme mit verteilten Systemen

Fehler und Teilausfälle

Cloud-Computing und Supercomputing

Unzuverlässige Netzwerke

Netzwerkfehler in der Praxis

Fehler erkennen

Timeouts und unbeschränkte Verzögerungen

Synchrone und asynchrone Netzwerke

Unzuverlässige Uhren

Monotone Uhren und Echtzeituhren

Uhrensynchronisierung und Genauigkeit

Sich auf synchronisierte Uhren verlassen

Prozesspausen

Wissen, Wahrheit und Lügen

Die Wahrheit wird von der Mehrheit definiert

Byzantinische Fehler

Systemmodell und Realität

Zusammenfassung

9Konsistenz und Konsens

Konsistenzgarantien

Linearisierbarkeit

Was macht ein System linearisierbar?

Auf Linearisierbarkeit setzen

Linearisierbare Systeme implementieren

Die Kosten der Linearisierbarkeit

Ordnungsgarantien

Ordnung und Kausalität

Ordnung nach Sequenznummern

Total geordneter Broadcast

Verteilte Transaktionen und Konsens

Atomarer Commit und Zwei-Phasen-Commit (2PC)

Verteilte Transaktionen in der Praxis

Fehlertoleranter Konsens

Mitgliedschafts- und Koordinationsdienste

Zusammenfassung

Teil III: Abgeleitete Daten

10Stapelverarbeitung

Stapelverarbeitung mit Unix-Tools

Einfache Protokollanalyse

Die Unix-Philosophie

MapReduce und verteilte Dateisysteme

MapReduce-Jobausführung

Reduce-seitige Verknüpfungen und Gruppierungen

Map-seitige Verknüpfungen

Die Ausgabe von Stapel-Workflows

Hadoop im Vergleich mit verteilten Datenbanken

Jenseits von MapReduce

Zwischenzustände materialisieren

Graphen und iterative Verarbeitung

Höhere APIs und Sprachen

Zusammenfassung

11Stream-Verarbeitung

Ereignisströme übertragen

Nachrichtensysteme

Partitionierte Protokolle

Datenbanken und Streams

Systeme synchron halten

Erfassen von Datenänderungen

Event Sourcing

Zustand, Streams und Unveränderlichkeit

Streams verarbeiten

Anwendungen der Stream-Verarbeitung

Überlegungen zur Zeit

Stream-Joins

Fehlertoleranz

Zusammenfassung

12Die Zukunft von Datensystemen

Datenintegration

Spezialisierte Tools durch Ableiten von Daten kombinieren

Batch- und Stream-Verarbeitung

Die Entflechtung von Datenbanken

Zusammenstellung verschiedener Datenspeichertechniken

Anwendungen datenflussorientiert entwickeln

Abgeleitete Zustände beobachten

Auf der Suche nach Korrektheit

Das Ende-zu-Ende-Argument für Datenbanken

Durchsetzung von Einschränkungen

Zeitnähe und Integrität

Vertrauen ist gut, Kontrolle ist besser

Das Richtige tun

Prädiktive Analytik

Datenschutz und Nachverfolgung

Zusammenfassung

13Glossar

Index

Einleitung

Computing ist Popkultur. […] Die Popkultur verachtet die Geschichte. In der Popkultur dreht sich alles um Identität und das Gefühl mitzumachen. Sie hat nichts mit Zusammenarbeit, Vergangenheit oder Zukunft zu tun – es geht um das Leben in der Gegenwart. Ich denke, das Gleiche gilt für die meisten Leute, die Code für Geld schreiben. Sie haben keine Ahnung, woher [ihre Kultur kommt].

– Alan Kay, im Interview mit Dr Dobb’s Journal (2012)

Wenn Sie in den letzten Jahren in der Softwareentwicklung gearbeitet haben und hier vor allem mit Server- und Backendsystemen, dann sind Sie wahrscheinlich mit einer Fülle von Schlagwörtern rund um die Speicherung und Verarbeitung von Daten bombardiert worden. NoSQL! Big Data! Skalierbarkeit! Sharding! Konsistenz! ACID! CAP-Theorem! Cloud-Dienste! MapReduce! Echtzeit!

Das letzte Jahrzehnt hat viele interessante Entwicklungen hervorgebracht, und zwar bei Datenbanken, in verteilten Systemen und in der Art und Weise, wie wir Anwendungen darauf aufbauen. Für diese Entwicklungen gibt es vielfältige Triebkräfte:

Internetfirmen wie Google, Microsoft, Amazon, Facebook, LinkedIn, Netflix und Twitter verarbeiten riesige Mengen an Daten und Datenverkehr, was sie dazu zwingt, neue Tools zu entwickeln, die in dieser Größenordnung effizient arbeiten.

Unternehmen müssen agil sein, Hypothesen kostengünstig testen und schnell auf neue Markterkenntnisse reagieren, indem sie die Entwicklungszyklen kurz und die Datenmodelle flexibel halten.

Freie und Open-Source-Software ist sehr erfolgreich geworden und wird heute in vielen Umgebungen kommerzieller oder intern entwickelter Software vorgezogen.

Während sich die Taktfrequenz der Prozessoren kaum mehr erhöht, sind Multi-Core-Prozessoren zum Standard avanciert, und Netzwerke werden immer schneller. Die Entwicklung strebt also in Richtung Parallelverarbeitung.

Dank Infrastructure as a Service (IaaS) wie zum Beispiel Amazon Web Services können Sie selbst in einem kleinen Team Systeme aufbauen, die über mehrere Computer und sogar über mehrere geografische Regionen hinweg verteilt sind.

Bei vielen Diensten erwartet man heutzutage, dass sie hochverfügbar sind. Längere Ausfallzeiten aufgrund von Störungen oder Wartungsarbeiten sind kaum noch tragbar.

Datenintensive Anwendungen erweitern die Grenzen des Machbaren, indem sie diese technologischen Entwicklungen nutzen. Eine Anwendung bezeichnen wir als datenintensiv, wenn sie vorrangig Probleme in Bezug auf Daten – die Menge der Daten, ihre Komplexität oder die Geschwindigkeit, mit der sie sich verändern – zu lösen hat, im Unterschied zu rechenintensiv, wenn die CPU-Takte einen Engpass darstellen.

Die Werkzeuge und Techniken, mit denen datenintensive Anwendungen Daten speichern und verarbeiten, haben sich diesen Änderungen schnell angepasst. Neue Arten von Datenbanksystemen (»NoSQL«) haben viel Aufmerksamkeit erregt, doch auch Nachrichtenwarteschlangen, Caches, Suchindizes, Frameworks für Batch- und Stream-Verarbeitung sowie verwandte Techniken sind auch sehr wichtig. Viele Anwendungen kombinieren diese Techniken.

Die diesbezüglichen Schlagwörter kennzeichnen die Begeisterung für die neuen Möglichkeiten, was prinzipiell zu begrüßen ist. Allerdings brauchen wir als Softwareentwickler und -architekten ein technisch genaues und präzises Verständnis für die verschiedenen Techniken und deren Abwägungen, wenn wir gute Anwendungen erstellen wollen. Für dieses Verständnis dürfen wir uns nicht mit Schlagwörtern begnügen, sondern müssen tiefer eintauchen.

Glücklicherweise stehen hinter diesen rasanten technischen Veränderungen dauerhafte Prinzipien, die ihre Gültigkeit behalten, unabhängig davon, welche Version eines bestimmten Tools Sie verwenden. Wenn Sie diese Prinzipien verstehen, können Sie sehen, wozu jedes Werkzeug passt, wie Sie es zweckmäßig einsetzen und wie Sie seine Fallstricke vermeiden. Hier kommt dieses Buch ins Spiel.

Es soll Ihnen helfen, sich in der vielfältigen und sich schnell verändernden Landschaft der Techniken zum Verarbeiten und Speichern von Daten zurechtzufinden. Dabei ist das Buch weder ein Tutorial für ein bestimmtes Tool noch ein Lehrbuch voller trockener Theorie. Vielmehr sehen wir uns Beispiele erfolgreicher Datensysteme an: Techniken, die die Grundlage vieler bekannter Anwendungen bilden und die täglich den Anforderungen an Skalierbarkeit, Leistung und Zuverlässigkeit in der Produktion gerecht werden müssen.

Wir dringen in die Interna dieser Systeme ein, nehmen ihre Schlüsselalgorithmen auseinander und diskutieren ihre Prinzipien und Abwägungen, die sie treffen müssen. Dabei streben wir nach zweckmäßigen Methoden, um über Datensysteme nachzudenken – nicht nur darüber, wie sie funktionieren, sondern auch, warum sie auf diese Weise funktionieren und welche Fragen wir stellen müssen.

Nachdem Sie dieses Buch gelesen haben, befinden Sie sich in einer komfortablen Position, um zu entscheiden, welche Technik für welchen Zweck geeignet ist, und um zu verstehen, wie sich die Tools kombinieren lassen, um die Grundlage für eine gute Anwendungsarchitektur zu bilden. Zwar werden Sie Ihre eigene Datenbank noch nicht von Grund auf neu erstellen können, doch zum Glück ist das auch kaum notwendig. Allerdings werden Sie ein gutes Gespür dafür entwickeln, was Ihre Systeme im Verborgenen tun, sodass Sie über ihr Verhalten nachdenken, gute Entwurfsentscheidungen treffen und eventuelle Probleme aufspüren können.

Wer sollte dieses Buch lesen?

Wenn Sie Anwendungen entwickeln, die Daten auf einer Art Server oder Backend speichern oder verarbeiten, und Ihre Anwendungen das Internet nutzen (zum Beispiel Webanwendungen, mobile Apps oder Sensoren mit Internetanschluss), dann ist dieses Buch genau richtig für Sie.

Die Zielgruppe dieses Buchs sind Softwareentwickler, Softwarearchitekten und technische Führungskräfte, die gerne programmieren. Es ist vor allem relevant, wenn Sie Entscheidungen über die Architektur der Systeme treffen müssen, an denen Sie arbeiten – wenn Sie zum Beispiel Tools für ein bestimmtes Problem auswählen und herausfinden müssen, wie Sie sie am besten anwenden. Doch selbst wenn Sie keinen Einfluss auf die Auswahl Ihrer Tools haben, hilft Ihnen das Buch, deren Stärken und Schwächen besser zu verstehen.

Sie sollten einige Erfahrungen im Erstellen von webbasierten Anwendungen oder Netzwerkdiensten mitbringen und mit relationalen Datenbanken und SQL vertraut sein. Wenn Sie nichtrelationale Datenbanken und andere datenbezogene Tools kennen, ist das prinzipiell von Vorteil, aber nicht unbedingt erforderlich. Hilfreich ist ein allgemeines Verständnis der gängigen Netzwerkprotokolle wie zum Beispiel TCP und HTTP. Welche Programmiersprache oder welches Framework Sie wählen, ist für dieses Buch nicht entscheidend.

Wenn einer der folgenden Punkte auf Sie zutrifft, wird Ihnen dieses Buch nützlich sein:

Sie möchten lernen, wie sich Datensysteme skalierbar machen lassen, um beispielsweise Webanwendungen oder mobile Apps mit Millionen von Benutzern zu unterstützen.

Sie müssen Anwendungen hochverfügbar machen (Ausfallzeiten minimieren) und betriebssicher gestalten.

Sie suchen nach Möglichkeiten, Systeme langfristig wartungsfreundlicher zu machen, selbst wenn sie wachsen und sich die Anforderungen und Techniken verändern.

Sie sind von Haus aus neugierig, wie die Dinge funktionieren, und wollen wissen, was bei den großen Websites und Onlinediensten hinter den Kulissen vor sich geht. Dieses Buch nimmt die Interna der verschiedenen Datenbanken und Datenverarbeitungssysteme auseinander, und es macht großen Spaß, die cleveren Überlegungen zu erkunden, die in ihr Design eingeflossen sind.

Wenn es um skalierbare Datensysteme geht, hört man manchmal Meinungen wie »Du bist nicht Google oder Amazon. Mach dir keine Gedanken um die Skalierbarkeit und nimm einfach eine relationale Datenbank.« In dieser Aussage liegt etwas Wahres: Ein System zu erstellen für eine Skalierung, die Sie noch nicht brauchen, ist unnützer Aufwand und kann Sie in ein unflexibles Design zwängen. Letztlich handelt es sich dabei um eine Form von vorschneller Optimierung. Allerdings ist es auch wichtig, das richtige Tool für den Job auszuwählen, und verschiedene Techniken zeichnen sich jeweils durch ihre eigenen Stärken und Schwächen aus. Wie wir sehen werden, sind relationale Datenbanken zwar wichtig, aber kein Allheilmittel für den Umgang mit Daten.

Das Themenspektrum dieses Buchs

Dieses Buch liefert keine detaillierten Anleitungen, wie Sie bestimmte Softwarepakete oder APIs installieren oder verwenden. Hierfür gibt es genügend Dokumentationen. Stattdessen erörtern wir die verschiedenen Prinzipien und Abwägungen, die für Datensysteme maßgeblich sind, und wir analysieren die verschiedenen Entwurfsentscheidungen, die für verschiedene Produkte typisch sind.

In den E-Book-Ausgaben sind Links zum vollständigen Text von Onlinequellen enthalten. Zwar wurden alle Links zeitnah zur Veröffentlichung überprüft, doch leider liegt es in der Natur des Webs, dass Links häufig ungültig werden. Falls Sie einen defekten Link finden oder die gedruckte Ausgabe dieses Buchs lesen, können Sie mit einer Suchmaschine nach den Quellen suchen. Bei akademischen Arbeiten können Sie nach dem Titel in Google Scholar nach frei zugänglichen PDFDateien suchen. Alternativ finden Sie alle Quellen unter https://github.com/ept/ddia-references, wo wir aktuelle Links pflegen.

Wir betrachten vor allem die Architektur von Datensystemen und ihre Integration in datenintensive Anwendungen. Der Platz im Buch reicht nicht aus, um Bereitstellung, Betrieb, Sicherheit, Verwaltung und andere Bereiche zu behandeln – solchen komplexen und wichtigen Themen könnten wir nicht gerecht werden, wenn wir sie zu oberflächlichen Randbemerkungen in diesem Buch machen. Sie verdienen eigene Bücher.

Viele der im Buch beschriebenen Techniken lassen sich dem Schlagwort Big Data zuordnen. Allerdings ist der Begriff »Big Data« so überstrapaziert und ziemlich schwammig definiert, dass er für eine ernsthafte technische Diskussion nicht viel taugt. Dieses Buch verwendet weniger zweideutige Begriffe, wie zum Beispiel Einzelknotensysteme vs. verteilte Systeme oder online/interaktiv vs. offline/Batch-Verarbeitungssysteme.

Dieses Buch betont Freie Software und Open-Source-Software, denn wenn man Quellcode selbst lesen, ändern und ausführen kann, versteht man viel besser, wie etwas im Detail funktioniert. Bei offenen Plattformen ist auch die Gefahr einer Anbieterabhängigkeit geringer. Wo es passend erscheint, diskutieren wir aber auch proprietäre Software (Closed-Source-Software, Software as a Service oder firmeninterne Software, die lediglich in der Literatur beschrieben, aber nicht als Code veröffentlicht wird).

Gliederung dieses Buchs

Dieses Buch ist in drei Teile gegliedert:

1. In

Teil I

beschäftigen wir uns mit den prinzipiellen Konzepten, die dem Design von datenintensiven Anwendungen zugrunde liegen. Zuerst erläutert

Kapitel 1

, was wir eigentlich anstreben: Zuverlässigkeit, Skalierbarkeit und Wartbarkeit, welche Überlegungen wir anstellen müssen und wie wir die Ziele erreichen können. In

Kapitel 2

vergleichen wir verschiedene Datenmodelle und Abfragesprachen und zeigen, wie sie sich für unterschiedliche Situationen eignen. In

Kapitel 3

geht es um Speichermodule: wie Datenbanken die Daten auf der Festplatte anordnen, damit wir sie effizient wiederfinden können.

Kapitel 4

erläutert die Formate für die Datencodierung (Serialisierung) und die Entwicklung von Schemas im Laufe der Zeit.

2. In

Teil II

gehen wir von den Daten, die auf einem Computer gespeichert sind, zu Daten über, die über mehrere Computer verteilt sind. Dies ist oftmals für Skalierbarkeit erforderlich, bringt aber auch eine Vielzahl von speziellen Problemen mit sich. Zuerst befassen wir uns mit Replikation (

Kapitel 5

), Partitionierung und Sharding (

Kapitel 6

) und Transaktionen (

Kapitel 7

). Dann gehen wir näher auf die Probleme mit verteilten Systemen ein (

Kapitel 8

) und zeigen, was es bedeutet, Konsistenz und Konsens in einem verteilten System zu erreichen (

Kapitel 9

).

3. In

Teil III

geht es um Systeme, die bestimmte Datenmengen aus anderen Datenmengen ableiten. Abgeleitete Daten sind häufig in heterogenen Systemen zu finden: Wenn eine Datenbank nicht alle Aufgaben gleichermaßen gut übernehmen kann, müssen Anwendungen mehrere verschiedene Datenbanken, Caches, Indizes usw. integrieren. Zu Beginn zeigt

Kapitel 10

das Konzept einer Stapelverarbeitung für abgeleitete Daten, und darauf aufbauend erläutert

Kapitel 11

eine Stream-Verarbeitung. Schließlich bringen wir in

Kapitel 12

alles zusammen und diskutieren Ansätze, um in Zukunft zuverlässige, skalierbare und wartbar Anwendungen zu erstellen.

Quellen und weiterführende Literatur

Das meiste von dem, was wir in diesem Buch besprechen, ist bereits an anderer Stelle in der einen oder anderen Form gesagt worden – auf Konferenzen, in Forschungsarbeiten, Blog-Posts, Code, Bugtracker-Berichten, Mailinglisten und dem mündlich überlieferten technischen Mythos. Dieses Buch fasst die wichtigsten Ideen aus vielen verschiedenen Quellen zusammen und gibt im gesamten Text Verweise auf die Originalliteratur an. Die Verweise am Ende jedes Kapitels bieten sich an, wenn Sie in einem Themenbereich weiter recherchieren wollen. Zudem sind die meisten davon online frei verfügbar.

Vorwort des Autors zur deutschen Übersetzung

Nachdem die englische Originalfassung dieses Buchs viele begeisterte Leser gefunden hat, war ich sehr erfreut von den Plänen zu erfahren, Designing Data-Intensive Applications in mehrere andere Sprachen zu übersetzen. Zu den meisten Übersetzungen, zum Beispiel auf Koreanisch, Russisch und Polnisch, habe ich allerdings keinen Beitrag leisten können, weil ich in Sprachen leider nicht so bewandert bin.

Die deutsche Übersetzung ist jedoch ein Ausnahmefall: Zwar wohne ich inzwischen seit 15 Jahren in Großbritannien, aber da ich in Deutschland aufgewachsen bin, ist mein Deutsch immerhin gut genug, um beim Korrekturlesen der deutschen Übersetzung zu helfen. Ich war von Frank Langenaus Übersetzung von Anfang an beeindruckt: Er hat den beabsichtigten Sinn präzise erfasst und zugleich gute, idiomatische Ausdrucksweisen gefunden – viel besser, als ich es selbst geschafft hätte.

Das vorliegende Buch habe ich komplett überprüft und bei der Übersetzung begleitet; dabei haben wir auch den Inhalt auf den neuesten Stand gebracht. Wir sind bei der Übersetzung auf einige Details gestoßen, die im englischen Original unklar formuliert waren: Diese haben wir nicht nur in der deutschen Fassung korrigiert, sondern auch als Verbesserungen in die nächste Aktualisierung der englischen Ausgabe übernommen. Somit ist die Übersetzung dem Buch doppelt zugutegekommen!

Mein großer Dank gilt also Frank Langenau für seine hervorragende Übersetzung des Buchs. Ebenfalls danken möchte ich Alexandra Follenius und dem Team bei O’Reilly Deutschland.

Danksagungen der englischen Originalfassung

Dieses Buch verschmilzt und systematisiert viele Ideen und Erkenntnisse anderer Menschen, wobei es Erfahrungen sowohl aus der akademischen Forschung als auch der industriellen Praxis zusammenbringt. In der Datenverarbeitung fühlen wir uns angezogen von neuen und modischen Dingen, doch meiner Ansicht nach können wir noch jede Menge lernen aus den Dingen, die in der Vergangenheit getan wurden. Im Buch finden Sie über 800 Verweise auf Artikel, Blogposts, Vorträge, Dokumentationen und mehr, die auch für mich wertvolle Lernunterlagen waren. Ich bin den Autoren dieses Materials sehr dankbar, dass sie ihr Wissen teilen.

Auch aus persönlichen Gesprächen habe ich viel mitgenommen, dank einer großen Anzahl von Menschen, die sich die Zeit genommen haben, Ideen zu diskutieren oder mir Sachverhalte geduldig zu erklären. Insbesondere möchte ich mich bedanken bei Joe Adler, Ross Anderson, Peter Bailis, Márton Balassi, Alastair Beresford, Mark Callaghan, Mat Clayton, Patrick Collison, Sean Cribbs, Shirshanka Das, Niklas Ekström, Stephan Ewen, Alan Fekete, Gyula Fóra, Camille Fournier, Andres Freund, John Garbutt, Seth Gilbert, Tom Haggett, Pat Helland, Joe Hellerstein, Jakob Homan, Heidi Howard, John Hugg, Julian Hyde, Conrad Irwin, Evan Jones, Flavio Junqueira, Jessica Kerr, Kyle Kingsbury, Jay Kreps, Carl Lerche, Nicolas Liochon, Steve Loughran, Lee Mallabone, Nathan Marz, Caitie McCaffrey, Josie McLellan, Christopher Meiklejohn, Ian Meyers, Neha Narkhede, Neha Narula, Cathy O’Neil, Onora O’Neill, Ludovic Orban, Zoran Perkov, Julia Powles, Chris Riccomini, Henry Robinson, David Rosenthal, Jennifer Rullmann, Matthew Sackman, Martin Scholl, Amit Sela, Gwen Shapira, Greg Spurrier, Sam Stokes, Ben Stopford, Tom Stuart, Diana Vasile, Rahul Vohra, Pete Warden und Brett Wooldridge.

Weitere Personen haben beim Schreiben dieses Buchs wertvolle Hilfe geleistet, indem sie Entwürfe begutachtet und Feedback gegeben haben. Für diese Beiträge danke ich Raul Agepati, Tyler Akidau, Mattias Andersson, Sasha Baranov, Veena Basavaraj, David Beyer, Jim Brikman, Paul Carey, Raul Castro Fernandez, Joseph Chow, Derek Elkins, Sam Elliott, Alexander Gallego, Mark Grover, Stu Halloway, Heidi Howard, Nicola Kleppmann, Stefan Kruppa, Bjorn Madsen, Sander Mak, Stefan Podkowinski, Phil Potter, Hamid Ramazani, Sam Stokes, and Ben Summers. Selbstverständlich übernehme ich die gesamte Verantwortung für alle verbliebenen Fehler oder nicht akzeptable Meinungen in diesem Buch.

Für die Unterstützung, dass dieses Buch Wirklichkeit werden konnte, und für ihre Geduld mit meinem langsamen Schreiben und den ausgefallenen Anfragen danke ich meinen Redakteuren Marie Beaugureau, Mike Loukides, Ann Spencer und dem ganzen Team bei O’Reilly. Bedanken möchte ich mich auch bei Rachel Head für Ihre Hilfe, die richtigen Worte zu finden. Dafür, dass sie mir die Zeit und Freiheit gegeben haben, trotz anderweitiger beruflicher Verpflichtungen zu schreiben, danke ich Alastair Beresford, Susan Goodhue, Neha Narkhede und Kevin Scott. Ein ganz besonderer Dank geht an Shabbir Diwan und Edie Freedman, die mit großer Sorgfalt die Karten zu den Kapiteln illustriert haben. Es ist wunderbar, dass sie die unkonventionelle Idee aufgegriffen haben, Landkarten zu erstellen, und dass sie sie so schön und ansprechend gestaltet haben.

Schließlich gilt meine Liebe meiner Familie und meinen Freunden, ohne die ich das Schreiben, das fast vier Jahre gedauert hat, nicht hätte durchstehen können. Ihr seid die Besten.

TEIL I

Grundlagen von Datensystemen

Die ersten vier Kapitel beschäftigen sich mit den fundamentalen Ideen, die auf alle Datensysteme anwendbar sind, egal, ob sie auf einem einzelnen Computer laufen oder über einen Cluster von Computern verteilt sind:

1.

Kapitel 1

führt die Terminologie und die Konzepte ein, die wir das gesamte Buch hindurch verwenden werden. Es untersucht, was wir mit Begriffen wir

Zuverlässigkeit, Skalierbarkeit

und

Wartbarkeit

tatsächlich meinen und wie sich diese Ziele erreichen lassen.

2.

Kapitel 2

vergleicht verschiedene Datenmodelle und Abfragesprachen – der offensichtlichste Unterscheidungsfaktor zwischen Datenbanken aus dem Blickwinkel des Entwicklers. Hier erfahren Sie, wie verschiedene Modelle für verschiedene Situationen geeignet sind.

3.

Kapitel 3

wendet sich den Interna der Speichermodule zu und zeigt, wie Datenbanken die Daten auf dem Datenträger anordnen. Verschiedene Speichermodule sind für unterschiedliche Arbeitsbelastungen optimiert, und die Auswahl des richtigen Moduls kann sich drastisch auf die Performance auswirken.

4.

Kapitel 4

vergleicht Formate für die Codierung (Serialisierung) von Daten und untersucht insbesondere, wie sie sich in einer Umgebung verhalten, in der sich die Anforderungen an die Anwendungen ändern und Schemas im Laufe der Zeit angepasst werden müssen.

Später wendet sich Teil II den speziellen Fragen verteilter Datensysteme zu.

KAPITEL 1

Zuverlässige, skalierbare undwartbare Anwendungen

Das Internet wurde so gut gemacht, dass die meisten Menschen es als eine natürliche Ressource wie den Pazifischen Ozean betrachten, und nicht als etwas, das vom Menschen geschaffen wurde. Wann war das letzte Mal eine Technologie in einer solchen Größenordnung so fehlerfrei?

– Alan Kay im Interview mit Dr Dobb’s Journal (2012)

Viele Anwendungen sind heutzutage datenintensiv im Gegensatz zu rechenintensiv. Die CPU-Leistung an sich ist für diese Anwendungen kaum ein begrenzender Faktor – größere Probleme ergeben sich üblicherweise aus dem Umfang der Daten, ihrer Komplexität und der Geschwindigkeit, mit der sie sich verändern.

Eine datenintensive Anwendung besteht normalerweise aus Standardbausteinen, die häufig benötigte Funktionalität bereitstellen. Zum Beispiel müssen viele Anwendungen

Daten speichern, damit sie oder andere Anwendungen die Daten später wiederfinden können (

Datenbanken

),

das Ergebnis einer aufwendigen Operation zwischenspeichern, um Lesevorgänge zu beschleunigen (

Caches

),

Benutzern ermöglichen, Daten nach Schlüsselwörtern zu durchsuchen oder nach verschiedenen anderen Methoden zu filtern (

Suchindizes

),

eine Nachricht an einen anderen Prozess senden, um eine asynchrone Verarbeitung zu veranlassen (

Streamverarbeitung

) und

regelmäßig eine große Menge akkumulierter Daten verarbeiten (

Stapelverarbeitung

).

Sollte das zu offensichtlich klingen, dann nur, weil diese Datensysteme eine so erfolgreiche Abstraktion sind: Wir verwenden sie die ganze Zeit, ohne groß darüber nachzudenken. Wenn ein Entwickler eine Anwendung erstellt, wird er kaum davon träumen, ein neues Speichermodul von Grund auf neu zu schreiben, denn für diese Aufgabe sind Datenbanken prädestiniert.

Die Realität sieht aber nicht so einfach aus. Es existieren viele Datenbanksysteme mit unterschiedlichen Eigenschaften, weil verschiedene Anwendungen unterschiedliche Anforderungen stellen. Für das Zwischenspeichern gibt es verschiedene Methoden, das Gleiche gilt für das Erstellen von Indizes usw. Wenn wir eine Anwendung erstellen, müssen wir immer noch herausfinden, welche Werkzeuge und welche Ansätze für die konkrete Aufgabe am besten geeignet sind. Und falls ein einzelnes Werkzeug diese Aufgabe nicht allein bewältigen kann, ist es mitunter schwierig, passende Tools zu kombinieren.

Dieses Buch führt Sie sowohl durch die Prinzipien als auch die praktischen Aspekte von Datensystemen und zeigt, wie Sie damit datenintensive Anwendungen erstellen können. Wir untersuchen, was verschiedene Werkzeuge gemeinsam haben, was sie unterscheidet und wie sie zu ihren Eigenschaften kommen.

In diesem Kapitel untersuchen wir zunächst die Grundlagen für das, was wir erreichen wollen: zuverlässige, skalierbare und wartbare Datensysteme. Wir machen deutlich, was diese Dinge bedeuten, umreißen Methoden, sie zu analysieren, und wenden uns den Basics zu, die für die späteren Kapitel erforderlich sind. In den folgenden Kapiteln fahren wir mit den einzelnen Ebenen nacheinander fort und sehen uns dabei die verschiedenen Entwurfsentscheidungen an, die beim Arbeiten an einer datenintensiven Anwendung betrachtet werden müssen.

Gedanken zu Datensystemen

In der Regel stellen wir uns Datenbanken, Warteschlangen, Caches usw. als vollkommen verschiedene Kategorien von Werkzeugen vor. Obwohl eine Datenbank und eine Nachrichtenwarteschlange einige oberflächliche Berührungspunkte aufweisen – beide speichern Daten für einen gewissen Zeitraum –, unterscheiden sie sich in ihren Zugriffsmustern. Das bedeutet verschiedene Leistungscharakteristika und somit sehr verschiedene Implementierungen.

Warum sollten wir sie alle unter einem Sammelbegriff wie Datensysteme zusammenfassen?

1In den letzten Jahren sind zahlreiche neue Tools für das Speichern und Verarbeiten von Daten entstanden. Optimiert für eine breite Vielfalt von Einsatzfällen lassen sie sich nicht mehr streng den herkömmlichen Kategorien zuordnen [1]. So gibt es zum Beispiel Datenspeicher, die auch als Nachrichtenwarteschlangen dienen (Redis), und Nachrichtenwarteschlangen mit datenbankähnlichen Beständigkeitsgarantien (Apache Kafka). Die Grenzen zwischen den Kategorien verschwimmen immer weiter.

Zweitens stellen heute immer mehr Anwendungen so anspruchsvolle oder breit gefächerte Anforderungen, dass ein einzelnes Tool nicht mehr sämtliche Ansprüche an die Verarbeitung und Speicherung der Daten realisieren kann. Stattdessen teilt man die Arbeit in Aufgaben auf, die ein einzelnes Tool effizient durchführen kann, und der Anwendungscode verknüpft diese verschiedenen Tools.

Haben Sie zum Beispiel eine von der Anwendung verwaltete Caching-Ebene (etwa mit einem Cache-Server wie Memcached) oder einen Server für die Volltextsuche (wie zum Beispiel Elasticsearch oder Solr) eingerichtet, die von Ihrer Hauptdatenbank getrennt sind, ist normalerweise der Code der Anwendung dafür zuständig, diese Caches und Indizes mit der Hauptdatenbank synchron zu halten. Abbildung 1-1 veranschaulicht dieses Prinzip (mehr Einzelheiten folgen in späteren Kapiteln).

Abbildung 1-1: Eine mögliche Architektur für ein Datensystem, das mehrere Komponenten kombiniert

Wenn Sie mehrere Tools kombinieren, um einen Dienst zu realisieren, verbirgt die Oberfläche des Diensts oder die API1 normalerweise diese Implementierungsdetails vor den Clients. Praktisch haben Sie nun ein neues spezialisiertes Datensystem aus kleineren, universellen Komponenten erzeugt. Das zusammengesetzte Datensystem kann bestimmte Garantien bieten: beispielsweise, dass der Cache korrekt ungültig gemacht oder bei Schreibvorgängen aktualisiert wird, sodass externe Clients konsistente Ergebnisse sehen. Jetzt sind Sie nicht nur Anwendungsentwickler, sondern auch Datensystemdesigner.

Beim Entwurf eines Datensystems oder eines Diensts tauchen viele knifflige Fragen auf. Wie stellen Sie sicher, dass die Daten korrekt und vollständig bleiben, selbst wenn intern etwas schiefläuft? Wie bieten Sie den Clients eine konstant gute Performance, selbst wenn Teile Ihres Systems ausfallen? Wie skalieren Sie, um einer wachsenden Belastung gerecht zu werden? Wie sieht eine gute API für den Dienst aus?

Viele Faktoren können das Design eines Datensystems beeinflussen, unter anderem die Fertigkeiten und Erfahrungen der beteiligten Entwickler, Abhängigkeiten von einem Legacysystem, die Lieferzeit, die Toleranz Ihres Unternehmens gegenüber verschiedenen Arten von Risiken, regulatorische Beschränkungen usw. Derartige Faktoren hängen stark von der jeweiligen Situation ab.

In diesem Buch konzentrieren wir uns auf drei Aspekte, die in den meisten Softwaresystemen wichtig sind:

Zuverlässigkeit

Das System sollte auch bei Widrigkeiten (Hardware- oder Softwarefehlern und sogar menschlichem Versagen) weiterhin korrekt arbeiten (die richtige Funktion auf dem gewünschten Leistungsniveau ausführen). Siehe Abschnitt »Zuverlässigkeit« unten.

Skalierbarkeit

Wenn das System wächst (in Bezug auf Datenvolumen, Verkehrsaufkommen oder Komplexität), sollte es vernünftige Maßnahmen geben, um mit diesem Wachstum umzugehen. Siehe Abschnitt »Skalierbarkeit« auf Seite 11.

Wartbarkeit

Im Laufe der Zeit arbeiten verschiedene Leute am System (Techniker und Betreiber, die sowohl das aktuelle Verhalten sicherstellen als auch das System an neue Einsatzfälle anpassen), und sie alle sollten daran produktiv arbeiten können. Siehe Abschnitt »Wartbarkeit« auf Seite 20.

Diese Begriffe werden oftmals in den Raum geworfen, ohne überhaupt ihre Bedeutung genau verstanden zu haben. Im Sinne einer nachvollziehbaren Herangehensweise werden wir im Rest dieses Kapitels Möglichkeiten untersuchen, um Überlegungen zu Zuverlässigkeit, Skalierbarkeit und Wartbarkeit anzustellen. In den darauffolgenden Kapiteln sehen wir uns dann die verwendeten Techniken, Architekturen und Algorithmen an, mit denen sich diese Ziele erreichen lassen.

Zuverlässigkeit

Wohl jeder hat eine intuitive Vorstellung davon, was zuverlässig oder unzuverlässig bedeutet. Zu den typischen Erwartungen an Software gehören:

Die Anwendung führt die Funktion aus, die der Benutzer erwartet.

Sie kann tolerieren, dass der Benutzer Fehler macht bzw. die Software auf unerwartete Art und Weise benutzt.

Ihre Leistung ist gut genug für den vorgesehenen Einsatzfall, unter der erwarteten Arbeitslast und für das anfallende Datenvolumen.

Das System verhindert jeden nicht autorisierten Zugriff und jeden Missbrauch.

Wenn all dies zusammengenommen »korrekt arbeiten« bedeutet, dann können wir Zuverlässigkeit ganz grob verstehen als »weiterhin korrekt arbeiten, auch wenn etwas schiefläuft«.

Die Dinge, die schiefgehen, bezeichnet man als Fehler, und Systeme, die Fehler einkalkulieren und bewältigen können, heißen fehlertolerant oder robust. Der erste Begriff ist etwas irreführend, suggeriert er doch, dass wir ein System gegenüber jeder Art von Fehlern tolerant machen könnten, was in der Praxis aber nicht realisierbar ist. Wenn man damit rechnet, dass ein schwarzes Loch den gesamten Planeten Erde (und alle Server auf ihm) verschluckt, müsste Webhosting im Weltraum stattfinden, um Fehlertoleranz für dieses Ereignis zu bieten – viel Erfolg dabei, diesen Haushaltsposten genehmigt zu bekommen. Es ist demnach nur sinnvoll, von der Toleranz gegenüber bestimmten Fehlerarten zu sprechen.

Beachten Sie, dass ein Fehler nicht dasselbe ist wie ein Ausfall [2]. Entsprechend der üblichen Definition ist ein Fehler eine Komponente des Systems, die von ihrer Spezifikation abweicht, während bei einem Ausfall das System als Ganzes aufhört, dem Benutzer den gewünschten Dienst bereitzustellen. Da es nicht möglich ist, die Wahrscheinlichkeit eines Fehlers auf null zu verringern, ist es normalerweise am besten, Fehlertoleranzmechanismen zu entwickeln, die verhindern, dass Fehler zu Ausfällen führen. In diesem Buch behandeln wir verschiedene Techniken, um zuverlässige Systeme aus unzuverlässigen Bestandteilen aufzubauen.

Auch wenn es der Intuition widerspricht, kann es in derartigen fehlertoleranten Systemen sinnvoll sein, die Fehlerrate zu erhöhen, indem man Fehler bewusst auslöst – zum Beispiel durch zufälliges Beenden einzelner Prozesse ohne Warnung. Viele kritische Bugs gehen auf eine schlechte Fehlerbehandlung zurück [3]. Indem Sie Fehler bewusst herbeiführen, stellen Sie sicher, dass der Fehlertoleranzmechanismus laufend beansprucht und getestet wird. Das kann Ihr Vertrauen stärken, dass Fehler ordnungsgemäß behandelt werden, wenn sie im regulären Betrieb auftreten. Ein Beispiel für dieses Konzept ist das Tool Chaos Monkey [4] von Netflix.

Obwohl wir im Allgemeinen Fehler lieber tolerieren als verhindern, gibt es Fälle, in denen Vorbeugen besser ist als Heilen (zum Beispiel, weil es keine Heilung gibt). Das ist unter anderem bei Sicherheitsfragen der Fall: Wenn ein Angreifer ein System gehackt und Zugriff auf vertrauliche Daten erlangt hat, lässt sich ein solches Ereignis nicht mehr rückgängig machen. Allerdings beschäftigt sich dieses Buch vorrangig mit solchen Fehlern, die sich beheben lassen, wie die folgenden Abschnitte beschreiben.

Hardwarefehler

Denkt man an die Ursachen für Systemausfälle, kommen einem schnell Hardwarefehler in den Sinn: ein Festplattencrash tritt auf, RAM-Zellen verlieren Speicherinhalte, das Stromnetz bricht kurzzeitig zusammen, ein falsches Netzwerkkabel wurde eingesteckt. Jeder, der schon einmal mit großen Datencentern gearbeitet hat, wird bestätigen, dass diese Dinge ständig passieren, wenn nur genügend Computer vorhanden sind.

Bei Festplatten beträgt die mittlere Betriebszeit bis zum Ausfall (Mean Time To Failure, MTTF) etwa 10 bis 50 Jahre [5, 6]. Folglich ist in einem Speichercluster mit 10.000 Festplatten im Durchschnitt ein Festplattenausfall pro Tag zu erwarten.

Unsere erste Reaktion darauf ist üblicherweise, die einzelnen Hardwarekomponenten mit mehr Redundanz auszustatten, um die Ausfallrate des Systems zu verringern. Festplatten lassen sich in einer RAID-Konfiguration betreiben, Server sind mit doppelten Stromversorgungen und Hot-Swap-fähigen CPUs ausgerüstet, und in Rechenzentren sichern Batterien und Dieselgeneratoren die Notstromversorgung ab.

Wenn eine Komponente kaputtgeht, kann die redundante Komponente ihren Platz einnehmen, während die defekte Komponente ersetzt wird. Zwar lässt sich mit diesem Konzept nicht komplett verhindern, dass Hardwareprobleme zu Ausfällen führen, doch es ist praktikabel und sorgt oftmals dafür, dass ein Computer jahrelang ununterbrochen läuft.

Bis vor Kurzem genügten redundante Hardwarekomponenten für die meisten Anwendungen, da dank dieser Maßnahme einzelne Computer nur ziemlich selten komplett ausfallen. Sofern Sie recht schnell eine Sicherung auf einem neuen Computer wiederherstellen können, ist die Ausfallzeit bei den meisten Anwendungen nicht dramatisch. Somit ist eine Redundanz mit mehreren Computern nur für eine kleine Anzahl von Anwendungen erforderlich, bei denen Hochverfügbarkeit an vorderster Stelle steht.

Wegen größerer Datenmengen und gestiegener rechentechnischer Anforderungen geht man jedoch bei immer mehr Anwendungen dazu über, eine größere Anzahl von Computern zu nutzen, wodurch die Rate der Hardwarefehler proportional zunimmt. Darüber hinaus kommt es bei manchen Cloudplattformen wie zum Beispiel Amazon Web Services (AWS) durchaus vor, dass Instanzen virtueller Computer ohne Vorwarnung unverfügbar werden [7], weil die Plattformen dafür konzipiert sind, Flexibilität und Elastizität2 über die Zuverlässigkeit einzelner Computer zu priorisieren.

Folglich gibt es eine Verschiebung hin zu Systemen, die den Verlust ganzer Computer tolerieren können, indem sie softwareseitige Fehlertoleranztechniken bevorzugen oder ergänzend zur Hardwareredundanz einsetzen. Solche Systeme bieten auch operative Vorteile: Bei einem Einzelserversystem müssen Sie die Stillstandszeiten planen, falls Sie den Computer neu starten müssen (um zum Beispiel Sicherheitspatches des Betriebssystems zu installieren), während sich ein System, das den Ausfall eines Computers tolerieren kann, knotenweise mit Patches versorgen lässt, ohne dass das gesamte System stillsteht (rollendes Upgrade; siehe Kapitel 4).

Softwarefehler

Normalerweise geht man davon aus, dass Hardwarefehler zufällig und unabhängig voneinander auftreten: Fällt bei einem Computer die Festplatte aus, heißt das nicht, dass die Festplatte in einem anderen Computer ebenfalls kaputtgeht. Es kann zwar schwache Korrelationen geben (etwa bei zu hohen Temperaturen im Servergestell), doch ansonsten ist es unwahrscheinlich, dass eine große Anzahl von Hardwarekomponenten gleichzeitig ausfällt.

Zu einer anderen Fehlerklasse gehören systematische Fehler innerhalb des Systems [8]. Derartige Fehler sind schwerer vorherzusehen, und weil sie über Knoten korreliert sind, verursachen sie mehr Systemausfälle als nicht korrelierte Hardwarefehler [5]. Beispiele dafür sind:

Ein Softwarebug bewirkt, dass jede Instanz eines Anwendungsservers abstürzt, wenn eine bestimmte Fehleingabe erfolgt. Denken Sie nur an die Schaltsekunde am 30. Juni 2012, die aufgrund eines Fehlers im Linux-Kernel [

9

] dazu führte, dass viele Anwendungen gleichzeitig hängenblieben.

Ein unkontrollierbarer Prozess erschöpft eine gemeinsam genutzte Ressource – CPU-Zeit, Arbeitsspeicher, Festplattenplatz oder Netzwerkbandbreite.

Ein Dienst, von dem das System abhängig ist, wird langsamer, reagiert nicht mehr oder gibt beschädigte Antworten zurück.

Kaskadenartiges Ausbreiten von Fehlern, wobei ein kleiner Fehler in der einen Komponente einen Fehler in einer anderen Komponente auslöst, die ihrerseits weitere Fehler auslöst [

10

].

Die Bugs, die für derartige Softwarefehler verantwortlich sind, schlummern oftmals eine ganze Zeit lang, bis sie durch das Zusammentreffen ungewöhnlicher Umstände zutage treten. In diesen Fällen zeigt sich, dass die Software bestimmte Annahmen über ihre Umgebung trifft – und normalerweise sind diese Annahmen auch richtig, doch schließlich treffen sie aus irgendeinem Grund nicht mehr zu [11].

Für das Problem systematischer Softwarefehler gibt es keine schnelle Lösung. Viele kleine Dinge können helfen: gründliches Nachdenken über Annahmen und Wechselwirkungen im System, umfangreiche Tests, Prozessisolierung; Zulassen, dass Prozesse abstürzen und neu starten; Messen, Überwachen und Analysieren des Systemverhaltens in der Produktion. Wenn man von einem System eine gewisse Garantie erwartet (dass zum Beispiel in einer Nachrichtenwarteschlange die Anzahl der eintreffenden Nachrichten gleich der Anzahl der ausgehenden Nachrichten ist), kann es sich im Betrieb ständig selbst überprüfen und einen Alarm auslösen, wenn es eine Abweichung feststellt [12].

Menschliche Fehler

Menschen entwerfen und erstellen Softwaresysteme, und die Betreiber, die die Systeme am Laufen halten, sind ebenfalls Menschen. Selbst wenn sie die besten Absichten haben, sind Menschen bekanntlich unzuverlässig. So geht aus einer Studie über große Internetdienste hervor, dass Konfigurationsfehler von Betreibern die Hauptursache für Ausfälle waren, während Hardwarefehler (Server oder Netzwerk) nur in 10 bis 25% der Ausfälle eine Rolle gespielt haben [13].

Wie machen wir nun unsere Systeme trotz unzuverlässiger Menschen zuverlässig? Die besten Systeme kombinieren mehrere Ansätze:

Systeme so entwerfen, dass Fehlermöglichkeiten minimiert werden. Beispielsweise erleichtern es gut konzipierte Abstraktionen, APIs und Administrationsoberflächen, »das Richtige« zu tun und »das Falsche« zu unterbinden. Wenn jedoch die Schnittstellen zu restriktiv sind, umgeht der Programmierer sie und negiert damit ihren Nutzen. Dadurch ist es schwierig, das richtige Gleichgewicht zu finden.

Die Stellen, an denen Programmierer die meisten Fehler machen, von den Stellen entkoppeln, wo sie Ausfälle hervorrufen können. Stellen Sie insbesondere voll ausgestattete

Sandbox

-Testumgebungen bereit, die Programmierer erkunden und in ihnen mit echten Daten gefahrlos experimentieren können, ohne dass wirkliche Benutzer davon betroffen sind.

Gründlich auf allen Ebenen testen, angefangen bei Komponententests (Unit Tests) bis hin zu Integrationstests mit dem gesamten System und manuellen Tests [

3

]. Automatisiertes Testen ist weit verbreitet, hinreichend bekannt und vor allem wertvoll, um die Grenzfälle abzudecken, die im normalen Betrieb selten auftreten.

Schnelle und einfache Wiederherstellung bei Fehlern, die auf den Menschen zurückgehen, ermöglichen, die Auswirkungen im Fehlerfall zu minimieren. Zum Beispiel: Konfigurationsänderungen schnell zurücksetzen, neuen Code schrittweise einführen (sodass unerwartete Bugs nur eine kleine Untergruppe von Benutzern betreffen) und Tools bereitstellen, mit denen sich Daten neu berechnen lassen (falls sich herausstellt, dass die alten Berechnungen nicht korrekt waren).

Detaillierte und klare Überwachung einrichten, wie zum Beispiel Leistungskennziffern und Fehlerquoten. Andere technische Bereiche sprechen hier von

Telemetrie

. (Nachdem eine Rakete den Boden verlassen hat, sind die Telemetriedaten unerlässlich, um das Geschehen verfolgen und Fehlerereignisse deuten zu können [

14

].) Die Überwachung liefert uns frühzeitige Warnsignale und erlaubt uns zu überprüfen, ob Annahmen oder Einschränkungen verletzt werden. Tritt ein Problem auf, sind Messwerte unabdingbar für die Diagnose der Ursache.

Gute Verwaltungspraktiken und Schulungen umsetzen – ein komplexer und wichtiger Aspekt, der aber über den Rahmen dieses Buchs hinausginge.

Wie wichtig ist Zuverlässigkeit?

Zuverlässigkeit hat nicht nur etwas mit Software für Kernkraftwerke oder Flugsicherung zu tun, sondern auch für eher alltägliche Anwendungen. Bugs in Geschäftsanwendungen bedeuten Produktivitätseinbußen (und rechtliche Risiken, wenn Zahlen falsch gemeldet werden), Ausfälle von E-Commerce-Sites verursachen riesige Kosten in Form von Umsatzverlusten und Rufschädigungen.

Selbst in »unkritischen« Anwendungen tragen wir eine Verantwortung gegenüber unseren Benutzern. Denken Sie beispielsweise an Eltern, die sämtliche Bilder und Videos ihrer Kinder in Ihrer Fotoanwendung speichern [15]. Wie würden sie sich fühlen, wenn diese Datenbank plötzlich beschädigt wäre? Wüssten sie, wie sie sie aus einer Datensicherung wiederherstellen könnten?

In manchen Situationen kann es besser sein, auf Zuverlässigkeit zu verzichten, um Entwicklungskosten zu verringern (zum Beispiel bei der Entwicklung eines Prototyps für einen unsicheren Markt) oder die Betriebskosten (zum Beispiel für einen Dienst mit einer sehr geringen Gewinnspanne) – wir sollten uns aber genau darüber im Klaren sein, wenn wir Abstriche machen.

Skalierbarkeit

Selbst wenn ein System heute zuverlässig arbeitet, heißt das nicht, dass es zwangsläufig auch zukünftig zuverlässig arbeiten wird. Eine Verschlechterung ist häufig auf eine erhöhte Belastung zurückzuführen: Vielleicht ist das System von 10.000 gleichzeitigen Benutzern auf 100.000 gleichzeitige Benutzer gewachsen oder von 1 Million auf 10 Millionen. Vielleicht verarbeitet es wesentlich umfangreichere Datenmengen als zuvor.

Mit Skalierbarkeit beschreiben wir die Fähigkeit eines Systems, steigende Belastungen verkraften zu können. Allerdings ist das kein eindimensionales Etikett, das wir einem System anheften können: Es ist sinnlos zu sagen »X ist skalierbar« oder »Y lässt sich nicht skalieren«. Vielmehr geht es bei einer Diskussion über Skalierbarkeit darum, Fragen zu klären wie zum Beispiel: »Wenn das System in bestimmter Art und Weise wächst, welche Optionen haben wir, um mit dem Wachstum klarzukommen?« oder »Mit welchen zusätzlichen rechentechnischen Ressourcen können wir die Mehrbelastung verarbeiten?«

Lasten beschreiben

Zuerst müssen wir kurz und bündig die aktuelle Last im System beschreiben. Nur dann können wir über Fragen des Wachstums diskutieren (was passiert, wenn sich die Last verdoppelt?). Die Last lässt sich mit einigen Kennwerten beschreiben, den sogenannten Lastparametern. Die beste Wahl von Parametern hängt von der Architektur Ihres Systems ab: Denkbar sind die Anzahl der Anfragen an einen Webserver pro Sekunde, das Verhältnis von Lese- zu Schreibzugriffen in einer Datenbank, die Anzahl der gleichzeitig aktiven Benutzer in einem Chatroom, die Trefferquote in einem Cache oder etwas anderes. Vielleicht ist für Sie der Durchschnittsfall relevant, oder der Engpass wird von wenigen Extremfällen dominiert.

Um diesen Gedanken konkreter zu fassen, nehmen wir als Beispiel Twitter und sehen uns Daten an, die im November 2012 veröffentlicht wurden [16]. Zwei Hauptoperationen von Twitter sind:

Tweet posten

Ein Benutzer kann eine neue Nachricht an seine Follower senden (4.600 Anfragen/Sekunde im Mittel, über 12.000 Anfragen/Sekunde in Spitzenzeiten).

Home-Timeline

Ein Benutzer kann Tweets ansehen, die von seinen Followern gepostet wurden (300.000 Anfragen/Sekunde).

Es wäre ein Leichtes, lediglich 12.000 Schreibvorgänge pro Sekunde zu verarbeiten (was dem Höchstwert für das Posting von Tweets darstellt). Die Herausforderung beim Skalieren von Twitter ist nicht in erster Linie das Tweet-Aufkommen, sondern der Lastfaktor3 – jeder Benutzer folgt vielen Personen und jedem Benutzer folgen viele Personen. Im Wesentlichen gibt es zwei Methoden, um diese beiden Operationen zu realisieren:

Abbildung 1-2: Einfaches relationales Schema für die Implementierung einer Home-Timeline von Twitter

Abbildung 1-3: Eine Datenpipeline von Twitter, um Tweets für die Follower bereitzustellen, mit den Lastparametern von November 2012 [16]

Die erste Version von Twitter hat mit Methode 1 gearbeitet. Da aber die Systeme mit der Belastung durch Abfragen von Home-Timelines zu kämpfen hatten, wechselte die Firma zu Methode 2. Diese Methode funktioniert besser, weil die durchschnittliche Rate der veröffentlichten Tweets fast zwei Größenordnungen niedriger liegt als die Rate der Lesevorgänge von Home-Timelines. In diesem Fall ist es also vorzuziehen, mehr Arbeit während der Schreibvorgänge zu erledigen und weniger während der Lesevorgänge.

Nachteilig bei Methode 2 ist jedoch, dass das Posten eines Tweets nun zusätzlichen Aufwand erfordert. Ein Tweet wird durchschnittlich an 75 Follower geliefert, sodass aus 4.600 Tweets pro Sekunde 345.000 Schreibvorgänge pro Sekunde in die Home-Timeline-Caches werden. Dieser Durchschnittswert verbirgt aber die Tatsache, dass die Anzahl der Follower je Benutzer stark variiert und manche Benutzer über 30 Millionen Follower haben. Ein einzelner Tweet kann somit zu über 30 Millionen Schreiboperationen in Home-Timelines führen! Diesen Vorgang zeitgerecht auszuführen – Twitter versucht, Tweets an Follower innerhalb von fünf Sekunden zu liefern –, ist eine erhebliche Herausforderung.

Im Twitter-Beispiel ist die Verteilung der Follower pro Benutzer (gegebenenfalls gewichtet nach der Häufigkeit, mit der diese Benutzer twittern) ein wesentlicher Lastparameter für eine Diskussion um Skalierbarkeit, da er die Ausgangslast bestimmt. Möglicherweise hat Ihre Anwendung ganz andere Eigenschaften, doch Überlegungen zu ihrer Last können Sie nach ähnlichen Prinzipien anstellen.

Die letzte Wendung in der Twitter-Geschichte sieht nun so aus: Nachdem Methode 2 robust umgesetzt ist, geht Twitter auf eine Hybridvariante aus beiden Methoden über. Die Tweets der meisten Benutzer werden weiterhin auf die Home-Timelines aufgefächert4, wenn sie gepostet werden, eine kleine Anzahl von Benutzern (d.h. Prominente) mit einer sehr großen Anzahl von Followern werden aber aus dieser Auffächerung ausgenommen. Tweets von Prominenten, denen ein Benutzer folgen kann, werden getrennt abgerufen und – wie in Methode 1 – mit der Home-Timeline dieses Benutzers zusammengebracht, wenn diese gelesen wird. Diese Hybridlösung ist in der Lage, eine beständig gute Performance zu liefern. In Kapitel 12 greifen wir dieses Beispiel noch einmal auf, nachdem wir weitere technische Grundlagen besprochen haben.

Performance beschreiben

Nachdem Sie die Belastung in Ihrem System beschrieben haben, können Sie sich der Frage zuwenden, was bei wachsender Belastung passiert. Das können Sie aus zwei Perspektiven betrachten:

Wie wird die Performance Ihres Systems beeinflusst, wenn Sie einen Lastparameter erhöhen und die Systemressourcen (CPU, Arbeitsspeicher, Netzwerkbandbreite usw.) unverändert lassen?

In welchem Maße müssen Sie die Ressourcen aufstocken, wenn Sie einen Lastparameter erhöhen und die Performance unverändert bleiben soll?

Da Sie beide Fragen nur anhand von Leistungskennziffern beantworten können, sehen wir uns kurz an, wie sich die Performance eines Systems beschreiben lässt.

In einem Stapelverarbeitungssystem wie Hadoop interessiert uns normalerweise der Durchsatz – die Anzahl der Datensätze, die wir pro Sekunde verarbeiten können, oder die notwendige Gesamtdauer, um einen Job auf einer Datenmenge einer bestimmten Größe auszuführen.5 In Onlinesystemen ist normalerweise die Antwortzeit des Diensts wichtiger – d.h. die Zeit, die vom Senden einer Anfrage durch einen Client bis zum Eintreffen einer Antwort verstreicht.

Latenz und Antwortzeit

Die Begriffe Latenz und Antwortzeit werden oft synonym verwendet, bezeichnen aber nicht dasselbe. Die Antwortzeit ist das, was der Client sieht: Neben der eigentlichen Verarbeitungszeit der Anforderung (der Servicezeit) schließt sie die Netzwerk- und Warteschlangenverzögerungen ein. Die Latenz gibt die Dauer an, die eine Anforderung auf ihre Verarbeitung wartet – in dieser Zeit ist sie latent und wartet auf Bedienung [17].

Selbst wenn Sie die gleiche Anforderung immer wieder ausführen, erhalten Sie bei jedem Versuch eine etwas andere Antwortzeit. In einem System, das ein breites Spektrum von Anfragen verarbeitet, können die Antwortzeiten in der Praxis stark variieren. Demzufolge dürfen wir uns die Antwortzeit nicht als eine einzelne Zahl vorstellen, sondern als Verteilung von Messwerten.

In Abbildung 1-4 stellt jeder graue Balken eine Dienstanforderung dar, wobei die Höhe des Balkens anzeigt, wie lange diese Anforderung gedauert hat. Die meisten Anforderungen sind halbwegs schnell, es gibt aber gelegentliche Ausreißer, die viel mehr Zeit benötigen. Die langsamsten Anfragen sind möglicherweise an sich teurer, weil sie zum Beispiel mehr Daten verarbeiten. Doch selbst in einem Szenario, in dem alle Anfragen die gleiche Zeit benötigen sollten, ist mit Variationen zu rechnen: Zusätzliche Latenzzeiten können durch Kontextwechsel, Hintergrundprozesse, den Verlust von Netzwerkpaketen, erneute TCP-Übertragungen, Pausen aufgrund der Garbage Collection, Seitenfehler, die ein neues Lesen von Festplatte erfordern, mechanische Schwingungen im Servergestell [18] oder viele andere Ursachen entstehen.

Abbildung 1-4: Mittelwert und Perzentile: Antwortzeiten für eine Stichprobe von 100 Anforderungen eines Diensts

Es ist üblich, die durchschnittliche Antwortzeit eines Diensts anzugeben. (Genau genommen bezieht sich der Begriff »Durchschnitt« nicht auf eine bestimmte Formel, sondern wird in der Praxis gewöhnlich als arithmetisches Mittel verstanden: Sind n Werte gegeben, summiert man alle Werte und teilt die Summe durch n.) Der Mittelwert ist jedoch keine gute Maßzahl, wenn Sie Ihre »typische« Antwortzeit wissen möchten, weil er nichts darüber aussagt, wie viele Benutzer tatsächlich diese Verzögerung erfahren haben.

In der Regel ist es besser, Perzentile (lat. Hundertstelwerte) zu verwenden. Wenn Sie die Liste der Antwortzeiten von der schnellsten zur langsamsten sortieren, teilt der Median die Werte in zwei Hälften: Liegt beispielsweise der Median der Antwortzeiten bei 200 ms, treffen bei der Hälfte Ihrer Anfragen die Antworten in weniger als 200 ms ein und bei der anderen Hälfte in mehr als 200 ms.

Der Median ist deshalb ein geeignetes Maß, wenn Sie wissen möchten, wie lange Benutzer normalerweise warten müssen: Die Hälfte der Benutzeranfragen werden in weniger als der Median-Antwortzeit bedient, und die andere Hälfte braucht länger als der Medianwert. Der Median wird auch als 50. Perzentil bezeichnet und manchmal mit P50 abgekürzt. Beachten Sie, dass sich der Median auf eine einzelne Anfrage bezieht; wenn der Benutzer mehrere Abfragen ausführt (im Verlauf einer Sitzung oder weil mehrere Ressourcen auf einer einzelnen Seite enthalten sind), ist die Wahrscheinlichkeit, dass wenigstens eine von ihnen langsamer als der Median ist, wesentlich größer als 50%.

Um herauszufinden, wie schlecht unsere Ausreißer sind, können Sie sich die höheren Perzentile ansehen: üblicherweise die 95., 99. und 99,9. Perzentile (abgekürzt mit P95, P99 und P999). Sie geben die Schwellenwerte der Antwortzeiten an, bei denen 95%, 99% bzw. 99,9% der Anfragen schneller als der jeweilige Schwellenwert sind. Wenn zum Beispiel das 95. Perzentil der Antwortzeit 1,5 Sekunden beträgt, brauchen 95 von 100 Anfragen weniger als 1,5 Sekunden und 5 von 100 Anfragen 1,5 Sekunden oder mehr. Abbildung 1-4 veranschaulicht dies.

Hohe Perzentile von Antwortzeiten, auch Latenzausreißer genannt, sind wichtig, weil sie die Benutzererfahrung des Diensts direkt beeinflussen. Zum Beispiel beschreibt Amazon die Anforderungen an die Antwortzeit für interne Dienste in Form des 99,9. Perzentils, selbst wenn dieser Wert nur 1 von 1.000 Anfragen betrifft. Das hängt damit zusammen, dass die Kunden mit den langsamsten Anfragen oftmals diejenigen mit den meisten Daten auf ihren Konten sind, weil sie viel eingekauft haben – d.h., sie sind die wertvollsten Kunden [19]. Es ist wichtig, diese Kunden mit einer besonders schnellen Website bei Laune zu halten: Amazon hat auch beobachtet, dass eine Zunahme der Antwortzeit von 100 ms den Verkauf um 1% verringert [20], und andere Onlineshops melden, dass eine Verlangsamung um 1 Sekunde die Kundenzufriedenheit um 16% senkt [21, 22].

Andererseits erscheint Amazon die Optimierung des 99,99. Perzentils (die eine langsamste Anfrage von 10.000 Anfragen) als zu teuer und zu wenig gewinnbringend. Es ist schwierig, die Antwortzeiten bei sehr hohen Perzentilen zu verringern, weil sie leicht durch zufällige Ereignisse beeinflusst werden, die sich Ihrer Kontrolle entziehen, und sich kein nennenswerter Nutzen mehr ergibt.

Zum Beispiel werden Perzentile oftmals in Service-Level-Objectives (SLOs) und Service-Level-Agreements (SLAs) verwendet. Das sind Verträge, die die erwartete Performance und Verfügbarkeit eines Diensts definieren. Ein SLA kann angeben, dass der Dienst als »aktiv« gilt, wenn der Median der Antwortzeit kleiner als 200 ms und das 99. Perzentil unter 1 s liegt (wenn die Antwortzeit größer ist, kann er genauso gut ausgefallen sein) und der Dienst mindestens 99,9% der Zeit aktiv ist. Diese Kennzahlen setzen Erwartungen für Clients des Diensts und erlauben den Kunden, eine Rückerstattung einzufordern, wenn der SLA nicht mehr erfüllt ist.

Verzögerungen durch Warteschlangen machen oftmals einen großen Anteil der Antwortzeit bei hohen Perzentilen aus. Da ein Server nur wenige Objekte parallel verarbeiten kann (zum Beispiel durch die Anzahl seiner CPU-Kerne begrenzt), genügen wenige langsame Anforderungen, um die Verarbeitung von darauffolgenden Anfragen aufzuhalten – ein Effekt, der auch als Head-of-Line-Blocking bezeichnet wird. Selbst wenn der Server diese nachfolgenden Anfragen schnell verarbeitet, erfährt der Client eine lange Gesamtantwortzeit, weil auf die Fertigstellung der vorherigen Anfrage gewartet wird. Wegen dieses Effekts ist es wichtig, die Antwortzeiten auf der Clientseite zu messen.

Perzentile in der Praxis

Hohe Perzentile werden besonders wichtig in Backenddiensten, die mehrfach aufgerufen werden, um ein und dieselbe Endbenutzeranfrage zu bedienen. Selbst wenn Sie die Aufrufe parallel ausführen, muss die Endbenutzeranfrage trotzdem warten, bis der langsamste der parallelen Aufrufe abgeschlossen ist. Wie Abbildung 1-5 veranschaulicht, genügt ein einziger langsamer Aufruf, um die gesamte Endbenutzeranfrage langsam zu machen. Auch wenn nur ein kleiner Prozentsatz der Backendaufrufe langsam ist, steigt die Wahrscheinlichkeit, einen langsamen Aufruf zu erhalten, wenn eine Endbenutzeranfrage mehrere Backendaufrufe durchführen muss. Somit wird am Ende ein höherer Anteil der Endbenutzeranfragen langsam sein (ein Effekt, den man als Latenzausreißerverstärkung bezeichnet [24]).

Wenn Sie die Antwortzeitperzentile in die Überwachungsdashboards für ihre Dienste aufnehmen, müssen Sie sie effizient und kontinuierlich berechnen. Hier bietet sich zum Beispiel ein gleitendes Fenster für die Antwortzeiten der Anfragen in den letzten 10 Minuten an. Den Median und die verschiedenen Perzentile berechnen Sie über den Werten in diesem Fenster und stellen die Kennzahlen in einem Diagramm dar.

Eine naive Implementierung führt eine Liste von Antwortzeiten für alle Anfragen innerhalb des Zeitfensters und sortiert diese Liste jede Minute. Falls Ihnen das zu ineffizient ist, können Sie auf Algorithmen zurückgreifen, die eine gute Annäherung von Perzentilen mit minimalen CPU- und Speicherkosten berechnen können, wie zum Beispiel Forward Decay [25], t-digest [26] und HdrHistogram [27]. Beachten Sie, dass die Mittelwertbildung von Perzentilen, um zum Beispiel die zeitliche Auflösung zu verringern oder Daten von mehreren Computern zusammenzufassen, mathematisch sinnlos ist – um Daten von Antwortzeiten richtig zusammenzufassen, sind die Histogramme zu addieren [28].

Abbildung 1-5: Wenn mehrere Backendaufrufe erforderlich sind, um eine Anfrage zu bedienen, genügt eine langsame Backendanfrage, um die gesamte Endbenutzeranfrage zu bremsen.

Will man die Skalierbarkeit eines Systems mithilfe von künstlichen Belastungen testen, muss der Client, der die Last generiert, beständig Anforderungen unabhängig von der Antwortzeit senden. Wenn der Client wartet, bis die vorherige Anfrage abgeschlossen ist, bevor er die nächste sendet, werden letztlich die Warteschlangen im Test künstlich kürzer gehalten als in der Realität, was die Messungen verzerrt [23].

Konzepte zur Bewältigung von Belastungen

Nachdem Sie die Parameter kennen, mit denen sich Belastungen und Kennzahlen zur Leistungsmessung beschreiben lassen, können wir uns ernsthaft der Skalierbarkeit zuwenden: Wie gewährleisten wir eine gute Performance, selbst wenn unsere Lastparameter um einen gewissen Betrag ansteigen?

Eine Architektur, die für ein bestimmtes Lastniveau ausgelegt ist, kommt höchstwahrscheinlich nicht mit dem Zehnfachen dieser Last zurecht. Wenn Sie an einem schnell wachsenden Dienst arbeiten, ist es demzufolge wahrscheinlich, dass Sie Ihre Architektur bei jeder Erhöhung der Last um eine Größenordnung – vielleicht auch noch häufiger – überdenken müssen.

Häufig spricht man von einer Zweiteilung zwischen vertikaler Skalierung (Übergang zu einem leistungsfähigeren Computer) und horizontaler Skalierung (Verteilen der Last auf mehrere kleinere Computer). Das Verteilen der Last auf mehrere Computer wird auch als Shared-Nothing-Architektur bezeichnet. Ein System, das auf einem einzelnen Computer laufen kann, ist oftmals einfacher, doch können High-End-Computer sehr teuer werden, sodass es sich oftmals nicht vermeiden lässt, sehr intensive Arbeitslasten horizontal zu skalieren. In der Praxis zeichnen sich gute Architekturen in der Regel durch eine pragmatische Mischung der Konzepte aus: Zum Beispiel kann die Verwendung mehrerer recht leistungsfähiger Computer immer noch einfacher und billiger sein als eine große Anzahl kleiner virtueller Computer.

Manche Systeme sind elastisch, d.h., sie können automatisch rechentechnische Ressourcen hinzufügen, wenn sie eine zunehmende Belastung feststellen, während andere Systeme manuell skaliert werden (ein Mensch analysiert die Kapazität und entscheidet, dem System weitere Computer hinzuzufügen). Ein elastisches System kann zweckmäßig sein, wenn die Last kaum vorhersagbar ist, aber manuell skalierte Systeme sind einfacher und bringen möglicherweise auch weniger Überraschungen im Betrieb mit sich (siehe Abschnitt »Rebalancing – Partitionen gleichmäßig belasten« auf Seite 222.)

Während es ziemlich einfach ist, zustandslose Dienste auf mehrere Computer zu verteilen, kann die Übertragung zustandsbehafteter Datensysteme von einem einzelnen Knoten auf ein verteiltes Setup eine ganze Menge zusätzlicher Komplexität mit sich bringen. Aus diesem Grund war es bis vor Kurzem gängige Lehrmeinung, die Datenbank auf einem einzelnen Knoten zu halten (Scale-up, vertikale Skalierung), bis die Skalierungskosten oder Hochverfügbarkeitsanforderungen dazu zwangen, die Datenbank zu verteilen.

Mit verbesserten Tools und Abstraktionen für verteilte Systeme kann sich diese Lehrmeinung ändern, zumindest für bestimmte Arten von Anwendungen. Es ist denkbar, dass verteilte Systeme in Zukunft zum Standard werden, selbst für Einsatzfälle, die weder mit großen Mengen von Daten noch mit umfangreichem Datenverkehr zu tun haben. Wir diskutieren auch, wie sie sich nicht nur in Bezug auf Skalierbarkeit, sondern auch hinsichtlich Benutzerfreundlichkeit und Wartbarkeit entwickeln.

Die Architektur von Systemen, die im großen Maßstab arbeiten, ist in der Regel stark anwendungsspezifisch – es gibt keine generische, universell für alle Größen skalierbare Architektur (inoffiziell als »magische Skalierungssauce« bekannt). Das Problem kann der Umfang der Lesevorgänge, der Umfang der Schreibvorgänge, der Umfang der zu speichernden Daten, die Komplexität der Daten, die Anforderungen an die Antwortzeit, die Zugriffsmuster oder (in der Regel) eine Mischung aus diesen und vielen weiteren Problemen sein.

Zum Beispiel sieht ein System, das für die Verarbeitung von 100.000 Anforderungen pro Sekunde mit einer Größe von jeweils 1 KB ausgelegt ist, ganz anders aus als ein System, das 3 Anfragen pro Minute mit jeweils 2 GB Datenvolumen verarbeiten soll – obwohl beide Systeme den gleichen Datendurchsatz haben.

Eine Architektur, die sich für eine bestimmte Anwendung gut skalieren lässt, baut auf Annahmen auf, welche Operationen häufig und welche selten vorkommen – die Lastparameter. Stellen sich diese Annahmen als falsch heraus, ist der technische Aufwand für die Skalierung bestenfalls verschwendet und im schlimmsten Fall kontraproduktiv. In einer frühen Inbetriebnahmephase oder bei einem noch nicht erprobten Produkt ist es normalerweise wichtiger, die Produktfeatures schnell abzuarbeiten als auf eine hypothetische zukünftige Belastung zu skalieren.

Skalierbare Architekturen sind zwar auf eine bestimmte Anwendung zugeschnitten, bestehen aber dennoch in der Regel aus universellen Bausteinen, die in bekannten Mustern angeordnet sind. In diesem Buch befassen wir uns mit solchen Bausteinen und Mustern.

Wartbarkeit

Es ist allgemein bekannt, dass die meisten Softwarekosten nicht in der anfänglichen Entwicklung anfallen, sondern in der ständigen Wartung – Fehlerbeseitigung, Gewährleisten des laufenden Betriebs, Untersuchung von Fehlern, Anpassen an neue Plattformen, Modifizieren für neue Einsatzfälle, Zurückzahlen technischer Schulden und Hinzufügen neuer Features.

Leider sind die meisten Menschen, die an Softwaresystemen arbeiten, von der Wartung sogenannter Legacy-Systeme nicht begeistert – möglicherweise müssten sie Fehler anderer Programmierer beheben oder mit Plattformen arbeiten, die inzwischen veraltet sind, oder Systeme krampfhaft zu Funktionen »überreden«, für die sie nie vorgesehen waren. Jedes Legacy-System ist auf seine Art unangenehm, und somit ist es schwierig, allgemeine Empfehlungen für den Umgang mit ihnen zu geben.

Allerdings können und sollten wir Software in einer Art und Weise entwerfen, die nach Möglichkeit die Mühen während der Wartungsphase minimiert, und somit vermeiden, die eigene Software zur Legacy-Software zu machen. In dieser Hinsicht achten wir besonders auf drei Entwurfsprinzipien für Softwaresysteme:

Bedienbarkeit

Alle Möglichkeiten schaffen, dass die Betreiberteams das System reibungslos am Laufen halten.

Einfachheit

Neuen Technikern das Verständnis des Systems erleichtern, indem möglichst viel Komplexität vom System entfernt wird. (Das ist nicht das Gleiche wie Einfachheit der Benutzeroberfläche.)

Evolvierbarkeit

Die Voraussetzungen schaffen, dass Techniker Änderungen am System in der Zukunft vornehmen können, um es bei geänderten Anforderungen an nicht vorgesehene Einsatzfälle anzupassen. Stichwörter hier sind Erweiterbarkeit, Modifizierbarkeit oder Plastizität.

Wie schon bei Zuverlässigkeit und Skalierbarkeit gibt es für das Erreichen dieser Ziele keine einfachen Lösungen. Stattdessen versuchen wir, Systeme zu entwerfen und damit immer Bedienbarkeit, Einfachheit und Evolvierbarkeit im Hinterkopf zu behalten.

Betriebsfähigkeit: Den Betrieb erleichtern

Man spricht davon, dass »gute Betriebsteams oftmals die Beschränkungen von schlechter (oder unvollständiger) Software umgehen können, gute Software aber mit schlechten Betriebsteams nicht zuverlässig laufen kann« [12]. Auch wenn einige Aspekte des Betriebs automatisiert werden können und sollten, liegt es immer noch beim Menschen, die Automatisierung überhaupt erst einzurichten und einen ordnungsgemäßen Ablauf sicherzustellen.

Betriebsteams sind für den reibungslosen Betrieb eines Softwaresystems unerlässlich. Ein gutes Betriebsteam ist normalerweise für die folgenden und für weitere Aufgaben zuständig [29]:

Die Systemintegrität überwachen und den Dienst schnell wiederherstellen, wenn er in einen ungünstigen Zustand übergeht

Die Ursache von Problemen aufspüren, wie zum Beispiel Systemausfälle oder sinkende Performance

Software und Plattformen auf dem neuesten Stand halten, einschließlich der Sicherheitspatches

Kontrollieren, wie sich verschiedene Systeme einander beeinflussen, um eine problematische Änderung von vornherein zu vermeiden, bevor sie einen Schaden verursacht

Zukünftige Probleme vorwegnehmen und lösen, bevor sie auftreten (zum Beispiel Kapazitätsplanung)

Optimale Methoden und Tools für Bereitstellung, Konfigurationsverwaltung und mehr etablieren

Komplexe Wartungsaufgaben durchführen, beispielsweise eine Anwendung von einer Plattform auf eine andere übertragen

Die Sicherheit des Systems aufrechterhalten, wenn Konfigurationsänderungen durchgeführt werden

Prozesse definieren, die den Betrieb vorhersagbar machen und dabei helfen, die Produktionsumgebung stabil zu halten

Das Wissen über das System im Unternehmen bewahren, selbst wenn einzelne Mitarbeiter neu hinzukommen oder Mitarbeiter das Unternehmen verlassen

Gute Bedienbarkeit heißt auch, Routineaufgaben leicht erledigen zu können, sodass sich das Betriebsteam auf die anspruchsvolleren Aufgaben konzentrieren kann. Datensysteme können unter anderem mit folgenden Mitteln dazu beitragen, Routineaufgaben zu erleichtern:

Das Laufzeitverhalten und die Interna des Systems mit guter Überwachung transparent gestalten

Gute Unterstützung für Automatisierung und Integration mit Standardtools bereitstellen

Abhängigkeit von einzelnen Computern vermeiden (ermöglichen, dass einzelne Computer zu Wartungszwecken heruntergefahren werden, während das System als Ganzes ununterbrochen weiterlaufen kann)

Gute Dokumentation und ein leicht verständliches Betriebsmodell bereitstellen (»Wenn ich X tue, wird Y passieren«)

Gutes Standardverhalten realisieren, aber auch Administratoren die Freiheit bieten, bei Bedarf die Standardwerte zu überschreiben

Selbstheilung, wo es angebracht ist, aber auch den Administratoren bei Bedarf die manuelle Kontrolle über den Systemzustand geben

Vorhersagbares Verhalten zeigen, Überraschungen minimieren

Einfachheit: Komplexität im Griff