Kapitel 1. Einführung in skalierbare Systeme
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
In den letzten 20 Jahren ist die Größe, Komplexität und Kapazität von Softwaresystemen in nie gekanntem Ausmaß gewachsen. Es ist unwahrscheinlich, dass sich dieses Wachstum in den nächsten 20 Jahren verlangsamen wird - wie die Systeme der Zukunft aussehen werden, ist heute noch fast unvorstellbar. Eines können wir jedoch garantieren: Immer mehr Softwaresysteme werden so gebaut werden müssen, dass ein ständiges Wachstum - mehr Anfragen, mehr Daten und mehr Analysen - die Hauptantriebskraft für die Entwicklung ist.
Skalierbar ist der Begriff, der in der Softwareentwicklung verwendet wird, um Softwaresysteme zu beschreiben, die mit dem Wachstum Schritt halten können. In diesem Kapitel werde ich untersuchen, was genau mit der Fähigkeit zur Skalierung gemeint ist, die (wenig überraschend) als Skalierbarkeit bezeichnet wird. Außerdem beschreibe ich ein paar Beispiele, die die Fähigkeiten und Eigenschaften moderner Anwendungen in Zahlen ausdrücken, und gebe einen kurzen Überblick über die Ursprünge der massiven Systeme, die wir heute routinemäßig bauen. Abschließend beschreibe ich zwei allgemeine Prinzipien zur Erreichung von Skalierbarkeit, nämlich Replikation und Optimierung, die im Rest des Buches in verschiedenen Formen wiederkehren werden, und untersuche die unauslöschliche Verbindung zwischen Skalierbarkeit und anderen Qualitätsmerkmalen der Softwarearchitektur.
Was ist Skalierbarkeit?
Intuitiv ist Skalierbarkeit ein ziemlich einfaches Konzept. Fragt man Wikipedia nach einer Definition, erfährt man: "Skalierbarkeit ist die Eigenschaft eines Systems, eine wachsende Menge an Arbeit zu bewältigen, indem dem System weitere Ressourcen hinzugefügt werden." Wir alle wissen, wie wir ein Autobahnsystem skalieren - wir fügen mehr Fahrspuren hinzu, damit es eine größere Anzahl von Fahrzeugen bewältigen kann. Einige meiner Lieblingsleute wissen, wie man die Bierproduktion skaliert - sie erhöhen die Kapazität in Bezug auf die Anzahl und Größe der Braukessel, die Anzahl der Mitarbeiter, die den Brauprozess durchführen und verwalten, und die Anzahl der Fässer, die sie mit frischem, leckerem Bier füllen können. Denk an ein beliebiges physisches System - ein Verkehrssystem, einen Flughafen, Aufzüge in einem Gebäude - und es ist ziemlich offensichtlich, wie wir die Kapazität erhöhen.
Anders als physische Systeme sind Softwaresysteme etwas Amorphes. Man kann nicht auf sie zeigen, sie sehen, anfassen, fühlen und von außen beobachten, wie sie sich im Inneren verhält. Ein Softwaresystem ist ein digitales Artefakt. In seinem Kern sind die Ströme von 1en und 0en, die den ausführbaren Code und die Daten bilden, für niemanden zu unterscheiden. Was bedeutet also Skalierbarkeit in Bezug auf ein Softwaresystem?
Vereinfacht ausgedrückt und ohne sich in Definitionskriege zu stürzen, definiert Skalierbarkeit die Fähigkeit eines Softwaresystems, ein Wachstum in einer bestimmten Dimension seines Betriebs zu bewältigen. Beispiele für betriebliche Dimensionen sind:
Die Anzahl der gleichzeitigen Benutzer- oder externen (z. B. Sensor-) Anfragen, die ein System verarbeiten kann
Die Menge der Daten, die ein System effektiv verarbeiten und verwalten kann
Der Wert, der aus den Daten, die ein System speichert, durch vorausschauende Analysen abgeleitet werden kann
Die Fähigkeit, eine stabile, konsistente Antwortzeit aufrechtzuerhalten, wenn die Anzahl der Anfragen steigt
Stell dir zum Beispiel vor, eine große Supermarktkette eröffnet schnell neue Filialen und erhöht die Anzahl der Selbstbedienungskassen in jedem Geschäft. Dies erfordert, dass die zentralen Supermarktsoftwaresysteme die folgenden Funktionen erfüllen:
Bewältige das steigende Volumen beim Scannen von Gegenständen, ohne die Reaktionszeit zu verkürzen. Um die Kunden zufrieden zu stellen, ist eine sofortige Reaktion auf das Scannen von Gegenständen erforderlich.
Verarbeite und speichere die größeren Datenmengen, die durch den gestiegenen Umsatz entstehen. Diese Daten werden für die Bestandsverwaltung, die Buchhaltung, die Planung und wahrscheinlich viele andere Funktionen benötigt.
Erstelle "Echtzeit"-Zusammenfassungen (z.B. stündlich) von Verkaufsdaten aus jedem Laden, jeder Region und jedem Land und vergleiche sie mit historischen Trends. Diese Trenddaten können helfen, ungewöhnliche Ereignisse in den Regionen zu erkennen (unerwartete Wetterbedingungen, großer Andrang bei Veranstaltungen usw.) und den betroffenen Läden zu helfen, schnell zu reagieren.
Entwickle das Teilsystem zur Vorhersage von Lagerbestellungen weiter, damit es in der Lage ist, die Verkäufe (und damit den Bedarf an Nachbestellungen) korrekt vorherzusagen, wenn die Zahl der Filialen und Kunden wächst.
Diese Dimensionen sind die Anforderungen an die Skalierbarkeit des Systems. Wenn die Supermarktkette innerhalb eines Jahres 100 neue Läden eröffnet und den Umsatz um das 400-fache steigert (einige der neuen Läden sind riesig!), dann muss das Softwaresystem so skalieren, dass es die notwendige Verarbeitungskapazität bietet, damit der Supermarkt effizient arbeiten kann. Wenn die Systeme nicht skalieren, könnten wir Umsätze verlieren, wenn die Kunden unzufrieden sind. Wir könnten Lagerbestände halten, die nicht schnell verkauft werden können, was die Kosten erhöht. Wir könnten Gelegenheiten verpassen, den Umsatz zu steigern, indem wir mit Sonderangeboten auf lokale Gegebenheiten reagieren. All diese Faktoren verringern die Kundenzufriedenheit und den Gewinn. Keiner davon ist gut fürs Geschäft.
Eine erfolgreiche Skalierung ist daher entscheidend für das Geschäftswachstum unseres imaginären Supermarkts und ist auch das Lebenselixier vieler moderner Internetanwendungen. Aber für die meisten Systeme in Unternehmen und Behörden ist Skalierbarkeit in den frühen Phasen der Entwicklung und des Einsatzes keine primäre Qualitätsanforderung. Neue Funktionen zur Verbesserung der Benutzerfreundlichkeit und des Nutzens sind die treibende Kraft in unseren Entwicklungszyklen. Solange die Leistung bei normaler Belastung ausreichend ist, fügen wir immer wieder neue Funktionen hinzu, um den geschäftlichen Nutzen des Systems zu erhöhen. Die Einführung einiger der ausgefeilten verteilten Technologien, die ich in diesem Buch beschreibe, bevor ein klarer Bedarf besteht, kann sich sogar nachteilig auf ein Projekt auswirken, da die zusätzliche Komplexität die Entwicklung träge macht.
Dennoch ist es nicht ungewöhnlich, dass sich Systeme in einen Zustand entwickeln, in dem verbesserte Leistung und Skalierbarkeit zu einer Frage der Dringlichkeit oder sogar des Überlebens werden. Attraktive Funktionen und ein hoher Nutzwert führen zum Erfolg, was wiederum zu mehr Anfragen und mehr zu verwaltenden Daten führt. Dies führt oft zu einem Wendepunkt, an dem Designentscheidungen, die bei geringer Belastung sinnvoll waren, plötzlich zu technischen Schulden werden.1 Solche Kipppunkte werden oft durch externe Ereignisse ausgelöst: Im März/April 2020 gab es in den Medien viele Berichte über Arbeitslosigkeit in der Regierung und Online-Bestellseiten von Supermärkten, die unter der durch die Coronavirus-Pandemie verursachten Nachfrage zusammenbrachen.
Die Erhöhung der Kapazität eines Systems in einer bestimmten Dimension durch die Vergrößerung der Ressourcen wird als Skalierung bezeichnet -auf den Unterschied zwischen diesen beiden Begriffen gehe ichspäter ein. Im Gegensatz zu physischen Systemen ist es außerdem oft genauso wichtig, die Kapazität eines Systems zu verringern, um die Kosten zu senken.
Das Paradebeispiel dafür ist Netflix, das eine vorhersehbare regionale Tageslast hat, die es verarbeiten muss. Um 21 Uhr schauen in einer Region viel mehr Menschen Netflix als um 5 Uhr morgens. So kann Netflix seine Verarbeitungsressourcen zu Zeiten mit geringerer Auslastung reduzieren. Das spart die Kosten für den Betrieb der Verarbeitungsknoten, die in der Amazon-Cloud verwendet werden, sowie gesellschaftlich wertvolle Dinge wie die Senkung des Stromverbrauchs im Rechenzentrum. Vergleiche dies mit einer Autobahn. Nachts, wenn nur wenige Autos unterwegs sind, ziehen wir die Fahrspuren nicht zurück (außer für Reparaturen). Die volle Straßenkapazität steht den wenigen Fahrern zur Verfügung, damit sie so schnell fahren können, wie sie wollen. In Softwaresystemen können wir unsere Verarbeitungskapazität in Sekundenschnelle erweitern und verringern, um der momentanen Belastung gerecht zu werden. Im Vergleich zu physischen Systemen sind die Strategien, die wir einsetzen, ganz anders.
Es gibt noch viel mehr zu bedenken, wenn es um die Skalierbarkeit von Softwaresystemen geht, aber lass uns auf diese Fragen zurückkommen, nachdem wir den Umfang einiger aktueller Softwaresysteme im Jahr 2021 untersucht haben.
Beispiele für die Systemgröße in den frühen 2000er Jahren
Der Blick nach vorn ist in diesem Technologiespiel immer mit Gefahren verbunden. Im Jahr 2008 schrieb ich:
"Während Petabyte-Datensätze und Gigabit-Datenströme heute die Grenzen für datenintensive Anwendungen darstellen, werden wir uns in zehn Jahren sicherlich gerne an Probleme dieser Größenordnung erinnern und uns über die Schwierigkeiten sorgen, die sich für Exascale-Anwendungen abzeichnen."2
Vernünftige Gefühle, das stimmt, aber Exascale? Das ist in der heutigen Welt fast schon alltäglich. Google meldete 2014 mehrere Exabytes für Gmail, und mittlerweile verwalten alle Google-Dienste ein Yottabyte oder mehr? Ich weiß es nicht. Ich bin mir nicht einmal sicher, ob ich weiß, was ein Yottabyte ist! Google verrät uns nichts über seine Speicherung, aber ich würde nicht dagegen wetten. Und wie viele Daten speichert Amazon in den verschiedenen AWS-Datenspeichern für seine Kunden? Und wie viele Anfragen verarbeitet DynamoDB pro Sekunde für alle unterstützten Client-Anwendungen zusammengenommen? Wenn du zu lange über diese Dinge nachdenkst, wird dein Kopf explodieren.
Eine gute Informationsquelle, die manchmal Einblicke in die aktuellen Betriebsgrößen gibt, sind die technischen Blogs der großen Internetunternehmen. Es gibt auch Websites, die den Internetverkehr analysieren und das Verkehrsaufkommen sehr anschaulich darstellen. Nehmen wir ein paar aktuelle Beispiele, um ein paar Dinge zu veranschaulichen, die wir heute wissen. Bedenke, dass diese in ein oder vier Jahren fast altmodisch aussehen werden:
Der Technik-Blog von Facebook beschreibt Scribe, die Lösung für das Sammeln, Aggregieren und Bereitstellen von Petabytes an Log-Daten pro Stunde, mit geringer Latenz und hohem Durchsatz. Die Computerinfrastruktur von Facebook besteht aus Millionen von Rechnern, von denen jeder einzelne Logdateien erzeugt, die wichtige Ereignisse in Bezug auf den System- und Anwendungszustand aufzeichnen. Die Verarbeitung dieser Logdateien, z. B. von einem Webserver, kann Entwicklungsteams Einblicke in das Verhalten und die Leistung ihrer Anwendung geben und die Fehlersuche unterstützen. Scribe ist eine benutzerdefinierte, gepufferte Warteschlangenlösung, die Protokolle von Servern mit einer Geschwindigkeit von mehreren Terabyte pro Sekunde transportieren und an nachgelagerte Analyse- und Data-Warehousing-Systeme liefern kann. Das, meine Freunde, sind eine Menge Daten!
Bei Internet Live Stats kannst du den Internetverkehr für zahlreiche Dienste live verfolgen. Wenn du dich umschaust, wirst du einige erstaunliche Statistiken finden: Google bearbeitet zum Beispiel 3,5 Milliarden Suchanfragen pro Tag, Instagram-Nutzer laden täglich etwa 65 Millionen Fotos hoch, und es gibt etwa 1,7 Milliarden Websites. Es ist eine unterhaltsame Seite mit vielen Informationen. Beachte, dass es sich nicht um echte Daten handelt, sondern um Schätzungen, die auf statistischen Analysen mehrerer Datenquellen beruhen.
Im Jahr 2016 veröffentlichte Google ein Papier, in dem die Eigenschaften seiner Codebasis beschrieben wurden. Zu den vielen verblüffenden Fakten gehört die Tatsache, dass "das Repository 86 TB an Daten enthält, darunter etwa zwei Milliarden Codezeilen in neun Millionen einzigartigen Quelldateien." Erinnere dich: Das war 2016.3
Dennoch bleiben echte, konkrete Daten über den Umfang der von den großen Internetseiten angebotenen Dienste unter dem Deckmantel der kommerziellen Geheimhaltung. Glücklicherweise können wir durch den jährlichen Nutzungsbericht eines Technologieunternehmens einige tiefe Einblicke in die Anfrage- und Datenmengen erhalten, die im Internet verarbeitet werden. Aber Vorsicht, der Bericht ist von Pornhub.4 Du kannst die unglaublich detaillierten Nutzungsstatistiken für 2019 hier einsehen. Es ist ein faszinierender Einblick in die Fähigkeiten von Systemen im großen Maßstab.
Wie sind wir hierher gekommen? Eine kurze Geschichte des Systemwachstums
Ich bin mir sicher, dass viele Leserinnen und Leser nur schwer glauben können, dass es ein zivilisiertes Leben vor der Internetsuche, YouTube und den sozialen Medien gab. Tatsächlich wurde der erste Video-Upload auf YouTube im Jahr 2005 durchgeführt. Ja, das ist selbst für mich schwer zu glauben. Werfen wir also einen kurzen Blick zurück in die Vergangenheit, um zu sehen, wie wir zu den heutigen Systemen gekommen sind. Im Folgenden findest du einige wichtige historische Meilensteine:
- 1980s
- Ein Zeitalter, das von zeitlich getrennten Großrechnern und Minicomputern dominiert wurde. PCs kamen Anfang der 1980er Jahre auf, waren aber nur selten vernetzt. Ende der 1980er Jahre hatten Entwicklungslabore, Universitäten und (zunehmend) Unternehmen E-Mail und Zugang zu primitiven Internetressourcen.
- 1990-95
- Die Netzwerke wurden immer umfassender und schufen ein Umfeld, das reif für die Schaffung des World Wide Web (WWW) mit der HTTP/HTML-Technologie war, die in den 1980er Jahren von Tim Berners-Lee am CERN entwickelt worden war. 1995 war die Zahl der Websites noch winzig, aber mit Unternehmen wie Yahoo! im Jahr 1994 und Amazon und eBay im Jahr 1995 wurden die Weichen für die Zukunft gestellt.
- 1996-2000
- Die Zahl der Websites wuchs von etwa 10.000 auf 10 Millionen, eine wahrhaft explosive Wachstumsphase. Auch die Bandbreite und der Zugang zu Netzwerken wuchsen rasant. Unternehmen wie Amazon, eBay, Google und Yahoo! leisteten Pionierarbeit bei vielen der Konstruktionsprinzipien und frühen Versionen der fortschrittlichen Technologien für hochskalierbare Systeme, die wir heute kennen und nutzen. Unternehmen stürzten sich auf die neuen Möglichkeiten, die das E-Business bot, was die Skalierbarkeit von Systemen in den Vordergrund rückte, wie im Abschnitt "Wie sich die Skalierung auf Geschäftssysteme auswirkte" erläutert wird .
- 2000-2006
- Die Zahl der Websites wuchs in dieser Zeit von etwa 10 Millionen auf 80 Millionen, und es entstanden neue Dienste und Geschäftsmodelle. Im Jahr 2005 wurde YouTube gestartet. 2006 wurde Facebook für die Öffentlichkeit zugänglich. Im selben Jahr ging Amazon Web Services (AWS), das 2004 ganz unauffällig begonnen hatte, mit seinen Diensten S3 und EC2 an den Start.
- 2007-heute
- Wir leben heute in einer Welt mit rund 2 Milliarden Websites, von denen etwa 20% aktiv sind. Es gibt etwa 4 Milliarden Internetnutzer. Riesige Rechenzentren, die von öffentlichen Cloud-Betreibern wie AWS, Google Cloud Platform (GCP) und Microsoft Azure betrieben werden, sowie unzählige private Rechenzentren, wie zum Beispiel die Betriebsinfrastruktur von Twitter, sind über den ganzen Planeten verstreut. In den Clouds werden Millionen von Anwendungen gehostet, wobei die Ingenieure ihre Rechen- und Datenspeichersysteme über hochentwickelte Cloud-Management-Portale bereitstellen und betreiben. Leistungsstarke Cloud-Dienste machen es möglich, dass wir unsere Systeme buchstäblich mit ein paar Mausklicks aufbauen, einsetzen und skalieren können. Alles, was die Unternehmen tun müssen, ist, die Rechnung ihres Cloud-Providers am Ende des Monats zu bezahlen.
Das ist die Welt, auf die dieses Buch abzielt. Eine Welt, in der unsere Anwendungen die wichtigsten Prinzipien für den Aufbau skalierbarer Systeme nutzen und hochskalierbare Infrastrukturplattformen einsetzen müssen. Vergiss nicht, dass der meiste Code in modernen Anwendungen nicht von deinem Unternehmen geschrieben wird. Er ist Teil der Container, Datenbanken, Nachrichtensysteme und anderer Komponenten, die du über API-Aufrufe und Build-Direktiven in deine Anwendung einbaust. Das macht die Auswahl und Verwendung dieser Komponenten mindestens genauso wichtig wie das Design und die Entwicklung deiner eigenen Geschäftslogik. Es handelt sich um architektonische Entscheidungen, die nicht einfach zu ändern sind.
Skalierbarkeit Grundlegende Gestaltungsprinzipien
Das grundlegende Ziel der Skalierung eines Systems ist es, seine Kapazität in einer anwendungsspezifischen Dimension zu erhöhen. Eine gängige Dimension ist die Erhöhung der Anzahl der Anfragen, die ein System in einer bestimmten Zeitspanne bearbeiten kann. Dies wird als Durchsatz des Systems bezeichnet. Anhand einer Analogie wollen wir zwei Grundprinzipien untersuchen, die uns für die Skalierung unserer Systeme und die Erhöhung des Durchsatzes zur Verfügung stehen: Replikation und Optimierung.
Im Jahr 1932 wurde die Sydney Harbour Bridge, eines der weltweit bekanntesten technischen Wunderwerke, eröffnet. Heute kann man mit ziemlicher Sicherheit davon ausgehen, dass das Verkehrsaufkommen im Jahr 2021 etwas höher ist als im Jahr 1932. Wenn du in den letzten 30 Jahren zufällig zur Hauptverkehrszeit über die Brücke gefahren bist, dann weißt du, dass ihre Kapazität jeden Tag deutlich überschritten wird. Wie können wir also den Durchsatz auf physischen Infrastrukturen wie Brücken erhöhen?
Dieses Problem wurde in Sydney in den 1980er Jahren sehr deutlich, als man erkannte, dass die Kapazität der Hafenquerung erhöht werden musste. Die Lösung war der weniger ikonische Sydney Harbour Tunnel, der im Wesentlichen auf der gleichen Strecke unter dem Hafen verläuft. Er bietet vier zusätzliche Fahrspuren und hat damit die Kapazität der Hafenquerung um etwa ein Drittel erhöht. Im nicht allzu weit entfernten Auckland hatte die Hafenbrücke ebenfalls ein Kapazitätsproblem, denn sie wurde 1959 mit nur vier Fahrspuren gebaut. Im Wesentlichen wurde dort die gleiche Lösung wie in Sydney gewählt, nämlich die Kapazität zu erhöhen. Aber anstatt einen Tunnel zu bauen, verdoppelten sie die Anzahl der Fahrspuren, indem sie die Brücke mit den lustigen "Nippon Clip-ons" erweiterten , die die Brücke auf jeder Seite verbreiterten.
Diese Beispiele veranschaulichen die erste Strategie, die wir in Softwaresystemen anwenden, um die Kapazität zu erhöhen. Im Grunde genommen replizieren wir die Softwareverarbeitungsressourcen, um mehr Kapazität für die Bearbeitung von Anfragen bereitzustellen und so den Durchsatz zu erhöhen, wie in Abbildung 1-1 dargestellt. Diese replizierten Verarbeitungsressourcen sind vergleichbar mit den Fahrspuren auf Brücken, die einen weitgehend unabhängigen Verarbeitungspfad für einen Strom ankommender Anfragen bieten.
Zum Glück kann man in cloudbasierten Softwaresystemen die Replikation per Mausklick erreichen, und wir können unsere Verarbeitungsressourcen effektiv tausendfach replizieren. In dieser Hinsicht haben wir es viel einfacher als Brückenbauer/innen. Dennoch müssen wir darauf achten, Ressourcen zu replizieren, um echte Engpässe zu vermeiden. Die Aufstockung von Kapazitäten auf Verarbeitungspfaden, die nicht überlastet sind, verursacht unnötige Kosten, ohne die Skalierbarkeit zu verbessern.
Die zweite Strategie für Skalierbarkeit lässt sich auch mit unserem Brückenbeispiel veranschaulichen. In Sydney hat ein aufmerksamer Mensch festgestellt, dass morgens viel mehr Fahrzeuge die Brücke von Norden nach Süden überqueren und nachmittags das umgekehrte Muster zu beobachten ist. Deshalb wurde eine clevere Lösung gefunden: Morgens werden mehr Fahrspuren für die stark nachgefragte Richtung zur Verfügung gestellt, und irgendwann am Nachmittag wird dies umgedreht. Auf diese Weise wurde die Kapazität der Brücke erhöht, ohne dass neue Ressourcen zugewiesen wurden - wir optimierten die Ressourcen, die wir bereits zur Verfügung hatten.
Diesen Ansatz können wir auch in der Software verfolgen, um unsere Systeme zu skalieren. Wenn wir unsere Verarbeitung irgendwie optimieren können, indem wir effizientere Algorithmen verwenden, zusätzliche Indizes in unsere Datenbanken einfügen, um Abfragen zu beschleunigen, oder sogar unseren Server in einer schnelleren Programmiersprache neu schreiben, können wir unsere Kapazität erhöhen, ohne unsere Ressourcen zu vergrößern. Das Paradebeispiel dafür ist das von Facebook entwickelte (und inzwischen eingestellte) HipHop for PHP, das die Geschwindigkeit der Facebook-Webseitengenerierung durch die Kompilierung von PHP-Code in C++ um das Sechsfache erhöhte.
Ich werde im Laufe dieses Buches immer wieder auf diese beiden Gestaltungsprinzipien - Replikation und Optimierung - zurückkommen. Du wirst sehen, dass die Übernahme dieser Prinzipien viele komplexe Auswirkungen hat, die sich aus der Tatsache ergeben, dass wir verteilte Systeme bauen. Verteilte Systeme haben Eigenschaften, die den Aufbau skalierbarer Systeme interessant machen, was in diesem Zusammenhang sowohl positiv als auch negativ konnotiert ist.
Skalierbarkeit und Kosten
Nehmen wir ein triviales, hypothetisches Beispiel, um die Beziehung zwischen Skalierbarkeit und Kosten zu untersuchen. Nehmen wir an, wir haben ein webbasiertes System (z. B. Webserver und Datenbank), das eine Last von 100 gleichzeitigen Anfragen mit einer durchschnittlichen Antwortzeit von 1 Sekunde bedienen kann. Wir erhalten die Anforderung, dieses System auf 1.000 gleichzeitige Anfragen mit derselben Antwortzeit zu erweitern. Ohne irgendwelche Änderungen vorzunehmen, zeigt ein einfacher Lasttest dieses Systems die in Abbildung 1-2 (links) dargestellte Leistung. Mit zunehmender Last steigt die mittlere Antwortzeit stetig auf 10 Sekunden an. Es ist klar, dass das System in seiner derzeitigen Konfiguration nicht unseren Anforderungen entspricht. Das System lässt sich nicht skalieren.
Um die erforderliche Leistung zu erreichen, ist ein gewisser technischer Aufwand erforderlich. Abbildung 1-2 (rechts) zeigt die Leistung des Systems, nachdem dieser Aufwand geändert wurde. Es liefert jetzt die angegebene Antwortzeit bei 1.000 gleichzeitigen Anfragen. Wir haben das System also erfolgreich skaliert. Die Party kann beginnen!
Es stellt sich jedoch eine wichtige Frage. Wie viel Aufwand und Ressourcen waren nötig, um diese Leistung zu erreichen? Vielleicht musste der Webserver einfach nur auf einer leistungsfähigeren (virtuellen) Maschine laufen. Eine solche Neubereitstellung in einer Cloud könnte höchstens 30 Minuten dauern. Etwas komplexer wäre es, das System so umzukonfigurieren, dass es mehrere Instanzen des Webservers betreibt, um die Kapazität zu erhöhen. Auch hier handelt es sich um eine einfache, kostengünstige Konfigurationsänderung für die Anwendung, die keine Codeänderungen erfordert. Das wären hervorragende Ergebnisse.
Die Skalierung eines Systems ist jedoch nicht immer so einfach. Die Gründe dafür sind vielfältig, aber hier sind einige Möglichkeiten:
Bei 1.000 Anfragen pro Sekunde reagiert die Datenbank nicht mehr so gut, so dass ein Upgrade auf eine neue Maschine erforderlich wird.
Der Webserver generiert viele Inhalte dynamisch, was die Antwortzeit unter Last verringert. Eine mögliche Lösung besteht darin, den Code so zu ändern, dass die Inhalte effizienter generiert werden und so die Verarbeitungszeit pro Anfrage verringert wird.
Die Last der Anfragen führt zu Hotspots in der Datenbank, wenn viele Anfragen gleichzeitig versuchen, auf dieselben Datensätze zuzugreifen und sie zu aktualisieren. Dies erfordert eine Umgestaltung des Schemas und ein anschließendes Neuladen der Datenbank sowie Codeänderungen an der Datenzugriffsschicht.
Bei der Wahl des Webserver-Frameworks stand die einfache Entwicklung im Vordergrund und nicht die Skalierbarkeit. Das Modell, das es erzwingt, bedeutet, dass der Code einfach nicht skaliert werden kann, um die geforderten Lastanforderungen zu erfüllen, und eine komplette Neufassung erforderlich ist. Ein anderes Framework verwenden? Oder gar eine andere Programmiersprache verwenden?
Es gibt noch unzählige andere mögliche Ursachen, aber hoffentlich verdeutlichen diese den zunehmenden Aufwand, der erforderlich sein könnte, wenn wir von Möglichkeit (1) zu Möglichkeit (4) übergehen.
Nehmen wir nun an, dass Option (1), das Upgrade des Datenbankservers, 15 Stunden Aufwand und tausend Dollar zusätzliche Cloud-Kosten pro Monat für einen leistungsfähigeren Server erfordert. Das ist nicht unerschwinglich teuer. Und nehmen wir an, dass Option (4), eine Neufassung der Webanwendungsschicht, aufgrund der Implementierung einer neuen Sprache (z. B. Java anstelle von Ruby) 10.000 Entwicklungsstunden erfordert. Die Optionen (2) und (3) liegen irgendwo zwischen den Optionen (1) und (4). Die Kosten von 10.000 Entwicklungsstunden sind nicht unerheblich. Noch schlimmer ist, dass die Anwendung während der Entwicklung möglicherweise Marktanteile und damit Geld verliert, weil sie nicht in der Lage ist, die Anforderungen der Kunden zu erfüllen. Solche Situationen können zum Scheitern von Systemen und Unternehmen führen.
Dieses einfache Szenario zeigt, dass die Dimensionen Ressourcen- und Aufwandskosten untrennbar mit der Skalierbarkeit verbunden sind. Wenn ein System nicht von vornherein auf Skalierbarkeit ausgelegt ist, können die nachgelagerten Kosten und Ressourcen für die Erhöhung der Kapazität zur Erfüllung der Anforderungen enorm sein. Bei einigen Anwendungen, wie z. B. HealthCare.gov, werden diese Kosten (mehr als 2 Mrd. USD) getragen und das System wird so angepasst, dass es schließlich den Geschäftsanforderungen entspricht. Bei anderen, wie der Gesundheitsbörse in Oregon, kann die Unfähigkeit, schnell und kostengünstig zu skalieren, zum teuren Todesurteil werden (in Oregon waren es 303 Millionen Dollar).
Wir würden nie erwarten, dass jemand versucht, die Kapazität eines Vorstadthauses in ein 50-stöckiges Bürogebäude zu verwandeln. Das Haus verfügt nicht über die Architektur, die Materialien und das Fundament, um dies auch nur im Entferntesten zu ermöglichen, ohne komplett abgerissen und neu gebaut zu werden. Genauso wenig sollten wir erwarten, dass Softwaresysteme, die keine skalierbaren Architekturen, Mechanismen und Technologien verwenden, schnell weiterentwickelt werden können, um größere Kapazitätsanforderungen zu erfüllen. Die Grundlagen der Skalierbarkeit müssen von Anfang an eingebaut werden, wobei zu berücksichtigen ist, dass sich die Komponenten im Laufe der Zeit weiterentwickeln werden. Durch die Anwendung von Design- und Entwicklungsprinzipien, die die Skalierbarkeit fördern, können wir Systeme schneller und kostengünstiger skalieren, um schnell wachsende Anforderungen zu erfüllen. Ich werde diese Prinzipien in Teil II dieses Buches erläutern.
Softwaresysteme, die exponentiell skaliert werden können, während die Kosten linear wachsen, werden als Hyperscale-Systeme bezeichnet, die ich wie folgt definiere: "Hyperskalierbare Systeme weisen ein exponentielles Wachstum der Rechen- und Speicherkapazitäten auf, während die Kosten für den Aufbau, den Betrieb, den Support und die Weiterentwicklung der erforderlichen Software- und Hardwareressourcen linear ansteigen." In diesem Artikel erfährst du mehr über Hyperscale-Systeme.
Kompromisse bei der Skalierbarkeit und Architektur
Skalierbarkeit ist nur eines der vielen Qualitätsattribute oder nichtfunktionalen Anforderungen, die die Sprache der Softwarearchitektur sind. Eine der anhaltenden Komplexitäten der Softwarearchitektur ist die Notwendigkeit von Kompromissen bei den Qualitätsmerkmalen. Grundsätzlich kann sich ein Entwurf, der ein Qualitätsmerkmal begünstigt, negativ oder positiv auf andere auswirken. Zum Beispiel möchten wir vielleicht Logmeldungen schreiben, wenn bestimmte Ereignisse in unseren Diensten auftreten, damit wir Forensik betreiben und die Fehlersuche in unserem Code unterstützen können. Wir müssen jedoch vorsichtig sein, wie viele Ereignisse wir erfassen, denn die Protokollierung führt zu Overhead und wirkt sich negativ auf Leistung und Kosten aus.
Erfahrene Softwarearchitekten bewegen sich ständig auf einem schmalen Grat, indem sie ihre Entwürfe so gestalten, dass sie wichtige Qualitätsmerkmale erfüllen und gleichzeitig die negativen Auswirkungen auf andere Qualitätsmerkmale minimieren.
Bei der Skalierbarkeit ist das nicht anders. Wenn wir die Skalierbarkeit eines Systems in den Mittelpunkt stellen, müssen wir sorgfältig abwägen, wie sich unser Design auf andere höchst wünschenswerte Eigenschaften wie Leistung, Verfügbarkeit, Sicherheit und die oft übersehene Verwaltbarkeit auswirkt. In den folgenden Abschnitten werde ich kurz auf einige dieser Kompromisse eingehen.
Leistung
Es gibt einen einfachen Weg, den Unterschied zwischen Leistung und Skalierbarkeit zu verstehen. Wenn wir uns ein Leistungsziel setzen, versuchen wir, einige gewünschte Kennzahlen für einzelne Anfragen zu erfüllen. Das kann eine mittlere Antwortzeit von weniger als 2 Sekunden sein oder ein Worst-Case-Ziel wie die 99-prozentige Antwortzeit von weniger als 3 Sekunden.
Die Verbesserung der Leistung ist im Allgemeinen eine gute Sache für die Skalierbarkeit. Wenn wir die Leistung einzelner Anfragen verbessern, schaffen wir mehr Kapazität in unserem System, was uns bei der Skalierbarkeit hilft, da wir die ungenutzte Kapazität nutzen können, um mehr Anfragen zu bearbeiten.
Allerdings ist das nicht immer so einfach. Wir können die Antwortzeiten auf verschiedene Arten verringern. Wir können unseren Code sorgfältig optimieren, indem wir z. B. unnötiges Kopieren von Objekten entfernen, eine schnellere JSON-Serialisierungsbibliothek verwenden oder den Code sogar komplett in einer schnelleren Programmiersprache neu schreiben. Diese Ansätze optimieren die Leistung, ohne den Ressourcenverbrauch zu erhöhen.
Ein alternativer Ansatz könnte darin bestehen, einzelne Anfragen zu optimieren, indem häufig genutzte Zustände im Speicher gehalten werden, anstatt bei jeder Anfrage in die Datenbank zu schreiben. Der Wegfall eines Datenbankzugriffs beschleunigt fast immer die Abläufe. Wenn unser System jedoch über einen längeren Zeitraum große Mengen an Daten im Speicher hält, müssen wir möglicherweise (und in einem stark ausgelasteten System auch) die Anzahl der Anfragen, die unser System bearbeiten kann, sorgfältig verwalten. Dies wird wahrscheinlich die Skalierbarkeit verringern, da unser Optimierungsansatz für einzelne Anfragen mehr Ressourcen (in diesem Fall Speicher) verbraucht als die ursprüngliche Lösung und somit die Systemkapazität verringert.
Dieses Spannungsverhältnis zwischen Leistung und Skalierbarkeit wird im Laufe dieses Buches immer wieder auftauchen. Manchmal ist es sogar sinnvoll, einzelne Anfragen etwas langsamer zu machen, damit wir zusätzliche Systemkapazitäten nutzen können. Ein gutes Beispiel dafür ist der Lastausgleich, den ich im nächsten Kapitel beschreibe.
Verfügbarkeit
Verfügbarkeit und Skalierbarkeit sind im Allgemeinen sehr kompatible Partner. Wenn wir unsere Systeme durch die Replizierung von Ressourcen skalieren, schaffen wir mehrere Instanzen von Diensten, die zur Bearbeitung von Anfragen beliebiger Nutzer genutzt werden können. Wenn eine unserer Instanzen fehlschlägt, bleiben die anderen verfügbar. Das System leidet lediglich unter der verringerten Kapazität aufgrund einer fehlgeschlagenen, nicht verfügbaren Ressource. Ähnlich verhält es sich mit der Replikation von Netzwerkverbindungen, Netzwerkroutern, Festplatten und so ziemlich jeder Ressource in einem Computersystem.
Kompliziert wird es mit der Skalierbarkeit und der Verfügbarkeit, wenn es um den Zustand geht. Denk an eine Datenbank. Wenn unser einzelner Datenbankserver überlastet ist, können wir ihn replizieren und Anfragen an beide Instanzen senden. Das erhöht auch die Verfügbarkeit, da wir den Ausfall einer Instanz tolerieren können. Dieses System funktioniert gut, wenn unsere Datenbanken nur lesbar sind. Aber sobald wir eine Instanz aktualisieren, müssen wir irgendwie herausfinden, wie und wann wir die andere Instanz aktualisieren. Hier kommt das Problem der Replikat-Konsistenz zum Vorschein.
Wann immer ein Zustand aus Gründen der Skalierbarkeit und Verfügbarkeit repliziert wird, müssen wir uns mit der Konsistenz befassen. Dies wird ein wichtiges Thema sein, wenn ich in Teil III dieses Buches über verteilte Datenbanken spreche.
Sicherheit
Sicherheit ist ein komplexes, hochtechnisches Thema, das ein eigenes Buch wert ist. Niemand möchte ein unsicheres System benutzen, und Systeme, die gehackt werden und Nutzerdaten gefährden, führen dazu, dass CTOs zurücktreten und im Extremfall Unternehmen fehlschlagen.
Die grundlegenden Elemente eines sicheren Systems sind Authentifizierung, Autorisierung und Integrität. Wir müssen sicherstellen, dass Daten bei der Übertragung über Netzwerke nicht abgefangen werden können und dass niemand auf die gespeicherten Daten zugreifen kann, der keine Zugriffsberechtigung für diese Daten hat. Im Grunde genommen möchte ich nicht, dass jemand meine Kreditkartennummer sieht, wenn sie zwischen Systemen übertragen oder in der Datenbank eines Unternehmens gespeichert wird.
Daher ist Sicherheit ein notwendiges Qualitätsmerkmal für alle Systeme, die mit dem Internet verbunden sind. Die Kosten für den Aufbau sicherer Systeme lassen sich nicht vermeiden. Deshalb wollen wir kurz untersuchen, wie sie sich auf Leistung und Skalierbarkeit auswirken.
Auf der Netzwerkebene nutzen Systeme routinemäßig das Transport Layer Security (TLS)-Protokoll, das auf TCP/IP aufsetzt (siehe Kapitel 3). TLS bietet Verschlüsselung, Authentifizierung und Integrität durch asymmetrische Kryptografie. Der Aufbau einer sicheren Verbindung ist mit Leistungseinbußen verbunden, da beide Parteien Schlüssel erzeugen und austauschen müssen. Der TLS-Verbindungsaufbau umfasst auch den Austausch von Zertifikaten, um die Identität des Servers (und optional des Clients) zu überprüfen, sowie die Auswahl eines Algorithmus, der sicherstellt, dass die Daten während der Übertragung nicht verfälscht werden. Sobald eine Verbindung hergestellt ist, werden die Daten während des Fluges mit symmetrischer Kryptografie verschlüsselt, was nur geringfügige Leistungseinbußen mit sich bringt, da moderne CPUs über spezielle Verschlüsselungshardware verfügen. Der Verbindungsaufbau erfordert normalerweise zwei Nachrichtenaustausche zwischen Client und Server und ist daher vergleichsweise langsam. Die Wiederverwendung von Verbindungen minimiert diese Leistungseinbußen so weit wie möglich.
Es gibt mehrere Möglichkeiten, Daten im Ruhezustand zu schützen. Beliebte Datenbank-Engines wie SQL Server und Oracle verfügen über Funktionen wie die transparente Datenverschlüsselung (TDE), die eine effiziente Verschlüsselung auf Dateiebene ermöglicht. Feinere Verschlüsselungsmechanismen bis hin zur Feldebene werden in regulierten Branchen wie dem Finanzwesen immer häufiger benötigt. Auch Cloud-Provider bieten verschiedene Funktionen an, die sicherstellen, dass die in Cloud-basierten Datenspeichern gespeicherten Daten sicher sind. Die Kosten für sichere Daten im Ruhezustand sind einfach Kosten, die getragen werden müssen, um Sicherheit zu erreichen - Studien zufolge liegen die Kosten im Bereich von 5-10%.
Eine andere Sichtweise auf die Sicherheit ist der CIA-Dreiklang, der für Vertraulichkeit, Integrität und Verfügbarkeit steht. Die ersten beiden sind ziemlich genau das, was ich oben beschrieben habe. Die Verfügbarkeit bezieht sich auf die Fähigkeit eines Systems, bei Angriffen von Gegnern zuverlässig zu funktionieren. Bei solchen Angriffen kann es sich um Versuche handeln, eine Schwäche im Systemdesign auszunutzen, um das System zum Absturz zu bringen. Ein anderer Angriff ist der klassische Distributed Denial of Service (DDoS), bei dem ein Gegner die Kontrolle über eine Vielzahl von Systemen und Geräten erlangt und eine Flut von Anfragen koordiniert, die ein System effektiv unzugänglich macht.
Im Allgemeinen sind Sicherheit und Skalierbarkeit gegensätzliche Kräfte. Sicherheit führt zwangsläufig zu einer Verschlechterung der Leistung. Je mehr Sicherheitsebenen ein System umfasst, desto stärker wird die Leistung und damit die Skalierbarkeit beeinträchtigt. Das wirkt sich letztendlich auf das Endergebnis aus - es werden mehr leistungsstarke und teure Ressourcen benötigt, um die Leistungs- und Skalierbarkeitsanforderungen eines Systems zu erfüllen.
Verwaltbarkeit
Da die Systeme, die wir bauen, immer verteilter und komplexer in ihren Interaktionen werden, rücken ihre Verwaltung und ihr Betrieb in den Vordergrund. Wir müssen darauf achten, dass jede Komponente so funktioniert, wie wir es erwarten, und dass die Leistung weiterhin den Erwartungen entspricht.
Die Plattformen und Technologien, die wir zum Aufbau unserer Systeme verwenden, bieten eine Vielzahl von standardbasierten und proprietären Überwachungs-Tools, die für diese Zwecke genutzt werden können. Überwachungs-Dashboards können verwendet werden, um den aktuellen Zustand und das Verhalten der einzelnen Systemkomponenten zu überprüfen. Diese Dashboards, die mit hochgradig anpassbaren und offenen Tools wie Grafana erstellt werden, können Systemmetriken anzeigen und Warnungen senden, wenn verschiedene Schwellenwerte oder Ereignisse eintreten, die die Aufmerksamkeit des Betreibers erfordern. Der Begriff für diese ausgefeilte Überwachungsfunktion lautet Observability.
Es gibt verschiedene APIs wie MBeans von Java, AWS CloudWatch und AppMetrics von Python, die Ingenieure nutzen können, um benutzerdefinierte Metriken für ihre Systeme zu erfassen - ein typisches Beispiel sind die Antwortzeiten von Anfragen. Mithilfe dieser APIs können Überwachungs-Dashboards so angepasst werden, dass sie Live-Diagramme und -Grafiken liefern, die tiefe Einblicke in das Verhalten eines Systems geben. Solche Einblicke sind von unschätzbarem Wert, um den laufenden Betrieb zu gewährleisten und Teile des Systems hervorzuheben, die möglicherweise optimiert oder repliziert werden müssen.
Die Skalierung eines Systems bedeutet immer, dass neue Systemkomponenten hinzugefügt werden - Hardware und Software. Wenn die Anzahl der Komponenten wächst, müssen wir mehr bewegliche Teile überwachen und verwalten. Das ist nie ohne Aufwand. Der Betrieb des Systems wird dadurch komplexer und es entstehen Kosten für den Überwachungscode, der entwickelt werden muss, und für die Weiterentwicklung der Überwachungsplattform.
Die einzige Möglichkeit, die Kosten und die Komplexität der Verwaltung zu kontrollieren, wenn wir skalieren, ist die Automatisierung. An dieser Stelle kommt die Welt von DevOps ins Spiel. DevOps ist eine Reihe von Praktiken und Werkzeugen, die Softwareentwicklung und Systembetrieb miteinander verbinden. DevOps verkürzt den Lebenszyklus der Entwicklung neuer Funktionen und automatisiert die laufenden Tests, die Bereitstellung, die Verwaltung, das Upgrade und die Überwachung des Systems. DevOps ist ein wesentlicher Bestandteil jedes erfolgreichen skalierbaren Systems.
Zusammenfassung und weiterführende Literatur
Die Fähigkeit, eine Anwendung schnell und kosteneffizient zu skalieren, sollte eine entscheidende Eigenschaft der Softwarearchitektur moderner Internetanwendungen sein. Es gibt zwei grundlegende Möglichkeiten, Skalierbarkeit zu erreichen, nämlich die Erhöhung der Systemkapazität, in der Regel durch Replikation, und die Leistungsoptimierung der Systemkomponenten.
Wie jedes Qualitätsmerkmal der Softwarearchitektur kann auch die Skalierbarkeit nicht isoliert betrachtet werden. Sie ist unweigerlich mit komplexen Kompromissen verbunden, die auf die Anforderungen einer Anwendung abgestimmt werden müssen. Auf diese grundlegenden Kompromisse werde ich im weiteren Verlauf dieses Buches eingehen, beginnend mit dem nächsten Kapitel, in dem ich konkrete Architekturansätze zum Erreichen von Skalierbarkeit beschreibe.
1 Neil Ernst et al., Technical Debt in Practice: How to Find It and Fix It (MIT Press, 2021).
2 Ian Gorton et al., "Data-Intensive Computing in the 21st Century", Computer 41, no. 4 (April 2008): 30-32.
3 Rachel Potvin und Josh Levenberg, "Why Google Stores Billions of Lines of Code in a Single Repository", Communications of the ACM 59, 7 (Juli 2016): 78-87.
4 Der Bericht ist nichts für Zartbesaitete. Hier ist ein anschaulicher Datenpunkt, der ab 13 Jahren freigegeben ist: Die Website wurde im Jahr 2019 42 Milliarden Mal besucht! Einige der Statistiken werden deine Augen sicherlich zum Leuchten bringen.
Get Grundlagen der skalierbaren Systeme now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.