Suche
Was die Qualität von Websystemen ausmacht - Teil 11: Skalierbarkeit

Wenn der Traffic steigt: Methoden für mehr Elastizität in Webanwendungen

Nicolas Bär, Daniel Takai

© Shutterstock / welcomia

Erfolg im Web ist ein zweischneidiges Schwert. Gehen Besucherzahlen plötzlich durch die Decke, sinkt die Performance in den Keller. Leider lässt sich aber nicht jedes System flexibel skalieren, um den wechselnden Anforderungen Rechnung zu tragen. In diesem Artikel präsentieren wir, was Skalierbarkeit für Architektur bedeutet und auf welche Punkte man achten sollte.

Es gibt viele Gründe für die plötzliche Popularität einer Website. Der bekannteste ist wohl der so genannte Slashdot-Effekt. Slashdot war mal ein sehr beliebter Newsservice mit hohen Besucherzahlen, der heute aber an Bedeutung verloren hat. Erschien damals ein Artikel mit einem Link auf eine weniger bekannte Seite, so verspürte diese einen rasanten Anstieg im Traffic. Sie wurde „geslashdottet“ und oft endete dies mit einem totalen technischen Versagen, weil die Kapazität nicht ausreichte. Genau hier kommt die Skalierbarkeit (Abb. 1) ins Spiel: Sie bestimmt, zu welchem Grad sich die Kapazität eines Systems anpassen lässt. Im Gegensatz zu einem DOS-Angriff, ist ein Abschuss durch Slashdotting ungewollt.

takai_skalierbarkeit_1

Abb.1: Konzepte rund um die Skalierbarkeit

Dank moderner Cloud-Infrastruktur haben wir heute Mittel und Wege, um dem Slashdot-Effekt wirkungsvoll begegnen zu können. Allerdings müssen wir hierfür einiges tun, denn geschenkt bekommt man die Skalierbarkeit leider nicht. Grundsätzlich unterscheidet man zwischen vertikaler und horizontaler Skalierbarkeit. Horizontale Skalierbarkeit (scaling out) bedeutet, dass mehr logische Einheiten verfügbar gemacht werden, z. B. durch das Aufschalten neuer Server. Vertikale Skalierung (scaling up) meint das Hinzufügen von mehr Ressourcen zu einer logischen Einheit, beispielsweise durch die Bereitstellung von zusätzlichem Hauptspeicher oder eine zusätzliche CPU. Hierfür musste man früher neue Mainboards installieren, heute ist es eine Konfigurationseinstellung im Hypervisor. In Cloud-Infrastrukturen heißt horizontale Skalierbarkeit Elastizität [1].

Die Elastizität ist zusammen mit der Resilience eine der beiden Hauptmerkmale einer Cloud-Native-Applikation. Damit eine Anwendung also tatsächlich von den Vorzügen einer Cloud-Infrastruktur profitieren kann, muss sie vollständig horizontal skalierbar sein. Es reicht also nicht, eine bestehende Anwendung auf einer VM zu installieren. Das Anwendungsdesign und die Systemarchitektur müssen für Cloud-native ausgelegt sein. Bei bestehenden, proprietären Systemen kommt außerdem das verwendete Lizenzmodell hinzu. Oft lassen sich diese Anwendungen deswegen gar nicht dynamisch skalieren, sodass Workarounds wie das Bridgehead Pattern notwendig werden (mehr im Abschnitt „Dynamisch dank Bridgehead“).

In der Regel lässt sich auch nur eine Cloud-Native-Anwendung dynamisch skalieren. Bei einer dynamischen Skalierung kann im laufenden Betrieb horizontal skaliert werden, das heißt, es werden neue Instanzen hinzugefügt. Vertikale Skalierung ist in der Cloud zwar theoretisch ebenfalls dynamisch denkbar, aber das kann nicht mal Amazon EC2. Hier muss bei der Anpassung der Instanzgröße die Maschine neu gestartet werden.

Skalierung in der Schichtenarchitektur

