Von allen Qualitätsmerkmalen erscheint die Systemkapazität oft als die Schwierigste. Sie hat die unangenehme Eigenschaft sich unter Last zu verändern. Kaum ist man der Meinung, nun laufe das System endlich stabil, taucht schon ein neues Szenario auf, das wieder eine Outage verursacht. Es ist zum Haare raufen – aber dafür bleibt es immer interessant!
„Capacity is fundamentally a measure of how much revenue the system can generate.“
Michael T. Nygard
Die Kapazität bezeichnet die Anzahl der Transaktionen, die ein System bei akzeptabler Performance leisten kann. Um sie zu ermitteln, entwirft man Lastszenarien und misst dann die Antwortzeit für 50, 100 oder 1 000 Benutzer. Die Kapazitätsgrenze ist der Punkt, an dem die Performance schließlich einbricht. Eigentlich ganz einfach, aber leider gibt es eine sehr große Anzahl von möglichen Lastszenarien, und das Verhalten der Anwendung kann je nach Szenario stark variieren. Man ist also gut beraten, sich auf die wahrscheinlichsten Szenarien zu konzentrieren. Aber welche sind das und welche Einflussfaktoren spielen die größte Rolle?
Die Diskussion um die Kapazität ist zentral, denn Überkapazitäten im Betrieb kosten bares Geld, wohingegen Unterkapazität zu schwindenden Besucherzahlen führt. Eine genaue Analyse ist also wesentlich für den Erfolg!
Die Kapazität ist ein Submerkmal der Performance. Abbildung 1 zeigt die Konzepte rund um das Thema. Wie im Kasten zur Terminologie beschrieben, definiert die Kapazität die maximale Bandbreite, die ein System unter Last bei akzeptabler Antwortzeit liefern kann. Um die Kapazität bestimmen zu können, sind im Wesentlichen die folgenden Schritte notwendig:
Zuerst werden die Anforderungen an die Kapazität und Performance ermittelt: Welche Antwortzeiten sind für das System akzeptabel? Wie viele Besucher soll das System bedienen können und wie viele von ihnen kommen gleichzeitig? Die Anforderungen sollten sich auf konkrete Geschäftsvorfälle des Systems beziehen, auch um den Stakeholdern einen besseren Zugang zu gewähren. Siehe hierzu auch die Qualitätsszenarien am Ende des Artikels.
In einem nächsten Schritt werden die gewünschten Anforderungen mit der Kontext- und Bausteinsicht des Systems verglichen. Ein Sanity Check ist besonders dann wichtig, wenn Teile des Systems nicht unter der eigenen Kontrolle stehen. Existiert beispielsweise ein externer Dienst, der für die Preisberechnung zuständig ist, sollte man sich zunächst Gewissheit über dessen Kapazität verschaffen, bevor man weitere Tests plant. Im Bereich der Infrastruktur lohnt es sich, Netzwerk und Storage zu analysieren, um schon im Vorfeld die Weichen richtig stellen zu können. Diese Erkenntnisse können dann in das Testdesign einfließen, um neuralgische Punkte besonders genau analysieren zu können. Auf Basis der gewünschten Qualitätsszenarien lassen sich dann beispielsweise diese drei Tests durchführen (siehe hierzu auch [1]):
Der Apdex ist ein Messwert für verschiedene Subjekte, der auf einer Skala von „exzellent“ bis „inaktezptabel“ leicht interpretiert werden kann. Er findet nicht nur in der Informatik Anwendung, kommt aber daher, und eignet sich für Diskussionen rund um die Performance sehr gut, da er auch vom Fach verstanden wird. Bei dieser Methode können die Antwortzweiten des Systems gemessen und in „zufrieden“, „tolerabel“ und „geht gar nicht“ durch zwei Schwellwerte unterteilt werden. Der Apdex berechnet sich dann nach folgender Formel:
Der beste Apdex ist also 1, aber das ist effektiv nicht zu erreichen. Werte über 0,9 sind schon sehr gut und nicht einfach zu erreichen.
Die Speicherung von Daten ist eine zentrale Funktionalität von Webapplikationen. Dabei können Daten entweder direkt in ein Filesystem oder eine Datenbank geschrieben werden. Als Storage-System findet man häufig große Storage Area Networks (SAN). Diese Systeme bieten hochverfügbaren, schnellen Storage. Reicht der Durchsatz dieser Systeme nicht mehr aus, müssen größere und schnellere Controller beschafft werden. Aus dem Blickwinkel der Kapazität ist ein SAN eine gute Sache, da diese Systeme ausreichend schnell dimensioniert werden können. Da hier aber ein eigenes dediziertes Netzwerk neben IP aufgebaut wird, sind auch die Kosten hoch. Im Gegensatz dazu steht die Verwendung von Commodity Hardware, also günstige Server mit lokalen Disks. Auf dieser günstigen Hardware können moderne NoSQL-Datenbanken betrieben werden, welche die Redundanz und das Clustering übernehmen.
In beiden Modellen muss man die passende Speichertechnologie auswählen. Mit dem Aufkommen von SSDs hat man neben konventionellen Festplatten und In-Memory-Speichern eine dritte Auswahlmöglichkeit. Die beiden wichtigsten Merkmale dabei sind die Anzahl an I/O-Operationen pro Sekunde (IOPS) und die verfügbare Speicherkapazität. Entscheidend ist, dass dann Daten auch je nach Anforderung auf der richtigen Speichertechnolgie abgelegt werden. Häufig abgefragte Daten gehören auf eine SSD lokal im Server, damit die Latenz möglichst tief ist. Daten, die sich wenig ändern, wie ein Logfile, kann man auf SATA-Festplatten zentral archivieren.
Neben den blockbasierten Speichertechnologien hat Amazon mit S3 [2] eine neue Art von Datenspeichern geschaffen. Dabei steht der Webapplikation für die Speicherung von Daten kein Filesystem zur Verfügung, sondern eine REST-Schnittstelle. Darüber lassen sich die typischen CRUD-Operationen ausführen. Amazon speichert die Daten hochverfügbar über mehrere Datacenter zu einem günstigen Preis und bietet damit eine Alternative zu den konventionellen Festplatten für die Speicherung und Archivierung von großen Datenmengen.
Die Netzwerkanbindung hat einen starken Einfluss auf die Kapazität einer Webanwendung. Eine optimierte Applikation und viel Rechenkapazität im Datacenter sind nutzlos, wenn zu wenig Bandbreite vorhanden ist, um die Inhalte an die Benutzer auszuliefern. Dabei kann die mindestens benötige Bandbreite einfach ausgerechnet werden: Die durchschnittliche Größe einer Page Views (Cold Cache, also inklusive aller Assets), multipliziert mit der maximal erwarteten Anzahl an Page Views, sollte einen guten Anhaltspunkt für die im Peak benötigte Bandbreite ergeben.
Dabei sollten Downloads und Videos nicht außer Acht gelassen werden.
Bilder bleiben der wichtigste Ansatzpunkt für Optimierung. Im Schnitt werden pro Seite 1,4 MB Bilddaten ausgeliefert.
Es kann häufig Sinn ergeben, diese über ein Content Delivery Network (CDN) verteilen zu lassen, um die eigene Netzwerkanbindung zu entlasten. Routet man den gesamten Traffic einer Webapplikation durch das CDN, enthält man zusätzliche Burst-Kapazität. Die bandbreitenintensiven Teile (Bilder, JavaScript, CSS) werden dann direkt ab CDN ausgeliefert und die eigene Netzwerkanbindung muss, wenn überhaupt, nur noch die eigentlichen HTML-Seiten oder Ajax Requests beantworten. Diese theoretisch „unlimitierte“ Burst-Kapazität hat natürlich ihren Preis: Die meisten CDN-Provider verrechnen eine feste Gebühr pro Gigabyte Traffic. Die Webapplikation überlebt dann zwar den viel höheren Traffic, die Kosten steigen aber linear. Der „Fluch des Erfolgs“ kann hier direkt auf das Portemonnaie durchschlagen.
Bevor also ein CDN eingesetzt wird, sollte man den Bandbreitenbedarf optimieren. Der erste Ansatzpunkt ist sicherlich die HTTP-Compression. Damit werden alle Inhalte komprimiert, was sehr viel Bandbreite einsparen kann. Die Implementierung ist einfach und wird von jedem gängigen Applikations- und Webserver unterstützt. HTTP/2 bietet ebenfalls interessante Funktionalitäten, um das Netzwerk zu entlasten [3]. Header Compression spart Bandbreite, indem wiederkehrende Informationen im HTTP-Header nur einmal pro Session übertragen werden. Die Verwendung von Multiplexing über eine einzelne TCP-Verbindung und einem binären Protokoll erlaubt die effizientere Nutzung der Netzwerkressourcen und steigert somit die Kapazität der Webapplikation.
Bilder bleiben der wichtigste Ansatzpunkt für die Optimierung des Bandbreitenbedarfs. Eine anschauliche Quelle für Daten in diesem Bereich ist das HTTP-Archive. Aktuell werden im Web im Schnitt pro Seite 1,4 MB Bilddaten ausgeliefert [4]. Damit keine Bandbreite verschwendet wird und der Benutzer eine möglichst schnelle Seite hat, müssen Bilder korrekt komprimiert werden. Dieser Umstand ist bereits in der Entwicklung vom Frontend zu berücksichtigen. Natürlich lassen sich Bilder auch später durch Optimierungssoftware verkleinern (beispielsweise mod_pagespeed von Google [5]), im Gegenzug steigert man aber die Komplexität und braucht zusätzliche Rechenkapazität.
Mit der zunehmenden Popularität von Microservices und einer fortgeschrittenen Integration von externen Diensten entstehen neue Herausforderungen, um die Kapazität unserer Services zu garantieren. Wir betrachten zwei Szenarien: Wie kann ich die Kapazität meines Webdiensts sicherstellen? Und welche Risiken entstehen durch unkontrollierbare externe Dienste?
Szenario 1: Wir möchten verschiedenen Plattformen für Preisvergleiche den Zugriff auf die Preise von unserem E-Commerce ermöglichen. Dafür entwerfen wir ein REST-API und öffnen das API für diese Partner. Dazu müssen wir nun sicherstellen, dass niemand die Kapazität unserer Systeme auslasten kann, damit wir unsere Verfügbarkeit gegenüber den externen Systemen garantieren können. Wir haben keine Kontrolle über die Anzahl der Anfragen, welche die Partnerfirmen absetzen. Dies führt zu einem großen Risiko. Throttling kann dieses Risiko abmildern.
Throttling kann in verschiedenen Formen implementiert werden. Zum Beispiel können wir ein Maximum von x Anfragen per Zeitfenster t zulassen, und wenn die Anzahl überschritten ist, nehmen wir keine Anfragen entgegen bis zum nächsten Zeitfenster. In unserem Beispiel ergibt es Sinn, einen solchen Mechanismus einzuführen, um sicherzustellen, dass kein Partner unsere Kapazität überschreitet. In anderen Fällen könnte es ebenfalls wertvoll sein, das API als Dienstleistung mit Quotas zu verkaufen. Ein Paket würde dann z. B. 10 000 Anfragen pro Monat beinhalten. Eine Grundvoraussetzung für Throttling ist außerdem die Identifikation von API-Clients. Unseren Partnern sollten wir beispielsweise dedizierte API-Keys zur Verfügung stellen, um herausfinden zu können, wer uns mit Anfragen überflutet.
Szenario 2: In letzter Minute erreicht uns die Anforderung vom Marketing, dass auf unserer E-Commerce-Startseite der Feed aus dem Produkteblog angezeigt werden soll. Die Logik wird im Backend implementiert, und die Startseite löst eine synchrone Abfrage auf den Blog aus. Somit binden wir die Antwortzeit der Startseite maßgeblich an den Feed. Das skalierbare Design des E-Commerce-Systems wird somit auf die Kapazität des Feeds beschränkt. Im schlimmsten Fall befindet sich der Feed auf einem Shared Hosting mit keinerlei Performancegarantien und SLAs. In diesem Beispiel scheint die Lösung trivial. Wir entfernen den Feed aus dem Backend und implementieren einen asynchronen clientseitigen Aufruf. Aber wenn es sich dabei um eine tief verankerte Geschäftslogik von einem Partner handelt, zum Beispiel eine Bonitätsprüfung, haben wir keinen Einfluss auf die Antwortzeit und können die Implementierung nur gering steuern.
Es ist sehr wichtig, dass wir solche Risiken frühzeitig erkennen (siehe Kapazitätstests unten). Wir können z. B. vertragliche Maßnahmen verhandeln, die ein SLA für den externen Dienst bestimmen. Als eine potenzielle technische Lösung eignet sich Caching. So könnte z. B. ein API-Gateway unsere Kommunikation mit dem externen Dienst koordinieren und für spezifische Anfragen einen Cache aufbauen. API-Gateways können die oben beschriebenen Szenarien unterstützen und bieten je nach Tool noch weitere Funktionalität wie z.B. Authentifizierung, Logging oder Monitoring etc. Die folgenden Open-Source-Tools implementieren solche Gateway-Funktionalitäten: Kong [6], apiman [7] und Tyk [8].
Es passiert immer wieder: Die neue E-Commerce-Lösung wurde in der Nacht ausgerollt und der Newsletter versendet, aber kaum sind die potenziellen Käufer aufgewacht und versuchen einzukaufen, brechen die Systeme zusammen. Im besten Fall skaliert das System automatisch, und neue Knoten lösen das Problem. Im schlimmsten Fall müssen wir wieder zurückrollen. Damit wir das Risiko von einem solchen Debakel minimieren können, führen wir Kapazitätsmessungen durch. Diese Messungen müssen auf das entsprechende Szenario abgestimmt werden. Eine Live-Streaming-Mobile-App erfordert andere Werkzeuge als eine Weblösung. In den nächsten Abschnitten zeigen wir die Methoden und das Vorgehen für Kapazitätsmessungen auf und veranschaulichen diese am Beispiel von Lasttests einer E-Commerce-Lösung.
Die Tests werden auf den Performancezielen der Applikation aufgebaut. Diese nicht funktionalen Anforderungen können in verschiedenen Ausprägungen definiert sein, z. B. in Zeitlimits für einen Business Case oder der Anzahl an erwarteten Benutzern für die Applikation. Diese Anforderungen werden für die Kapazitätstests weiter definiert und anhand der folgenden Faktoren klassifiziert [1]: Verfügbarkeit, Concurrency, Throughput, Antwortszeit, Netzwerkverbrauch und VM-Ausnutzung. Wir möchten beispielsweise, dass 200 Benutzer gleichzeit einkaufen können und unsere geplante Uptime 99,97 Prozent erfüllt. Wichtig ist bei der Verfeinerung der Ziele, dass wir deren Prüfbarkeit garantieren und in Form von Berichten ausweisen können.
Nachdem die Ziele klar definiert sind, müssen die Vorbedingungen für den Kapazitätstest erfüllt werden. Diese sind über verschiedene Disziplinen verteilt und erfordern eine sehr gute Kommunikation [9]. Der Test benötigt ein prüfbares Acceptance-System (dem Produktionssystem möglichst ähnlich) mit einer stabilen Version (Code Freeze) und sollte mit externen Diensten kommunizieren können (keine Mocks). Ebenfalls muss sichergestellt werden, dass das Testsystem für die Dauer des Tests nicht von anderen Stakeholdern verwendet wird. Es ist sehr unglücklich, wenn der Projektleiter zur selben Zeit wie dem Kapazitätstest eine Redaktionsschulung anberaumt hat.
In diesem Beispiel betrachten wir das Testdesign von einem E-Commerce-Lasttest. Im ersten Schritt wird definiert, welche Application Usage Models in einem Test abgebildet werden [10]. Ein solches Modell beinhaltet einen Geschäftsvorfall des E-Commerce-Systems, z. B. das Produkt einkaufen, Checkout etc. Da ein System aus einer großen Anzahl von Cases besteht, muss sichergestellt werden, dass nur die wichtigsten implementiert werden. Die Priorität wird nicht anhand der Komplexität der dahinterstehenden Prozesse und deren Implementierung gewählt, sondern muss der Anwendung entsprechen. Falls bereits ein System im Einsatz ist, können Analytics-Daten hier wichtigen Input liefern.
Der Case Checkout als Beispiel beinhaltet mehrere Schritte: Warenkorb, Adressdaten, Bezahlung und Bestätigung. In der Implementierung eines solchen Usage Models müssen wir berücksichtigen, dass ein Benutzer zwischen diesen Schritten Zeit benötigt, um Formulare auszufüllen. Diese Zeit wird „Think Time“ genannt und sollte nicht statisch hinterlegt werden, sondern pro simulierten Benutzer und Aktion variieren, damit wir eine realistische Verteilung der Anfragen simulieren.
Die Lastszenarien definieren die Anzahl und Frequenz der simulierten Benutzer und welche Aktionen diese ausführen [10]. In unserem E-Commerce-Beispiel nehmen wir eine Conversion von 10 Prozent an. Dies bedeutet, dass in unserem Test ebenfalls 10 Prozent der simulierten Benutzer den Checkout durchlaufen sollen. Die Anzahl simulierter Benutzer kann sich je nach Lastszenario unterscheiden. Ein Volume-Test beinhaltet die erwartete Anzahl Benutzer auf dem System. Für unseren E-Commerce-Shop erwarten wir vor allem während der Geschäftszeit Kunden (B2B). Das heißt, der Volume-Test kann über die erwartete Dauer von 10 Stunden durchgeführt werden. Zusätzlich können wir einen Endurance-Test implementieren, der über 24 Stunden das System testet. Ebenfalls empfiehlt es sich, einen Stresstest durchzuführen, um einerseits die Grenzen des Systems festzustellen und andererseits das Recovery-Verhalten zu testen. Während des Stresstests wird die Anzahl simulierter Benutzer stetig erhöht, bis das System überlastet ist.
Bei den verschiedenen Lastszenarien muss definiert werden, wie wir Last auf das System einführen. Wir unterscheiden hier zwischen Ramp-up, Big Bang und Delay [1]. Big Bang bedeutet, dass simulierte Benutzer zum gleichen Zeitpunkt starten, aber nicht unbedingt das gleiche Ausführen. Das Ramp-up-Verfahren ermöglicht es, inkrementell die Last auf das System zu erhöhen, und Delay verzögert die simulierten Benutzer. Die Verfahren lassen sich auch für komplexe Anwendungsfälle kombinieren.
Um das definierte Testdesign des E-Commerce-Lasttests umzusetzen, können sowohl kommerzielle als auch Open-Source-Tools eingesetzt werden. Apache JMeter [11] und Gatling [12] sind Beispiele aus dem Open-Source-Bereich. Das folgende Codebeispiel skizziert einen einfachen Lasttests mit Gatling. Darin ist das Szenario Produktansicht abgebildet, das zuerst eine Anfrage auf die Kategorie absetzt und nach der Think Time auf die Produktseite navigiert. Gatling-Lasttests werden in Scala geschrieben und halten sich an eine spezifische DSL:
setUp(
scenario("View Product")
.exec(http("Browse Category").get("/category/random"))
.pause(thinkTimeMin, thinkTimeMax)
.exec(http("BrowseProduct").get("/product/books/a-million-random-digits"))
.inject(rampUsers(100) over (1 minute))
).protocols(http.baseURL("http://www.a-random-shop.com"))
Abschließend lassen sich die Performanceziele in Form eines Reportings ausweisen und beurteilen. Es lohnt sich hier, nicht nur die minimalen Metriken zu messen, um unsere Ziele zu veranschaulichen, sondern erste Vorarbeit zu leisten für eine spätere Bottleneck-Analyse. Wir möchten zum Beispiel wissen, ob unsere Datenbank die Last verarbeiten kann. Aber es wäre auch gleich hilfreich, die langsamen Datenbankabfragen zu loggen, um diese bei Problemen analysieren zu können. Es empfiehlt sich, das Reporting in verschiedenen Sichten zu gliedern: Lastsimulation, System und kombinierte Metriken.
Die Lastsimulation kann uns detaillierte Aussagen zur Lastverteilung, Antwortszeiten oder Verfügbarkeit etc. liefern. Anhand von Monitoring können wir die Auslastung unseres Systems messen (Knoten, Datenbank, Cache, Message Queues, externe Dienste, Netzwerk etc.). In der kombinierten Darstellung können wir z. B. die Anzahl an Anfragen pro Sekunde der CPU-Auslastung von unseren Systemen gegenüberstellen. Das Reporting hilft uns ebenfalls, Prognosen für die Skalierbarkeit zu erstellen. Wir möchten sicherstellen, dass wir z. B. weitere Knoten hinzufügen können, wenn wir Traffic Bursts erfahren.
Das Vokabular rund um die Diskussion der Kapzität ist einfach, sollte jedoch trotzdem gut verstanden sein. Die Definitionen in diesem Artikel sind an [13] und [14] angelehnt.
Eine Page View bezeichnet das Laden einer Seite mit allen benötigten Ressourcen. Ein Request ist der Abruf einer einzigen Ressource. Eine einzelne Page besteht immer aus einem einzigen HTTP-Request für das HTML sowie mehreren Requests für alle von der Seite für die Darstellung benötigten Inhalte (CSS Style Sheets, JavaScript-Dateien und Bilder). Eine Verfeinerung der Page View heißt „Above the fold“: Hier geht es darum, nur den Bereich zu messen, den ein Benutzer beim Laden der Seite tatsächlich sehen kann. Der Begriff kommt aus der Zeitungsbranche: „Über der Falz“ ist das, was die Leute morgens am Kiosk schon in der Zeitung sehen können, wenn sie noch im Regal liegt.
Die Summe der eingehenden und ausgehenden Daten heißt Traffic. Traffic kann nur sehr schwer vorhergesagt werden.
Die Performance ist die benötigte Zeit um eine Transaktion auszuführen. Man muss für jedes System individuell definieren, was eine Transaktion ist. Man kann beispielsweise einen Request oder eine Page View als Transaktion definieren. Wichtig ist, dass es die Performance ist, die den Besucher interessiert. Ihm ist daran gelegen, dass seine Anfrage möglichst zügig beantwortet wird. Tabelle 1 gibt das Verhältnis zwischen Performance und Eindruck beim Benutzer an. Der Eindruck wird heute oft als Apdex angegeben.
Der Durchsatz (engl. Throughput) ist die Anzahl der Transaktionen, die ein System pro Zeiteinheit verarbeiten kann.
Die Kapazität ist die maximale Bandbreite, die ein System unter einer bestimmten Last (engl. Workload) bei akzeptabler Performance aufrecht erhalten kann. Damit bestimmt das konkrete Lastszenario die Kapazität. Da sich Lastszenarien nicht kontrollieren lassen, kann es sein, dass sich die Kapazität eines Systems plötzlich dramatisch verändern kann.
Die Skalierbarkeit beschreibt, wie einfach sich die Kapazität eines Systems verändern lässt.
Performance | Eindruck beim Benutzer |
---|---|
0 ¬– 100 ms | Sofort |
100 – 300 ms | Merkbare Latenz |
300 – 1’000 ms | Anwendung funktioniert |
1’000 + ms | Kontextwechsel wahrscheinlich |
10’000 + ms | Abbruch |
Tabelle 1: Subjektive Performance nach Grigorik [13]
Die folgenden Szenarien eignen sich zur Diskussion mit dem Fach. Wie immer, sollten die Szenarien möglichst früh mit dem Fach verhandelt werden.
Wir haben gesehen, dass die Kenntnis der Kapazität zentral für geschäftskritische Systeme ist. Sie muss analysiert und gemessen werden, um Sicherheit im Servicelevel erlangen zu können. Insbesondere die Messung im laufenden Betrieb ist wichtig, denn schon eine geringe Veränderung der Lastszenarien kann unsere Kapazität entscheidend verändern. Neben der Analyse der Kapazität ist also auch eine gute Prüfbarkeit des Systems eine Vorraussetzung [15]. Die Durchführung solcher Maßnahmen benötigt jedoch Budget, bei komplexen Systemen mit vielen integrierten Services sogar erhebliches Budget. Eine interessante Frage in diesem Zusammenhang ist, ob Kapazitätstests von einem Systemhaus im Rahmen der „Due Diligence“ stets gemacht werden sollten oder ob die Kosten anderweitig getragen werden müssen. Dies führt dann zur Frage nach dem Risiko und der Verantwortlichkeit, wenn man keine Kapazitätstests durchführt. Dafür, dass wir in einer chaotischen Welt leben, geschehen ja erstaunlich wenig Fehler! Dennoch: Ein geschäftsrelevantes System sollte möglichst genau vermessen werden, denn Outages senken das Vertrauen der Benutzer und führen zu Kundenverlust.
Aufmacherbild: Pulling Out My Hair von Shutterstock / Urheberrecht: Cameron Whitman