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.