Eine moderne, weltweit verteile Webanwendung besteht aus dem Frontend und dem Content Delivery Network (CDN), skizziert in Abbildung 2. Das Frontend lebt auf dem Browser und skaliert unkontrolliert über die Menge der Benutzer, die das System verwenden. Das CDN skaliert durch den betreibenden Provider automatisch und hält unsere Assets lokal beim Benutzer vor. Hier haben wir nur die Möglichkeit der Skalierung durch die Auswahl des richtigen Providers. Das Backend und die Integrationsschicht sind durch ihre Architektur durch uns kontrollierbar, hier können wir dann auch flexibel skalieren, solange die Architektur das zulässt. In beiden Schichten kommen unterschiedliche Technologien zum Einsatz. Im Rahmen des Trends hin zu Microservices verlagern sich die Geschäfts- und Datendienste stetig auf die Integrationsschicht, sodass die Backend-Schicht immer leichtgewichtiger wird. Auf der anderen Seite finden wir im Backend die schwergewichtigen, proprietären Monolithen großer CMS- und E-Commerce-Systeme, die sich schon aufgrund ihrer Lizenzmodelle nicht dynamisch skalieren lassen. Hinter der Integrationsschicht finden wir in Abbildung 2 zusätzliche zu integrierende Dienste, die wir nicht selbst kontrollieren können.

In Bezug auf die Skalierung ist so eine Schichtenarchitektur heikel, weil es zum einen pro Schicht eine eigene Skalierungsstrategie benötigt und zum anderen die Skalierung einer Schicht von der Skalierbarkeit der darunter liegenden Schicht abhängig ist. Angenommen, man hat für eine linear skalierende Integrationsschicht gesorgt, aber diese hängt leider von einem Service ab, der sich nicht oder nur schwierig skalieren lässt. Solch einen Service nennt man dann einen Bottleneck. Bottlenecks sollten im Vorfeld in der Architektur erkannt und möglichst gut aufgelöst werden.

Das skalierbarste Design heißt shared nothing. Bei diesem Design ist jede Instanz völlig unabhängig von allen anderen Systemteilen und kann dadurch linear skalierbare Kapazität erreichen. Fantastisch, aber das muss man erstmal schaffen. Das bedeutet unter anderem, das gesamte Messaging asynchron machen zu müssen.

takai_skalierbarkeit_2

Abb. 2: Schichtenarchitektur eines Websystems

Stateless versus stateful

Eine Grundbedingung, um eine Schicht unserer Applikation horizontal skalieren zu können, sind die gespeicherten Informationen (State), die diese Schicht enthält. Betrachten wir als Beispiel einen einfachen E-Commerce Store. Das HTTPS-Protokoll, auf dem wir unser Backend erreichen können, ist stateless. HTTPS speichert keine Informationen, die einen Benutzer identifizieren. Um die eindeutige Identifikation von einem Kunden zu ermöglichen, verwenden wir ein Cookie. Das Cookie beinhaltet eine Identifikationsnummer, die uns ermöglicht, Aktionen des Kunden festzuhalten und personalisierte Informationen darzustellen. Unser Backend muss z. B. den Warenkorb des Kunden speichern. Wo wir diesen Warenkorb abspeichern, wird die Skalierbarkeit unseres Backends beeinflussen.

Die beste Performance erreicht das Backend, wenn der Warenkorb direkt im Memory vom System abgelegt wird. Wenn wir aber horizontal skalieren möchten, dann sprechen wir mit potenziell dutzenden Systemen und müssen aber den Warenkorb von einer Person ausfindig machen können. Man spricht in diesem Fall von „sticky sessions“: Anfragen von einer Session müssen immer auf das gleiche System geroutet werden. Mit einem Load Balancer kann man dieses Routing sicherstellen, aber man verliert die Möglichkeit, die Systeme beliebig zu skalieren, da ein Benutzer immer mit dem gleichen System kommunizieren muss. Wenn wir also horizontal skalieren und neue Systeme zu unserem Backend hinzufügen, können wir den bestehenden Traffic nicht einfach neu verteilen, sondern nur neue Sessions verteilen. Wenn wir ein System wieder entfernen, müssen wir erst die Sessions auslaufen lassen, damit der Kunde nicht während des Einkaufs plötzlich einen leeren Warenkorb hat.

