Fünfzig Nutzer

Irgendwann um 2005 ging ein Portal live, an dem ich mitgebaut hatte. Es basierte auf Java EE, einer relationalen Datenbank und einem Portalserver — Enterprise-Technologie für Enterprise-Anforderungen. Kurz darauf brach das System bei 50 gleichzeitigen Nutzern zusammen. Fünfzig. Nicht fünfhundert, nicht fünftausend — fünfzig.

In den Tests war alles gut gewesen. Saubere Antwortzeiten, schneller Seitenaufbau. Nur hatte niemand die Lastgrenzen getestet. Ein Kollege bekam das Problem einigermaßen in den Griff — mit viel Koffein, größeren Caches und ein paar Datenbankoptimierungen. Wir fanden irgendwann auch bessere Lösungen — aber eine Frage ließ mich danach nicht mehr los: Was wissen die Unternehmen, die Millionen von Nutzern bedienen, was ich nicht weiß?

Diese Thema beschäftigt mich mittlerweile seit über zwanzig Jahren. In dieser Serie möchte ich aufschreiben, was ich auf dem Weg gelernt habe — von den mathematischen Grundlagen, mit denen man abschätzen kann, was eine einzelne Instanz leistet oder wie man einen Threadpool konfigurieren sollte, bis zu den organisatorischen Strukturen und wie man auch diese skalieren kann.

Faktor 2 ist Optimierung, Faktor 10 ist Architektur

Wenn man ein System, das 100 Requests pro Sekunde verarbeiten kann, mit etwas Tuning auf 200 Requests pro Sekunde bringt, hat man kein Skalierbarkeitsproblem gelöst. Das System hatte noch ausreichend Headroom. Indexe anlegen, Queries optimieren, den Garbage Collector konfigurieren, eine größere Instanz mieten — notwendige Arbeit, aber nichts davon verschiebt architektonische Grenzen.

Faktor 10 ist eine andere Kategorie. Und Caching ist hier nicht die Lösung (Exkurs).

Von 100 auf 1.000 Requests pro Sekunde — das erreicht man nicht mehr durch Tuning. Was bei 100 Requests funktioniert — Sessions auf dem Server, Joins über fünf Tabellen, ein einzelner Datenbankserver — bricht bei 1.000 zusammen. Der Connection-Pool läuft voll, Locks auf der Datenbank werden zum Engpass, die Antwortzeiten explodieren. Nicht weil irgendetwas schlecht gebaut war, sondern weil die Architektur für eine andere Größenordnung entworfen wurde.

Jede Stufe darüber hinaus bringt neue qualitative Probleme mit sich. Es reicht dann nicht mehr aus, dasselbe System zu beschleunigen – man braucht ein anderes System.

Jede Größenordnung erfordert andere Mittel — von Tuning über Architekturwechsel bis zur Plattform

Unser Fehler 2005 war nicht die Wahl der Technologie. Wir haben schlicht unterschätzt, wo die Skalierungsgrenzen der Architektur lagen — und wie schnell wir sie erreichen würden.

Zwei Arten zu scheitern

Es gibt aber noch eine andere Art, an Grenzen zu stoßen.

Als ich 2009 zu OTTO kam, basierte die E-Commerce-Plattform auf einer kommerziellen Shoplösung — und brauchte 200 Instanzen für 200 Seitenaufrufe pro Sekunde. Das funktionierte technisch. Aber die Kosten waren absurd, und mehr Instanzen hätten das Problem nicht gelöst, sondern nur teurer gemacht.

Das ist die zweite Art von Skalierungsgrenze: Ein System bewältigt die Last und ist trotzdem nicht skalierbar — weil der Preis in keinem Verhältnis zum Ergebnis steht.

Zwei Arten zu scheitern: Links bricht der Durchsatz bei Überlast ein, rechts explodieren die Kosten während der Durchsatz kaum noch steigt

Die eigentliche Frage ist deshalb nie „Ist mein System skalierbar?” — sondern: Um welchen Faktor muss es noch wachsen? Was kostet die nächste Verdopplung — relativ zum Status quo? Und wie viel Vorlauf brauche ich, um rechtzeitig handeln zu können?

„Mehr Server” ist keine Antwort

Der erste Reflex bei Skalierungsproblemen: eine größere Instanz. Der zweite: mehr Instanzen. Beides sind nachvollziehbare Reaktionen — und beides reicht nicht.

Wer einfach Instanzen hinzufügt, skaliert entlang einer einzigen Dimension. Das funktioniert, solange die Instanzen unabhängig sind — keine geteilte Datenbank, kein geteilter Zustand, keine gemeinsame Deployment-Pipeline. In der Praxis ist mindestens eine dieser Bedingungen verletzt. Und dann skaliert man nicht das System, sondern den Engpass.

Es gibt drei Dimensionen, entlang derer ein System horizontal skaliert werden kann:

  1. identische Kopien betreiben,
  2. funktional in unabhängige Teile zerschneiden, oder
  3. nach Daten, Kundengruppen, Regionen oder anderen Kriterien partitionieren.

Die meisten Systeme, die ernsthaft skalieren, kombinieren mindestens zwei davon. „Design for at least two axes of scale” — so die Empfehlung von Abbott und Fisher in The Art of Scalability. Einer der nützlichsten Ratschläge, die ich zu diesem Thema kenne.

Dieselben Gesetze, andere Ebene

Was mich an Skalierbarkeit am meisten interessiert hat: Die Prinzipien lassen sich auf alle Möglichen Situationen übertragen. Die Gesetze, die erklären, warum ein Service unter Last zusammenbricht, erklären auch, warum 50 Entwickler an einer gemeinsamen Codebase weniger pro Kopf liefern als 20.

Geteilte Ressourcen, serielle Engpässe, quadratisch wachsender Kommunikationsaufwand — das sind keine Metaphern. Es sind dieselben mathematischen Zusammenhänge, angewandt auf ein anderes System. Und die Lösung ist dieselbe — aber darauf kommen wir noch.

Zuerst muss man verstehen, was eine einzelne Instanz kann.

Was eine Instanz verrät

Diese Serie fängt nicht mit Microservices an, nicht mit Kubernetes, nicht mit Cloud-Autoscaling — sondern mit einer einzelnen Instanz eines einfachen Services. Wie viele Requests kann sie verarbeiten? Wo liegt ihre Grenze? Und warum kommt diese Grenze fast immer überraschend — obwohl man sie auf einer Serviette hätte ausrechnen können?

Es gibt ein Werkzeug dafür. Es stammt aus dem Jahr 1961, es passt in eine Zeile, und es hätte uns 2005 eine Menge Ärger erspart.


Quellen

Kommentare