Ich habe einige Jahre im Service Frameworks-Team von Amazon gearbeitet. Unser Team hat Tools geschrieben, mit denen die Besitzer von AWS-Services wie Amazon Route 53 und Elastic Load Balancing ihre Dienste schneller erstellen können. Service-Kunden können diese Dienste einfacher aufrufen. Andere Amazon-Teams stellten den Service-Besitzer Funktionen wie die Erfassung, Authentifizierung, Überwachung, Generierung von Client-Bibliotheken und Dokumentationserstellung zur Verfügung. Anstatt dass jedes Serviceteam diese Features manuell in seine Services integrieren muss, hat das Service Frameworks-Team diese Integration einmal durchgeführt und die Funktionalität für jeden Service durch Konfiguration verfügbar gemacht.

Eine Herausforderung bestand darin, zu ermitteln, wie vernünftige Standardeinstellungen bereitgestellt werden können, insbesondere für leistungsbezogene oder verfügbarkeitsbezogene Funktionen. Zum Beispiel konnten wir kein standardmäßiges clientseitiges Zeitlimit festlegen, da unser Framework keine Ahnung hatte, wie die Latenzmerkmale eines API-Aufrufs aussehen könnten. Dies wäre für Service-Besitzer oder Kunden nicht einfacher gewesen, als sich selbst ein Bild zu machen. Deshalb haben wir es weiter versucht und dabei einige nützliche Erkenntnisse gewonnen.

Eine häufig gestellte Frage war, wie viele Verbindungen der Server standardmäßig gleichzeitig für Clients öffnen darf. Diese Einstellung soll verhindern, dass ein Server zu viel Arbeit auf sich nimmt und überlastet wird. Insbesondere wollten wir die maximalen Verbindungseinstellungen für den Server im Verhältnis zu den maximalen Verbindungen für den Load Balancer konfigurieren. Dies war vor den Tagen des Elastic Load Balancing, sodass Hardware-Load Balancer weit verbreitet waren.

Wir möchten Amazon-Service-Besitzern und Service-Kunden dabei helfen, den idealen Wert für die maximale Anzahl der auf dem Load Balancer einzustellenden Verbindungen und den entsprechenden Wert für die von uns bereitgestellten Frameworks zu ermitteln. Wir beschlossen, dass wir, wenn wir herausfinden könnten, wie wir mit menschlichem Urteilsvermögen eine Wahl treffen können, Software schreiben könnten, um dieses Urteil zu emulieren.

Das Bestimmen des idealen Wertes war eine große Herausforderung. Wenn die maximale Anzahl der Verbindungen zu niedrig eingestellt war, konnte der Load Balancer die Anzahl der Anforderungen verringern, selbst wenn der Dienst über ausreichend Kapazität verfügte. Wenn die maximale Anzahl der Verbindungen zu hoch eingestellt war, reagierten die Server langsam und nicht mehr. Wenn die maximale Anzahl von Verbindungen genau für eine Workload festgelegt wurde, verschiebt sich die Workload oder die Abhängigkeitsleistung ändert sich. Dann wären die Werte wieder falsch, was zu unnötigen Ausfällen oder Überlastungen führen würde.

Am Ende stellten wir fest, dass das Konzept der maximalen Verbindungen zu ungenau war, um eine vollständige Antwort auf das Rätsel zu geben. In diesem Artikel werden andere Ansätze beschrieben, z. B. Lastabwurf, von denen wir fanden, dass sie gut funktioniert haben.

Die Anatomie der Überlastung

Bei Amazon vermeiden wir Überlastungen, indem wir unsere Systeme proaktiv skalieren, bevor es zu Überlastungen kommt. Zum Schutz von Systemen gehört jedoch der Schutz in Schichten. Dies beginnt mit der Auto Scaling, umfasst jedoch auch Mechanismen zum ordnungsgemäßen Ableiten von Überlast, die Möglichkeit zur Überwachung dieser Mechanismen und vor allem kontinuierliche Tests.
 
Beim Auslastungstest unserer Dienste stellen wir fest, dass die Latenz eines Servers bei geringer Auslastung geringer ist als die Latenz bei hoher Auslastung. Unter hoher Last werden Thread-Konflikte, Kontextwechsel, Garbage Collection und E/A-Konflikte ausgeprägter. Schließlich erreichen Services einen Wendepunkt, an dem sich ihre Leistung noch schneller verschlechtert.
 
Die Theorie hinter dieser Beobachtung ist als das Universelle Skalierbarkeitsgesetz bekannt, eine Ableitung des Amdahlschen Gesetzes. Diese Theorie besagt, dass der Durchsatz eines Systems durch Parallelisierung zwar erhöht werden kann, letztendlich jedoch durch den Durchsatz der Serialisierungspunkte (d. h. durch die Aufgaben, die nicht parallelisiert werden können) begrenzt ist.
 
Leider ist der Durchsatz nicht nur durch die Ressourcen eines Systems begrenzt, sondern er verschlechtert sich auch normalerweise, wenn das System überlastet ist. Wenn ein System mehr Arbeit erhält als seine Ressourcen unterstützen, wird es langsam. Computer übernehmen die Arbeit auch dann, wenn sie überlastet sind, verbringen jedoch immer mehr Zeit mit der Kontextumschaltung und werden zu langsam, um nützlich zu sein.
 
In einem verteilten System, in dem ein Client mit einem Server spricht, wird der Client in der Regel ungeduldig und wartet nicht mehr darauf, dass der Server nach einiger Zeit antwortet. Diese Dauer wird als Timeout bezeichnet. Wenn ein Server so überlastet ist, dass seine Latenz das Zeitlimit seines Clients überschreitet, schlagen Anforderungen fehl. Das folgende Diagramm zeigt, wie sich die Antwortzeit des Servers erhöht, wenn der angebotene Durchsatz (in Transaktionen pro Sekunde) zunimmt und die Antwortzeit schließlich einen Wendepunkt erreicht, an dem sich die Situation schnell verschlechtert.