E-Commerce-Systeme bieten normalerweise die Möglichkeit, eine Session extern zu speichern und somit den State zu verlagern und stateless zu werden. Je nach Anspruch an die Performance kann die Session in eine Datenbank, ein Dateisystem oder eine Caching-Lösung geschrieben werden. Die Session muss bei jedem Aufruf abgefragt werden. Es eignet sich eine In-Memory-Lösung, wie Memcached oder Redis zu verwenden. Wenn wir nun den State aus unserem Backend verlagert haben, kann dieses beliebig skaliert werden – mit dem State haben wir potenziell aber auch das Bottleneck verlagert.

Daten konsistent halten

Wir führen das Beispiel von oben weiter und speichern die Session (Warenkorb) in einer Datenbank. Unser E-Commerce-System gewinnt an Beliebtheit, und bei jedem Newsletter mit dem nächsten Sonderangebot bleiben unsere Anfragen an der Datenbank hängen, weil diese einfach nicht genug Kapazität hat. Gemäß dem CAP-Theorem können wir unsere Datenbank nicht beliebig skalieren und gleichzeitig die Konsistenz der Daten garantieren [2]. Eine Datenbank kann auf zwei verschiedene Arten skaliert werden: Replikation und Sharding.

Die Replikation ermöglicht, die Daten hochverfügbar auf mehrere Instanzen als Kopie zu halten. Das heißt, wir können eine Datenbank auf drei Systeme replizieren. Wenn wir nun ein Update an unserem Warenkorb vornehmen, muss die Datenbank sicherstellen, dass die Änderungen auf allen drei Systemen erfolgt. Je nach Performanceansprüchen kann die Transaktion beendet werden, wenn ein System die Änderung speichert. Man spricht von „Eventual Consistency“, da alle Anfragen auf die anderen zwei Systeme die Änderungen zu diesem Zeitpunkt noch nicht beinhalten. Wenn wir einem Kunden, nachdem er ein Produkt zum Warenkorb hinzufügt, einen leeren Warenkorb anzeigen, weil die Abfragen auf verschiedenen Systemen unsere Datenbank ausgeführt wurde, wäre das sehr verwirrend. Wir möchten also die Konsistenz der Daten garantieren.

Die zweite Möglichkeit zu skalieren bietet Sharding. Mit Sharding verteilt man die Daten auf mehrere Systeme anhand von einer bestimmten Eigenschaft, beispielsweise einem Sharding Key. Sharding ist eine Shared-nothing-Architektur für Datenbanken zur Steigerung der Performance. Wir können unsere Warenkorbtabelle nach Benutzer auf verschiedene Systeme verteilen – Benutzername als Sharding Key –, und z. B. unsere Produkte im Shop ebenfalls auf die verschiedenen Systeme verteilen – Artikelnummer als Sharding Key. Wenn wir nun aber zu unserem Warenkorb die Produkte anzeigen möchten, müssen wir die zwei Tabellen joinen. Da wir unsere Daten auf mehrere Systeme verteilen, muss ein solcher Join ebenfalls über die verschiedenen Systeme verteilt werden. Das kostet sehr viel Performance.

Wenn unsere Ansprüche an die Konsistenz nicht zeitsensitiv sind, kann die „Eventual Consistency“ die Skalierbarkeit eines Systems erhöhen. Content Management bietet sich hier als gutes Beispiel an. Wenn wir einen neuen Artikel schreiben, muss dieser nicht innerhalb von Nanosekunden für die Benutzer ersichtlich sein. Dieser Anspruch erlaubt uns, den Artikel an alle unsere Content Management Backends per Replikation zu verteilen.

Cookie-Cutter-Architektur: Unterlast vermeiden

