© Dragon Ball. Goku.Der Programmierer-Verteidiger erwartet zu jeder Zeit und an jedem Ort im Code das Auftreten potenzieller Probleme und schreibt den Code so, dass er sich vorab vor ihnen schützt. Und wenn Sie sich nicht gegen ein Problem verteidigen können, stellen Sie zumindest sicher, dass die Folgen und Auswirkungen auf die Benutzer minimal sind.
Ich erinnere mich an den
FlashForward- Effekt von Hollywood-Blockbustern, als der Protagonist die bevorstehende Katastrophe sieht und äußerst ruhig bleibt, weil er im Voraus weiß, dass dies passieren wird, und Schutz davor hat. Die Idee hinter defensiver Programmierung ist es, sich vor Problemen zu schützen, die schwer oder unmöglich vorherzusehen sind. Ein Sicherheitsprogrammierer erwartet, dass Fehler überall im System und zu einem bestimmten Zeitpunkt auftreten, um sie zu verhindern, bevor sie Schäden verursachen. Das Ziel ist jedoch nicht, ein System zu erstellen, das niemals abstürzt, es ist immer noch unmöglich. Ziel ist es, ein System zu schaffen, das bei unvorhergesehenen Problemen ordnungsgemäß
abstürzt .
Lassen Sie uns genauer verstehen, was im Konzept "anmutig fallen" enthalten ist.
- Fallen Sie schnell. Im Falle eines unerwarteten Fehlers sollten alle Vorgänge sofort abgeschlossen werden, insbesondere wenn nachfolgende Berechnungen schwierig sind oder zu einer Beschädigung der Daten führen können.
- Ordentlich fallen. Wenn ein Fehler auftritt, sollte das Programm alle Ressourcen freigeben, Sperren entfernen, temporäre und halb aufgezeichnete Dateien löschen und Verbindungen schließen. Warten Sie, bis kritische Vorgänge abgeschlossen sind, deren Unterbrechung zu unvorhersehbaren Ergebnissen führen kann. Oder ein sicherer Weg, um diese Operationen zum Absturz zu bringen.
- Klar und schön fallen. Wenn etwas kaputt geht, sollte die Fehlermeldung einfach und präzise sein und wichtige Details aus dem Kontext des Systems enthalten, in dem der Fehler aufgetreten ist. Dies hilft dem Team, das für das System verantwortlich ist, das Problem so schnell wie möglich herauszufinden und zu beheben.
Aber Sie haben vielleicht eine Frage.
Warum Zeit mit Problemen verschwenden, die in Zukunft auftreten können?
Jetzt sind sie nicht da, der Code funktioniert einfach perfekt. Darüber hinaus können Probleme niemals auftreten. Schließlich machen Profis kein Engineering aus Gründen des Engineerings (
YAGNI - Sie werden es nicht brauchen)!
Die Hauptsache ist Pragmatismus
Andrew Hunt gibt in seinem Buch "Programmer-Pragmatist" die folgende Definition von defensiver Programmierung - "
pragmatische Paranoia ".
Schützen Sie Ihren Code vor:
- eigene Fehler;
- Fehler anderer Leute;
- Fehler und Ausfälle in anderen Systemen, in die Ihr integriert ist;
- Fehler von Eisen, Umgebungen und Plattformen, auf denen Ihre Anwendung funktioniert.
Lassen Sie uns verschiedene taktische und strategische Techniken für die defensive Programmierung diskutieren, die ein zuverlässiges und vorhersehbares System schaffen, das gegen willkürliche Fehler resistent ist.
Einige Tipps scheinen „Kapitäns“ zu sein, aber in der Praxis folgen viele Entwickler ihnen nicht einmal. Wenn Sie sich jedoch an einfache Praktiken und Ansätze halten, erhöht dies die Stabilität Ihres Systems erheblich.
Vertraue niemandem
Benutzerdaten sind standardmäßig unzuverlässig. Benutzer verstehen oft falsch, was uns (als Systementwickler) offensichtlich erscheint. Erwarten Sie falsche Eingaben und überprüfen Sie diese immer.
Überprüfen Sie auch die Menge der Eingabe. Es kann sein, dass der Benutzer zu viele von ihnen sendet. Gleichzeitig ist dies aus Sicht der Geschäftslogik das richtige Szenario. Dies kann jedoch zu einer zu langen Verarbeitung führen. Was kann man damit machen? Führen Sie es beispielsweise asynchron aus, wenn die Menge der Eingabedaten einen bestimmten Schwellenwert überschreitet und die Besonderheiten des Unternehmens es Ihnen ermöglichen, die Daten im Hintergrund zu verarbeiten.
Anwendungseinstellungen (z. B. Konfigurationsdateien) unterliegen auch dem Auftreten falscher Daten. Oft werden Programmeinstellungen in JSON-, YAML-, XML-, INI- und anderen Formaten gespeichert. Da dies alles Textdateien sind, sollte man erwarten, dass früher oder später jemand etwas daran ändert und Ihr Programm nicht mehr richtig funktioniert. Es kann sich entweder um einen Endbenutzer oder um jemanden aus Ihrem Team handeln.
Datenbanken, Dateien, zentralisierte Speicher von Konfigurationen, Registrierung - all diese Orte können von anderen Personen abgerufen werden, und früher oder später werden sie dort etwas ändern (
Murphys Gesetz ).
Müllabfuhr → Müllabfuhr
Eingaben, die die Validierung bestehen und mit der Verarbeitung beginnen, sollten sauber sein, wenn Ihr Code genau das tun soll, was Sie von ihm erwarten.
Es wird jedoch empfohlen, zusätzliche Datenüberprüfungen durchzuführen, auch wenn die Verarbeitung bereits begonnen hat. An kritischen Stellen (Abrechnung, Autorisierung, persönliche und vertrauliche Daten usw.) ist dies fast eine zwingende Voraussetzung. Dies ist erforderlich, um bei Fehlern im Code oder Problemen mit dem Eingabedaten-Validator den Ausführungsfluss so schnell wie möglich zu stoppen. Es ist schwierig, eine Qualitätsüberprüfung durchzuführen, indem alle möglichen Fehlerszenarien überprüft werden. Daher können Sie mit einfacheren Methoden überprüfen, ob das Programm noch ordnungsgemäß ausgeführt wird - Zusicherungen und Ausnahmen.
Gesunde Paranoidität ist ein charakteristisches Merkmal aller professionellen Entwickler. Es ist jedoch sehr wichtig, das optimale Gleichgewicht zu finden und zu verstehen, wann die Lösung bereits gut genug ist.
Separate Konfigurationen in Umgebungen
Eine häufige Ursache für Probleme ist die unzureichende Trennung von Konfigurationen zwischen Umgebungen oder das Fehlen einer solchen Trennung.
Dies kann zu vielen Problemen führen, zum Beispiel:
- Die Testumgebung beginnt mit dem Lesen und / oder Schreiben von Daten aus Produktion, Datenbanken, Warteschlangen und anderen Ressourcen.
- Die Testumgebung verwendet externe Integrationen und Services mit einem Produktionskonto.
- Mischen von Statistiken, Metriken, Fehlern aus verschiedenen Umgebungen;
- Sicherheitsverletzung (Entwickler, Tester und andere Teammitglieder erhalten Zugriff auf Produktionsressourcen);
- Schwer zu untersuchende Fehler in der Produktion (z. B. geht ein Teil der Nachrichten in der Warteschlange verloren, weil die Testumgebung mit dem Lesen beginnt).
Dies sind nur Beispiele. Eine vollständige Liste der Probleme, die durch eine unzureichend verantwortliche Trennung der Konfigurationen verursacht werden können, ist nahezu endlos und hängt von den Besonderheiten des Projekts ab.
Eine verantwortungsvolle Trennung der Konfigurationsdaten nach Umgebung kann die Wahrscheinlichkeit einer sofortigen ganzen Klasse von Problemen, die mit folgenden Problemen verbunden sind, erheblich verringern.
- Sicherheit
- Zuverlässigkeit;
- Support und Bereitstellung (DevOps-Ingenieure werden es Ihnen danken).
Darüber hinaus empfiehlt es sich, geheime Daten (Schlüssel, Token, Kennwörter) an einem separaten Ort zu speichern, der speziell zum Speichern und Verarbeiten von Geheimnissen entwickelt wurde. Solche Systeme verschlüsseln Daten sicher, verfügen über flexible Mittel zum Verwalten von Zugriffsrechten und ermöglichen es Ihnen, Schlüssel schnell zu ändern, wenn sie kompromittiert wurden. In diesem Fall müssen Sie keine Änderungen am Code vornehmen und die Anwendung erneut bereitstellen. Dies ist besonders wichtig für Systeme, die mit Finanztransaktionen, vertraulichen oder persönlichen Daten arbeiten.
Denken Sie an den Kaskadeneffekt
Eine häufige Ursache für den Fall großer und komplexer Systeme ist der Kaskadeneffekt. Der Ausfall oder die Verschlechterung der Funktionalität eines der Teile des Systems tritt auf, und nacheinander beginnen die anderen damit verbundenen Subsysteme zu versagen. Kaskadiert, bis das gesamte System nicht mehr zugänglich ist.
Ein paar schützende Tricks:
- Verwenden Sie progressive (exponentielle) Zeitüberschreitungen mit einem zufälligen Element.
- Legen Sie angemessene Werte für das Verbindungszeitlimit und das Socket-Zeitlimit fest.
- Im Falle eines Ausfalls einzelner Dienste ist ein Rückfall im Voraus vorgesehen. Es ist besser, einige Funktionen vorübergehend zu beeinträchtigen, die Dienste insgesamt zu deaktivieren, aber nicht das gesamte System zu beschädigen. Stellen Sie sich jedoch vor, dass der Benutzer in diesem Fall eine verständliche und nicht beängstigende Nachricht sieht und das Support- und Entwicklungsteam so schnell wie möglich von dem Problem erfährt.
Probleme schnell melden
Alle Systeme fallen aus. Manchmal passiert etwas Seltsames in ihnen, das die Schöpfer "alle 10 Jahre" erwarten. Integrationen und externe APIs sind regelmäßig nicht mehr verfügbar oder reagieren falsch. Ein Fallback für all diese Fälle ist oft schwierig, langwierig oder einfach unmöglich. Nehmen Sie diese Situation im Voraus vorweg und melden Sie sie so schnell wie möglich. Protokollierung auf der FEHLER-Ebene oder im Überwachungssystem - selbstverständlich. Das Hinzufügen einer zusätzlichen Validierung zum Healthcheck ist noch besser. Ideal ist es, eine Nachricht aus dem Code an Slack, Telegram, PagerDuty oder einen anderen Dienst zu senden, der Ihr Team sofort über das Problem informiert.
Es ist jedoch wichtig zu verstehen, wann es sinnvoll ist, Nachrichten direkt zu senden. Nur wenn ein Fehler, eine verdächtige oder atypische Situation mit Geschäftsprozessen verbunden ist und es wichtig ist, dass eine bestimmte Person oder Gruppe von Personen in einem Team so schnell wie möglich eine Benachrichtigung erhält und reagieren kann.
Alle anderen technischen Probleme und Abweichungen sollten mit Standardmitteln behandelt werden - Überwachung, Alarmierung, Protokollierung.
Häufig verwendete und / oder aktuelle Daten zwischenspeichern
Programme und Personen haben eines gemeinsam: Sie verwenden häufig wiederverwendete oder kürzlich angetroffene Daten. In stark ausgelasteten Systemen sollten Sie sich dies immer merken und Daten an den heißesten Stellen im System zwischenspeichern.
Die Caching-Strategie hängt stark von den Besonderheiten des Projekts und den Daten ab. Wenn die Daten veränderbar sind, muss der Cache ungültig gemacht werden. Überlegen Sie sich daher im Voraus, wie Sie dies tun werden. Denken Sie auch darüber nach, welche Risiken bestehen können, wenn veraltete Daten im Cache angezeigt werden, der Cache nicht mehr in Ordnung ist usw.
Ersetzen Sie teure Operationen durch billige
Das Arbeiten mit Zeichenfolgen ist eine der häufigsten Operationen in einem Programm. Und wenn dies nicht optimal durchgeführt wird, kann dies eine teure Operation sein. In verschiedenen Programmiersprachen können die Besonderheiten der Arbeit mit Zeichenfolgen variieren, aber Sie sollten sich immer daran erinnern.
In großen Anwendungen mit einer großen Codebasis wird häufig vor vielen Jahren geschriebener Code gefunden, der fehlerfrei funktioniert, jedoch hinsichtlich der Leistung nicht optimal ist. Oft führt eine banale Änderung der Datenstruktur von einem Array / einer Liste zu einer Hash-Tabelle zu einem ernsthaften Schub (selbst wenn nur an einer lokalen Stelle im Code).
Manchmal können Sie die Leistung verbessern, indem Sie den Algorithmus neu schreiben, um bitweise Operationen zu verwenden. Aber selbst in den seltenen Fällen, in denen dies gerechtfertigt ist, ist der Code sehr komplex. Berücksichtigen Sie daher bei einer Entscheidung die Lesbarkeit des Codes und die Tatsache, dass er unterstützt werden muss. Gleiches gilt für andere knifflige Optimierungen: Fast immer ist ein solcher Code schwer zu lesen und sehr schwer zu warten. Wenn Sie sich dennoch für knifflige Optimierungen entscheiden, vergessen Sie nicht, Kommentare zu schreiben, in denen beschrieben wird,
was dieser Code tun soll und
warum er so geschrieben ist.
Gleichzeitig sollte die Optimierung mit gesundem Pragmatismus behandelt werden:
- Wenn Sie als Entwickler einige Sekunden oder Minuten brauchen, ist es sinnvoll, dies sofort zu tun.
- Wenn mehr, ist es sinnvoll, dies sofort nur dann zu tun, wenn Sie sich der Notwendigkeit zu 100% sicher sind. In allen anderen Fällen ist es sinnvoll, es zu verschieben, in TODO-Code zu schreiben, weitere Informationen zu sammeln, Kollegen zu konsultieren usw.
Vorzeitige Optimierung ist die Wurzel allen Übels (Donald Knuth)
Schreiben Sie in einer niedrigeren Sprache um
Dies ist eine extreme Maßnahme. Low-Level-Sprachen sind fast immer schneller als höhere Sprachen. Diese Lösung hat jedoch einen Preis - die Entwicklung eines solchen Programms ist länger und schwieriger. Wenn Sie kritische Teile des Systems in einer einfachen Sprache umschreiben, können Sie manchmal die Produktivität erheblich steigern. Es gibt jedoch Nebenwirkungen - normalerweise verlieren solche Lösungen plattformübergreifend und ihre Unterstützung ist teurer. Treffen Sie daher eine sorgfältige Entscheidung.
Allein auf dem Feld ist kein Krieger
Abschließend möchte ich noch eine wichtige Sache erwähnen, vielleicht die wichtigste. Die Maßnahmen, die wir in den vorherigen Absätzen in Betracht gezogen haben, funktionieren nur, wenn alle Teammitglieder diese einhalten und jeder weiß, wer für was verantwortlich ist und was in einer kritischen Situation getan werden muss. Nach der Behebung des Problems ist es wichtig, ein Meeting (Post Mortem) mit allen interessierten Personen abzuhalten und herauszufinden, warum dieses Problem aufgetreten ist und was getan werden kann, um zu verhindern, dass dieses Problem in Zukunft auftritt. In vielen Fällen sind sowohl technische als auch Prozessänderungen erforderlich. Mit jedem neuen Post Mortem wird Ihr System zuverlässiger, das Team wird erfahrener und kohärenter und die Entropie im Universum wird etwas geringer sein;)
Der Artikel verwendet teilweise Materialien aus Warum defensive Programmierung der beste Weg für eine robuste Codierung ist (Ravi Shankar Rajan).