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

Seite 2: Let it crash

Inhaltsverzeichnis

Worauf will der Autor nun hinaus? Auf ein Paradigma von Joe Armstrong, einem der Erlang-Väter, der in seiner Doktorarbeit folgende Thesen aufgestellt und mit dem Postulat "Let it crash" versehen hat:

  • Lass einen anderen Teil des Programms die Behandlung von Fehlern erledigen.
  • Wenn du nicht tun kannst, was du tun willst, stirb.
  • Programmiere nicht defensiv.

Seine Doktorarbeit bezog sich – nicht verwunderlich – auf Erlang. Die Sprache enthält und implementiert Aspekte, die für den erfolgreichen Einsatz des "Let it crash"-Paradigmas erforderlich sind:

  • "Billige" Akteure (auch Actors oder Aktoren genannt), und zwar Hunderttausende bis diverse Millionen pro laufender VM-Instanz. Man muss es sich so vorstellen, dass das Programm in viele theoretisch und je nach Hardware und Konfiguration auch praktisch parallel laufende Verarbeitungseinheiten geschnitten wird. Die Einheiten, die bei Erlang Prozesse heißen und ansonsten auch (zum Beispiel in der Programmiersprache Scala) als Actors bezeichnet werden, kapseln eine kleine Unit of Work, sodass die gesamte Programmverantwortung auf so viele Schultern verteilt wird, dass der Wegfall eines einzigen Actor die Stabilität des ganzen Programms nicht entscheidend beeinflussen kann.
  • Eine virtuelle Maschine, die die Aktoren gut isoliert und für ihren Datenaustausch als einziges Mittel das Message Passing anbietet. Die VM teilt jedem parallel laufenden Actor eine gewisse kleine Menge Rechenzeit zu, sodass sich die Parallelität auch auf einem einzigen Rechenkern simulieren lässt. Die eigentliche Parallelität steckt aber in der Isolation der Prozesse voneinander.
  • Mechanismen zur Externalisierung des Zustands, sodass ein Actor sich verabschieden und ein anderer, eventuell frisch gestarteter seine Arbeit mit demselben Zustand wieder aufnehmen kann. Das Zustands-Pattern ist in der objektorientierten Programmierung zwar bekannt, wird allerdings oft zugunsten der Datenkapselung in den Objekten komplett ignoriert. Behielten Aktoren ihre Daten für sich, würde der Ausfall eines einzelnen zum teilweisen Datenverlust führen.
  • Möglichkeiten, die Zustandsexternalisierung in einer zwiebelartigen Struktur aufzubauen, sodass sich Zustände weit weg von der Gefahrenzone halten lassen. Das heißt, dass der Zustand immer weiter nach innen zum Kern des so parallelisierten Programms verlagert wird, denn auch ein solches Programm würde einen funktionalen Kern benötigen, der die Koordination der parallel laufenden Actors untereinander übernimmt. Selbst wenn der Kern minimal ist, ließen sich die Zustandsdaten so näher zum Kern bringen. Dann wären die Aktoren, die als Serverinstanzen die Kommunikation mit Clients übernehmen und somit zum Beispiel für Fehler und absichtliche Irritation angreifbar sind, rein für die Verarbeitung zuständig, sodass der Tod eines einzigen dem Gesamtsystem nicht viel anhaben kann.
  • Funktionale Programmierung, die Weitergabe des Zustands impliziert und ohne explizite Kapselung auskommt. Bei ihr ist der gierige Datenhaltungsmodus nur auf einen einzigen Rekursionslauf beschränkt. Natürlich kann auch dieser ewig dauern beziehungsweise niemals enden, aber man versucht stattdessen, die Verarbeitungszyklen kurz zu halten und möglichst schnell wieder in die Rekursion zurückzukehren. Ideal für ein solches Vorgehen ist die Verarbeitung einer einzigen Nachricht, die kurz und endlich sein sollte.
  • Mechanismen, die andere Aktoren über den "Tod" des fehlerhaften Actor benachrichtigen und darauf entsprechend reagieren. Somit stellt man bestimmte Aktoren über die anderen, versucht sie redundant zu halten und gänzlich von der Datenverarbeitung zu befreien. Sie nehmen eine überwachende und kompensierende Rolle ein.
  • Trennung und hierarchische Organisation von Prozessen, die wiederum andere überwachen (sog. Supervisor), und von solchen, die ausschließlich Arbeit verrichten (Worker).
  • Mechanismen zur deklarativen Steuerung der Strategie für die Fälle, in denen Actors krachen. In Erlang beziehungsweise mit den sogenannten OTP-Behaviours lassen sich Worker konfigurieren, sodass nicht nur der aufzurufende Code hinterlegt wird, sondern auch, was mit den Workern passieren soll, wenn sie krachen. Sie können in ganzen Gruppen gestartet werden oder einer nach dem anderen oder schlichtweg still sterben, sodass es kein anderer mehr merkt. Auch Schwellwerte für die Anzahl der Neustarts lassen sich hinterlegen, denn der Zustand, den man einem frisch gestarteten Actor zufügt, kann so "kaputt" sein, dass der Aktor immer wieder "kracht".