Wenn im vorherigen Diagramm die Antwortzeit das Client-Timeout überschreitet, ist klar, dass die Dinge schlecht sind, aber das Diagramm zeigt nicht, wie schlecht sie sind. Um dies zu veranschaulichen, können wir die vom Kunden wahrgenommene Verfügbarkeit zusammen mit der Latenz darstellen. Anstatt eine generische Reaktionszeitmessung zu verwenden, können wir auf die mittlere Reaktionszeit umstellen. Die mittlere Antwortzeit bedeutet, dass 50 Prozent der Anfragen schneller waren als der mittlere Wert. Wenn die mittlere Latenz des Dienstes dem Client-Timeout entspricht, läuft die Hälfte der Anforderungen ab, sodass die Verfügbarkeit 50 Prozent beträgt. Hier verwandelt eine Erhöhung der Latenz ein Latenzproblem in ein Verfügbarkeitsproblem. Hier ist eine Grafik davon:

Leider ist diese Grafik schwierig zu lesen. Eine einfachere Möglichkeit, das Verfügbarkeitsproblem zu beschreiben, besteht darin, Goodput und Throughput zu unterscheiden. Der Durchsatz ist die Gesamtzahl der Anforderungen pro Sekunde, die an den Server gesendet werden. Goodput ist die Teilmenge des Durchsatzes, die fehlerfrei und mit einer ausreichend geringen Latenz verarbeitet wird, damit der Client die Antwort nutzen kann.

Positive Rückkopplungsschleifen

Der heimtückische Teil einer Überlastsituation ist, wie sie sich in einer Rückkopplungsschleife verstärkt. Wenn ein Client das Zeitlimit überschreitet, ist es schon schlimm genug, dass dem Client eine Fehlermeldung angezeigt wird. Was noch schlimmer ist, ist, dass alle Fortschritte, die der Server bisher bei dieser Anfrage gemacht hat, verschwendet werden. Und das Letzte, was ein System in einer Überlastsituation tun sollte, in der die Kapazität begrenzt ist, ist Abfallarbeit.

Erschwerend kommt hinzu, dass Kunden ihre Anfrage oft wiederholen. Dies multipliziert die angebotene Belastung des Systems. Und wenn es in einer serviceorientierten Architektur ein ausreichend tiefes Anrufdiagramm gibt (das heißt, ein Client ruft einen Service auf, der andere Services aufruft, die andere Services aufrufen), und wenn jeder Layer eine Reihe von Wiederholungsversuchen ausführt, entsteht im unteren Bereich eine Überlastung Layer verursacht kaskadierende Wiederholungsversuche, die die angebotene Last exponentiell verstärken.

Wenn diese Faktoren kombiniert werden, erzeugt eine Überlastung eine eigene Rückkopplungsschleife, die zu einer Überlastung im eingeschwungenen Zustand führt.

Verhindern, dass Arbeit verschwendet wird

An der Oberfläche ist der Lastabwurf einfach. Wenn sich ein Server einer Überlastung nähert, sollte er anfangen, übermäßige Anforderungen abzulehnen, damit er sich auf die Anforderungen konzentrieren kann, die er zulässt. Ziel des Lastabwurfs ist es, die Wartezeit für die vom Server akzeptierten Anforderungen so gering wie möglich zu halten, damit der Dienst antwortet, bevor das Zeitlimit des Clients abläuft. Bei diesem Ansatz behält der Server eine hohe Verfügbarkeit für die von ihm akzeptierten Anforderungen bei, und nur die Verfügbarkeit des überschüssigen Datenverkehrs ist betroffen.

Indem Sie die Latenz durch den Verlust von Überlast in Grenzen halten, wird das System verfügbarer. Die Vorteile dieses Ansatzes sind jedoch in der vorherigen Grafik nur schwer zu veranschaulichen. Die Gesamtverfügbarkeitsgrenze sinkt immer noch, was schlecht aussieht. Der Schlüssel ist, dass die Anforderungen, die der Server akzeptiert hat, verfügbar bleiben, da sie schnell bearbeitet wurden.
Durch Lastabwurf kann ein Server seinen Goodput beibehalten und so viele Anforderungen wie möglich ausführen, selbst wenn der angebotene Durchsatz steigt. Der Vorgang des Lastabwurfs ist jedoch nicht kostenlos, sodass der Server schließlich Amdahls Gesetzen zum Opfer fällt und der Goodput sinkt.

Testen

Wenn ich mit anderen Ingenieuren über Lastabwurf spreche, möchte ich darauf hinweisen, dass sie davon ausgehen sollten, dass der Dienst fehlschlagen wird, wenn sie ihren Dienst nicht bis zu einem Punkt getestet haben, an dem er nicht mehr funktioniert der am wenigsten wünschenswerte Weg möglich. Bei Amazon verbringen wir viel Zeit damit, unsere Dienste zu testen. Das Generieren von Diagrammen wie den zuvor in diesem Artikel beschriebenen hilft uns dabei, die Grundüberlastungsleistung zu ermitteln und zu verfolgen, wie wir im Laufe der Zeit Änderungen an unseren Diensten vornehmen.

Es gibt mehrere Arten von Belastungstests. Einige Lasttests stellen sicher, dass eine Flotte automatisch mit zunehmender Last skaliert, während andere eine feste Flottengröße verwenden. Wenn bei einem Überlastungstest die Verfügbarkeit eines Dienstes mit zunehmendem Durchsatz schnell auf Null sinkt, ist dies ein gutes Zeichen dafür, dass der Dienst zusätzliche Mechanismen zur Lastabfuhr benötigt. Das ideale Ergebnis für den Belastungstest ist, dass der Datendurchsatz auf ein Plateau steigt, wenn der Dienst fast voll ausgelastet ist, und dass er auch dann flach bleibt, wenn mehr Datendurchsatz angewendet wird.

