Die Kapazität einer Instanz: Little's Law in der Praxis
An einem Dienstagnachmittag läuft der Suchdienst stabil: 100 Anfragen pro Sekunde, Antwortzeiten unter 100 Millisekunden. Dann versendet das Marketing-Team den Wochennewsletter. Innerhalb von drei Minuten klettert der Traffic auf 500 Anfragen pro Sekunde. Die Antwortzeiten explodieren: erst auf 800 Millisekunden, dann auf mehrere Sekunden.
Der Load Balancer markiert Instanzen als nicht erreichbar, erst eine, dann alle: Nutzer sehen Fehlerseiten. Ein Blick in die Logs: keine Exceptions, keine langsamen Queries, keine Netzwerkprobleme. Dafür aber Timeouts. Viele Timeouts.
Hier zeigt sich eine Skalierbarkeitsgrenze — innerhalb einer einzelnen Instanz. Bevor man über zusätzliche Server nachdenkt, stellt sich eine grundlegendere Frage: Was kann diese eine Instanz überhaupt leisten? Und warum ist sie an einer Stelle kollabiert, die niemand auf dem Schirm hatte?
Die Antwort liefert ein Werkzeug der Warteschlangentheorie aus dem Jahr 1961.
Drei Größen, die jedes System beschreiben
Jeder Service, der Anfragen entgegennimmt und beantwortet, lässt sich als Wartesystem auffassen und mit drei Werten charakterisieren:
Durchsatz $\lambda$ (Lambda): Die mittlere Anzahl der Anfragen, die pro Sekunde fertig bearbeitet werden. Wenn ein System unter Last zusammenbricht und Anfragen abbricht oder aufstaut, verarbeitet es weniger als ankommt. Durchsatz beschreibt, was tatsächlich durchkommt, nicht was angefragt wird.
Antwortzeit W: Die mittlere Zeit, die eine Anfrage im System verbringt, vom Eintreffen bis zur fertigen Antwort. Für den Suchdienst im Beispiel lag W (für waiting) bei 90 Millisekunden, davon entfielen etwa 70 Millisekunden auf die Datenbankzugriffe.
Concurrency L: Die mittlere Anzahl an Anfragen, die sich zu einem Zeitpunkt gleichzeitig im System befinden. Steigt dieser Wert an — etwa weil plötzlich mehr Requests pro Sekunde eintreffen — kann das zum Zusammenbruch führen.
Diese drei Größen sind nicht unabhängig. Sie hängen über eine Formel zusammen, die John Little 1961 bewiesen hat.
Little’s Law: Eine Formel, drei Größen
Die Intuition hinter Little’s Law lässt sich am Beispiel einer Supermarktkasse verstehen, einem typischen Wartesystem. Kunden kommen an, warten in der Schlange, bezahlen und verlassen den Markt. Der Durchsatz $\lambda$ wird durch den Kassierer begrenzt: Alle Produkte einscannen, abkassieren. Wie viele Kunden stehen dabei im Schnitt an der Kasse?
Die Antwort hängt von zwei Dingen ab: wie viele Kunden pro Minute abkassiert werden und wie lange jeder im Durchschnitt an der Kasse verbringt. Bei zwei Kunden pro Minute und einem Gesamtaufenthalt von drei Minuten stehen zu jedem Zeitpunkt im Mittel sechs Kunden an.
Little bewies 1961, dass genau diese Beziehung für jedes stabile Wartesystem gilt, unabhängig davon, wie die Ankünfte verteilt sind, wie viele Kassen offen sind oder welche Prioritätsregeln gelten:
\[L = \lambda \times W\] \[\text{Gleichzeitige Aufträge} = \text{Durchsatz} \times \text{Antwortzeit}\]Der Begriff „stabil” bedeutet in diesem Zusammenhang, dass das System in der Lage ist, mit der Ankunftsrate Schritt zu halten — dass es nicht überlastet ist. Ist das nicht der Fall, warten von Minute zu Minute mehr Kunden, die Antwortzeit steigt, und die Gleichung gilt nicht mehr. Zeit, eine weitere Kasse zu öffnen.
Little’s Law auf den Suchdienst angewandt: Bei 100 Anfragen pro Sekunde und 90 ms Antwortzeit sind 9 Anfragen gleichzeitig im System:
\[L = 100 \text{ req/s} \times 0{,}09 \text{ s} = 9 \text{ req}\]Neun gleichzeitige Anfragen. Der Thread-Pool mit 200 Plätzen ist weit entfernt von seiner Grenze. Und beim Newsletter-Peak?
\[L = 500 \text{ req/s} \times 0{,}09 \text{ s} = 45 \text{ req}\]45 gleichzeitige Anfragen. Bei einem Pool für 200 Threads klingt das unkritisch – und ist es auch, denn der Thread-Pool war auch gar nicht der Engpass: Das Problem lag eine Ebene tiefer.
Ein Service, mehrere Wartesysteme
Ein Web-Service ist eine Kette von Wartesystemen. Jede Anfrage durchläuft mehrere Stationen, und an jeder kann es eng werden: Request → Thread-Pool → Connection-Pool → Datenbank.
Little’s Law lässt sich auf jede dieser Stationen einzeln anwenden — und zwar unabhängig davon, ob die Station eine oder fünfzig parallele Bedienstellen hat (warum das funktioniert, erklärt der Exkurs zu G/G/1 vs. G/G/c). Wer die Formel umstellt (maximaler Durchsatz = Poolgröße geteilt durch Antwortzeit), kann den Engpass identifizieren:
- Für den Thread-Pool: 200 Threads geteilt durch 0,09 s Antwortzeit = rund 2.200 Anfragen pro Sekunde. Weit jenseits des Bedarfs.
- Für den Connection-Pool: 10 Verbindungen geteilt durch 0,07 s Antwortzeit (die Datenbank beansprucht den Löwenanteil der 90 Millisekunden) ergibt rund 143 Anfragen pro Sekunde.
Da war der Engpass. Der Connection-Pool war auf 10 Verbindungen konfiguriert, den Framework-Default. Eine Zahl, die niemand hinterfragt hatte, weil sie bei 100 Anfragen pro Sekunde ausreichte. Beim Newsletter-Peak brauchte der Service 35 gleichzeitige Datenbankverbindungen ($L = 500 \text{ req/s} \times 0{,}07 \text{ s}$), der Connection-Pool war also zu klein dimensioniert.
Sobald alle Connections belegt waren, blockierten Threads auf eine freie Verbindung — und hielten dabei ihren Platz im Thread-Pool besetzt, bis auch dieser erschöpft war und Requests abgelehnt wurden. Die durchschnittliche Antwortzeit wuchs parallel von 90 Millisekunden auf mehrere Sekunden.
Das System wurde instabil und war nicht mehr in der Lage, alle ankommenden Anfragen zu bedienen. Eine einzige falsch kalibrierte Zahl — und der Service kollabierte nicht bei zehntausend gleichzeitigen Nutzern, sondern beim Fünffachen des normalen Traffics.
Die Lösung — und ihre Grenzen
Die naheliegende Frage: Warum nicht einfach den Connection-Pool hochsetzen? Für eine einzelne Instanz ist genau das die richtige Antwort. 35 Verbindungen im Peak, plus 30% Puffer: ein Pool von 50 Verbindungen löst das Problem.
Warum dann nicht 200 oder 500 Verbindungen? Weil die Datenbank nicht beliebig viele Verbindungen gleichzeitig bedienen kann — schon ab einer vergleichsweise niedrigen Schwelle sinkt der Durchsatz, statt zu steigen. Wie immer gibt es einen optimalen Bereich: groß genug, damit Threads nicht warten müssen, klein genug, damit die Datenbank nicht unter der Koordination langsamer wird.
Vertiefung: Pool-Sizing in der Praxis
Brett Wooldridge, der Autor des Connection-Pool-Frameworks HikariCP, argumentiert in einem viel zitierten Artikel, dass der optimale Pool sehr viel kleiner ist als man intuitiv vermutet: oft nahe an
2 × Anzahl CPU-Kerne + Anzahl Festplatten. Der Grund: Mehr Verbindungen erzeugen mehr Kontextwechsel und Lock Contention auf der Datenbank. Der Tomcat-Standard von 200 Threads war in unserem Szenario nie das Problem; der HikariCP-Default von 10 Verbindungen dagegen schon.
Die eigentliche Architekturgrenze zeigt sich erst, wenn mehrere Instanzen dieselbe Datenbank teilen, und jede ihren eigenen Connection-Pool mitbringt. Warum es fast immer die Datenbank ist, die zuerst nicht mehr mitskaliert, ist das Thema von Teil 4 dieser Serie.
Was die Antwortzeit enthält — und warum Thread-Pools groß werden
Die Antwortzeit W enthält alles: CPU-Berechnung, Wartezeiten auf die Datenbank, externe Aufrufe, und auch das Warten auf eine freie Verbindung aus dem Pool. Bei den meisten Web-Services dominiert das Warten so stark, dass die eigentliche Rechenzeit praktisch irrelevant wird. Beim Suchdienst: 70 von 90 Millisekunden für die Datenbank. 78% der Zeit ist der Thread blockiert und wartet. In der Praxis dürfen es sogar 90% oder mehr sein. Das macht Thread-Pools groß, oft irritierend groß: Die Threads sind die meiste Zeit untätig und belegen trotzdem ihre Slots.
„Thread-Pool” ist insofern ein schmeichelhafter Name. „Thread-Wartezimmer” wäre ehrlicher.
Vertiefung: Reaktive Frameworks und Virtual Threads
Reaktive Frameworks wie Spring WebFlux oder Vert.x und — seit Java 21 — Virtual Threads gehen das Problem direkt an: Sie entkoppeln die Verarbeitungskapazität von der Anzahl blockierter OS-Threads. Das ändert den Optimierungsansatz, nicht aber die Grundaussage von Little’s Law: Die Antwortzeit bleibt die Antwortzeit, und $L = \lambda \times W$ gilt weiterhin. Und es ändert nichts am Connection-Pool: Ob ein OS-Thread oder ein Virtual Thread auf eine Datenbankverbindung wartet, die Ressource wird so oder so benötigt.
Little’s Law gibt das Minimum für den laufenden Betrieb. Die Anlaufphase nach einem Deployment (Connection-Pool füllen, Caches aufwärmen, etc.) ist nicht eingerechnet. Diese Puffer kommen on top.
Der Sättigungspunkt
Little’s Law setzt voraus, dass das System stabil ist: Die Verarbeitungsrate kann mit der Ankunftsrate Schritt halten. Im stabilen Bereich wächst die Concurrency proportional zum Durchsatz. Alles ist im Gleichgewicht.
Sobald die Last die Kapazität übersteigt, dreht sich dieses Verhalten um. Am Beispiel Kasse: Wenn pro Minute mehr Kunden ankommen, als abgefertigt werden können, wächst die Warteschlange und mit ihr die Wartezeit unaufhörlich.
Diesen Übergang nennt man den Sättigungspunkt.
Exkurs: Backpressure — Wenn Systeme Nein sagen lernen Ein System am Sättigungspunkt hat zwei Optionen: alle Requests akzeptieren und für alle langsam werden — oder gezielt Nein sagen, damit der Rest schnell bleibt.
Warum das System trotzdem nicht rund läuft
Wer jetzt erwartet, dass bis zur Kapazitätsgrenze alles glattläuft und danach eine saubere Stufe kommt, wird in der Praxis enttäuscht. Ein System läuft bei 70% Auslastung stabil, bei 80% beginnt die Antwortzeit zu steigen, bei 90% bricht es ein. Lange flach, dann steil — wie ein Hockeyschläger.
Warum kippt ein System, das laut Little’s Law noch Kapazität hat? Die kurze Antwort: Nicht jede Anfrage braucht gleich lang. Welche Rolle diese Variabilität spielt, ist das Thema des nächsten Posts.
Von der Formel zur Praxis
Little’s Law liefert die Kalibrierung für unsere Pools: Wie groß müssen sie konfiguriert werden, damit der erwartete Traffic bedient werden kann?
Die Formel funktioniert auch in eine andere Richtung. Ich habe einmal erlebt, dass ein Team zwanzig Instanzen für einen im Grunde trivialen Service betrieb. Zwanzig Instanzen — für einen Service, der wenig mehr tat als Daten aus einer Datenbank zu lesen und als JSON zurückzugeben. Eine kurze Überschlagsrechnung mit Little’s Law hätte gezeigt, dass drei Instanzen gereicht hätten und das das Performance-Problem, was das Team mit den 20 Instanzen adressieren wollte, an einer vollkommen anderen Stelle verursacht wurde.
Little’s Law ist also nicht nur ein Planungswerkzeug, sondern auch ein wunderbares Diagnosewerkzeug für überflüssigen Ressourcenverbrauch und unangemessene Skalierung.
Die Formel ist allerdings nur so gut wie ihre Eingaben. Die Antwortzeit ist kein fester Wert. Sie hängt von der Datenbankauslastung ab, vom Füllstand der Caches. Nach einem Deployment, wenn die Caches kalt sind, sieht W anders aus als zwei Stunden später. Und der Peak-Durchsatz? Newsletter-Peaks lassen sich noch vorhersagen, virale Social-Media-Posts nicht.
Deshalb kommt man um Lasttests nicht herum. Little’s Law sagt, wo man anfangen soll. Der Lasttest zeigt, was in der Praxis passiert. Und wenn die gemessenen Werte deutlich von der Berechnung abweichen, ist das kein Grund, einfach mal blind die Pool-Größen hochzudrehen. Es ist ein Signal, dass man etwas Wesentliches übersehen hat: eine versteckte Abhängigkeit, ein Lock in der Datenbank, eine Wartezeit, die unter niedriger Last unsichtbar war.
Die Grenzen einer Instanz — und was danach kommt
Little’s Law beantwortet die erste Frage der Skalierbarkeit: Wie viel kann eine Instanz leisten? Innerhalb einer Instanz gibt es zwei Hebel:
-
Die Servicezeit senken — schnellerer Code, weniger blockierende Aufrufe, Caching häufig genutzter Ergebnisse, Optimierung des Algorithmus.
-
Kapazität erhöhen — Pools vergrößern, mehr Threads, mehr Datenbankverbindungen.
Beide Hebel haben Grenzen. Die Antwortzeit hat ein Minimum, das durch die Implementierung und die Datenbank bestimmt wird. Pools können nicht beliebig wachsen, weil die darunter liegende Ressource — die Datenbank — nicht mitskaliert.
Sind diese Hebel ausgeschöpft, kommt der nächste Schritt: eine zweite Instanz, ein Load Balancer, der die Last verteilt. Damit wächst der Durchsatz, aber nicht ohne Grenzen. Alle Instanzen teilen sich die Datenbank. Jede bringt ihren eigenen Connection-Pool mit, und die Datenbank muss alle bedienen. Ab einem bestimmten Punkt ist nicht die Anwendung der Engpass, sondern die Schicht darunter. Mehr dazu in Teil 4 und 6 dieser Serie.
Aber es gibt noch eine subtilere Grenze: Serielle Abschnitte im Code — Locks, synchronisierte Blöcke, sequenzielle Abhängigkeiten — begrenzen den erreichbaren Parallelismus, egal wie viele Kerne oder Instanzen verfügbar sind. Je mehr parallel arbeitet, desto stärker machen sich diese seriellen Anteile bemerkbar. Dazu kommen wir in Teil 3 über Amdahl’s Law.
Quellen
- Little (1961) — A Proof for the Queuing Formula: L = λW. Operations Research, 9(3), 383–387.
- Abbott & Fisher (2015a) — The Art of Scalability. 2nd ed. Addison-Wesley.
- Wooldridge: About Pool Sizing — HikariCP Wiki.
Kommentare