Let it Crash: Paradigma für fehlertolerante, massiv-parallele Software

Know-how  –  52 Kommentare

Ständig wird propagiert, dass Entwickler Fehler und Ausnahmen im Programm tunlichst vermeiden oder zur Not abfangen und sinnvoll behandeln sollen. Oft ist jedoch Software nicht fehlerfrei, und wenn doch, scheitert man an der darunter liegenden Maschine. Eine unorthodoxe Antwort darauf kommt aus der Welt der Programmiersprache Erlang. "Let it crash" funktioniert aber auch anderswo.

Möglicherweise beschäftigt sich der Leser mit Ideen und Ansätzen robuster beziehungsweise fehlertoleranter Software. Er tut sein Bestes, regelmäßig mit statischer Code-Analyse bereits im Vorfeld potenzielle Schwachpunkte einer Software zu identifizieren.

Aber kennt nicht jeder einen Berufsgenossen namens Murphy? Einer, der Programme schreibt, veröffentlicht, Hardware designt, das Ganze testet und später auch nutzt. Murphy behauptet, dass alles, was schiefgehen kann, auch schiefgehen wird. Das verdeutlicht allein schon die Art, wie er die Wahrscheinlichkeit eines Betriebsausfalls definiert: Wenn die Wahrscheinlichkeit dessen, dass etwas richtig Schlimmes im Betrieb passieren kann, größer null ist, wird es definitiv passieren.

Auch ist es in manchen Situationen immer noch zu wenig oder schlichtweg unmöglich, an alle Eventualitäten zu denken. Insbesondere dann, wenn es nichtdeterministisch wird. Parallelität, insbesondere massive Parallelität, und große Datenmengen ignorieren den Determinismus komplett.

Hier gibt es Raum für andere Ansätze – etwa aus der Programmiersprache Erlang beziehungsweise der Plattform Erlang/OTP. Die Philosophie von Erlang besagt, dass es oft effektiver ist, Teile der Software "krachen" zu lassen und sie bei Bedarf wiederzubeleben, statt alle denkbaren Problem- und Absturzpotenziale im Programm von vornherein zu erkennen.

Zur Veranschaulichung sei die folgende Java-Codezeile herangezogen:

C = A / B; 

In ihr kann einiges schiefgehen, etwa:

  • A und B haben inkompatible Typen, also eines ist keine Zahl, und
  • B == 0.

Die Typeninkompatibilität ist in Java kein Problem. Aber auch hinter dem Autoboxing können Gefahren lauern, etwa Rundung. Geschweige denn, wenn es sich um Objekte statt primitive Typen handelt – diese neigen grundsätzlich gerne dazu, gleich null zu sein. Außerdem ist nicht jede Programmiersprache statisch typisiert. Bei dynamisch typisierten Sprachen wird es in dieser Zeile noch "bunter".

Und wenn man den Scope der Ausführung dieser Zeile in Betrachtung zieht, gibt es abhängig von der eingesetzten Programmiersprache noch mehr:

  • C, A und B sind nicht Thread-safe.
  • C == 0 durch Rundung.
  • Man erwartet C als String im weiteren Programmverlauf.
  • Das ist Copy/Paste-Code aus anderer Quelle, der hier ohne Anpassung gelandet ist.

Es handelte sich bei dem Beispiel nur um eine einzige Zeile. Doch was geschieht bei hunderten oder tausenden. Der eingebaute oder antrainierte Instinkt verleitet den Entwickler dazu, sich vor Problemen zu schützen.

Entwickler schützen sich vor Dingen, die schiefgehen, indem sie Dinge am Schiefgehen hindern. Das kommt daher, weil ihnen beigebracht wurde, defensiv zu programmieren. Sie würden zunächst etwas in der Art implementieren:

public class C { 
protected Integer divide(Integer A, Integer B) {
Integer C = 0;
try {
C = A / B;
} catch (ArithmeticException a) {
//hem, what now?..
}

return C; //great: C == 0 in case of error
}
}