Tools wie Chaos Monkey helfen bei der Durchführung von Chaos Engineering-Tests für Services. Beispielsweise können sie die CPU überlasten oder Paketverluste verursachen, um Bedingungen zu simulieren, die während einer Überlastung auftreten. Eine andere Testmethode, die wir verwenden, besteht darin, einen vorhandenen Lastgenerierungstest oder einen Canary-Test durchzuführen, um eine anhaltende Last (anstatt eine Last zu erhöhen) in Richtung einer Testumgebung zu fahren, aber Server aus dieser Testumgebung zu entfernen. Dies erhöht den angebotenen Durchsatz pro Instanz, sodass der Instanzdurchsatz getestet werden kann. Diese Technik zum künstlichen Erhöhen der Last durch Verringern der Flottengröße ist nützlich, um einen Dienst isoliert zu testen, ersetzt jedoch nicht die Volllasttests. Ein vollständiger Ende-zu-Ende-Auslastungstest erhöht auch die Auslastung der Abhängigkeiten dieses Dienstes, wodurch andere Engpässe aufgedeckt werden könnten.

Während der Tests stellen wir sicher, dass die vom Client wahrgenommene Verfügbarkeit und Latenz sowie die serverseitige Verfügbarkeit und Latenz gemessen werden. Wenn die clientseitige Verfügbarkeit abnimmt, wird die Last weit über diesen Punkt hinausgeschoben. Wenn der Lastabwurf funktioniert, bleibt der Goodput stabil, auch wenn der angebotene Durchsatz deutlich über die skalierten Funktionen des Service hinausgeht.

Überlasttests sind von entscheidender Bedeutung, bevor Mechanismen untersucht werden, um eine Überlastung zu vermeiden. Jeder Mechanismus bringt Komplexität mit sich. Betrachten Sie zum Beispiel alle Konfigurationsoptionen in den Service-Frameworks, die ich am Anfang des Artikels erwähnt habe, und wie schwierig es war, die Standardeinstellungen zu korrigieren. Jeder Mechanismus zur Vermeidung von Überlast fügt unterschiedliche Schutzmaßnahmen hinzu und ist nur begrenzt wirksam. Durch Tests kann ein Team die Engpässe seines Systems erkennen und die Kombination der Schutzmaßnahmen bestimmen, die erforderlich sind, um eine Überlastung zu bewältigen.

Sichtbarkeit

Unabhängig davon, mit welchen Techniken wir unsere Services vor Überlastung schützen, denken wir bei Amazon sorgfältig über die Metriken und die Sichtbarkeit nach, die erforderlich sind, wenn diese Überlastungsgegenmaßnahmen wirksam werden.

Wenn der Brownout-Schutz eine Anforderung ablehnt, verringert diese Ablehnung die Verfügbarkeit eines Dienstes. Wenn der Dienst einen Fehler macht und eine Anforderung ablehnt, obwohl er über Kapazität verfügt (z. B. wenn die maximale Anzahl von Verbindungen zu niedrig eingestellt ist), wird ein falsches positives Ergebnis generiert. Wir bemühen uns, die False-Positive-Rate eines Services auf Null zu halten. Wenn ein Team feststellt, dass die Falsch-Positiv-Rate seines Dienstes regelmäßig ungleich Null ist, ist der Dienst entweder zu empfindlich eingestellt oder einzelne Hosts sind ständig und zu Recht überlastet, und es liegt möglicherweise ein Skalierungs- oder Lastausgleichsproblem vor. In Fällen wie diesem müssen wir möglicherweise die Anwendungsleistung optimieren oder auf größere Instance-Typen umstellen, damit Lastungleichgewichte besser verarbeitet werden können.

In Bezug auf die Sichtbarkeit stellen wir sicher, dass wir beim Zurückweisen von Ladungsverlusten über geeignete Instrumente verfügen, um zu wissen, wer der Kunde war, welche Operation er anrief und über alle anderen Informationen, die uns bei der Optimierung unserer Schutzmaßnahmen helfen. Wir verwenden auch Alarme, um festzustellen, ob die Gegenmaßnahmen ein erhebliches Verkehrsaufkommen zurückweisen. Wenn es zu einem Brownout kommt, ist es unsere Priorität, die Kapazität zu erhöhen und den aktuellen Engpass zu beheben.

Es gibt noch eine andere subtile, aber wichtige Überlegung zur Sichtbarkeit beim Lastabwurf. Wir haben festgestellt, dass es wichtig ist, die Latenzmetriken unserer Services nicht mit einer fehlgeschlagenen Anforderungslatenz zu belasten. Immerhin sollte die Latenzzeit für den Lastabwurf einer Anforderung im Vergleich zu anderen Anforderungen extrem niedrig sein. Wenn ein Dienst beispielsweise 60 Prozent seines Datenverkehrs auslastet, sieht die mittlere Latenz des Dienstes möglicherweise erstaunlich aus, auch wenn die erfolgreiche Anforderungslatenz schrecklich ist, da sie aufgrund schnell fehlgeschlagener Anforderungen nicht ausreichend gemeldet wird.

Auswirkungen des Lastabwurfs auf das Auto Scaling und den Ausfall der Availability Zone

Bei falscher Konfiguration kann durch Lastabwurf das reaktive Auto Scaling deaktiviert werden. Betrachten Sie das folgende Beispiel: Ein Dienst ist für die CPU-basierte reaktive Skalierung konfiguriert und verfügt über eine Lastverteilung, die so konfiguriert ist, dass Anforderungen auf einem ähnlichen CPU-Ziel zurückgewiesen werden. In diesem Fall verringert das Lastabwurfsystem die Anzahl der Anforderungen, um die CPU-Last niedrig zu halten, und die reaktive Skalierung empfängt oder erhält niemals ein verzögertes Signal zum Starten neuer Instances.

