Zwei Wege, um mehr Last zu tragen
Willkommen
Wenn ein Dienst unter Last nachgibt, steht ein Operator vor einer Wahl. Die bestehende Maschine vergrößern (mehr CPU, mehr RAM, schnellere Festplatten). Oder mehr Maschinen hinzufügen, die jeweils dieselbe Arbeit verrichten.
Der erste Weg wird als vertikale Skalierung (Scale up) bezeichnet. Der zweite als horizontale Skalierung (Scale out).
Diese Lektion erklärt, warum fast jede moderne Web-Architektur die horizontale Skalierung wählt und welche Eigenschaft der Arbeitslast diese Wahl ermöglicht. Die Antwort verbirgt sich in einem Wort: Zustand.
Am Ende wirst du verstehen:
- Die Kostenkurven von vertikaler vs. horizontaler Skalierung & wo jeweils sinnvoll ist
- Was „stateful“ & „stateless“ in der Praxis bedeuten & warum sich eines davon günstig multipliziert
- Die Mathematik, die eine Replikaten-Flotte unter erwarteter & Spitzenlast dimensioniert
- Die Pufferregel, die verhindert, dass eine Schicht über das Knie der Warteschlange hinaus kollabiert
- Wo der Zustand leben muss (er verschwindet nie) & wie man ihn aus den Schichten herausdrängt, die skaliert werden müssen
Warum horizontale Skalierung ab einem Schwellenwert gewinnt
Vertikale Skalierung: Ein größeres System
Vorteile: einfach. Keine Codeänderungen. Keine Koordination. Der gleiche Prozess hat nun mehr CPU.
Nachteile: Obergrenze. Die größte kommerziell verfügbare VM hat eine endliche Menge an RAM & Kernen. Darüber hinaus kauft kein Geld mehr Kapazität. Die Kosten steigen überproportional, sobald man den Sweet Spot des Angebots eines Anbieters überschreitet. Ein Ausfall dieser einen Maschine bringt den gesamten Dienst zum Erliegen.
Horizontale Skalierung: Viele kleinere Maschinen
Vorteile: keine Obergrenze (bis zu Ihrer Bereitschaft, für Maschinen zu bezahlen & diese zu koordinieren). Die Kapazität wächst linear mit der Anzahl der Replikate, vorhersehbar. Ein Ausfall eines einzelnen Replikats entfernt 1/N der Kapazität, nicht 100 %.
Nachteile: erfordert, dass die Arbeitslast dies unterstützt. Einige Arbeitslasten (eine einzelne große Datenbank, ein zustandsbehafteter Game-Server, der aktive Sitzungen hält) widerstehen der horizontalen Skalierung. Koordination & Lastverteilung werden zu betrieblichen Herausforderungen.
Der Wendepunkt: Jeder Produktionsdienst, der einen Ausfall einer einzelnen Maschine überstehen muss, muss auf mindestens zwei Maschinen laufen. Sobald man zwei akzeptiert, hat man bereits die horizontale Skalierung gewählt. Ab dort lautet die Frage nicht mehr „Sollten wir das tun?“, sondern „Wie günstig können wir das nächste Replikat hinzufügen?“.
Der entscheidende Enabler: eine Arbeitslast, die keinen pro Anfrage bezogenen Zustand auf der Maschine selbst hält. Dann kann jedes Replikat jede Anfrage beantworten, & das Hinzufügen eines Replikats erhöht die Kapazität ohne Koordination.
Stateful vs. Stateless in der Praxis
Zustand verschwindet nie, er bewegt sich nur
Zustandsbehaftetes Komponente: Hält Informationen, deren Verlust das Verhalten verändern würde. Eine Datenbank, die Benutzerkonten speichert. Ein Cache, der Sitzungstokens hält. Ein Worker, der eine langlaufende Streaming-Verbindung an einen bestimmten Benutzer bindet.
Zustandslose Komponente: Hält keine Informationen, deren Verlust von Bedeutung wäre. Eine Web-Schicht, die eine Anfrage liest, eine Datenbank abfragt & eine Antwort schreibt. Jede Anfrage steht für sich; die Schicht merkt sich nichts zwischen den Anfragen.
Wichtige Erkenntnis: Zustand verschwindet nie aus einem System. Er bewegt sich in eine Schicht, die dafür konzipiert ist, ihn zu halten (eine Datenbank, ein Redis-Cluster, ein Objektspeicher). Die Schichten, die dem Verkehr ausgesetzt sind, können dann zustandslos sein, & zustandslose Schichten skalieren horizontal, da jede Replik jede Anfrage beantworten kann.
Praktischer Test: Wenn man einen Prozess in dieser Schicht zufällig beendet & neu startet, würde ein Benutzer eine falsche Antwort oder eine verlorene Sitzung erleben? Wenn ja, hält er Zustand. Wenn nein, tut er es nicht.
Beispiele
- Ein Python-Webprozess, der Anfragen liest, Postgres abfragt und JSON zurückgibt: zustandslos. Der Zustand befindet sich in Postgres.
- Ein Python-Webprozess, der Benutzer-Warenkörbe im lokalen Speicher hält: zustandsbehaftet. Das Beenden des Prozesses führt zum Verlust der Warenkörbe.
- Ein WebSocket-Server, der offene Verbindungen zu Chat-Nutzern aufrechterhält: zustandsbehaftet im Sinne der Verbindung. Wird der Prozess beendet, fallen die Verbindungen ab; Clients müssen neu verbinden. Oft lassen sich diese dennoch mit Vorsicht horizontal skalieren (sticky sessions, konsistentes Hashing).
- Ein Redis-Cache vor Postgres: zustandsbehaftet für den Cache-Inhalt, aber akzeptabel, wenn Cache-Misses tolerierbar sind. Ein Replikatsausfall bedeutet einen Cache-Miss, keinen Datenverlust.
Design für horizontale Skalierung = Zustände aus der Ebene herausdrücken, die skaliert werden muss.
Eine verdächtige Ebene auditieren
Ein Team betreibt eine Empfehlungs-API auf 6 Backend-VMs hinter einem Reverse Proxy. Die Anwendung: liest eine Benutzer-ID aus der Anfrage, ruft die kürzliche Aktivität des Benutzers aus Postgres ab, führt einen Scoring-Algorithmus aus und gibt eine Liste empfohlener Elemente zurück. Zwei nicht-standardmäßige Verhaltensweisen:
- Die Anwendung hält einen „kürzliche Benutzeraktivität“-Cache im Prozessspeicher, der bei der ersten Anfrage für einen Benutzer befüllt und bei nachfolgenden Anfragen wiederverwendet wird.
- Die Anwendung verwendet sticky sessions: Sobald ein Benutzer VM #3 erreicht, gehen alle seine nachfolgenden Anfragen an VM #3 (der Proxy ist für sticky Routing anhand eines Cookies konfiguriert).
Die Replikationsformel
Die einfachste Kapazitätsformel
Sobald eine Ebene zustandslos ist, wird das Sizing zur Arithmetik. Du brauchst genügend Replikate, damit die Last im stationären Zustand mit derselben Rate eintrifft und abfließt, mit Puffer für Spitzen.
Die Formel:
Replikate = ⌈ (Spitzenlast × Spitzenfaktor) / Kapazität pro Replikat ⌉ + Puffer
Dabei gilt:
- Spitzenlast: die maximalen anhaltenden Anfragen pro Sekunde, die du im Normalbetrieb erwartest
- Spitzenfaktor: ein Multiplikator, der kurze Ausreißer über der Spitze abdeckt (üblicherweise 1,5x bis 2x für vorhersehbaren Traffic, 3x oder mehr für virale / unvorhersehbare Last)
- Kapazität pro Replikat: Anfragen pro Sekunde, die ein Replikat bei akzeptabler Latenz und Auslastung bewältigt (üblicherweise bei 70 % CPU gemessen, nicht bei Sättigung)
- Puffer: zusätzliche Replikate, damit der Ausfall weniger Replikate die Ebene nicht zum Einsturz bringt (üblicherweise 1–2 Replikate für kleine Flotten, 10–20 % für größere)
Beispielrechnung: Ein Backend verarbeitet 100 req/s bei 70 % CPU-Auslastung pro Replik. Die Spitzenlast beträgt 600 req/s. Sie erwarten gelegentliche 2-fache Lastspitzen. Sie möchten, dass das System 2 Replik-Ausfällen standhält.
Repliken = ⌈ (600 × 2) / 100 ⌉ + 2 = 12 + 2 = 14 Repliken
Die 80-%-Regel
Die Kapazität pro Replik ist nicht der Sättigungspunkt. Messen Sie die Kapazität bei 70–80 % CPU-Auslastung, niemals bei 100 %.
Ab etwa 80 % Auslastung steigen die Wartezeitkurven steil an: Eine Warteschlange, die bei 60 % Auslastung in 10 ms abgearbeitet wird, benötigt bei 90 % Auslastung 80 ms. Die Latenz, nicht die Durchsatzrate, bricht zuerst zusammen. (Die begleitende Lektion geometry_of_stateless_horizontal_scaling leitet diese Kurve mathematisch her.)
Autoscaling vs. statische Bereitstellung
Statisch: Bereitstellung für Spitzenlast × Puffer für Lastspitzen und Akzeptanz der Kosten durch niedrige Auslastung außerhalb der Spitzenzeiten.
Autoscaling: Ein Controller fügt Repliken hinzu und entfernt sie basierend auf der beobachteten Auslastung, der Ziel-Latenz oder der Warteschlangenlänge.
Hinweis zu Autoscaling: Die Kaltstartzeit ist entscheidend. Wenn ein neuer Replikats-Instanz 2 Minuten zum Hochfahren benötigt, kann das Autoscaling nicht auf einen 30-Sekunden-Spitzenlastanstieg reagieren. Ausgereiftes Autoscaling hält einen warmen Pool vorprovisionierter Replikate knapp unterhalb der Skalierungsschwelle vor.
Eine Flotte für einen neuen Dienst dimensionieren
Euer Team plant den Launch einer Video-Metadaten-API. Benchmarks zeigen, dass ein einzelnes Replikat 250 req/s bei 70 % CPU-Auslastung und 50 ms p99-Latenz bewältigt. Marketing prognostiziert eine Spitzenlast von 4.000 req/s während der Prime-Time-Stunden. Ein geplantes Werbeevent könnte kurzfristig zu einem 3-fachen Anstieg der Spitzenlast führen. Ihr wollt, dass der Dienst 3 gleichzeitige Replikatausfälle übersteht, ohne dass die verbleibenden Replikate eine Auslastung von 80 % überschreiten.
Cold Start, langsames Draining & andere reale Grenzen
Reale Flotten haben reale Grenzen
Die Formel geht davon aus, dass Replikate sofort erscheinen, sofort Traffic akzeptieren und sofort Traffic abgeben. In der Produktion trifft keine dieser Annahmen zu.
Cold Start: Ein neues Replikat muss das Betriebssystem starten, den Prozess einleiten, Konfigurationen laden, Caches aufwärmen und Health-Checks bestehen. Das dauert je nach Fall zwischen 5 Sekunden (Container-Neustart) und 5 Minuten (kompletter VM-Start + Image-Download). Autoscaling kann nicht auf Lastspitzen reagieren, die kürzer als diese Verzögerung sind.
Langsames Draining: Ein Replikat, das aus dem Pool entfernt wird, benötigt Zeit, um laufende Anfragen abzuschließen, bevor es beendet wird. Andernfalls sehen Benutzer abgeschnittene Antworten. Reverse Proxies unterstützen das Draining (neue Anfragen nicht mehr annehmen, aktive beenden), was jedoch Sekunden bis Minuten dauern kann.
Warm Pool: Produktionsflotten halten einen Pool vorprovisionierter, aber inaktiver Replikaten bereit, die bei Signal sofort Traffic übernehmen können. Dies tauscht eine geringen, konstanten Kosten gegen eine schnelle Reaktion auf Lastspitzen.
Connection Draining vs. sofortiges Beenden: Ein geordnetes Herunterfahren ist wichtig. Ein SIGTERM, das ein Draining auslöst, dauert länger als ein SIGKILL, bricht aber keine aktiven Benutzeranfragen ab.
Health-Check-Fenster: Ein gerade erst gestartetes Replikat könnte seinen ersten Health-Check bestehen, bevor sein Datenbank-Verbindungspool warm ist; das Proxy leitet dann echten Traffic weiter & die ersten Dutzend Anfragen sind langsam. Passen Sie Health-Checks so an, dass sie den tatsächlichen Pfad testen, nicht nur die Prozesslebensfähigkeit.
Stickiness Creep: Selbst nominell zustandslose Ebenen entwickeln mit der Zeit eine Bindung (CDN-Caches, DNS-Resolver-Caches, Verbindungspools). Seien Sie misstrauisch gegenüber „identischen Replikaten“, die sich dennoch unterschiedlich verhalten.
Warm Pool oder reaktives Autoscaling?
Ihre Video-Metadaten-API (die gleiche aus der vorherigen Frage, dimensioniert auf 51 Replikate für stabile Spitzen + Lastspitzen) erlebt einen 30-sekündigen Anstieg auf das 5-fache der normalen Last, wann immer ein neues virales Video hochgeladen wird. Das aktuelle Autoscaling benötigt 90 Sekunden, um ein neues Replikat aus dem kalten Zustand hinzuzufügen (Image-Pull + Warmup). Während dieser 90-Sekunden-Lücke steigt die Latenz stark an & einige Anfragen laufen in die Zeitüberschreitung.
Entwurf einer zustandslosen Ebene unter Constraints
Synthese
Du hast gelernt, warum horizontales Skalieren ab einer bestimmten kleinen Schwelle überlegen ist, was Zustand in der Praxis bedeutet, wie man eine Flotte unter Berücksichtigung von erwarteter und Spitzenlast dimensioniert und wo horizontales Skalieren an den Grenzen an seine Grenzen stößt.
Wende alle vier an.
Entwirf eine Backend-Schicht für feed.example.com, eine Social-Feed-API. Einschränkungen: Kapazität pro Replik 200 req/s bei 70 % CPU; erwartete Spitzenlast 1500 req/s; Surge-Faktor 2,5x (gelegentliche Trending-Stories); Überleben von 2 gleichzeitigen Replik-Ausfällen; Kaltstartzeit 60 Sekunden; Bursts können 45 Sekunden dauern; das Budget erlaubt etwas Leerlaufkapazität, aber keine permanente Bereitstellung von 2,5x.
Wohin dieser Kurs als Nächstes geht
Wohin dieser Kurs als Nächstes führt
Sie haben nun ein funktionierendes mentales Modell einer zustandslosen Ebene: warum sie skaliert, wie man sie dimensioniert, was an ihren Grenzen zusammenbricht und wohin der Zustand verschoben werden muss, wenn man sie über die Schicht hinaus ausdehnt, die wachsen muss.
Die nächste Lektion in diesem Kurs (cs_distsys_ingress_egress_separation) behandelt ein subtileres Problem: Selbst eine perfekt dimensionierte zustandslose Ebene kann auf überraschende Weise versagen, wenn eingehender und ausgehender Verkehr denselben Netzwerkpfad teilen. Das klassische Beispiel betrifft einen Proxy, der versucht, eine Verbindung zu sich selbst herzustellen; die Lösung besteht darin, eine Ebene in zwei mit unterschiedlichen Verantwortlichkeiten aufzuteilen.
Begleitende Lektion: geometry_of_stateless_horizontal_scaling leitet die Warteschlangenkurve ab, wendet das Little'sche Gesetz auf eine Replikatenflotte an und erklärt die geometrische Bedeutung des Knies bei 80 % Auslastung.
Gut gemacht. Weiter geht's.