Wie man sieht, hat der Autor bereits alles abgefangen, was als potenzielles Risiko erkennbar war. Trotzdem kann es je nach Programmiersprache, Stand der Sonne, Sternekonstellation etc. immer noch "rauchen". Was man dann normalerweise tut, sieht etwa wie folgt aus:

public class C { 
protected Integer divide(Integer A, Integer B) {
Integer C = 0;
if (A instanceof Integer &&
B instanceof Integer &&
(Integer)A * 10 > (Integer)B &&
(Integer)B != 0) {
try {
C = (Integer)A / (Integer)B;
} catch (ArithmeticException a) {
//hm, what now?
} catch (WrongStellarConstellationException w1) {
//move the stars
} catch (WrongSunPositionException w2) {
//move after the sun
} catch (Angle45DegreeToHorizon w3) {
//run for your life
} catch (Exception e) {
//just for the case
} catch (Throwable t) {
//if smth. weird is going on
}
}

return C; //great: still C == 0 in case of error
}
}

Der Code ist womöglich sicher gegen alle Eventualitäten. Egal wie falsch die übergebenen Parameter sind, scheitert das Programm noch vor der Ausführung der Teilung. Aber zu welchem Preis: Doppelte Typabfragen, hart kodierte Semantik der Grenzwertprüfung und eine ArtithmeticException, die wohl nie geworfen wird. Und dazu noch unendlich lange catch-Blöcke, die zunehmend breitere und hypothetischere Fehler abfangen sollen.

Das ist klassisches Beispiel defensiver Programmierung. Egal wie gut sich der Entwickler schützt, er wird immer irgendetwas übersehen, was das Programm zum Absturz bringt. Etwa ein Beispiel, bei dem außerhalb des divide-Aufrufs nicht mit C == 0 gerechnet wird.

Es ist also schwer, an alle Eventualitäten zu denken, selbst wenn man alle kennt. Und es ist schlicht unmöglich, alle Probleme zu erahnen, wenn es um riesige Datenmengen, eine enorme Anzahl von Ereignissen, parallele Verarbeitung und verteilte Systeme geht.

Dynamisch typisierte Sprachen neigen grundsätzlich mehr dazu, unerwartete Datenkonstellationen aufzuwerfen. Man kann sich zwar manchmal mit Pseudotypisierung (weichen Typdefinitionen wie bei Erlang) und statischer Code-Analyse behelfen. Doch was tun bei Schlamperei? Was tun, wenn auch das beste Analyse-Tool nichts findet? Auf statische Code-Analyse lässt sich allein nicht bauen.

Außerdem wären da noch Web-2.0-Applikationen. Sie arbeiten mit benutzergeneriertem Content und von "irgendwem" erzeugten Inhalten. Dieser "Irgendjemand" kann bösartig sein, ob absichtlich oder unabsichtlich, versehentlich oder unwissentlich. Diese Inhalte und die durch sie entstehenden Probleme und Fehlersituationen sind schlicht nicht vorhersagbar.

Dann ist noch das ganze Kupfer und Blech. Es ist wohl ein für alle Mal zu akzeptieren, dass sowohl Hardware als auch Netzwerke unzuverlässig sind. Egal wie teuer oder redundant sie sind. Und mit jeder Zeile Code, die für die Fehlerbehandlung implementiert wird, werden Systeme immer unwartbarer, weil der Code keine oder nur geringe fachliche Bedeutung trägt. Zudem ist Fehlerbehandlungscode selbst nicht frei von Fehlern.

Selbst der vermeintlich fehlerfreie Code-Block {} kann zu Problemen führen – die virtuelle Maschine kann sich ja schließlich auch verabschieden. Jedes Programm ist eine potenzielle Problemquelle, weil die Software niemals frei von Bugs ist.