Nun ein paar Beispiele aus Erlang, ohne allzu tief ins Detail zu gehen. Dafür sei auf die Literaturangaben am Ende des Artikels verwiesen.

register(?MODULE,
spawn(?MODULE, loop, [])),
?MODULE ! {start}.

Hier wird ein Prozess gestartet, der in Erlang die Instanz einer Funktion ist, im Beispiel loop. Der Prozess wird unter einem VM-instanzweit eindeutigen Namen registriert. Anschließend sendet man an ihn eine einfache Nachricht. Das heißt, Aktoren lassen sich aus anderen Aktoren heraus jederzeit starten und auch explizit beenden. Der "normale" Tod eines Prozesses in Erlang ist das Erreichen der Abbruchbedingung der Rekursion.

Einen Prozess kann man nicht nur einfach so starten, sondern gleich mit ihm verlinken. Zudem darf jeder Prozess sich über die anderen Prozesse stellen, indem er auf deren Sterbenachrichten lauscht, die die VM automatisch versendet. Stellt man keinen Prozess über den anderen, sterben die miteinander verlinkten Prozesse gemeinsam, ohne dass einer von ihnen auf diesen Umstand reagieren kann.

register(?MODULE,
spawn_link(?MODULE, loop, [])),
?MODULE ! {start}.
...
loop() ->
receive
{start} ->
process_flag(trap_exit, true),
loop();
{'EXIT', _Pid, stop} -> ...

Eine andere Überwachungsmöglichkeit ist das Monitoring. Dabei kann ein Prozess explizit einen anderen überwachen, indem er auf dessen Sterbenachrichten lauscht. Der überwachte Prozess weiß nichts von der Überwachung, bei der Verlinkung kennen sich hingegen beide Prozesse. Beim Monitoring hat der Tod des überwachten Actor keinerlei Auswirkung auf das Leben des Überwachers.

erlang:monitor(process, From),
...
loop() ->
receive
{'DOWN', _Ref, process, Pid, Reason} -> ...

Die OTP-Behaviours helfen beim Aufbau der Supervisor/Worker-Hierarchien und bieten komfortable Konfigurationsmöglichkeiten für die Strategie im Umgang mit dem Tod von Workern.

-behaviour(supervisor).
...
init(Args) ->
{ok, {{one_for_one, 2, 10}, [
{the_server,
{test_server, start_link,
[Args]}, permanent, 2000,
worker, [test_server]}
]}}.

Zu guter Letzt ein kleines Beispiel dafür, wie mit den OTP-Strukturen die Zustandsexternalisierung komfortabel, durchdacht und zuverlässig implementiert wird:

-behaviour(gen_server).
...
init(Args) ->
...
State = #state{id = Id, session_id = SessionId},
{ok, State}.
...
handle_cast(stop, State) ->
dr_session:stop(State#state.session_id),
{noreply, State};

Mit jedem Aufruf des Callbacks zur Nachrichtenverarbeitung (und anderen Callbacks auch, denn OTP-Behaviours bauen intensiv auf Callbacks) wird der bisherige Zustand übergeben, und es kommt ein gegebenenfalls modifizierter zurück, der sicher bis zum nächsten Aufruf verwahrt wird.

Das alles funktioniert mit geringen bis gar keinen Code-Änderungen in richtig verteilten Szenarien, in denen die Prozesse nicht nur innerhalb einer VM-Instanz verwaltet werden, sondern auf irgendwelchen anderen Maschinen laufen können. Erlang wurde eben für ein Umfeld erfunden, in dem es um unzählige Events geht, die automatisch – ohne menschliches Zutun – Routing, Fehlerbehandlung etc. erledigen.