Bei der Festlegung der Auto Scaling-Grenzwerte für die Behandlung von Availability Zone-Fehlern wird auch die Load-Shedding-Logik berücksichtigt. Services werden so skaliert, dass die Kapazität einer Availability Zone unter Beibehaltung unserer Latenzziele möglicherweise nicht mehr verfügbar ist. Amazon-Teams untersuchen häufig Systemkennzahlen wie die CPU, um zu schätzen, wie weit ein Dienst von seiner Kapazitätsgrenze entfernt ist. Beim Lastabwurf kann es jedoch vorkommen, dass eine Flotte viel näher an dem Punkt läuft, an dem Anforderungen abgelehnt werden, als es die Systemmetriken anzeigen, und dass möglicherweise nicht die überschüssige Kapazität bereitgestellt wird, um einen Verfügbarkeitszonenfehler zu beheben. Beim Lastabwurf müssen wir besonders darauf achten, unsere Dienste auf Bruch zu testen, um zu jedem Zeitpunkt die Kapazität und den Headroom unserer Flotte zu verstehen.

Tatsächlich können wir mithilfe des Lastabwurfs Kosten sparen, indem wir unkritischen Datenverkehr außerhalb der Stoßzeiten gestalten. Wenn beispielsweise eine Flotte den Website-Verkehr für amazon.com verarbeitet, ist es möglicherweise nicht sinnvoll, den Such-Crawler-Verkehr für die vollständige Redundanz der Availability Zone zu skalieren. Mit diesem Ansatz gehen wir jedoch sehr vorsichtig um. Nicht alle Anforderungen kosten gleich viel, und der Nachweis, dass ein Service gleichzeitig Redundanz in der Availability Zone für den Datenverkehr von Personen und für den Abbau von übermäßigem Crawler-Datenverkehr bieten sollte, erfordert sorgfältiges Design, kontinuierliche Tests und das Buy-in des Unternehmens. Und wenn die Clients eines Dienstes nicht wissen, dass ein Dienst auf diese Weise konfiguriert ist, sieht sein Verhalten während eines Verfügbarkeitszonenfehlers möglicherweise nach einem massiven kritischen Verfügbarkeitsverlust aus, anstatt nach einem unkritischen Lastabwurf. Aus diesem Grund versuchen wir in einer serviceorientierten Architektur, diese Art der Formgebung so früh wie möglich voranzutreiben (z. B. in dem Service, der die erste Anforderung vom Client erhält), anstatt zu versuchen, globale Priorisierungsentscheidungen im gesamten Stapel zu treffen.

Lastabwurfmechanismen

Bei der Erörterung von Lastabwurf und unvorhersehbaren Szenarien ist es auch wichtig, sich auf die vielen vorhersehbaren Bedingungen zu konzentrieren, die zu einem Spannungsabfall führen. Bei Amazon verfügen die Dienste über genügend überschüssige Kapazität, um Fehler von Availability Zones zu beheben, ohne dass zusätzliche Kapazität hinzugefügt werden muss. Sie setzen Throttling ein, um die Fairness zwischen den Kunden sicherzustellen.

Trotz dieser Schutzmaßnahmen und betrieblichen Praktiken verfügt ein Dienst zu jedem Zeitpunkt über eine bestimmte Kapazität und kann daher aus verschiedenen Gründen überlastet werden. Zu diesen Gründen zählen unerwartete Verkehrsstöße, plötzlicher Kapazitätsverlust der Flotte (aufgrund von schlechten Bereitstellungen oder auf andere Weise), der Übergang von billigen Anfragen (wie zwischengespeicherten Lesevorgängen) zu teuren Anfragen (wie Cache-Fehlern oder Schreibvorgängen). Wenn ein Service überlastet wird, muss er die Anforderungen beenden, die er angenommen hat. Das heißt, die Dienste müssen sich vor einem Brownout schützen. Im Rest dieses Abschnitts werden einige der Überlegungen und Techniken besprochen, die wir im Laufe der Jahre angewendet haben, um Überlastungen zu bewältigen.

Verständnis der Kosten für das Löschen von Anfragen

Wir stellen sicher, dass Sie unsere Dienste weit über den Punkt hinaus testen, an dem Goodput-Plateaus auftreten. Einer der Hauptgründe für diesen Ansatz besteht darin, sicherzustellen, dass die Kosten für das Löschen der Anforderung so gering wie möglich sind, wenn wir Anforderungen während des Lastabwurfs löschen. Wir haben festgestellt, dass es leicht möglich ist, eine versehentliche Protokollanweisung oder eine Socket-Einstellung zu übersehen, wodurch das Löschen einer Anfrage weitaus teurer wird, als sie sein muss.

In seltenen Fällen kann das schnelle Löschen einer Anfrage teurer sein als das Festhalten an der Anfrage. In diesen Fällen verlangsamen wir die Ablehnung von Anfragen, um (mindestens) die Latenz erfolgreicher Antworten zu erreichen. Dies ist jedoch wichtig, wenn die Kosten für das Festhalten an Anfragen so gering wie möglich sind. zum Beispiel, wenn sie keinen Anwendungsthread binden.

Priorisieren von Anforderungen

Wenn ein Server überlastet ist, hat er die Möglichkeit, eingehende Anforderungen zu prüfen, um zu entscheiden, welche akzeptiert und welche abgelehnt werden sollen. Die wichtigste Anforderung, die ein Server erhält, ist eine Ping-Anforderung von einem Load Balancer. Wenn der Server nicht rechtzeitig auf Ping-Anfragen antwortet, sendet der Load Balancer für einen bestimmten Zeitraum keine neuen Anfragen mehr an diesen Server, und der Server befindet sich im Leerlauf. Und in einem Brownout-Szenario wollen wir als letztes die Größe unserer Flotten reduzieren. Über Ping-Anforderungen hinaus variieren die Priorisierungsoptionen für Anforderungen von Service zu Service.