Besteht eine Architektur aus verschiedenen Services, so stellt sich die Frage, wie man diese am besten verteilt. Stellt man für jeden Service eine eigene Maschine bereit, benötigt man hierfür sehr viele und es kann sein, dass viele von ihnen nur wenig Last erhalten, d. h. die Nutzung der Ressourcen ist nicht effektiv. Ein Architekturmuster zur Vermeidung von Unterlast ist die Cookie-Cutter-Architektur. Hierbei werden alle Services auf einem Image provisioniert, das dann horizontal skaliert wird. So ist es dann möglich, die Last auf eine optimale Anzahl von Maschinen zu verteilen. Allerdings hat dieses Muster nicht nur Vorteile, denn alle Services müssen denselben Technologiestack nutzen, und dies erzeugt Abhängigkeiten unter den Services, die wir sonst nicht hätten.

Dynamik dank Bridgehead-Pattern

Das Bridgehead-Pattern haben wir selbst erfunden, bzw. haben es selbst erfinden müssen. Wenn wir mit großen proprietären E-Commerce-Frameworks arbeiten, sind diese skalierbar, aber nicht dynamisch. Zudem ist die Skalierung mit erheblichen Lizenzkosten verbunden, sodass es sich für den Auftraggeber nicht rechnet, die theoretische Maximalkapazität zu lizensieren. Zu bestimmten Zeitpunkten wird diese Maximalkapazität jedoch erreicht, man denke nur an das Weihnachtsgeschäft oder den berüchtigten Cyber Monday. Oft überlegt sich das Marketing dann auch zu just diesem Zeitpunkt, die besten Angebote des Jahres zu veröffentlichen, die große Scharen von Käufern anlocken. Ein Workaround um diese restriktiven Lizenzmodelle ist eine Entkoppelung dieser Spezialangebote vom Rest des Systems. Man baut dafür dann eine minimale Shared-Nothing-Umgebung mit asynchronem Bestellprozess. Dabei wird die Bestellung des Kunden aufgenommen, die Bezahlung findet jedoch später statt, wenn das System erneut Kapazität hat. Man kann den Käufer beispielsweise per E-Mail informieren, dass er nun seine Zahlungsdetails angeben darf. So kann man die Last auf das System steuern und eine ausgezeichnete User Experience bieten (very snappy), verliert jedoch unter Umständen die Gruppe der Impulskäufer. Ein kleiner Preis dafür, dass das System überleben kann, finden wir.

Infrastructure as Code

Nun sind wir soweit, unser CDN ist international verteilt, das Backend ist stateless, die Datenbank kann mittels Sharding skaliert werden und unsere Integrationen sind asynchron: Wir sind bereit für den nächsten Cyber Monday.

Nicht ganz! Wir müssen noch sicherstellen, dass die entsprechenden Systeme von der Infrastruktur bereitgestellt werden. Für den Cyber Monday können wir die Systeme eine Woche vorher bestellen und die Zeit für das Set-up antizipieren. Beim Slashdot-Effekt haben wir diese Chance nicht. Automatische Skalierbarkeit der Infrastruktur ist der nächste Schritt, um auch bei spontanen Traffic Bursts die Verfügbarkeit der Seite zu gewährleisten. Es gibt viele verschiedene Möglichkeiten, automatisch neue Systeme zu provisionieren. Als Grundvoraussetzung definieren wir die folgenden Punkte: Configuration Management zur Nachvollziehbarkeit und Wiederholbarkeit eines System-Set-up, Automatisches Deployment der Applikation, Dynamisches Load Balancing und ein Virtualisierungslayer mit API. Erst wenn wir Systeme so bereitstellen, dass wir sie jederzeit zerstören und automatisch neu aufbauen können, kann die Infrastruktur unsere Skalierung ermöglichen.

Mögliche Qualitätsszenarien

