Bei Slash wickeln wir jährlich Ausgaben in Milliardenhöhe ab. Mit einem Wachstum von über 1000 % in den letzten 12 Monaten lösen wir ständig neue Probleme in immer größerem Umfang. Mit zunehmender Datenmenge und zunehmender Komplexität unseres Systems wurden unsere Annahmen über Datenbeziehungen oft widerlegt. Wir stießen auf Probleme aus verschiedenen Quellen: fehlerhafte Backfills, Bugs im Produktionscode und kleine Fehler bei der Datenmigration. Mit der Zeit wurde es immer schwieriger, die Richtigkeit unserer Daten zu überprüfen, was uns dazu veranlasste, das zu entwickeln, was wir heute als Health Checks bezeichnen – ein System, das die Richtigkeit der Daten in der Produktion kontinuierlich überprüft.
Gesundheitschecks sind ein Werkzeug, mit dem wir Invarianten in unserer Codebasis überprüfen können. Wenn wir Systeme entwickeln, entwerfen wir diese immer mit einer Reihe impliziter und expliziter Invarianten. Dabei handelt es sich um Regeln/Annahmen, von denen wir erwarten, dass sie immer zutreffen, damit wir darauf aufbauend komplexere Systeme entwickeln können.
Ein triviales Beispiel für eine Invariante: In derKarteTabelle in unserer Datenbank, in der jede Zeile eine Kreditkarte darstellt, gibt es eineStatusSpalte, in der einer der Werte „geschlossen” sein kann, und einegeschlossenGrundSpalte, die NULLABLE ist. Eine Invariante ist, dass dieStatusDie Spalte ist „geschlossen“, **wenn und nur wenn** diegeschlossenGrundDas Feld ist NICHT NULL.
Das obige Beispiel ist nicht unbedingt eine Bedingung, für die wir einen Gesundheitscheck schreiben würden, sondern ein Beispiel für eine Invariante, deren Gültigkeit wir bei der Arbeit mit diesem Bereich des Codebasis sicherstellen.
Unser ursprüngliches Design für den Gesundheitscheck
Unsere erste Version der Gesundheitschecks sah in etwa wie folgt aus:
Wir haben Gesundheitschecks ursprünglich so konzipiert, dass sie durch SQL-Abfragen definiert werden. Wir würden eine SQL-Abfrage für eine Tabelle ausführen, um „potenzielle Fehler” zu finden. Da die Ausführung von SQL-Abfragen für große Tabellen jedoch sehr aufwendig ist, führten wir diese Checks anhand einer letztendlich konsistenten Lesereplik (in unserem Fall Snowflake) durch. Für jedes Ergebnis der Abfrage führten wir anschließend einen Check anhand unserer Hauptproduktionsdatenbank durch, um sicherzustellen, dass es sich nicht um einen Fehlalarm handelte. Dieses Design hatte einige Probleme:
- Wir mussten zwei Abfragen pflegen. Die erste Abfrage würde für die gesamte Tabelle gegen die Lesereplik ausgeführt werden. Die Betreuer müssten sich auch explizit Gedanken über die Paginierung für die Abfrage machen und diese vornehmen. Die zweite Abfrage würde für jedes einzelne Ergebnis, das die erste Abfrage zurückgeben würde, gegen die Produktion ausgeführt werden.
- Abfragen für die gesamte Tabelle würden immer komplexer und unlesbarer/schwerer zu warten werden, da wir versuchen würden, eine Reihe von Geschäftslogiken in einer einzigen SQL-Abfrage zu kodieren.
Zweite Wiederholung der Gesundheitschecks
Wir haben beschlossen:
- Gesundheitschecks sollten immer anhand unserer primären Produktionsdatenbank durchgeführt werden. Andernfalls könnten wir möglicherweise nicht jedes tatsächlich auftretende Problem erkennen.
- Das Durchführen von „vollständigen Tabellenscans“ in einer SQL-Abfrage ist ein absolutes No-Go, aber das Durchführen eines kontrollierten „vollständigen Tabellenscans“ durch Iterieren über jede Zeile und Durchführen einer Zustandsprüfung ist eigentlich in Ordnung – solange die Last konstant und vorhersehbar ist und keine Spitzen auftreten, ist in der Regel alles in Ordnung.
- Die Durchführung einer zeitlichen Iteration über Tabellen in der Produktion (und deren Abstraktion für den Entwickler) vereinfacht unsere DX erheblich:
- Wir müssen nicht mehr zwei komplexe SQL-Abfragen mit Paginierung pflegen. Wir müssen nur noch die Anwendungslogik zum Überprüfen des Zustands eines einzelnen Elements definieren.
- Jeder Gesundheitscheck wird über eine gesamte Tabelle definiert (in Zukunft werden wir möglicherweise Gesundheitschecks so erweitern, dass sie speziell über eine Indexdefinition iterieren können).
Eine wichtige Erkenntnis, die wir als Team im Laufe der Zeit gewonnen haben, ist, dass vorhersehbare Systeme, die eine konstante Last ausüben, ideal sind. Der größte Verursacher von Produktionsproblemen sind in der Regel plötzliche, unvorhersehbare Veränderungen, wie z. B. ein starker Anstieg der DB-Last oder eine plötzliche Änderung im internen Abfrageplaner einer Datenbank.
Der kontraintuitive Aspekt einer konstanten Auslastung von Systemen, insbesondere in Bezug auf Datenbanken und Warteschlangen, ist, dass dies verschwenderisch erscheinen kann. Früher war ich der Meinung, dass es besser sei, Systeme standardmäßig nicht zu belasten und nur bei Bedarf einige Male am Tag zu arbeiten. Dies kann jedoch zu sprunghaften Arbeitslasten führen, die unser System manchmal unerwartet beeinträchtigen würden. Tatsächlich haben wir festgestellt, dass es in der Regel besser ist, wenn über einen langen Zeitraum eine geringe konstante Last auf die Datenbank wirkt, selbst wenn diese konstante Last etwa 1 bis 5 % der gesamten CPU-Auslastung rund um die Uhr ausmacht. Wenn die Last vorhersehbar ist, lässt sich die Auslastung insgesamt leicht überwachen. Diese Vorhersehbarkeit ermöglicht es uns, horizontal zu skalieren und potenzielle zukünftige Leistungsprobleme im Voraus zu planen.
Das Team von AWS hat dazu einen aufschlussreichen Artikel verfasst: Zuverlässigkeit, konstante Arbeit und eine gute Tasse Kaffee
Die zweite Version der Gesundheitschecks sieht nun wie folgt aus:
Mit diesem Design wird es sehr einfach, neue Zustandsprüfungen zu einer einzelnen Entität hinzuzufügen, da jede Zustandsprüfung als asynchrone Einzelfunktion definiert ist. Wir könnten diese Zustandsprüfungen mehrmals täglich über ganze Tabellen hinweg wiederholen, ohne Kompromisse hinsichtlich der Häufigkeit eingehen zu müssen, da die Last konstant ist. Bei der oben genannten Zustandsprüfung führt jede Entität eine schnelle Suche in der Tabelle „LimitRule“ durch. Diese Prüfungen laufen kontinuierlich und belasten die Datenbank zu jedem Zeitpunkt nur minimal, aber konstant.
Die Funktionsweise dieses Systems lässt sich vereinfacht wie folgt beschreiben:
- Wir führen unsere Überprüfungen zusätzlich zu Temporal durch, um eine endgültige Ausführung zu gewährleisten. Wir verwenden das Produkt „Schedules” von Temporal, um sicherzustellen, dass unser Cron jede Minute ausgeführt wird.
- Jede Minute überprüfen wir alle Prüfungen, die JETZT geplant werden sollten, und führen diese Prüfungen in einer Aufgabenwarteschlange aus. Wir drosseln unsere Aufgabenwarteschlange entsprechend, sodass niemals mehr als eine bestimmte RPS an unsere Datenbank gesendet wird.
- Jede ausgeführte Aktivität führt eine Art „konstante“ Arbeit aus. Keine O(N) oder andere Zeitkomplexität, die proportional zur Gesamtzahl der Entitäten in der Datenbank wächst. Dadurch wird sichergestellt, dass Aktivitäten schnell ausgeführt werden, leicht vorhersehbar sind und die Aufgabenwarteschlange selbst nicht blockieren.
Was kommt als Nächstes?
Gesundheitschecks bilden heute eine wichtige Grundlage in unserer Testpipeline. Sie sind die Basis für unsere laufenden Produktionsverifizierungstests. Mit unserem Wachstum wurden immer mehr allgemeine Tests erforderlich, um die Stabilität und Funktionalität unseres Produkts zu gewährleisten. Wir haben mehrere Anläufe gebraucht, um dies richtig hinzubekommen. Seit drei Jahren wollten wir eine Form von Ledger-Tests/Datenverifizierung in der Produktion implementieren. Aber erst vor anderthalb Jahren haben wir endlich etwas entwickelt. Insgesamt haben wir einige wichtige Lektionen gelernt:
- Versenden Sie etwas, egal was, und wiederholen Sie dies im Laufe der Zeit. Wir hätten die zweite Version unserer Gesundheitschecks nicht erreichen können, ohne die erste Version versandt zu haben.
- Halten Sie es einfach – wir möchten es Ingenieuren so einfach wie möglich machen, Gesundheitschecks zu erstellen und zu warten. Die Arbeit mit komplexer Geschäftslogik, die in mehrere SQL-Abfragen eingebettet ist, macht niemals Spaß.
- Führen Sie unsere Prüfungen direkt in der Produktion durch – das ist unsere Quelle der Wahrheit. Wenn wir die Prüfungen anhand anderer Kriterien durchführen, wird es zwangsläufig komplexer und anfälliger für falsche Positive/Negative.
Heute sind Gesundheitschecks der erste Schritt, den wir unternommen haben, um unsere Systeme außerhalb traditionellerer Formen wie Tests und Beobachtbarkeit besser zu testen und zu überprüfen. Während wir weiter wachsen, werden wir nach und nach neue Wege finden, um die Stabilität unseres Produkts zu gewährleisten.