Kapitel 4. Schnittstellen
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Du solltest kein Unbehagen an irgendeinem Teil der Architektur haben. Sie sollte nichts enthalten, was nur dazu dient, dem Chef zu gefallen. Sie sollte nichts enthalten, was für dich schwer zu verstehen ist. Du bist derjenige, der es umsetzen wird; wenn es für dich keinen Sinn ergibt, wie kannst du es dann umsetzen?
Steve McConnell, Code Complete (Microsoft Press)
In Kapitel 3 haben wir über die Architektur gesprochen. Traditionell denkt man bei der Architektur eines Systems an die Kästchen im Diagramm, aber noch wichtiger sind die Linien, die die Kästchen verbinden. Diese Linien können viele Dinge bedeuten und sind eine Abstraktion davon, wie diese Systeme miteinander verbunden sind und kommunizieren. In diesem Kapitel werden wir einen wichtigen Teil dieser Linien untersuchen: die Schnittstellen.
In diesem Kapitel erfährst du, was eine Schnittstelle ist, was du beim Erstellen oder Verbinden mit einer Schnittstelle beachten musst und welche Konstrukte die Cloud-Provider für diese Leitungen am häufigsten anbieten.
Die Einführung eines modernen Anwendungsdesigns, das aus kleineren, unabhängigen Komponenten oder Diensten besteht, ermöglicht es dir, dich auf die Entwicklung zu konzentrieren und die besten Entscheidungen zu treffen, wie du jedes Problem lösen willst. Du solltest dich auf die Funktionen und die Geschäftslogik konzentrieren können, und die Infrastruktur sollte dich entlasten. Aber das gibt es nicht zum Nulltarif. Du musst auf jeden Punkt achten, an dem eine Kopplung auftreten kann, und diese Kopplung so weit wie möglich minimieren. Wie bereits in Kapitel 3 beschrieben, brauchst du Regeln und Standards für die Schnittstellen deiner Dienste und ihre Interaktion mit anderen Diensten. Du wirst aber auch Software implementieren müssen, die die Schnittstellen anderer Dienste innerhalb oder außerhalb deines Unternehmens nutzt, wie z. B. Stripe oder Twilio. Wir werden uns damit beschäftigen, wie du am besten mit Schnittstellen zu Software umgehst, die du nicht kontrollierst, und zwar aus beiden Perspektiven.
Dein Dienst hängt von anderen Diensten ab. Andere Dienste werden sich auf deinen Dienst verlassen. Der Schlüssel zum Erfolg ist, dass du deinen Dienst zielgerichtet und vorausschauend gestaltest. Du wirstKompromisse eingehen. Dokumentiere diese Kompromisse, lege sie offen und halte an ihnen fest, bis sie nicht mehr notwendig sind.
Schnittstellen: Einige Montage erforderlich
Im Rahmen dieses Kapitels ( ) ist eine Schnittstelle die Fläche zwischen zwei Komponenten der Anwendung. Sie ist die Art und Weise, wie sie miteinander verbunden werden, um einen größeren Zweck zu erfüllen. Diese Komponenten können intern oder extern, proprietär oder Open Source, selbst gehostet oder verwaltet sein. Für unsere Zwecke interessieren wir uns für die Struktur und das Schema der Nachrichten, die weitergegeben werden, wie sie weitergegeben werden und was passiert, wenn sich diese Aktionen nicht wie erwartet verhalten.
Die Botschaft
Die Nachricht ist das, was zwischen den Komponenten gesendet wird und wie diese Nachricht verpackt ist. Sie kann Informationen über den Anfragenden, Kopfzeilen, Sitzungen und/oder Informationen enthalten, um zu überprüfen, ob eine bestimmte Anfrage oder Aufgabe autorisiert ist. In der Regel werden diese Nachrichten in JSON gekapselt.
Das Protokoll
Das allgegenwärtigste Anwendungsprotokoll der Welt ist heute HTTP. (Und vergiss das S nicht.) Erinnere dich daran, dass man Netzwerken nicht trauen kann, niemals. Sie sind nicht sicher und nicht verlässlich. HTTPS bietet zumindest eine gewisse Sicherheit, dass Nachrichten nicht missbräuchlich verändert werden.
Achte auf Abstraktionen, wenn du über Schnittstellen sprichst oder sie debuggst. Wenn ein Entwickler zum Beispiel HTTP sagt, meint er in der Regel HTTP über TLS (HTTPS) über TCP über IP über Ethernet oder Glasfaser. Jeder einzelne Teil dieses Stacks kann fehlschlagen, Probleme oder Einschränkungen verursachen oder auf andere Weise die Details deiner Implementierung beeinflussen.
Die API, mit der du deinem Cloud-Provider Befehle erteilst, ist über HTTP implementiert, und HTTP wird sogar von den Cloud-Instanzen verwendet, um Anmeldeinformationen für die Verbindung mit diesen APIs zu erhalten. Du bist jedoch nicht auf HTTP beschränkt. Viele Anbieter haben die Möglichkeit, mit Kunden über WebSockets zu kommunizieren. Deine Funktionen können jede Art von ausgehender Netzwerkverbindung nutzen, um mit anderen Systemen zu kommunizieren. SFTP wird zum Beispiel immer noch häufig verwendet, um Daten und sogar Geld in nächtlichen Batch-Aufträgen zu übertragen.
Der Vertrag
Schließlich enthält den Vertrag oder die Erwartung, was als Ergebnis einer bestimmten Nachricht passieren wird. Dies ist die Funktionalität, die du den Softwarekunden deiner Komponente mitteilst, in der Regel über die Dokumentation. Was soll zum Beispiel passieren, wenn ein Kunde versucht, dieselbe E-Mail-Adresse zweimal zu einer Mailingliste hinzuzufügen? Das sind die Entscheidungen, die du treffen musst, und du musst ein von Menschen lesbares Artefakt bereitstellen, um denjenigen, die deinen Dienst integrieren, Versprechen und Erwartungen zu vermitteln.
Serverlose Schnittstellen
Bevor wir auf über die Gestaltung von Schnittstellen sprechen, wollen wir uns die Optionen und Bausteine ansehen, die in Serverless zur Verfügung stehen, und einige der Merkmale von Serverless Compute-Komponenten in deinen Systemen.
Wenn wir die architektonischen Boxen unserer serverlosen Funktionen verbinden, können wir zwischen zwei Arten von Aufrufen wählen: synchron und asynchron. Synchrone oder Anfrage/Antwort-Aufrufe sind blockierende Operationen, d.h. der Aufrufer wartet darauf, dass die Funktion ihre Anfrage beendet, bevor er eine Antwort zurückgibt. Asynchrone Aufrufe, auch als Ereignisse bezeichnet, sind nicht blockierend und müssen nicht warten, bis die Anfrage abgeschlossen ist, bevor sie beantwortet werden.
Eine gute Faustregel lautet: Wenn die Aktion oder Logik, die eine Funktion aufruft, sich für das Ergebnis der Funktion interessiert, um ihre eigenen Ziele zu erreichen, passt sie in das synchrone Modell. Wenn sie sich nicht direkt für das Ergebnis der Funktion interessiert (außer dass sie weiß, dass sie ausgelöst wurde), ist das asynchrone oder das Ereignismodell am besten geeignet. Im asynchronen Modell sind das Ergebnis oder die Aktionen, die von einer Funktion ausgeführt werden, wahrscheinlich für die gesamte Anwendung wichtig, aber nicht speziell für die Aktion oder Logik, die sie ausgelöst hat.
Einige von deinem Cloud-Provider angebotene Integrationen überraschen dich vielleicht durch die Art des Aufrufs. Zum Beispiel ist die Verarbeitung eines Datenstroms, der in die Datenbank geschrieben wurde, eine sehr asynchrone Aktion. Sie ist buchstäblich ein Baustein einer ereignisgesteuerten Architektur. Da der Datenstrom jedoch der Reihe nach verarbeitet wird, sind die eigentlichen Funktionsaufrufe synchron, zumindest wenn DynamoDB verwendet wird. Das liegt daran, dass die verborgene Komponente, die dafür verantwortlich ist, ihren Platz in der Verarbeitung des Streams zu behalten und deine Funktionen mit deinem Geschäftscode auszulösen, auf das Ergebnis jedes Aufrufs angewiesen ist, um den Status zu aktualisieren und den nächsten auszulösen.
Automatische Wiederholungsversuche und Dead Letter Queues
Das automatische Senden von fehlgeschlagenen Funktionsaufrufen an eine Warteschlange für Fehlschläge oder eine Dead-Letter-Warteschlange ist ein grundlegender Baustein einer effektiven serverlosen Komponente.
Was Serverless angeht, werden asynchrone Aufrufe in AWS automatisch bis zu dreimal wiederholt, ohne dass du bestimmen kannst, wie. Danach kannst du fehlgeschlagene Aufrufe in eine Warteschlange für Fehlschläge stellen. Mit Google Cloud Functions hast du die Möglichkeit, Wiederholungsversuche für Hintergrundfunktionen zu aktivieren. Google weist jedoch darauf hin, dass die Aufrufe bis zu sieben Tage lang wiederholt werden, sodass sie nur für wiederholbare Fehler verwendet werden sollten. Azure bietet Dead Letter Queues und Wiederholungsversuche für bestimmte Arten von Integrationen.
Gleichzeitigkeit
Eine wichtige Komponente von Serverless Compute ist die Möglichkeit, eine Nebenläufigkeit pro Funktion sowie eine maximale Nebenläufigkeit für alle Funktionen festzulegen. Die Gleichzeitigkeit ist die Anzahl der gleichzeitigen Aufrufe, die zu einem bestimmten Zeitpunkt verarbeitet werden. Der größte Vorteil der Granularität des Einsatzes von serverlosen Funktionen ist die Möglichkeit, eine Funktion unabhängig von anderen nach Bedarf zu skalieren, und diese Einstellung ist buchstäblich die Skalierung einer bestimmten Funktion.
Warum nicht einfach das Maximum einstellen? Erstens willst du unerwartetes Verhalten verhindern, also ist es am besten, keine Option unbegrenzt zu lassen. Eine ausufernde Komponente kann andere Teile des Systems in Mitleidenschaft ziehen, ganz zu schweigen von deiner monatlichen Rechnung. Die unbegrenzte Skalierung von Serverless ist mächtig und wird andere Komponenten zerstören, wenn sie nicht unter Kontrolle gehalten wird.
Erinnere dich auch daran, dass dein Cloud-Provider Standardgrenzen für die Gleichzeitigkeit deines gesamten Kontos hat, die du in deine Zukunftsplanung einbeziehen solltest. Wenn du keinen Supportvertrag mit deinem Cloud-Provider abgeschlossen hast, kann es eine Woche dauern, bis er auf eine Erhöhung des Service-Limits reagiert.
Endliche versus unendliche Skala
Serverless als Paradigma wird andere Dienste, die nicht für eine massive und sofortige Skalierung ausgelegt sind, zerstören. Was wirst du für das Caching verwenden? Wie skaliert es im Verhältnis zur Nachfrage?
Vergleiche deine Werkzeuge oder finde andere, die das getan haben. Habe etwas geplant, um damit umzugehen. Vielleicht kannst du sogar eine Funktion verwenden.
Deine Kunden werden immer der am wenigsten vorhersehbare Punkt in deinem System sein. Wird eine Welle von Neuanmeldungen einen Zustrom von Aufrufen zu deinem Dienst verursachen? Sicherlich kann dein serverloser Compute skalieren, aber andere, nicht serverlose Komponenten deiner Anwendung sind möglicherweise nicht in der Lage, den plötzlichen Anstieg der Last zu bewältigen. Eine interessante Lösung ist die Verwendung von Funktionen, mit denen du andere Teile deiner Infrastruktur je nach Bedarf hoch- oder herunterskalieren kannst .
Entwerfen deiner Schnittstellen
In der FIRST Robotics Competition,1 wird die maximale Breite von Robotern auf 36 Zoll begrenzt, weil das die Breite einer Tür ist. Man könnte den Robotern erlauben, diese Breite zu überschreiten, indem sie zerlegt oder sogar gedreht werden, aber die Durchsetzung dieser Begrenzung vereinfacht den Transport aller für den Wettbewerb entwickelten Roboter erheblich. Behalte solche vernünftigen Ideen im Hinterkopf, wenn du die Standardarbeitsanweisungen für deine Dienste entwickelst.
Lass dich aber nicht von diesen Standards in deinen technischen Möglichkeiten einschränken. Ein weit verbreiteter, aber nicht perfekter Pseudostandard ist JSON, denn inzwischen kann wahrscheinlich sogar dein Lichtschalter JSON kodieren und dekodieren.
Konsistenz verbessert die Zuverlässigkeit, Ausfallsicherheit und Skalierbarkeit deines Systems nicht durch Zauberei, sondern durch die Festlegung und Kommunikation klarer Erwartungen an die Interaktion der Komponenten untereinander und reduziert den kognitiven Aufwand bei der Entwicklung, Fehlersuche und Wartung deiner Anwendungen.
Da du in deinem serverlosen System viele verschiedene unabhängige Komponenten, wie z. B. Funktionen, haben wirst, ist ein striktes Design für die Schnittstellen zwischen den Diensten entscheidend für die langfristige Stabilität.
Die Dienstleistungen werden immer mehr verteilt. Diese Verteilung bringt auch mehr Komplikationen mit sich. Wie in Kapitel 1 beschrieben, ist ein kleiner Dienst mit einer klar definierten Verantwortung einfach. Eine Konstellation aus solchen einfachen Diensten ist komplex. Im weiteren Verlauf dieses Kapitels geht es um bewährte Methoden, wie dein Dienst mit anderen Diensten interagiert und von ihnen abhängt, und wie andere Dienste mit deinem Dienst interagieren und von ihm abhängen.
Nachrichten/Payloads
Es ist wichtig, sowohl die Eingangs- als auch die Ausgangsnutzlast eines Systems sorgfältig zu planen.
JSON
Die meisten Nachrichten werden in JSON übermittelt. JSON ist nicht perfekt, aber es ist allgegenwärtig. Wie bei jedem universell genutzten Werkzeug wird auch JSON nicht jeden einzelnen Anwendungsfall mit Anmut und Perfektion behandeln. Zum Beispiel funktioniert der Zahlentyp von JSON nicht immer so, wie du es erwartest, denn 64-Bit-Zahlen in JavaScript sind keine 64-Bit-Ganzzahlen. Dies ist ein perfektes Beispiel dafür, wie sich deine Komponenten an ihre Schnittstelle anpassen müssen und wie sich die Schnittstellen auf die Implementierungsdetails auswirken. Auch wenn dies ein Problem ist, das minimiert werden sollte, war JSON keine bewusste Entscheidung: Es wurde von der Bevölkerung gewählt.
Ein durchdachtes Design deiner Payloads sollte auch ein Standardformat für Fehlermeldungen beinhalten, wenn eine Arbeitseinheit auf ein Problem stößt. Erinnere dich: Nur weil du erwartest, dass etwas funktioniert und dein Code keine Ausnahme auslöst oder mit einem Fehler zurückkehrt, heißt das nicht, dass er wie erwartet funktioniert.
Sicherung von Nachrichten im Ruhezustand
HTTPS bietet eine Verschlüsselung während der Übertragung, um die Nachrichten vor Abhörern zu schützen. Bei der Verschlüsselung im Ruhezustand wird sichergestellt, dass die Daten verschlüsselt sind, wenn sie auf einer Festplatte liegen. Die Nutzdaten eines Funktionsaufrufs können auf der Festplatte gespeichert werden, aber nicht alle Nutzdaten werden sicher gespeichert. Behalte dies im Hinterkopf, wenn du entscheidest, welche Daten du in Nachrichten weitergibst, und verwende eine angemessene Verschlüsselung für alle Daten, die auf der Festplatte landen könnten. Vergewissere dich, dass deine Warteschlangen im Ruhezustand verschlüsselt sind, sofern dies möglich ist. Vermeide es, sensible Daten zu protokollieren .
Sitzungen und Benutzer/Authentifizierung
Ein wichtiger Teil deiner Schnittstellen, den du berücksichtigen musst, ist die Authentifizierung. Authentifizierung bedeutet, zu wissen, dass eine Entität diejenige ist, die sie vorgibt zu sein. Je nachdem, wie eine Funktion aufgerufen wird oder eine Komponente eine Aufgabe bearbeitet, gibt es entweder eine implizite oder explizite Autorisierungskomponente, die von dieser Authentifizierung abhängt. Bei der Autorisierung wird sichergestellt, dass eine identifizierte Entität eine Aktion ausführen oder auf bestimmte Daten zugreifen darf. Vertraue niemals einer Nutzlast allein, denn dem Netzwerk ist nicht zu trauen. Wenn die Funktion ausgeführt wurde, kannst du in der Regel davon ausgehen, dass der Aufrufer dazu berechtigt ist. Einige serverlose Muster verlassen sich jedoch auf Informationen über die Benutzersitzung, die von einem API-Gateway bereitgestellt werden. Nimm diese Daten nie für bare Münze: Überprüfe sie immer auf irgendeine Weise. Für einige Systeme bedeutet das, dass JSON-Web-Tokens (JWTs) verwendet; für andere bedeutet es, dass die Sitzungsinformationen mit einem anderen Dienst validiert werden.
Unbegrenzte Anfragen vermeiden
Einige Anfragen sind nicht zeitlich gebunden und verwenden Zeitüberschreitungen, um dies auszugleichen. Wenn du deinen Code schreibst, dann schreibe nicht nur für den Moment, sondern auch für die Zukunft und sorge für Konsistenz und Standardisierung. Ein solcher Standard wäre zum Beispiel, niemals eine unbegrenzte Anfrage standardmäßig zuzulassen. Zum Beispiel muss eine SQL-Abfrage standardmäßig mit einer LIMIT
-Klausel versehen sein, um zu verhindern, dass sie mit zunehmender Nutzung immer komplexer wird, und um die wertvolle Ressource Datenbank zu schützen.
HTTP hat sich unter anderem wegen seiner Vielseitigkeit durchgesetzt. Es ist zwar leistungsfähig, aber kein perfektes Protokoll, und Entwickler haben Schwierigkeiten, seine volle Leistungsfähigkeit auszuschöpfen. Eine zu wenig genutzte Funktion sind Header, die eine großartige Möglichkeit sind, Metadaten über eine Anfrage zu kapseln, die mit dem Namensraum X-
erweitert werden können, um einen nicht standardmäßigen Header anzugeben. Die meisten benutzerdefinierten Kopfzeilen werden mit einem zusätzlichen Namensraum wie X-LEARNING-SERVERLESS
implementiert.
Statuscodes sind für den Erfolg von HTTP als Transportmechanismus unerlässlich, aber deine Dienste sollten genau definieren, was jeder Status bedeutet. Außerdem solltest du auf die Ideologie der Statuscodes externer Dienste achten. Im Allgemeinen sind Statuscodes im Bereich 200
oder 2xx
erfolgreiche Anfragen, Statuscodes im Bereich 4xx
weisen auf ein Problem mit der Gültigkeit der Anfrage hin, und Statuscodes im Bereich 5xx
sind für serverseitige Probleme und Fehler reserviert. Aber nicht alle Status sind im Buch implementiert. Wenn du zum Beispiel ein privates GitHub-Repository besuchst, während du abgemeldet bist oder ein Konto verwendest, das keinen Zugriff auf dieses Repository hat, bekommst du eine 404
oder File Not Found
. Die Anwendung sagt dir, dass es nicht gefunden wurde, obwohl es existiert. GitHub hat es tatsächlich gefunden, festgestellt, dass du es nicht sehen konntest, und anstatt Daten über seine Existenz preiszugeben, hat es gelogen und gesagt, es sei nicht gefunden worden. Dies wird von vielen als bewährte Methode angesehen und ist ein weiterer Grund, warum die Implementierung deiner Statuscodes standardisiert und gut dokumentiert sein sollte.
Ein weiteres Beispiel für die Stärke von granularen Statuscodes ist die Mitteilung, dass ein Ergebnis erfolgreich war, das System aber bereits davon wusste. Du möchtest vielleicht eine Erfolgsmeldung zurückgeben, unabhängig vom vorherigen Status, weil das Endergebnis dasselbe ist. Vielleicht möchtest du auch einen spezifischeren Status zurückgeben, wie z. B. 208, Already reported
. Aber du möchtest solche Informationen vielleicht nicht nach außen geben, da es für Hacker nützlich sein könnte, zu wissen, ob ein Benutzer mit einem geleakten Passwort ein Konto auf deinem System hat. Oftmals gibt eine Website mit strenger Ratenbegrenzung und Überwachung falscher Anmeldeversuche Informationen darüber preis, welche E-Mails auf einem anderen Endpunkt registriert sind. Lass niemals zu, dass deine Schnittstellen versehentlich durchsickern.
Schnittstelle versus Implementierung
Genauso wie eine Schnittstelle nicht die Implementierung vorschreiben sollte, sollte eine Implementierung nicht die Schnittstelle vorschreiben. Ich habe an einem System mit einer Reihe von Regeln gearbeitet, die in einer YAML-Datei kodiert waren. Während ich einen anderen Ingenieur in das Team aufnahm, führte ein Fehler in dieser Datei dazu, dass ein Teil des Systems nicht mehr funktionierte. Der Ingenieur wollte einen Testfall für die CI/CD-Pipeline erstellen, der verhindern würde, dass eine fehlerhafte Konfiguration bereitgestellt wird. Klingt nach einem soliden Anwendungsfall für bewährte Methoden...oder? Bis ich ihm erklärte: "Das ist keine Datei, das ist eine Datenbank. Die Datei bestand aus Regeln, die unabhängig voneinander funktionieren sollten. Ein Fehler in einem Eintrag sollte nicht dazu führen, dass das ganze System nicht mehr funktioniert. Die Datenbank ist zufällig eine Datei, weil wir keine Datenbank brauchen. Ein fehlerhafter Eintrag in dieser Datei sollte nicht verhindern, dass ein guter Eintrag im selben Commit oder Deployment veröffentlicht wird. Es ist wichtig, dass die Datei keine Syntaxfehler hat (korrupte Datenbank) und vielleicht auch, dass die Daten im richtigen Layout sind (Validierung der Daten vor dem Speichern). In diesem Beispiel ist die Schnittstelle nicht die Implementierung. Im Moment interessiert uns nur, wie die Regeln verarbeitet wurden, nicht wie sie gespeichert wurden.
Erinnere dich daran, dass deine Schnittstelle keine Implementierungsdetails preisgeben sollte, da du dich sonst auf eine Art und Weise festlegst, die Dinge zu tun. Du willst flexibel sein, wie du sie implementierst.
Vermeiden Sie versteckte Kopplungen und Schnittstellen
Was passiert, wenn du einen Datenspeicher wie Redis mit einem anderen Dienst teilst? (Redis ist ein In-Memory-Datenspeicher, der häufig für das Caching oder die Speicherung temporärer Daten wie z. B. Benutzersitzungen verwendet wird). Manchmal kann selbst die gemeinsame Nutzung von so scheinbar harmlosen Dingen wie S3 oder Bucket-Speichern die Schnittstelle eines Dienstes unterbrechen und zu Problemen für alle Beteiligten führen. Du kannst einen intelligenten Umleitungscode wie 30X
verwenden, um Anfragen an die zugrundeliegende Ressource umzuleiten, aber wenn du die Anfrage an deinen Dienst weiterleitest, um die Ressource abzurufen, sparst du dir eine Menge Ärger, wenn du das Verhalten dieser Komponente oder sogar die zugrundeliegende Speicherung ändern möchtest.
Linien mit Logik
Wenn wir an ein Architekturdiagramm heranzoomen, sehen wir, dass die Linien in Wirklichkeit eher Kisten sind - und diese Kisten sind gefedert. Sie nehmen die Last auf, aber wenn sie zu stark belastet und nicht entlastet werden, können sie fehlschlagen. Ich habe diese Komponenten im vorigen Kapitel vorgestellt, und wir werden uns jetzt einige Möglichkeiten ansehen, sie zu gestalten.
Warteschlangen
Warteschlangen sind eine gute Möglichkeit, zwei Komponenten eines Systems zu entkoppeln. Du kannst eine Nachricht oder eine Arbeitseinheit zuverlässig zwischen Systemen weiterleiten, ohne dass sie direkt miteinander interagieren müssen, und du kannst Nachrichten speichern, während eine Komponente ausgefallen ist. Sie sind wie eine Voicemail für deine Systeme! Und genau wie bei der Voicemail gibt es auch hier Grenzen und eine automatische Löschung veralteter Nachrichten. Achte darauf, dass du die Versprechen, die deine Warteschlange als Teil ihrer Schnittstelle macht, verstehst, wenn du sie in dein System integrierst.
Ströme/Ereignis-Bus
Ein Stream oder Ereignisbus verbindet zwei Elemente auf entkoppelte und skalierbare Weise miteinander. Mit diesen Komponenten können Aktionen in deinem System Reaktionen auslösen, ohne dass du die Reaktionen explizit in der ursprünglichen Quelle der Aktion codieren musst. Du profitierst auch davon, dass du Aufgaben, die nicht sofort als Folge einer Aktion geschehen müssen, sondern fast in Echtzeit ausgeführt werden können, aufschieben kannst, anstatt dass die ursprüngliche Aktion fehlschlägt, weil sie keine Reaktion auslösen kann.
Den unglücklichen Weg entwerfen
Ja, es ist an der Zeit, über das Lieblingsthema des Autors zu sprechen, das Scheitern.
Die Oberfläche zwischen den Diensten bzw. die Art und Weise, wie ihre Schnittstellen interagieren, ist der kritischste Fehlerpunkt und erfordert ein angemessenes Design, um richtig entkoppelt zu werden.
Ein Eckpfeiler eines effektiven Ingenieurs ist die Fähigkeit, so viel unerwartetes Verhalten wie möglich in erwartetes Verhalten umzuwandeln. Da wir nicht unendlich viel Zeit haben, können wir das nicht für alle Aspekte tun, aber manchmal reicht es schon aus, etwas Unerwartetes so zu dokumentieren, dass es erwartet wird.
Eingabe validieren
Vergewissere dich, dass alle Eingaben, die in deine Komponenten fließen, validiert; vertraue nicht einmal den Metadaten der Anfrage selbst. Du kannst nie wissen, ob die von deinem Cloud-Provider "authentifizierte" Anfrage nicht versehentlich Daten umleitet oder Daten durchlässt, die nicht authentifiziert sind. Deshalb wird empfohlen, auch diese Daten zu validieren, um sicherzustellen, dass sie authentisch sind. Nur weil du unter npm install
ein Plug-in findest, das dir die Authentifizierung ermöglicht, oder weil du in der Konsole deines Cloud-Providers auf eine Schaltfläche klickst, ist deine Integrationsarbeit noch lange nicht getan. Du musst alle deine Dienste validieren. Erinnere dich daran, dass die Natur des Netzwerks bedeutet, dass du Ereignisse auch nach dem Austausch des Codes, der sie erzeugt hat, erhältst und dass du sogar Nachrichten erhältst, die für andere Dienste bestimmt sind, die zuvor dieselbe IP-Adresse belegt haben könnten.
Auch Webhooks (die wir später unter "Webhooks" besprechen ) von Dienstleistern wie Stripe müssen validiert werden. Es gibt keine Möglichkeit, den Absender der Nachricht allein über das Netzwerk zu überprüfen. Daher musst du die von ihnen bereitgestellte Signatur als authentisch bestätigen, bevor du auf der Grundlage der Nachricht irgendwelche Aktionen durchführst.
Versäumnisse
Wenn Schnittstellen die Fläche zwischen den Komponenten deiner Anwendung sind, sind Fehler Risse, die sich über diese Schnittstellen ausbreiten wollen. Jeder Ort, an dem zwei Komponenten miteinander verbunden sind, ist ein Punkt, an dem ein Fehler auftreten kann. Ein durchdachtes Schnittstellendesign kann Ausfälle minimieren, aber ihr Auftreten kann nie auf Null reduziert werden. Deshalb musst du sie in deinen Systemen einplanen, um ein Maximum an Ausfallsicherheit zu erreichen und ein Minimum an Weckrufen zur Reparatur defekter Dienste.
Teilweise Ausfälle
Ein Teilfehler ist eine Aufgabenausführung, die einen Teil der Arbeit erledigt hat, bevor sie fehlgeschlagen ist. Das ist ein Problem bei der Entwicklung robuster Systeme, da einige Schritte einer Aufgabe erfolgreich sein können und ein erneuter Versuch aufgrund dieses Teilerfolgs zu einem Fehlschlag führen kann. Bei der Besprechung von Verträgen in "Der Vertrag" haben wir gefragt, wie du vorgehen willst, wenn du versuchst, einen Benutzer zu einer Mailingliste hinzuzufügen, der bereits registriert ist. Wenn du dich dafür entschieden hast, in dieser Situation einen Fehlschlag zu melden, kann das den erneuten Versuch einer Aufgabe verhindern, die davon abhängt, dass dieser Schritt erfolgreich abgearbeitet wird. In diesen Fällen ist Idempotenz dein Freund: Das heißt, dieselbe Aktion wird mehrmals mit demselben Ergebnis ausgeführt. Es kann sein, dass du für den idempotenten Schritt unabhängig vom vorherigen Zustand eine Erfolgsmeldung zurückgeben möchtest, weil das Endergebnis dasselbe ist. Das kann dir helfen, mit Teilausfällen umzugehen, damit sie erfolgreich wiederholt werden können.
Das wird aber nicht bei allen Aktionen der Fall sein. Daher musst du beim Schreiben des Anwendungscodes für deine Funktionen besonders vorsichtig sein, um Schritte zu behandeln, die möglicherweise bereits erfolgreich abgeschlossen wurden. Du denkst vielleicht nicht, dass dies Teil deiner Schnittstelle ist, aber es wird auf jeden Fall offengelegt und sollte nicht nur bei der Implementierung, sondern auch im Vertrag und in den kommunizierten Erwartungen derKomponenteberücksichtigt werden.
Kaskadierende Ausfälle
Kaskadierende Ausfälle sind, wenn sich ein Ausfall in einem Teil des Systems oder der Anwendung auf das gesamte System ausbreitet. Willst du eine kurze Vorstellung davon haben? Wenn du eine klassische "dreistufige" Anwendung betreibst, stell dir vor, was passieren würde, wenn du die Datenbank ausfallen lässt. Je nach Implementierung deines Dienstes würde dies wahrscheinlich zu Verzögerungen oder Zeitüberschreitungen führen und deinen Dienst lahmlegen. Der Ausfall hat sich ausgebreitet.
Stell dir stattdessen vor, jemand führt eine Datenbankmigration durch, die die Benutzertabelle so sperrt, dass die Anmeldung nicht mehr möglich ist. Irgendwann verbrauchen mehrere Benutzer, die unbeabsichtigt auf die Anmeldung einhämmern, alle Ressourcen des Verbindungspools (du verwendest doch einen Verbindungspool, oder?), und alle Datenbankverbindungen werden von Prozessen belegt, die darauf warten, dass die Tabelle entsperrt wird. Die Aktionen der Benutzer, die die Website durchsuchen konnten, verlangsamen sich bis zum totalen Ausfall, bei dem alle verfügbaren Instanzen, auf denen die monolithische Webanwendung läuft, mit Anfragen belegt sind, die auf die Datenbank warten, und alle neu gesponnenen Instanzen auf Datenbankverbindungen warten, die völlig erschöpft sind.
Um diese Art von Ausfällen zu vermeiden, musst du Dienste isolieren und entkoppeln sowie Ausfälle abgrenzen.
Die Giftpille oder die Bedeutung der Schnittstellenstabilität
Bei synchronen Ereignissen liegt die Handhabung von Wiederholungen beim Aufrufer der Funktion. Bei verwalteten Integrationen, wie in unserem Beispiel mit den Streams, bei denen die Aufrufe synchron sind, die Komponente für dich aber insgesamt asynchron erscheint, ist die Implementierungslogik des Cloud-Providers für die Wiederholungen zuständig. Im Fall der DynamoDB-Streams gibt es eine Metrik, die du konsumieren oder auf die du aufmerksam machen kannst und die IteratorAge
heißt. Daran kannst du den Status der internen AWS-Logik erkennen, die den Stream verarbeitet, oder die iterator
. Daran erkennst du, dass dein Stream blockiert ist, was allgemein als Giftpille bekannt ist. Die Giftpille ist ein gutes Beispiel für die Bedeutung von Schnittstellen. Wenn eine Nachricht in einem Stream nicht verarbeitet werden kann, verhindert sie, dass der Verbraucher dieses Streams zur nächsten Nachricht weitergeht. Eine einzige fehlerhafte Codezeile kann dein ganzes System lahm legen. Eine fehlerhafte Komponente kann dazu führen, dass andere in einer Reihe von Kaskadenfehlern fehlschlagen.
Nicht lautlos fehlschlagen
Lass wichtige Fehler nicht unbemerkt und unbehoben auf den Boden fallen. Abgesehen von der bereits erwähnten Wiederholung bestimmter asynchroner Funktionsaufrufe werden Fehler standardmäßig unbemerkt bleiben. Nicht jeder Fehler muss die Alarmglocken läuten lassen, aber ein guter Ausgangspunkt ist die Verwendung einer Warteschlange für tote Buchstaben, wenn du kannst, und einer Plattform zur Überwachung von Ausnahmen wie Sentry. Jede Aufgabe und Meldung in deinem System hat eine gewisse Bedeutung und sollte nicht zu einem Datenpunkt auf einer Fehlertabelle degradiert werden. Ingenieure machen vielleicht Witze darüber, dass sie ihren Code nur in der Produktion testen, aber selbst wenn du eine umfassende Test-Suite hast, gibt es keine bessere Quelle für die Wahrheit darüber, was gerade kaputt ist, als die Fehler, die in der Realität des Produktionsverkehrs auftreten.
Später, in Kapitel 6, werden wir die Überwachung besprechen, damit deine Systeme dich auf ihren eigenen Zustand und auf eine mögliche Verschlechterung des Dienstes hinweisen können.
Strategien zur Integration mit anderen Diensten
Schließlich gibt es einige Funktionen, die du bei der Integration mit anderen Diensten in dein Systemdesign berücksichtigen solltest.
Time-Outs
Jeder Betrieb kann fehlschlagen, aber in der Regel handelt es sich um einen Betrieb, der sich auf das Netzwerk oder eine andere Komponente des Computers als die CPU oder den Arbeitsspeicher verlässt. Wenn du Probleme mit der CPU oder dem Arbeitsspeicher hast, hast du es mit viel größeren Problemen zu tun; bei Funktionen oder Containern sollte der defekte Knoten schließlich fehlschlagen und wieder hochgefahren werden. Wenn du jedoch Daten über das Netzwerk sendest oder empfängst oder sogar eine Datei aus der lokalen Speicherung liest, solltest du auf Zeitüberschreitungen achten.
Computer sind sehr gehorsam. Wenn du dem Computer sagst, dass er eine Datei über das Netzwerk von einem nicht reagierenden System abrufen soll, wird er standardmäßig ewig warten! Stell dir vor, du schickst deinen Hund nach draußen, um die Zeitung zu holen, aber die Zeitung ist nicht mehr im Geschäft. Dein Hund wird gehorsam draußen sitzen und ewig warten. Du wärst überrascht, wie schlecht die Standardeinstellungen für Zeitüberschreitungen in vielen gängigen Sprachen und Bibliotheken oder sogar in der Netzwerkimplementierung auf Kernel-Ebene sind.
Zum Glück haben serverlose Funktionen standardmäßig eine Zeitüberschreitung eingebaut. Wenn du eine Funktion hast, die eine diskrete und wiederholbare Arbeitseinheit ist, und es in Ordnung ist, dass sie teilweise fehlschlägt und erneut versucht wird, hast du jetzt Time-outs! Aber wann und wo solltest du Time-outs verwenden? Die kurze Antwort lautet: immer und überall.
Zum Glück gibt es in der Welt der Funktionen eine Abkürzung. Wenn deine Funktion eine Sache erledigt, aber ein paar Netzwerkverbindungen braucht, um sie zu erledigen, kannst du eine Zeitüberschreitung für deine Funktion festlegen. Das musst du sogar tun. Eine Zeitüberschreitung, die sich nur auf die Verbindung bezieht, schützt dich nicht vor einer sehr langsamen, aber aktiven Antwort, die über das Netzwerk eintrifft. Aber nehmen wir an, du hast eine einminütige Auszeit für deine Funktion. Wenn du viele HTTP-Anfragen in einem Funktionsaufruf erledigen willst, solltest du für jede dieser Anfragen ein angemessenes Zeitlimit festlegen. Erkundige dich bei der Bibliothek, die du verwendest, nach ihren Standardeinstellungen. Einige Bibliotheken haben standardmäßig keine Zeitüberschreitungen. Bei anderen kannst du aus gutem Grund mehrere Zeitüberschreitungen festlegen. Wahrscheinlich gibt es eine Zeitüberschreitung für den Verbindungsaufbau und eine Zeitüberschreitung für die maximale Wartezeit auf Pakete von einem Server sowie eine allgemeine Zeitüberschreitung. Es kann sein, dass eine Verbindung schnell aufgebaut wird und der Server immer wieder mit zusätzlichen Informationen antwortet, aber das reicht vielleicht nicht aus, um zu verhindern, dass die Anfrage zu lange dauert.
Achte bei der Gestaltung deiner Time-outs auf die Service-Limits und Time-outs. Denke daran, dass Amazon API Gateway zum Beispiel eine maximale Timeout-Zeit von 29 Sekunden hat. Wenn dein Lambda 60 Sekunden braucht, erhalten deine Nutzer eine Antwort von 502
. Dein Lambda wird denken, dass alles gut gelaufen ist, und dein Nutzer wird denken, dass es überhaupt nicht funktioniert hat. Der Nutzer wird es erneut versuchen, und du musst dieselbe Arbeit zweimal ausführen. Dann denkt er, dass es nicht funktioniert hat, und versucht es erneut. Passe deine Time-outs an die Time-outs deiner Dienste an.
Wiederholungen
Wiederholungsversuche Arbeit ist immer mit einem gewissen Gleichgewicht verbunden. Wenn du es zu früh, zu oft oder zu oft wiederholst, kann dein Versuch, sicherzustellen, dass eine Arbeitseinheit erledigt wird, schnell dazu führen, dass im gesamten System keine Arbeit mehr erledigt wird.
Ein unheilbarer, oder endgültiger Fehler ist ein Fehler, der bei einem erneuten Versuch keine Chance auf ein erfolgreiches Ergebnis hat. In Wirklichkeit handelt es sich vielleicht nur um einen vorübergehenden Zustand, bei dem die Chance auf ein erfolgreiches Ergebnis nahe genug bei Null liegt, um abzurunden. Je nach Beobachter oder Designer des Systems kannst du bestimmen, ob ein Fehler, der bei einem erneuten Versuch wahrscheinlich erfolgreich sein wird, in der aktuellen Situation als beendet angesehen werden sollte. Ein einfaches Beispiel wäre ein Lambda mit einem Timeout-Limit von 60 Sekunden, das versucht, auf ein abgestürztes System zuzugreifen, das mindestens 5 Minuten braucht, um sich zu erholen. Sicher, der Fehler selbst ist nicht endgültig, aber angesichts aller verfügbaren Parameter hat er eine 0 %ige Chance, erfolgreich zu sein. Das heißt aber nicht, dass die Arbeit nicht noch einmal versucht werden sollte. Selbst wenn diese Arbeitseinheit bis zu ihrer natürlichen Erschöpfung in einer Fehlerwarteschlange wiederholt wird, kann es sein, dass das andere System, sobald es dort ankommt, bereits läuft und nicht mehr im Endstadium ist. Du solltest einplanen, wie du Ausfälle in deinen Warteschlangen überprüfen und/oder wiederholen kannst. Wenn du einfach die Schleusen öffnest und die gesamte Ausfallwarteschlange gegen einen Dienst wendest, der sich gerade erholt und den Rückstau an Wiederholungsversuchen von anderen Komponenten abarbeitet, kannst du ihn leicht wieder fehlschlagen lassen. Indem du deine Systeme mit denen deiner Kollegen koordinierst, kannst du größere und gefährlichere Ausfälle besser verhindern.
Exponentieller Backoff
Exponential Backoff ist die Strategie, bei der die Zeit zwischen den Wiederholungen exponentiell erhöht wird. Sie verhindert, dass eine Komponente, die bereits mit der Ausführung einer Aufgabe zu kämpfen hat, mit Wiederholungen überfordert wird. Durch eine exponentiell ansteigende Verzögerung können sich mehrere verteilte Komponenten ohne Koordination auf eine Wiederholungsstrategie einigen.
Diese Funktion ist für jede Art von netzwerkbasierter Anfrage nützlich, die fehlschlagen kann und wiederholt werden sollte. Du kannst sie für die Verbindung zu deiner Datenbank, die Interaktion mit APIs von Drittanbietern oder sogar für die Wiederholung von Fehlern aufgrund von Service- oder Ratenbeschränkungen verwenden.
Webhooks
Webhooks sind der Name für eine eingehende HTTP-Anfrage, die von der Drittanbieter-API zu einem Endpunkt kommt, den du bei ihnen registrierst. REST-APIs sind nicht bidirektional. Wenn du also eine beliebte API wie Stripe verwendest, werden sie Webhooks einsetzen, um dich über Änderungen zu informieren, so dass du nicht nach Updates fragen musst. Die interface
für den Webhook, d.h. das Schema und das Verhalten, das er implementieren soll, wird von der Drittpartei definiert.
Ein externer Dienst wie Stripe sendet dir sehr wichtige Webhooks, z. B. wenn ein Abonnement nicht verlängert wurde oder eine Rückbuchung erfolgt ist.
Überlegen wir uns das mal in der Welt der Altlasten. Stell dir vor, dein Zahlungsabwickler ruft dich an und teilt dir mit, dass die Zahlung eines Nutzers geplatzt ist. Würdest du ihn in die Warteschleife legen, während du herausfindest, was du mit dieser Information tun sollst? Oder schreibst du sie auf, überprüfst vielleicht, ob du die Informationen richtig hast (und überprüfst die Identität/Authentizität der Informationen), speicherst sie an einem wichtigen Ort und sagst ihnen, dass du sie erhalten hast? Es ist ihnen egal, was du mit den Informationen machst; das liegt nicht in ihrem Aufgabenbereich. Ihre Aufgabe ist es, dich zu informieren. Deine Aufgabe ist es, die Informationen getreu zu empfangen und dafür zu sorgen, dass etwas passiert. Wenn du eine synchrone Aktion in eine asynchrone Aktion umwandeln willst, funktioniert das auch.
Eine enge Kopplung in deinen Anwendungen kann zu kaskadenartigen Fehlern führen. Diese können sogar anwendungsübergreifend auftreten. Du betreibst vielleicht ein SaaS-Angebot, das Webhooks für andere Anwendungen im Internet bereitstellt. Wenn sie diese HTTP-Anfrage eng an ihre Datenbank koppeln, kann ein Ansturm von Datenverkehr einen Ausfall verursachen. Das ist häufiger der Fall, als du denkst. Entkopple alles, was du kannst.
In diesem Fall nimmst du eine HTTP-Anfrage über ein API-Gateway für einen Funktionsaufruf entgegen. Validiere die Nutzdaten als gültig und authentisch und wirf sie dann in eine Warteschlange, einen Stream oder einen Messaging-Bus. Gib den entsprechenden HTTP-Statuscode für die Nutzdaten an den Absender des Webhooks zurück. Das ist sehr wichtig, denn es hilft dir auch in anderer Hinsicht... Nehmen wir an, deine Datenbank ist ausgefallen. Dem Absender des Webhooks ist das vielleicht völlig egal. Du gibst ihm einen 5xx
Statuscode, also versucht er es immer wieder. Nun bauen diese Wiederholungen langsam einen DoS-Angriff auf deine Systeme auf, da sie dir die Zustellung dieser Nachrichten und Wiederholungen versprochen haben. Wenn stattdessen ein anderer Dienst ausfällt, kannst du die ganze Arbeit zwischenspeichern und sie wieder aufnehmen, wenn es wichtig ist.
Externe Dienstleistungen evaluieren
Wenn du den Luxus hast, Dienste zur Integration auszuwählen oder zu empfehlen, und das tust du wahrscheinlich, wenn du dieses Buch liest, suche im Internet nach anderen Entwicklern, die sich darüber beschweren, was der andere Dienst nicht kann. Welche Probleme haben sie? Wie viele Issues haben sie auf GitHub geöffnet? Wonach suchen sie auf Stack Overflow über dieses System? Wie viele sind zu einem Konkurrenten gewechselt, nachdem sie ein großes Problem hatten?
Tolle APIs auswählen
Wähle einen Dienst mit großartigen APIs. Suche nach einer sauberen Abstraktion für schwierige Prozesse, die du nicht verwalten willst. Wenn der Dienst in Zukunft nicht mehr in der Lage ist, deinen Anwendungsfall zu unterstützen, kannst du immer noch die API nutzen, die du integriert hast, und deine eigene Implementierung erstellen. Du musst dich nicht an den Service des Unternehmens binden, aber du sparst Zeit, wenn du dich an die API hältst.
Lies ihre Dokumente
Lies (oder scanne) alle der Dokumente, bevor du einen Dienst einführst oder auswählst. Achte auf die Kompromisse, die sie eingehen mussten. Was sind die Einschränkungen? Lies dir die Dinge durch, auch wenn du noch nicht weißt, was du mit ihnen machen willst. Vielleicht wirst du inspiriert. Vielleicht entdeckst du ein verborgenes Wissen. Vielleicht findest du heraus, dass du für die Funktion x die Aktion y durchführen musst. (Wir werden in Kapitel 11 darüber sprechen, wie du deinen Dienst mit einem Runbook dokumentieren kannst).
Raten-Grenzwerte
Die Dienste, mit denen du zusammenarbeitest, haben wahrscheinlich Ratenbeschränkungen. Deshalb solltest du nicht nur die Verwendung von Ratenbeschränkungen für deine eigenen Schnittstellen in Betracht ziehen, sondern auch, wie du die Ratenbeschränkungen höflich handhabst. Nur weil es Ratenbeschränkungen gibt, heißt das nicht, dass du API-Anfragen mit roher Gewalt bearbeiten musst, bis sie erfolgreich sind. Verwende Gleichzeitigkeitslimits für Funktionen, die mit ratenbegrenzten Diensten kommunizieren, und erinnere dich daran, dieses Limit auf alle Funktionen zu verteilen, die mit diesem Dienst interagieren, und auf alle Regionen, wenn du mehrere Regionen verwendest. Wenn du 100 Anfragen pro Sekunde ausführen darfst und in 2 Regionen arbeitest, solltest du die Gleichzeitigkeit in jeder Region auf 50 begrenzen. Unabhängig davon solltest du Wiederholungsmechanismen wie das exponentielle Backoff nutzen, um sicher zu sein, dass du es erneut versuchen kannst, wenn du auf eine Grenze stößt.
Fazit
Wenn du dein System entwirfst, denke nicht nur an die Boxen, sondern auch an die Linien, die Schnittstellen. Letztendlich wird die Wahl deiner Schnittstellen die Kultur und die Normen deiner technischen Organisation widerspiegeln, aber die Kodierung und der Transport wird wahrscheinlich eine Form von JSON über HTTP sein. Vertraue niemals einer Nachricht, die auf der Annahme beruht, dass sie gültig sein muss, wenn du sie empfangen konntest. Genauso wie du einen Fehler an die Produktion weitergeben kannst, kann dies auch das Netzwerkteam deines Cloud-Providers tun. Zu guter Letzt solltest du immer mit Fehlern und Ausfällen rechnen und planen, wie du die Auswirkungen von vermeidbaren Problemen minimieren kannst.
Herzlichen Glückwunsch! Du hast jetzt die grundlegenden Informationen zum Systemdesign, die du brauchst, um mit Serverless zu beginnen.
Get Serverless lernen now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.