Qualitätsszenarien zur Skalierbarkeit beschäftigen sich hauptsächlich mit den Auswirkungen des Hinzufügens oder Wegnehmens von Ressourcen, und die Messungen reflektieren die Änderungen an Last und Verfügbarkeit [1]. Wie bei allen Qualitätsszenarien ist die Verständlichkeit für alle beteiligten Parteien die Leitidee, damit die schließlich resultierende Architektur fair verhandelt werden kann. In einem ersten Beispiel könnte ein Verhandlungsergebnis sein, dass das System nicht automatisch skalieren kann, weil beispielsweise das erforderliche Budget hierfür nicht ausreicht oder die benötigten Kompetenzen in Entwicklung und Betrieb nicht vorhanden sind. Denn wie wir oben festgestellt haben, braucht es viel, um echte Automation sicherzustellen: Möchte das Marketing an bestimmten Tagen besondere Promotionen anbieten, so ist dies mindestens einen Monat vorher mit Architektur und Betrieb abzustimmen, damit zum Zeitpunkt der Promotion ausreichend Kapazität verfügbar gemacht werden kann.

Ein weiteres Beispiel beschäftigt sich mit dem potenziell viralen Effekt von Marketingkampagnen. Hier ist vorher nicht absehbar, welche Last das System aushalten muss. Beispielsweise habe ich diese Woche einen ungebetenen Newsletter von Adobe erhalten, offenbar der erste seiner Art von einem neuen System. Bei dem Versuch der Abmeldung war das Zielsystem offenbar vom Ansturm der Unsubscriptions so überwältigt, dass es nichts außer weiße Seiten ausliefern konnte. Solche Vorfälle werfen ein schlechtes Licht auf die verantwortliche Gesamtorganisation. Verhandeln Sie also im Vorfeld beispielsweise folgendes Szenario, wenn alle einverstanden sind, dass der Traffic durch die Decke gehen könnte. Geben Sie jedoch hierfür im Vorfeld auch das zu erwartende Budget für die Lösung an. Seien sie dabei nicht zu konservativ, denn erfahrungsgemäß benötigt Autoskalierung pro Applikation einiges an Tuning und damit entsprechende Mehraufwände: Steigt die Anzahl der Besucher an, so soll das System die benötigte Kapazität selbstständig regulieren, sodass 90 Prozent der Page-Views in weniger als zwei Sekunden ausgeliefert werden können.

Bei diesem Szenario fällt auf, dass es unbounded ist. Je nach Betriebsmodell ist es ratsam, eine Obergrenze festzusetzen, denn sonst kann die Rechnung für die aufgewendete Bandbreite höher als erwartet ausfallen. Insbesondere wenn das Marketing emotionale Erlebnisse mit Full-HD-Videos zu 50 MB das Stück bevorzugt.

Fazit

Die Skalierbarkeit ist eines der wesentlichen Merkmale von Cloud-Native-Anwendungen, benötigt jedoch Aufwände in Betrieb und Entwicklung, insbesondere dann, wenn es gilt, die Skalierbarkeit automatisch herstellen zu wollen. Bei E-Commerce-Anwendungen kann sich eine exakte Kalkulation der benötigten Betriebskosten jedoch lohnen, schließlich lassen sich Systeme nicht nur hoch- sondern auch runterskalieren, sodass Einsparungen zum wirtschaftlichen Erfolg beitragen können.

Aufmacherbild: Highway Traffic at Sunset via Shutterstock / Urheberrecht: welcomia

Verwandte Themen:

Geschrieben von
Nicolas Bär
Nicolas Bär
Nicolas Bär arbeitet als IT-Architekt bei der Unic AG in Zürich. Als Performanceenthusiast optimiert er Webapplikationen und engagiert sich in seiner Freizeit im Open-Source-Umfeld.
Daniel Takai
Daniel Takai
Daniel Takai ist Enterprise-Architekt, arbeitet in der Schweiz und ist auf die digitale Transformation spezialisiert. Er berichtet regelmäßig im Java Magazin über seine Erfahrungen und Erkenntnisse. Im Frühling 2016 erscheint sein Buch über Methoden der Entwicklung von Cloud-basierten und serviceorientierten Websystemen.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.