Fehler in Softwaresystemen sind unvermeidlich. Die Art und Weise, wie mit diesen Fehlern umgegangen wird, kann sich erheblich auf die Systemleistung, Zuverlässigkeit und das Geschäftsergebnis auswirken. In diesem Beitrag möchte ich die positiven Seiten von Fehlern diskutieren. Warum Sie Fehler suchen sollten, warum Fehler gut sind und warum die Vermeidung von Fehlern die Zuverlässigkeit Ihrer Anwendung verringern kann. Wir beginnen mit der Diskussion über Fail-Fast vs. Fail-Safe; dies führt uns zur zweiten Diskussion über Fehler im Allgemeinen.
Nebenbei bemerkt: Wenn Ihnen der Inhalt dieses und der anderen Beiträge dieser Reihe gefällt, schauen Sie sich meine
Fail-Fast-Systeme sind so konzipiert, dass sie bei einem unerwarteten Zustand sofort aufhören zu funktionieren. Dieser sofortige Ausfall hilft dabei, Fehler frühzeitig zu erkennen, was die Fehlerbehebung vereinfacht.
Der Fail-Fast-Ansatz stellt sicher, dass Fehler sofort erkannt werden. In der Welt der Programmiersprachen verkörpert Java beispielsweise diesen Ansatz, indem es beim Auftreten eines null
sofort eine NullPointerException
erzeugt, das System stoppt und den Fehler klarstellt. Diese sofortige Reaktion hilft Entwicklern, Probleme schnell zu identifizieren und zu beheben, wodurch verhindert wird, dass sie schwerwiegender werden.
Durch frühzeitiges Erkennen und Stoppen von Fehlern verringern Fail-Fast-Systeme das Risiko kaskadierender Fehler, bei denen ein Fehler zu weiteren führt. Dadurch können Probleme leichter eingedämmt und gelöst werden, bevor sie sich im System ausbreiten, und die Gesamtstabilität bleibt erhalten.
Für Fail-Fast-Systeme ist es einfach, Unit- und Integrationstests zu schreiben. Dieser Vorteil ist noch ausgeprägter, wenn wir den Testfehler verstehen müssen. Fail-Fast-Systeme weisen normalerweise direkt im Fehler-Stacktrace auf das Problem hin.
Allerdings bergen Fail-Fast-Systeme insbesondere in Produktionsumgebungen ihre eigenen Risiken:
Ausfallsichere Systeme verfolgen einen anderen Ansatz. Sie zielen darauf ab, sich selbst bei unerwarteten Bedingungen wiederherzustellen und weiter zu funktionieren. Dadurch sind sie besonders für unsichere oder volatile Umgebungen geeignet.
Microservices sind ein Paradebeispiel für ausfallsichere Systeme, deren Architektur Resilienz gewährleistet. Leistungsschalter, sowohl physisch als auch softwarebasiert, trennen fehlerhafte Funktionen, um kaskadierende Ausfälle zu verhindern und so den Betrieb des Systems aufrechtzuerhalten.
Ausfallsichere Systeme stellen sicher, dass Systeme selbst rauen Produktionsumgebungen standhalten, und verringern so das Risiko eines katastrophalen Ausfalls. Dadurch eignen sie sich besonders für unternehmenskritische Anwendungen, wie etwa in Hardwaregeräten oder Luft- und Raumfahrtsystemen, bei denen eine reibungslose Wiederherstellung nach Fehlern von entscheidender Bedeutung ist.
Allerdings haben ausfallsichere Systeme auch Nachteile:
Es ist schwierig zu entscheiden, welcher Ansatz besser ist, da beide ihre Vorteile haben. Fail-Fast-Systeme bieten sofortiges Debugging, ein geringeres Risiko kaskadierender Fehler sowie eine schnellere Erkennung und Behebung von Fehlern. Dies hilft dabei, Probleme frühzeitig zu erkennen und zu beheben und ihre Ausbreitung zu verhindern.
Ausfallsichere Systeme gehen reibungslos mit Fehlern um und eignen sich daher besser für unternehmenskritische Systeme und volatile Umgebungen, in denen katastrophale Ausfälle verheerende Folgen haben können.
Um die Stärken jedes Ansatzes zu nutzen, kann eine ausgewogene Strategie wirksam sein:
Ein ausgewogener Ansatz erfordert außerdem eine klare und konsistente Implementierung in allen Codierungs-, Überprüfungs-, Tooling- und Testprozessen, um eine nahtlose Integration sicherzustellen. Fail-Fast lässt sich gut in Orchestrierung und Observability integrieren. Dadurch wird der Fail-Safe-Aspekt effektiv auf eine andere OPS-Ebene verschoben, anstatt auf die Entwicklerebene.
Hier wird es interessant. Es geht nicht darum, zwischen Fail-Safe und Fail-Fast zu wählen. Es geht darum, die richtige Schicht dafür auszuwählen. Wenn beispielsweise ein Fehler in einer tiefen Schicht mit einem Fail-Safe-Ansatz behandelt wird, wird er nicht bemerkt. Das mag in Ordnung sein, aber wenn dieser Fehler negative Auswirkungen hat (Leistung, Datenmüll, Beschädigung, Sicherheit usw.), dann werden wir später ein Problem haben und keine Ahnung davon haben.
Die richtige Lösung besteht darin, alle Fehler in einer einzigen Schicht zu behandeln. In modernen Systemen ist die oberste Schicht die OPS-Schicht und das ist am sinnvollsten. Sie kann den Fehler an die Ingenieure melden, die am besten für die Behebung des Fehlers qualifiziert sind. Sie können aber auch sofortige Abhilfe schaffen, z. B. einen Dienst neu starten, zusätzliche Ressourcen zuweisen oder eine Version zurücksetzen.
Kürzlich war ich bei einem Vortrag, bei dem die Redner ihre aktualisierte Cloud-Architektur auflisteten. Sie entschieden sich für eine Abkürzung zu Microservices, indem sie ein Framework verwendeten, das ihnen im Fehlerfall einen erneuten Versuch ermöglicht. Leider verhält sich ein Fehler nicht so, wie wir es gerne hätten. Sie können ihn nicht allein durch Tests vollständig ausschließen. Ein erneuter Versuch ist nicht ausfallsicher. Tatsächlich kann er eine Katastrophe bedeuten.
Sie haben ihr System getestet und „es funktioniert“, sogar in der Produktion. Aber nehmen wir an, dass eine katastrophale Situation eintritt, dann kann ihr Wiederholungsmechanismus als Denial-of-Service-Angriff auf ihre eigenen Server wirken. Die Anzahl der Möglichkeiten, wie Ad-hoc-Architekturen wie diese versagen können, ist schwindelerregend.
Dies ist insbesondere dann wichtig, wenn wir Fehler neu definieren.
Bei Fehlern in Softwaresystemen handelt es sich nicht nur um Abstürze. Ein Absturz kann als einfacher und unmittelbarer Fehler angesehen werden, es gibt jedoch komplexere Probleme, die berücksichtigt werden müssen. Tatsächlich sind Abstürze im Zeitalter von Containern wahrscheinlich die besten Fehler. Ein System wird nahtlos und ohne Unterbrechung neu gestartet.
Datenbeschädigungen sind weitaus schwerwiegender und heimtückischer als ein Systemabsturz. Sie haben langfristige Folgen. Beschädigte Daten können zu Sicherheits- und Zuverlässigkeitsproblemen führen, die schwer zu beheben sind und umfangreiche Nachbearbeitungen erfordern. Möglicherweise sind die Daten nicht wiederherstellbar.
Cloud Computing hat zu defensiven Programmiertechniken wie Leistungsschaltern und Wiederholungsversuchen geführt, wobei umfassende Tests und Protokollierung im Vordergrund stehen, um Fehler zu erkennen und ordnungsgemäß zu behandeln. In gewisser Weise hat uns diese Umgebung in Bezug auf die Qualität zurückgeworfen.
Ein Fail-Fast-System auf Datenebene könnte dies verhindern. Die Behebung eines Fehlers geht über eine einfache Korrektur hinaus. Es erfordert das Verständnis seiner Grundursache und die Verhinderung eines erneuten Auftretens, was umfassende Protokollierung, Tests und Prozessverbesserungen einschließt. Dadurch wird sichergestellt, dass der Fehler vollständig behoben wird, wodurch die Wahrscheinlichkeit eines erneuten Auftretens verringert wird.
Wenn es sich um einen Fehler in der Produktion handelt, sollten Sie die Produktion wahrscheinlich rückgängig machen, wenn Sie dies nicht sofort tun können. Dies sollte immer möglich sein, und wenn nicht, sollten Sie daran arbeiten.
Fehler müssen vollständig verstanden werden, bevor eine Lösung vorgenommen wird. In meinen eigenen Unternehmen habe ich diesen Schritt aufgrund des Drucks oft übersprungen, in einem kleinen Startup ist das verzeihlich. In größeren Unternehmen müssen wir die Grundursache verstehen. Eine Kultur der Nachbesprechung von Fehlern und Produktionsproblemen ist unerlässlich. Die Lösung sollte auch eine Prozessminderung umfassen, die verhindert, dass ähnliche Probleme in die Produktion gelangen.
Fail-Fast-Systeme sind viel einfacher zu debuggen. Sie haben von Natur aus eine einfachere Architektur und es ist einfacher, ein Problem in einem bestimmten Bereich zu lokalisieren. Es ist wichtig, auch bei geringfügigen Verstößen (z. B. Validierungen) Ausnahmen auszulösen. Dies verhindert kaskadierende Fehlertypen, die in losen Systemen vorherrschen.
Dies sollte durch Unit-Tests weiter verstärkt werden, die die von uns definierten Grenzwerte überprüfen und sicherstellen, dass die richtigen Ausnahmen ausgelöst werden. Wiederholungsversuche sollten im Code vermieden werden, da sie das Debuggen außerordentlich erschweren. Ihr richtiger Platz ist die OPS-Schicht. Um dies weiter zu erleichtern, sollten Timeouts standardmäßig kurz sein.
Fehler können wir weder vermeiden, vorhersagen noch umfassend testen. Das Einzige, was wir tun können, ist, die Auswirkungen eines Fehlers abzumildern. Diese „Abmilderung“ wird häufig durch Langzeittests erreicht, die extreme Bedingungen so gut wie möglich nachbilden und die Schwachstellen unserer Anwendungen aufdecken sollen. Dies reicht jedoch selten aus. Bei robusten Systemen müssen diese Tests häufig anhand echter Produktionsfehler überarbeitet werden.
Ein gutes Beispiel für eine Ausfallsicherung wäre ein Cache mit REST-Antworten, der es uns ermöglicht, weiter zu arbeiten, selbst wenn ein Dienst ausgefallen ist. Leider kann dies zu komplexen Nischenproblemen wie Cache Poisoning oder einer Situation führen, in der ein gesperrter Benutzer aufgrund des Caches noch Zugriff hatte.
Fail-Safe wird am besten nur in der Produktion/Staging und in der OPS-Schicht angewendet. Dadurch wird die Anzahl der Änderungen zwischen Produktion und Entwicklung reduziert. Wir möchten, dass sie so ähnlich wie möglich sind, aber es ist immer noch eine Änderung, die sich negativ auf die Produktion auswirken kann. Die Vorteile sind jedoch enorm, da die Beobachtbarkeit ein klares Bild von Systemfehlern liefern kann.
Die Diskussion hier ist ein wenig geprägt von meinen neueren Erfahrungen mit dem Aufbau beobachtbarer Cloud-Architekturen. Dasselbe Prinzip gilt jedoch für jede Art von Software, egal ob eingebettet oder in der Cloud. In solchen Fällen entscheiden wir uns oft dafür, Ausfallsicherheit im Code zu implementieren. In diesem Fall würde ich vorschlagen, sie konsequent und bewusst in einer bestimmten Schicht zu implementieren.
Es gibt auch einen Sonderfall von Bibliotheken/Frameworks, die in diesen Situationen oft inkonsistentes und schlecht dokumentiertes Verhalten bieten. Ich selbst bin in einigen meiner Arbeiten solcher Inkonsistenzen schuldig. Es ist ein Fehler, der leicht passiert.
Dies ist mein letzter Beitrag zur Theorie der Debugging-Reihe, die Teil meines Buches/Kurses zum Debugging ist. Wir denken oft, dass Debugging die Aktion ist, die wir ausführen, wenn etwas fehlschlägt. Das ist es nicht. Debugging beginnt in dem Moment, in dem wir die erste Codezeile schreiben. Wir treffen Entscheidungen, die sich beim Coden auf den Debugging-Prozess auswirken. Oft sind wir uns dieser Entscheidungen erst bewusst, wenn ein Fehler auftritt.
Ich hoffe, dieser Beitrag und diese Serie helfen Ihnen, Code zu schreiben, der auf das Unbekannte vorbereitet ist. Beim Debuggen geht es naturgemäß um das Unerwartete. Tests können dabei nicht helfen. Aber wie ich in meinen vorherigen Beiträgen gezeigt habe, gibt es viele einfache Praktiken, die wir anwenden können, um die Vorbereitung zu erleichtern. Dies ist kein einmaliger Prozess, sondern ein iterativer Prozess, der eine Neubewertung der getroffenen Entscheidungen erfordert, wenn wir auf Fehler stoßen.