Stellen Sie sich einen Webdienst vor, der Daten zum Rendern von amazon.com bereitstellt. Ein Serviceabruf, der das Rendern von Webseiten für einen Suchindex-Crawler unterstützt, ist in der Regel weniger kritisch als eine Anfrage, die von einem Menschen stammt. Crawler-Anfragen sind wichtig, aber im Idealfall können sie in eine Zeit außerhalb der Spitzenzeiten verschoben werden. In einer komplexen Umgebung wie amazon.com, in der eine große Anzahl von Diensten zusammenarbeitet, kann jedoch die systemweite Verfügbarkeit beeinträchtigt und Arbeit verschwendet werden, wenn Dienste widersprüchliche Priorisierungsheuristiken verwenden.

Priorisierung und Drosselung können zusammen verwendet werden, um strenge Drosselungsobergrenzen zu vermeiden und gleichzeitig einen Dienst vor Überlastung zu schützen. In Fällen, in denen wir zulassen, dass Clients über ihre konfigurierten Drosselungsgrenzwerte hinausgehen, werden die überzähligen Anforderungen dieser Clients möglicherweise mit einer niedrigeren Priorität behandelt als Anforderungen anderer Clients, die innerhalb des Kontingents liegen. Wir verwenden viel Zeit darauf, uns auf Platzierungsalgorithmen zu konzentrieren, um die Wahrscheinlichkeit zu minimieren, dass die Burst-Kapazität nicht mehr verfügbar ist. Angesichts der Kompromisse ziehen wir jedoch die vorhersehbare bereitgestellte Arbeitslast der unvorhersehbaren Arbeitslast vor.

Die Uhr im Auge behalten

Wenn ein Service eine Anfrage zu einem bestimmten Zeitpunkt bearbeitet hat und eine Zeitüberschreitung des Clients feststellt, kann er den Rest der Arbeit überspringen und die Anfrage an diesem Punkt nicht bearbeiten. Andernfalls arbeitet der Server weiter an der Anforderung, und die späte Antwort ähnelt einem Baum, der in den Wald fällt. Aus der Sicht des Servers wurde eine erfolgreiche Antwort zurückgegeben. Aus der Sicht des Kunden war dies jedoch ein Fehler.

Eine Möglichkeit, um diese unnötige Arbeit zu vermeiden, besteht darin, dass Clients in jede Anforderung Zeitüberschreitungshinweise einfügen, die dem Server mitteilen, wie lange sie bereit sind, zu warten. Der Server kann diese Hinweise auswerten und zum Scheitern verurteilte Anforderungen mit geringen Kosten verwerfen.

Dieser Timeout-Hinweis kann entweder als absolute Zeit oder als Dauer ausgedrückt werden. Leider sind Server in verteilten Systemen notorisch schlecht darin, die genaue aktuelle Uhrzeit zu bestimmen. Der Amazon Time Sync Service gleicht dies aus, indem die Uhren Ihrer Amazon Elastic Compute Cloud (Amazon EC2) -Instanzen mit einer Flotte redundanter satellitengesteuerter und atomarer Uhren in jeder AWS-Region synchronisiert werden. Gut synchronisierte Uhren sind bei Amazon auch für Protokollierungszwecke wichtig. Durch den Vergleich von zwei Protokolldateien auf Servern mit nicht synchronen Uhren wird die Fehlerbehebung noch schwieriger als zunächst.

Die andere Möglichkeit, „auf die Uhr zu schauen“, besteht darin, die Dauer auf einer einzelnen Maschine zu messen. Server können die abgelaufene Zeitspanne lokal gut messen, da sie keine Übereinstimmung mit anderen Servern erzielen müssen. Leider hat das Ausdrücken von Zeitüberschreitungen in Bezug auf die Dauer auch seine Probleme. Zum einen muss der von Ihnen verwendete Timer monoton sein und darf nicht zurückgehen, wenn der Server mit dem Network Time Protocol (NTP) synchronisiert. Ein weitaus schwierigeres Problem besteht darin, dass der Server zum Messen einer Dauer wissen muss, wann eine Stoppuhr gestartet werden muss. In einigen extremen Überlastungsszenarien können große Mengen von Anforderungen in TCP-Puffern (Transmission Control Protocol) anstehen. Bis der Server die Anforderungen aus seinen Puffern liest, ist das Zeitlimit für den Client bereits abgelaufen.

Wann immer Systeme bei Amazon Client-Timeout-Hinweise ausdrücken, versuchen wir, diese transitiv anzuwenden. In Umgebungen, in denen eine serviceorientierte Architektur mehrere Hops umfasst, wird die „verbleibende Zeit“ zwischen den einzelnen Hops verteilt, sodass ein nachgeschalteter Service am Ende einer Anrufkette erkennen kann, wie viel Zeit für die Reaktion benötigt wird .

Sobald ein Server die Client-Frist kennt, stellt sich die Frage, wo die Frist in der Service-Implementierung durchgesetzt werden soll. Wenn ein Service eine Anforderungswarteschlange hat, verwenden wir diese Gelegenheit, um das Timeout nach dem Löschen jeder Anforderung zu bewerten. Dies ist jedoch immer noch recht kompliziert, da wir nicht wissen, wie lange die Anfrage voraussichtlich dauern wird. Bei einigen Systemen wird geschätzt, wie lange API-Anforderungen dauern, und Anforderungen werden vorzeitig gelöscht, wenn die vom Client gemeldete Frist eine geschätzte Wartezeit überschreitet. So einfach ist das jedoch selten. Cache-Treffer sind beispielsweise schneller als Cache-Fehler, und der Schätzer weiß nicht, ob es sich um Treffer oder Fehler handelt. Oder die Back-End-Ressourcen des Dienstes sind möglicherweise partitioniert und nur einige Partitionen sind langsam. Es gibt eine Menge Möglichkeiten für Cleverness, aber es ist auch möglich, dass diese Cleverness in einer unvorhersehbaren Situation nach hinten losgeht.

Nach unserer Erfahrung ist die Durchsetzung von Client-Timeouts auf dem Server trotz der Komplexität und Kompromisse immer noch besser als die Alternative. Anstatt dass sich Anfragen häufen und der Server möglicherweise an Anfragen arbeitet, die für niemanden mehr von Bedeutung sind, hat es sich als hilfreich erwiesen, eine "Lebensdauer pro Anfrage" zu erzwingen und zum Scheitern verurteilte Anfragen zu verwerfen.

Fertigstellen, was begonnen wurde

Wir möchten nicht, dass nützliche Arbeiten verloren gehen, insbesondere bei einer Überlastung. Das Wegwerfen von Arbeit führt zu einer positiven Rückkopplungsschleife, die die Überlastung erhöht, da Clients häufig eine Anforderung wiederholen, wenn ein Dienst nicht rechtzeitig antwortet. In diesem Fall werden aus einer ressourcenintensiven Anforderung viele ressourcenintensive Anforderungen, wodurch sich die Auslastung des Dienstes vervielfacht. Wenn Clients eine Zeitüberschreitung feststellen und es erneut versuchen, hören sie häufig auf, bei ihrer ersten Verbindung auf eine Antwort zu warten, während sie eine neue Anforderung für eine separate Verbindung stellen. Wenn der Server die erste Anforderung beendet und antwortet, wartet der Client möglicherweise nicht auf eine Antwort von seiner erneuten Anforderung.

Dieses Problem der Verschwendung von Arbeit ist der Grund, warum wir versuchen, Dienste so zu gestalten, dass sie begrenzte Arbeit leisten. An Stellen, an denen eine API verfügbar gemacht wird, die einen großen Datensatz (oder eine beliebige Liste) zurückgeben kann, wird sie als API bereitgestellt, die Paginierung unterstützt. Diese APIs geben Teilergebnisse und ein Token zurück, mit dem der Client weitere Daten anfordern kann. Wir haben festgestellt, dass es einfacher ist, die zusätzliche Auslastung eines Dienstes zu schätzen, wenn der Server eine Anforderung verarbeitet, die eine Obergrenze für die Menge an Arbeitsspeicher, CPU und Netzwerkbandbreite aufweist. Es ist sehr schwierig, die Zugangskontrolle durchzuführen, wenn ein Server keine Ahnung hat, wie er eine Anfrage bearbeiten soll.

Eine subtilere Möglichkeit, Anforderungen zu priorisieren, besteht darin, wie Clients die APIs eines Dienstes verwenden. Angenommen, ein Dienst verfügt über zwei APIs: start() und end(). Um ihre Arbeit zu beenden, müssen Clients beide APIs aufrufen können. In diesem Fall sollte der Dienst end() -Anfragen über start() -Anfragen priorisieren. Wenn die Priorität auf start() gesetzt wird, können Kunden die von ihnen gestartete Arbeit nicht abschließen, was zu Stromausfällen führt.

Die Paginierung ist ein weiterer Ort, an dem Sie auf verschwendete Arbeit achten müssen. Wenn ein Client mehrere aufeinanderfolgende Anforderungen stellen muss, um die Ergebnisse eines Dienstes zu paginieren, und nach Seite N-1 ein Fehler auftritt und die Ergebnisse verwirft, werden N-2-Dienstaufrufe und alle Wiederholungsversuche auf dem Weg verschwendet. Dies legt nahe, dass Anforderungen der ersten Seite wie Anforderungen von end() Vorrang vor Paginierungsanforderungen der folgenden Seite haben sollten. Dies unterstreicht auch, warum wir Dienste so entwerfen, dass sie begrenzte Arbeit leisten und nicht endlos durch einen Dienst paginieren, den sie während eines synchronen Vorgangs aufrufen.

Nach Warteschlangen Ausschau halten

Es ist auch hilfreich, die Anforderungsdauer bei der Verwaltung interner Warteschlangen zu überprüfen. Viele moderne Dienstarchitekturen verwenden speicherinterne Warteschlangen, um Thread-Pools zu verbinden und Anforderungen in verschiedenen Arbeitsphasen zu verarbeiten. Vor einem Web-Service-Framework mit einem Executor ist wahrscheinlich eine Warteschlange konfiguriert. Bei jedem TCP-basierten Dienst verwaltet das Betriebssystem einen Puffer für jeden Socket, und diese Puffer können ein großes Volumen an aufgestauten Anforderungen enthalten.

Wenn wir Arbeit aus Warteschlangen ziehen, nutzen wir diese Gelegenheit, um zu untersuchen, wie lange die Arbeit in der Warteschlange gestanden hat. Zumindest versuchen wir, diese Dauer in unseren Service-Metriken zu erfassen. Wir haben festgestellt, dass es nicht nur wichtig ist, die Größe der Warteschlangen zu begrenzen, sondern auch, wie lange eine eingehende Anforderung in einer Warteschlange gespeichert ist. Wenn sie zu alt ist, wird sie gelöscht. Auf diese Weise kann der Server neuere Anforderungen bearbeiten, bei denen die Erfolgsaussichten größer sind. Als extreme Version dieses Ansatzes suchen wir nach Möglichkeiten, stattdessen eine LIFO-Warteschlange (Last In, First Out) zu verwenden, sofern das Protokoll dies unterstützt. (HTTP/1.1-Pipelining von Anforderungen auf einer bestimmten TCP-Verbindung unterstützt keine LIFO-Warteschlangen, HTTP/2 jedoch im Allgemeinen.)

Load Balancer können auch eingehende Anforderungen oder Verbindungen in eine Warteschlange stellen, wenn die Dienste überlastet sind. Dies erfolgt mithilfe einer Funktion, die als Surge Queues bezeichnet wird. Diese Warteschlangen können zu einem Brownout führen, da ein Server, wenn er eine Anfrage erhält, keine Ahnung hat, wie lange die Anfrage in der Warteschlange war. Ein im Allgemeinen sicherer Standard ist die Verwendung einer Überlaufkonfiguration, die schnell fehlschlägt, anstatt übermäßige Anforderungen in die Warteschlange zu stellen. Bei Amazon floss dieses Wissen in die nächste Generation des Elastic Load Balancing (ELB)-Services ein. Der Classic Load Balancer verwendete eine Überspannungswarteschlange, der Application Load Balancer lehnt jedoch übermäßigen Datenverkehr ab. Unabhängig von der Konfiguration überwachen die Teams bei Amazon die relevanten Load-Balancer-Metriken wie die Warteschlangentiefe oder die Anzahl der Überläufe für ihre Dienste.

Nach unserer Erfahrung kann die Bedeutung des Wartens auf Warteschlangen nicht genug betont werden. Ich bin oft überrascht, Warteschlangen im Arbeitsspeicher zu finden, bei denen ich nicht intuitiv daran gedacht habe, sie in Systemen und Bibliotheken zu suchen, auf die ich angewiesen bin. Wenn ich mich mit Systemen befasse, ist es hilfreich anzunehmen, dass sich an einer Stelle Warteschlangen befinden, von denen ich noch nichts weiß. Überlastungstests liefern natürlich mehr nützliche Informationen als das Eingraben in Code, solange ich die richtigen realistischen Testfälle finden kann.

Schutz vor Überlastung in unteren Schichten

Services bestehen aus mehreren Schichten - vom Load-Balancer über Betriebssysteme mit Netfilter- und Iptables-Funktionen bis hin zu Service-Frameworks und Code - und jede Schicht bietet Funktionen zum Schutz des Service.

HTTP-Proxys wie NGINX unterstützen häufig eine maximale Verbindungsanzahl (max_conns), um die Anzahl der aktiven Anforderungen oder Verbindungen zu begrenzen, die an den Back-End-Server weitergeleitet werden. Dies kann ein hilfreicher Mechanismus sein, aber wir haben gelernt, ihn als letzten Ausweg anstelle der Standardschutzoption zu verwenden. Mit Proxys ist es schwierig, wichtigen Datenverkehr zu priorisieren, und die Nachverfolgung der Anzahl der Anfragen während des Flugs liefert manchmal ungenaue Informationen darüber, ob ein Dienst tatsächlich überlastet ist.

Am Anfang dieses Artikels habe ich eine Herausforderung aus meiner Zeit im Service Frameworks-Team beschrieben. Wir haben versucht, Amazon-Teams einen empfohlenen Standard für maximale Verbindungen zur Verfügung zu stellen, die auf ihren Load Balancern konfiguriert werden können. Am Ende schlugen wir vor, dass die Teams die maximale Anzahl von Verbindungen für ihren Load Balancer und Proxy festlegen und den Server genauere Algorithmen zur Lastverteilung mit lokalen Informationen implementieren lassen. Es war jedoch auch wichtig, dass der maximale Verbindungswert die Anzahl der Listener-Threads, Listener-Prozesse oder Dateideskriptoren auf einem Server nicht überschreitet, damit der Server über die Ressourcen verfügt, um kritische Integritätsprüfungsanforderungen vom Load Balancer zu verarbeiten.

Betriebssystemfunktionen zur Begrenzung der Serverressourcennutzung sind leistungsstark und können in Notfällen hilfreich sein. Und weil wir wissen, dass es zu Überlastungen kommen kann, bereiten wir uns darauf vor, indem wir die richtigen Runbooks mit spezifischen Befehlen bereitstellen. Das Dienstprogramm iptables kann eine Obergrenze für die Anzahl der vom Server akzeptierten Verbindungen festlegen und überschüssige Verbindungen weitaus billiger als jeder Serverprozess ablehnen. Es kann auch mit komplexeren Steuerelementen konfiguriert werden, z. B. um neue Verbindungen mit einer begrenzten Rate oder sogar mit einer begrenzten Verbindungsrate oder Anzahl pro Quell-IP-Adresse zuzulassen. Quell-IP-Filter sind leistungsstark, gelten jedoch nicht für herkömmliche Load Balancer. Ein ELB-Network Load Balancer behält jedoch die Quell-IP des Anrufers auch auf Betriebssystemebene durch Netzwerkvirtualisierung bei, sodass iptables-Regeln wie Quell-IP-Filter wie erwartet funktionieren.

In Schichten schützen

In einigen Fällen gehen einem Server die Ressourcen aus, um Anforderungen sogar abzulehnen, ohne dass dies verlangsamt wird. Vor diesem Hintergrund untersuchen wir alle Hops zwischen einem Server und seinen Clients, um herauszufinden, wie sie zusammenarbeiten und dabei helfen können, Überlast abzubauen. Beispielsweise enthalten einige AWS-Services standardmäßig Optionen zum Lastabwurf. Wenn wir einen Dienst mit Amazon API Gateway bereitstellen, können wir eine maximale Anforderungsrate konfigurieren, die von jeder API akzeptiert wird. Wenn unsere Dienste von API Gateway, einem Application Load Balancer oder Amazon CloudFront bereitgestellt werden, können wir AWS WAF so konfigurieren, dass übermäßiger Datenverkehr in einer Reihe von Dimensionen abgeleitet wird.

Sicht schafft eine schwierige Spannung. Eine frühzeitige Ablehnung ist wichtig, da dies der billigste Ort ist, um übermäßigen Datenverkehr abzuleiten. Die Sichtbarkeit wird jedoch beeinträchtigt. Aus diesem Grund schützen wir in Schichten: Damit ein Server mehr übernimmt, als er verarbeiten kann, und um den Überschuss zu löschen, und um genügend Informationen zu protokollieren, um zu wissen, welcher Datenverkehr gelöscht wird. Da ein Server nur so viel Datenverkehr hat, dass er ausfallen kann, verlassen wir uns darauf, dass der Layer davor vor extremem Datenverkehr geschützt ist.

Überlastung anders denken

In diesem Artikel haben wir diskutiert, wie die Notwendigkeit einer Lastreduzierung sich aus der Tatsache ergibt, dass Systeme langsamer werden, wenn sie paralleler arbeiten, da Kräfte wie Ressourcenbeschränkungen und Konflikte auftreten. Die Überlastungsrückkopplungsschleife wird durch die Latenz angetrieben, die letztendlich zu Arbeitsverschwendung, Anforderungsratenverstärkung und noch mehr Überlastung führt. Diese Kraft, die sich nach dem Universal Scalability Law und dem Amdahl-Gesetz richtet, ist wichtig, um zu vermeiden, dass übermäßige Belastungen abbauen und eine vorhersehbare, konsistente Leistung bei Überlastung aufrechterhalten werden. Die Konzentration auf vorhersehbare, konsistente Leistung ist ein zentrales Konstruktionsprinzip, auf dem die Dienste von Amazon aufbauen.

Beispielsweise ist Amazon DynamoDB ein Datenbankdienst, der eine vorhersehbare Leistung und Verfügbarkeit im Maßstab bietet. Selbst wenn eine Workload schnell platzt und die bereitgestellten Ressourcen überschreitet, behält DynamoDB eine vorhersehbare Goodput-Latenz für diese Workload bei. Faktoren wie die Auto Scaling von DynamoDB, die adaptive Kapazität und On-Demand reagieren schnell, um die Goodput-Raten zu erhöhen und sich an eine höhere Arbeitslast anzupassen. Während dieser Zeit bleibt der Goodput stabil, sodass ein Service in den Schichten über DynamoDB mit vorhersehbarer Leistung erhalten bleibt und die Stabilität des gesamten Systems verbessert wird.

AWS Lambda bietet ein noch umfassenderes Beispiel für die Fokussierung auf vorhersehbare Leistung. Wenn wir einen Dienst mit Lambda implementieren, wird jeder API-Aufruf in einer eigenen Ausführungsumgebung ausgeführt, der konsistente Mengen an Rechenressourcen zugewiesen sind, und diese Ausführungsumgebung kann jeweils nur für diese eine Anforderung ausgeführt werden. Dies unterscheidet sich von einem serverbasierten Paradigma, bei dem ein bestimmter Server mit mehreren APIs arbeitet.

Das Isolieren jedes API-Aufrufs auf seine eigenen unabhängigen Ressourcen (Computer, Speicher, Datenträger, Netzwerk) umgeht das Amdahl-Gesetz in gewisser Weise, da die Ressourcen eines API-Aufrufs nicht mit den Ressourcen eines anderen API-Aufrufs konkurrieren. Wenn der Durchsatz den Goodput übersteigt, bleibt der Goodput daher unverändert, anstatt wie in einer traditionelleren serverbasierten Umgebung zu sinken. Dies ist kein Allheilmittel, da Abhängigkeiten langsamer werden und die Parallelität zunehmen kann. In diesem Szenario gelten jedoch zumindest die in diesem Artikel beschriebenen Arten von Host-Ressourcenkonflikten nicht.

Diese Ressourcenisolierung ist ein subtiler, aber wichtiger Vorteil moderner, serverloser Computerumgebungen wie AWS Fargate, Amazon Elastic Container Service (Amazon ECS), und AWS Lambda. Wir bei Amazon haben festgestellt, dass die Implementierung von Load-Shedding viel Arbeit erfordert, vom Optimieren von Thread-Pools bis zur Auswahl der perfekten Konfiguration für maximale Load-Balancer-Verbindungen. Sinnvolle Standardeinstellungen für diese Art von Konfigurationen sind schwierig oder unmöglich zu finden, da sie von den einzigartigen Betriebseigenschaften jedes Systems abhängen. Diese neueren, serverlosen Computing-Umgebungen bieten eine Ressourcenisolation auf niedrigerer Ebene und offenbaren übergeordnete Steuerelemente wie Throttling- und Concurrency-Steuerelemente zum Schutz vor Überlastung. In mancher Hinsicht können wir, anstatt den perfekten Standardkonfigurationswert zu suchen, diese Konfiguration komplett umgehen und vor Überlastungskategorien schützen, ohne überhaupt eine Konfiguration vornehmen zu müssen.

Weitere Lektüre

Universelles Skalierbarkeitsgesetz
Amdahlsches Gesetz
Staged event-driven architecture (SEDA)
Little'sches Gesetz (beschreibt die Parallelität in einem System und wie die Kapazität von verteilten Systemen bestimmt wird)
Geschichten über Little’s Law erzählen, Marc’s Blog
Elastic Load Balancing Deep Dive und Best Practices, Präsentation auf der re: Invent 2016 (beschreibt die Entwicklung des Elastic Load Balancing, um zu verhindern, dass übermäßige Anforderungen in die Warteschlange gestellt werden)
• Burgess, Thinking in Promises: Designing Systems for Cooperation, O’Reilly Media, 2015



Über den Autor

David Yanacek ist Senior Principal Engineer und arbeitet an AWS Lambda. David ist seit 2006 Softwareentwickler bei Amazon. Zuvor arbeitete er an Amazon DynamoDB und AWS IoT sowie an internen Web-Service-Frameworks und Automatisierungssystemen für den Flottenbetrieb. Eine von Davids Lieblingsaktivitäten bei der Arbeit ist die Durchführung von Protokollanalysen und das Durchsuchen von Betriebsmetriken, um Wege zu finden, wie Systeme im Laufe der Zeit immer reibungsloser funktionieren.

Timeouts, Wiederholungsversuche und Backoff mit Jitter Implementierung von Zustandsprüfungen Instrumentieren verteilter Systeme für Einblicke in die Betriebsabläufe