Bei Erlang geht es im Kern nicht um leichtgewichtige Prozesse und Message Passing (2023)
(stevana.github.io)- Der Schlüssel dazu, dass Erlang zuverlässige verteilte Systeme ermöglichen konnte, lag weniger in den leichtgewichtigen Prozessen selbst als in den behaviours, die wiederkehrende Muster für Nebenläufigkeit und Fehlerbehandlung verallgemeinern
- behaviours stellen wie Interfaces Implementierungspunkte bereit, verbergen aber zugleich, wie bei
gen_server, die Nebenläufigkeitsverarbeitung in gemeinsamen Komponenten und lassen Anwendungscode eher wie sequenzielle Logik wirken - Joe Armstrongs Dissertation behandelt, wie große Systeme wie AXD301 aus kleinen Bausteinen wie
gen_server,gen_event,gen_fsm,supervisor,applicationundreleaseaufgebaut wurden supervisorstartet fehlgeschlagene Prozesse mit Strategien wieone_for_oneoderone_for_allneu; die Philosophie „Let it crash!“ verlagert die Verantwortung für Recovery in den Supervisor-Baum- behaviours in Form von Zustandsmaschinen erleichtern Simulationstests und formale Verifikation; Entwickler können sich auf die Semantik des Problems konzentrieren statt auf die Komplexität von Nebenläufigkeit und Simulatoren
Das Problem, das Erlang lösen sollte
- Erlang begann als Werkzeug zum Bau zuverlässiger verteilter Systeme
- Anfangs war es eine Prolog-Bibliothek zum Bau zuverlässiger verteilter Systeme; später wurde es über einen Prolog-Dialekt zu einer eigenständigen Sprache
- Es wurde zur Programmierung von Telefonvermittlungen bei Ericsson eingesetzt und bewältigte in den 80er- und 90er-Jahren Traffic in der Größenordnung von Hunderten Millionen Menschen sowie strenge SLAs
- Ericsson verbot 1998 die Nutzung von Erlang; das Entwicklungsteam argumentierte, wenn man es schon verbiete, solle man es als Open Source veröffentlichen, und Ericsson setzte dies um
- Joe Armstrong war eine der Schlüsselfiguren bei Design und Implementierung von Erlang; 2002 begann er am SICS mit seiner Doktorarbeit und schloss 2003 Making reliable distributed systems in the presence of software errors ab
- Die Dissertation konzentriert sich weniger auf Mathematik oder Theorie als auf die Ideen hinter Erlang und die Erfahrungen beim Bau zuverlässiger verteilter Systeme
- Die große Idee von Erlang sind nicht leichtgewichtige Prozesse und Message Passing, sondern allgemeine Komponenten, die in Erlang behaviours genannt werden
behaviours: Verbindung aus Interface und Infrastruktur
- Erlang-behaviours sind Bündel von Typsignaturen, die wie Interfaces in Java oder Go mehrere Implementierungen haben können
- Wenn Programmierer eine Implementierung dieses Interfaces bereitstellen, können sie generische Funktionen verwenden, die gegen dieses Interface geschrieben wurden
- Der zentrale Unterschied ist, dass behaviours über einfache Interfaces hinaus auch Infrastrukturcode mitliefern
- Anwendungsprogrammierer schreiben die Semantik des Problems, also die Business-Logik
- Infrastrukturcode wie Nebenläufigkeit wird automatisch vom behaviour bereitgestellt
- behaviours enthalten Best Practices, die von erfahrenen Experten geschrieben wurden
- Wenn das Gesamtsystem eine kleine Menge von behaviours wiederverwendet, kann das System ohne Codeänderungen profitieren, sobald die behaviour-Implementierung verbessert wird
- Die Nutzung von behaviours erzwingt Struktur und erleichtert Tests sowie formale Verifikation
gen_server: Nebenläufigkeit hinter sequenziellem Code verborgen
- Das Beispiel für
gen_serverist ein Key-Value-Store:storespeichert Schlüssel-Wert-Paare,lookupsucht den Wert eines Schlüssels - Der zentrale Callback
handle_callaktualisiert beistore-Anfragen den ZustandDictund sucht beilookup-Anfragen den Schlüssel im Zustand - Wenn man diese Implementierung an
gen_serverübergibt, entsteht ein Server, der gleichzeitig eintreffendestore- undlookup-Anfragen verarbeiten kann - Wichtig ist, dass
handle_callselbst vollständig sequenziell ist- Nebenläufigkeit ist im generischen
gen_server-Baustein verborgen - Anwendungscode nähert sich der Form an, einen Zustand und eine Eingabe zu nehmen und daraus einen neuen Zustand sowie eine Ausgabe zu erzeugen
- Nebenläufigkeit ist im generischen
- Joe Armstrongs Dissertation erweitert diesen Ansatz auf
gen_server,gen_event,gen_fsm,supervisor,applicationundrelease
Der Fall AXD301 und die Größenordnung von behaviours
- Joe Armstrong verwendet Ericssons Telefonvermittlung AXD301 als Fallbeispiel
- Das AXD301-Projekt enthielt die folgenden Komponenten
- 122
gen_server-Instanzen - 36
gen_event-Instanzen - 10
gen_fsm-Instanzen - 20 Supervisor
- 6 Applications
- Das Ganze wurde als ein Release paketiert
- 122
- AXD301 war ein System mit mehr als einer Million Zeilen Erlang-Code
- Dass ein System dieser Größenordnung aus einer kleinen Menge von behaviours zusammengesetzt wurde, ist ein zentraler Beleg für die Struktur von Erlang
Die Rolle anderer behaviours
gen_eventist ein generischer Event-Manager- Man registriert Event-Handler, und wenn der Event-Manager entsprechende Nachrichten empfängt, führt er die Handler aus
- Joe Armstrong nennt Fehler-Logging als Beispiel
- Der Beispiel-Logger kann die letzten fünf Fehlermeldungen aufzeichnen und berichten
gen_fsmist ein Zustandsmaschinen-behaviour, das später ingen_statemumbenannt wurde- Es eignet sich besser für Protokollimplementierungen
- Protokolle werden häufig als Zustandsmaschinen spezifiziert
- Man kann davon ausgehen, dass jeder
gen_serverauch alsgen_statemimplementiert werden kann und umgekehrt
applicationbesteht aus einem Supervisor-Baum und den übrigen Elementen, die für die Auslieferung der Anwendung nötig sindreleasepaketiert eine oder mehrere Applications- Es enthält auch Code zur Behandlung von Upgrades
- Wenn ein Upgrade fehlschlägt, muss ein Rollback auf den vorherigen stabilen Zustand möglich sein
supervisor und „Let it crash!“
supervisorist ein Prozess, der überprüft, ob andere Prozesse ordnungsgemäß funktionieren- Wenn ein überwachter Prozess fehlschlägt, kann der Supervisor ihn gemäß einer vordefinierten Strategie neu starten
- Die Beispielstrategie
{one_for_one, 5, 1000}bedeutet Folgendes- Wenn einer von
packet_assembler,kvodersimple_loggerfehlschlägt, wird nur der fehlgeschlagene Prozess neu gestartet - Wenn innerhalb von 1000 Sekunden mehr als fünf Neustarts nötig sind, schlägt der Supervisor selbst fehl
- Wenn einer von
permanent, 500, workerbedeutet, dass der betreffende Prozess ein Worker ist, der immer laufen muss, und dass der Supervisor ihm beim Versuch eines Neustarts 500 ms für ein geordnetes Beenden gibt- Bei der Strategie
one_for_allwerden alle Kindprozesse neu gestartet, wenn ein Prozess fehlschlägt - Ein Supervisor kann andere Supervisor überwachen und muss nicht zwingend auf demselben Rechner laufen
- Diese Struktur arbeitet nicht auf der Ebene von Docker-Containern, sondern auf der Ebene von Threads/leichtgewichtigen Prozessen, weshalb sie sich schwerlich als „einfach Kubernetes“ beschreiben lässt
- „Let it crash!“ setzt voraus, dass ein fehlgeschlagener Prozess neu gestartet wird
- Das Programm beschreibt nur den Normalpfad
- Wenn im Normalpfad ein Problem auftritt, versucht es nicht selbst, sich zu erholen, sondern crasht
- Ein anderes Programm weiter oben im Supervisor-Baum übernimmt die Behandlung
- Langfristige Stabilitätsnachweise für AXD301 wurden nicht systematisch gesammelt, aber in Joe Armstrongs Dissertation steht, dass eine PowerPoint-Zahl eines großen Kunden den Betrieb eines 11-Knoten-Systems mit 99,9999999 % Zuverlässigkeit angab
- Wie diese Zahl ermittelt wurde, ist nicht dokumentiert
- Auch andere Zahlen aus Downtime-Berichten sind mit dem Hinweis versehen, dass die Berechnungsmethode unklar ist
Wenn man behaviours in anderen Sprachen implementieren wollte
- Leichtgewichtige Prozesse und Message Passing allein machen Erlang nicht für zuverlässige Systeme geeignet
- Die ehrlichere Erklärung ist die Struktur, die behaviours bereitstellen, und die Art, wie diese Struktur zu zuverlässiger Software führt
- Die Interface-Signatur von
gen_serverlässt sich als folgende Form betrachtenInput -> State -> (State, Output)- Das bedeutet, dass Eingabe und aktueller Zustand genommen werden, um einen neuen Zustand und eine Ausgabe zu erzeugen
- Wie diese sequenzielle Signatur in die Verarbeitung nebenläufiger Anfragen überführt werden kann, lässt sich etwa so denken
- Ein HTTP-Server wandelt Requests in
Inputum und legt sie in eine Queue - Eine Event Loop nimmt Eingaben aus der Queue und übergibt sie an die sequenzielle Implementierung
- Die Ausgabe wird als Antwort an den Client zurückgegeben
- Um mehrere
gen_serverzu unterstützen, kann man jedem Server einen Namen geben und in Requests sowohl Namen als auch Eingabe enthalten
- Ein HTTP-Server wandelt Requests in
gen_eventkönnte implementiert werden, indem man das Registrieren von Callbacks für bestimmte Event-Typen in der Queue erlaubtsupervisorlässt sich vereinfacht als Mechanismus betrachten, dergen_server-Funktionsaufrufe in Exception-Handler einhüllt und bei einer Exception den Supervisor benachrichtigt- Wenn der Supervisor nicht auf demselben Rechner läuft, wird es komplexer
applicationundreleasesind wichtig, weil Konfiguration, Deployment und Upgrades schwierige Probleme sind, werden hier aber nicht vertieft behandelt
behaviours und Testbarkeit
- Die Struktur von Erlang-behaviours führt zu Simulationstests und formaler Verifikation
- Ein aktuelles Interessengebiet sind Simulationstests verteilter Systeme im Stil von FoundationDB
- Bei Simulationstests wird das System in einer simulierten Welt ausgeführt, wobei die Simulation den Zeitpunkt der Zustellung von Netzwerknachrichten vollständig kontrolliert
- FoundationDB entwickelte für Simulationstests einen C++-Dialekt mit Actors oder eine eigene Programmiersprache
- Dieser Ansatz hätte vermutlich schon mit dem folgenden Zustandsmaschinentyp recht weit kommen können
Input -> State -> (State, [Output])[Output]ist eine Sequenz von Ausgaben
- Der Simulator verwaltet Nachrichten in einer nach Ankunftszeit sortierten Priority Queue
- Er entnimmt eine Nachricht
- Er stellt die Uhr auf die Ankunftszeit dieser Nachricht vor
- Er übergibt die Nachricht an die empfangende Zustandsmaschine
- Er erzeugt neue Ankunftszeiten für Ausgabenachrichten und legt sie wieder in die Queue
- Wenn alles deterministisch ist und Ankunftszeiten aus einem Seed erzeugt werden, lassen sich verschiedene Interleavings erkunden und reproduzierbare Fehler erhalten
- Diese Methode ist schneller als Jepsen, weil Nachrichten im Speicher verarbeitet werden und die Uhr auf die Ankunftszeit vorgestellt wird, statt auf Timeouts zu warten
- Eine solche Zustandsmaschinenform wird als „network normal form“ bezeichnet und führt zu der Vermutung, dass sich jedes Programm, das über das Netzwerk empfängt und sendet, in diese Form refaktorisieren lässt
- Dass
gen_serverundgen_statemim Grunde dieselbe Typstruktur haben, kann als Hinweis verstanden werden, dass diese Struktur nicht willkürlich ist
Formale Verifikation und Zustandsmaschinenstruktur
- Joe Armstrong sagte einmal in einem Talk, dass es schwierig sei, verteilte Leader Election korrekt zu implementieren
- Mit einem Simulator könnte dieses Problem deutlich vereinfacht werden
- So wie Windkanäle beim Flugzeugbau helfen, könnten Extrembedingungen wie instabile Netzwerke oder Stromausfälle vor der Production getestet werden
- Dieser Simulator könnte generisch oder parametrisiert für behaviours sein
- Entwickler müssten keinen eigenen Simulator schreiben
- Die Komplexität wäre wie der Nebenläufigkeitscode von
gen_serverhinter dem behaviour verborgen
- Kyle „aphyr“ Kingsbury twitterte über die Tests von FoundationDB sinngemäß, dass sie deutlich strenger wirkten als seine eigenen Jepsen-Tests
- Auch formale Verifikation wird einfacher, wenn Programme als Zustandsmaschinen geschrieben sind
- Lamports Arbeit zum TLA+-Model-Checking setzt voraus, dass Spezifikationen Zustandsmaschinen sind
- Kleppmann zeigt, wie man das Problem der Zustandsexplosion mit struktureller Induktion auf Basis der Zustandsmaschinenstruktur angehen kann
- Die aus Erlang-behaviours gewonnene Struktur kann wiederverwendet werden, um die Probleme leichter zu lösen, die Joe Armstrong als schwierig bezeichnete
Verwandte Arbeiten und Referenzen
- Verwandte Arbeiten, die Ideen aus Erlang aufgegriffen haben, sind unter anderem
- Arbeiten, die Ideen von Martin Thompsons LMAX Disruptor und aeron übernehmen, um eine schnelle Event Loop zu bauen, in der behaviours ausgeführt werden
- Arbeiten, die Zustandsmaschinentypen um async I/O erweitern
- Arbeiten, die supervisors detaillierter implementieren
- Hot Code Swapping für Zustandsmaschinen
- Als Referenzen werden Joe Armstrongs Doktorarbeit, OTP design principles sowie die Dokumentation zu
gen_server,gen_event,gen_statem,supervisor,applicationund release genannt - Zum Verhältnis zwischen Actor Model und Erlang wird zusammengefasst, dass die Erlang-Entwickler Erlang schufen, ohne das Actor Model zu kennen, und dass Carl Hewitts Paper die Unterschiede zwischen Erlang-Prozessen und dem Actor Model dokumentiert
- Akka hat zwar „actors“ und Supervisor Trees, scheint aber nichts zu haben, was den anderen Erlang-behaviours entspricht; auch Akkas Begriff „behavior“ unterscheidet sich von Erlang-behaviours
1 Kommentare
Kommentare auf Hacker News
Das Erstaunliche an Erlang und BEAM ist die Tiefe der Funktionalität. Im ursprünglichen Artikel waren Behaviour/Interface der zentrale Eindruck, aber für mich wiegt stärker, dass man damit komplexe Systeme mit deutlich weniger Entwicklungsressourcen bauen kann als in anderen Sprachen.
Auch OTP selbst enthält sehr viel. Ich habe einmal daran gearbeitet, Elixir so zu kompilieren, dass es auf iOS-Geräten läuft. Dabei konnte ich nicht nur den Release-Prozess von Erlang nutzen, sondern auch mit der ei-Bibliothek einen Node in C kompilieren, der mit anderen Erlang-Nodes wie Erlang, Elixir, Gleam usw. wie in einem normalen verteilten Netzwerk kommunizieren konnte.
Außerdem konnte ich über die rpc-Bibliothek von Erlang sogar aus C heraus Funktionen einer Elixir-Anwendung aufrufen. Es gibt Overhead beim Encoding/Decoding, und FFI wäre schneller, aber es passte locker in unser Latenzbudget, und wir konnten eine Funktionalität, von der ich zuvor noch nie gehört hatte, innerhalb weniger Tage aufbauen.
Der größere Punkt ist: Erlang hat viele Probleme, mit denen moderne Tech-Stacks ringen, schon vor Jahrzehnten gelöst – sowohl in Bezug auf Skalierung als auch Implementierungskosten. HN zeigt zwar eine gewisse clickbaitartige Zuneigung zu Erlang/Elixir, aber das führt nicht wirklich zu Adoption; zugleich gibt es Unternehmen, die Geld verbrennen, um Dinge zu implementieren, die man im Erlang-Stack standardmäßig gratis bekommt.
Das Projekt, an dem ich arbeitete, war eine Backend-Datenpipeline, und es wurden nicht einmal besonders viele Daten verarbeitet. Trotzdem war es unglaublich schwierig, den entscheidenden Bug sauber einzugrenzen.
Dabei lernte ich mehrere Eigenschaften von Node.js kennen, und im Vergleich zu Elixir/Erlang/OTP kam ich zu dem Schluss, dass Node.js konstruktionsbedingt weniger zuverlässig ist.
Ich habe auch viel Ruby gemacht und etwas Python genutzt, aber viele Sprachplattformen der aktuellen Generation tun sich schwer damit, zuverlässige verteilte Systeme zu bauen. Die BEAM-VM und die OTP-Plattform haben diesen Teil bereits gelöst.
Ein GenServer, der in den meisten BEAM-Systemen die Rolle eines Workers übernimmt, läuft am Ende zum Beispiel darauf hinaus, mehrere Funktionen mit einfachen Parametern aufzurufen.
Daher kann man diese Funktionen direkt aufrufen, die Parameter manuell übergeben und nur die Ausgabe prüfen. Man muss kein komplexes Testsystem für asynchronen Code aufsetzen oder in Tests blockieren, während man auf den Abschluss einer Arbeit wartet.
Juniors übersehen das oft, aber wenn man es einmal verstanden hat, fühlt es sich ziemlich befreiend an.
Ich habe erlebt, dass einige Leute, meist Manager, auf Basis unserer Erfahrungen ein Buch schreiben wollten. Es war immer frustrierend, dass sie jeweils andere Dinge als entscheidend dafür ansahen, warum unsere Arbeit erfolgreich war. Was ich für essenziell hielt, stuften sie oft zu etwas herab, das nur „nice to have“ sei.
Auch hier sagt jemand, leichtgewichtige Prozesse und Message Passing seien nicht die geheime Zutat, übersieht aber, dass die Sicht auf Erlang als kommunizierende sequenzielle Prozesse (CSP) nicht von diesen Eigenschaften zu trennen ist. Gleichzeitig wird CSP wiederholt als Teil der geheimen Zutat erwähnt.
Etwa wenn Anwendungsprogrammierer sequenziellen Code schreiben und Nebenläufigkeit in Behaviours verborgen wird, wenn Business-Logik sequenziell ist und neue Teammitglieder leichter einsteigen können, oder wenn Supervisoren und die „let it crash“-Philosophie zuverlässige Systeme ermöglichen.
Behaviours sind interessant, waren in den 80ern verbreitet und lösen ein Problem, das manche auch in den 2000ern noch bearbeitet haben. In Erlang sind sie aber zugleich Zweck und Mittel. Sie sind die Art, wie diese anderen Eigenschaften implementiert wurden; ob es zwingend genau so sein musste, damit Erlang weiterhin Erlang bleibt, weiß ich nicht.
CSP ist die Linie, die über occam und andere Sprachen die Go-Channels inspiriert hat. Die Synchronisation über ungepufferte Channels ist der deutlichste Unterschied, daneben gibt es Unterschiede wie Pattern Matching auf Mailboxen im Actor Model.
Die Debatte CSP vs. Actor Model ist ziemlich interessant, weil sie oberflächlich ähnlich wirken, die tatsächlichen Implikationen aber stark auseinandergehen.
Ich finde, das Actor Model passt besser, aber das kann je nach Person stark variieren.
Ich bin hierhergekommen, weil ich mehr darüber wissen wollte, warum Ericsson aufgehört hat, Erlang zu verwenden, und warum Joe entlassen wurde.
Kurz gesagt scheint Ericsson neue Projekte auf Java umgestellt zu haben, wodurch Erlang marginalisiert wurde. Danach gründeten Joe und Kollegen 1998 Bluetail, das von Nortel übernommen wurde.
Nortel war ein Telekommunikationsriese, der etwa ein Drittel des Werts der Börse von Toronto ausmachte. Im Jahr 2000 stieg die Aktie bis auf 125 Dollar pro Anteil, fiel 2002 aber unter 1 Dollar. Das war Teil des Dotcom-Crashs, und zusammen mit dem starken Rückgang der Telekom-Ausgaben traf es Nortel besonders hart.
Joes Entlassung sollte man wohl am besten als Fall betrachten, in dem „seine Abteilung auf einem sinkenden Schiff zuerst unter Wasser geriet“. Nortel entließ 60.000 Menschen, also mehr als zwei Drittel der gesamten Belegschaft. Diese Entlassung war kein Signal dafür, dass Joe seine Arbeit nicht machte, und auch kein Beleg für Ineffizienz seiner Geschäftseinheit, sondern eine verzweifelte groß angelegte Maßnahme.
„fired“ enthält eine starke Wertung und legt nahe, dass jemand wegen Fehlverhaltens oder aus einem bestimmten Grund gekündigt wurde. Selbst wenn es tatsächlich so gewesen wäre, könnte der Autor des ursprünglichen Artikels das nicht wissen – und es geht ihn auch nichts an.
Wegen der leichtgewichtigen Prozesse und Message Passing habe ich angefangen, mir Erlang wieder anzusehen; bis jetzt waren Behaviours für mich eher zweitrangig.
Das Projekt besteht darin, visuelle Flow-Based Programming (FBP) nach Erlang zu bringen. FBP wirkt, als wäre es für Erlang gemacht, daher war ich überrascht, dass ich keine bestehende Implementierung finden konnte.
Das Tool, das ich für FBP hauptsächlich verwendet habe, war Node-RED. Die Grundidee ist also, das Node-RED-Frontend an ein Erlang-Backend anzubinden und jeden Node zu einem Prozess zu machen. Das Node-RED-Frontend eignet sich gut, um Message Passing zwischen Nodes zu modellieren, sodass eine sehr einfache 1:1-Abbildung auf Erlang-Prozesse und -Messages möglich ist.
Einige Grundfunktionen habe ich implementiert und begonnen, Flows als Unit Tests aufzubauen, um die Funktionalität schrittweise zu erweitern. Schön wäre eine 100%ige Kompatibilität mit Node-RED, dem Node.js-Backend. Details stehen im GitHub-Repository → https://github.com/gorenje/erlang-red
Insgesamt passt Erlang erstaunlich gut dazu, und ich wundere mich, warum niemand etwas Ähnliches gemacht hat. Gab es das vielleicht schon?
[1] = https://jpaulm.github.io/fbp/index.html
Für mich liegt die Stärke von Erlang/Elixir weniger in der Actor-Model-Implementierung, dem Matching aus Prolog, Immutability oder den Behaviours selbst, sondern in Joes Willen zu zeigen, dass man mit weniger mehr erreichen kann.
Es ist ein gut entworfenes und erprobtes Rechensystem mit einer Konsistenz, die man in anderen Sprachen und erst recht in der „Web“-Welt selten sieht. Perfekt ist es nicht, aber ziemlich beeindruckend.
Leider gibt es in der Softwarewelt meiner Ansicht nach deutlich zu wenig Anerkennung und Übernahme dessen, was Einfachheit ermöglicht. Komplexität macht Menschen zu Experten, erlaubt Managern, große Teams und viele Meetings zu rechtfertigen, und sorgt dafür, dass Experten Experten bleiben.
Erlang wurde in einer Zeit entwickelt, in der Unternehmen Softwarelösungen mit weniger Personal und begrenzter Performance umsetzen wollten. Nachdem in den folgenden Jahrzehnten sehr viel Geld in diesen Bereich floss, wirkte der Wert „weniger bedeutet mehr, und zwar auf eine Weise, die allen nützt“ weniger attraktiv.
Das interessanteste Konzept in Erlang/BEAM ist, dass partielle Wiederherstellung von Anfang an eingebaut ist.
Wenn ein unerwarteter Zustand auftritt, wird nicht der gesamte Prozess beendet oder einfach weitergemacht und Beschädigung in Kauf genommen, sondern auf der feinsten möglichen Ebene zu einem bekannten guten Zustand zurückgekehrt.
Diese Idee wurde schon vor langer Zeit unter dem Namen „Microreboots“ erforscht und steht auch mit „crash-only software“ in Zusammenhang, aber nur Erlang/BEAM hat sie zu einem First-Class-Konzept des Betriebssystems gemacht.
Erlang bietet ein gutes Modell, um mit solchen Problemen umzugehen, aber das heißt nicht, dass man das Denken komplett abschalten kann. Wer mit der Haltung herangeht, man könne es einfach neu starten lassen und alles werde schon gut, kann sich die Finger verbrennen.
Erlang, OTP und BEAM bieten mehr als Behaviours. Die VM ist eher ein virtueller Kernel mit Supervisors, isolierten Prozessen und einem verteilten Modus, der mehrere physische/virtuelle Maschinen wie einen einzigen Ressourcenpool behandelt.
OTP stellt außerdem mehrere nützliche Modi bereit, darunter die Datenbank Mnesia sowie atomare Counter und ETS-Tabellen fürs Caching. Die Runtime unterstützt auch Hot Reloading von Bytecode, sodass Patches ohne Systemunterbrechung eingespielt werden können. Die Syntax ist nicht gerade Screenreader-freundlich, aber lesbar.
Als Plattform, die mir in ihrer Fähigkeit, Ressourcen mehrerer Maschinen wie einen einzigen Pool zu behandeln, ähnlich wie BEAM erscheint, fällt mir höchstens Apache Mesos[1] ein.
Vor gut einem Jahr hat meine eigene Beratungsfirma Erlang als Backend-Sprache eingeführt. Seitdem haben wir begonnen, das Innere der BEAM zu erkunden, etwa indem wir einen TCP-basierten Stack auf QUIC umstellen oder Rust-Patches integrieren.
Für leichtgewichtige Systeme mit hohem Durchsatz, die außer bei Kernel Panic oder Stromausfall nicht ausfallen, ist es wirklich eine hervorragende Wahl. Derzeit bauen wir sehr geschäftige, hochgradig nebenläufige Software wie Tracker und Pipeline-Manager für Film-/Game-Produktionen und bereiten außerdem F&E für einen Verwaltungsdienst für Privatkliniken vor.
[1]: https://mesos.apache.org/
Es ist gut möglich, dass wir für Dinge, die auf Elixir-Seite nicht gepflegt werden, eigene interne Bindings oder Implementierungen warten müssten.
Außerdem gibt es viel syntaktischen Zucker, und die Nutzer zeigen eine eigenartige Besessenheit davon, Dinge hinter DSL-Interfaces zu abstrahieren.
Auf die Frage „Warum kopieren Sprach- und Bibliotheksdesigner nur die Ideen von leichtgewichtigen Prozessen und Message Passing, stehlen aber nicht die Struktur hinter Erlang behaviours?“ gibt es einen Grund.
Die Funktionssignaturen von Erlang behaviours sind stark mit anderen Eigenschaften von Erlang verknüpft, insbesondere mit seiner ungewöhnlichen Art, Immutability zu nutzen. Deshalb braucht ein Server auch einen separaten init-Aufruf, und auch die Zustandsverwaltung braucht eine sehr ausgeprägte Form, wenn sie auf dieselbe Weise funktionieren soll.
In anderen Sprachen gilt aber fast immer: Um dasselbe Ziel zu erreichen, sollte man nicht einfach exakt kopieren, was Erlang tut. Wenn jemand sagt, er habe „gen_server in eine andere Sprache portiert“, und dann exakt dieselbe Schnittstelle wie Erlang zeigt, sehe ich das als Portierung, die nicht wirklich tief verstanden hat, was Erlang tut.
Als ich die Idee des supervisor tree nach Go übertragen habe[1], habe ich es ebenfalls auf Go-typische Weise getan. In modernem Go ist die richtige Schnittstelle für etwas „Überwachbares“ nicht dieselbe Signatur wie in Erlang, sondern nur diese:
type Service interface {
Serve(context.Context)
}
In Go ist das alles, und man sollte nicht mehr als das verwenden. In anderen Sprachen kann es anders sein. Go hat Channels, daher braucht man kein „handle_event/2“, sondern sollte Channels verwenden. Nicht weil Channels besser oder schlechter wären, sondern weil die Sprache so funktioniert.
In anderen Sprachen kann man andere Dinge verwenden, und in anderer Infrastruktur kann man Ereignisse statt „handle_event/2 aufzurufen“ auch an Kafka oder einen Cloud Event Bus schicken. Der Kern ist, ein ereignisgetriebenes System zu bauen, nicht Erlangs exakte Implementierung zu kopieren.
In der Erlang-Community gibt es das Problem, dass sie übermäßig davon überzeugt ist, an Erlangs exakter Vorgehensweise sei etwas enorm Besonderes, und jede Abweichung sei sofort falsch und unzuverlässig. 2005 mag das gestimmt haben, 2025 nicht mehr.
Eine Zeit lang war Erlang fast die einzige sinnvolle Antwort, aber 2025 besteht das Problem darin, sich durch eine Flut von Antworten zu bewegen. Für zuverlässige Software von Erlang zu lernen, empfehle ich ausdrücklich; Erlangs genaue Art, die Dinge zu erreichen, blind in andere Sprachen zu portieren, lehne ich jedoch entschieden ab. In fast jedem anderen Sprachkontext ist das die falsche Antwort. Selbst andere immutable Sprachen sind strukturell verschieden genug, dass man es nicht einfach kopieren kann.
[1]: https://jerf.org/iri/post/2930/
Als jemand, der Elixir beruflich nutzt, ist das für mich die interessanteste Frage zu Erlang.
Wenn man sieht, wie erfolgreich k8s, Kafka und die verteilten Systemprodukte von AWS geworden sind, ist die Nachfrage nach Werkzeugen, die beim Entwurf zuverlässiger nebenläufiger Systeme helfen, eindeutig enorm. Warum haben Erlang/Elixir sich davon also keinen Anteil gesichert?
Ich diskutiere das oft mit Freunden, kenne die Antwort aber nicht.
Ich vermute, Erlang hat die Verarbeitung von Netzwerknachrichten abstrahiert. Außerdem habe ich für Dinge wie langlebige Server drei oder vier Varianten des vorgeschlagenen Musters gesehen.
Ich stimme völlig zu, dass man beim Portieren idiomatische Bausteine verwenden sollte. Aber Sprachen können verborgene Mechanismen haben, durch die beim Portieren der wertvolle Kern verloren geht. Eine Art Antirelativismus der Programmiersprachen.
„Channels? Einfach in X wrappen“ kann für Interoperabilität deutlich schädlicher sein, als es klingt. Nimmt man zum Beispiel Go-http.Handler: Er ist einfach, aber die praktische Konsequenz, dass er in der Standardbibliothek steckt, ist groß. Dadurch entsteht auch ohne vorherige Abstimmung ein Middleware-Ökosystem, das weitgehend kompatibel ist.
io.Reader und seine Verwandten sind ähnlich. Solche extrem einfachen Schnittstellen können wertvoller sein als die Implementierung.
Nur eine Vermutung, aber wenn Erlang viele Schnittstellen für zuverlässige verteilte Systeme richtig getroffen hat, könnte genau das der Faktor sein, der das Ganze ermöglicht hat.
Ich stimme nicht zu. Interfaces sind ein triviales Konzept, das man an jede Sprache anhängen kann. Selbst in Sprachen ohne offizielle Interface-Syntax kann man sie im Programmraum nachbilden.
BEAM war erfolgreich, weil es auf einem einzelnen Node eine Million Prozesse ausführen kann, komplexe verteilte Zustandsmaschinen leicht ausdrückbar macht und Teile des Systems ohne Downtime neu starten kann. Daneben gibt es viele weitere Gründe.
Ich glaube wirklich nicht, dass behaviour/interface das zentrale Puzzlestück ist.
Isolierte Prozess-Heaps kann man mit OS-Prozessen haben, für supervisor trees kann man Kubernetes verwenden, Message Passing lässt sich in Java mit zwei Threads und einer gemeinsamen Queue bauen, Hot Code Loading kann Java auch, und bei niedriger Latenz kann ein getunter LMAX Disruptor Erlang vielleicht sogar schlagen.
Entscheidend ist aber, all das in einer Plattform oder Bibliothek zusammenzubringen. OS-Prozesse sind schwergewichtig, und es ist nicht leicht, zwei Millionen davon auf einem Server laufen zu lassen. Nutzt man Green Threads oder Promises, verliert man die isolierten Heaps.
Kubernetes kann man bis zu einem gewissen Grad ebenfalls nutzen, aber verschachtelte supervisor trees behandelt es nicht gut. Möglich wäre es wohl, aber zusätzlich zum Code bekommt man dann alle möglichen Dinge wie Pods, Controller und Volumes.
Mit Actor-Bibliotheken in verschiedenen Sprachen kann man Message Passing umsetzen, aber sie integrieren nicht transparent Pattern Matching in receive oder das Senden an andere Threads auf anderen Nodes.
Hot Code Loading ist ebenfalls möglich, aber die Frage ist, wie man Laufzeitdatenstrukturen und Zustand behandelt. Erlang wurde darum herum gebaut, und gen_server hat, weil der Zustand immutable und explizit ist, Callbacks, um nicht nur den Code, sondern auch den Zustand selbst zu aktualisieren.
Ich würde nicht sagen, dass BEAM dieses Problem beseitigt, aber es scheint die Steigung der Kurve zu verringern. Seine in sich konsistenten Idiome und Funktionen, einschließlich Deployment, automatischer Wiederherstellung und Lastverteilung, reduzieren die Reibung zwischen Modulen.
Es ermöglicht Systeme, in denen zwölf Engineers 30 Endpunkte leicht verwalten können und die Oberfläche trotzdem weiterhin einem Potenzgesetz folgen kann.
Ich stimme dem Inhalt dieses Beitrags nicht zu. Behaviours werden durch die zugrunde liegende Architektur des Systems ermöglicht.
Ein Behaviour ist keine Schnittstelle, sondern eher ein abstraktes Objekt wie in Sprachen à la Java. Es implementiert grundlegende, in sich geschlossene Funktionalität, die hinter einer Kollaborationsschnittstelle verborgen ist. Ohne die zugrunde liegende Infrastruktur, die garantiert, dass jeder Prozess vollständig von anderen Prozessen getrennt ist, dass alle Prozesse ohne Speicher- oder Ressourcenlecks sicher beendet werden können und dass zwischen zwei Prozessen keine gefährlichen Pointer geteilt werden können, lässt sich damit aber nicht viel anfangen.
Was Joe in seiner Arbeit gezeigt hat, war, wie man mit einer gegebenen Menge von Lego-Bausteinen zuverlässige Systeme und darüber hinaus bis zu einem gewissen Grad zuverlässige verteilte Systeme bauen kann.
Um so etwas korrekt umzusetzen, braucht man die Erlang-VM; auf anderen VMs lässt es sich nicht vollständig implementieren. Ohne die darunterliegende Verkabelung wird der Supervision Tree undicht. In Java kann man keinen Thread, der Ressourcen festhält, einfach killen und erwarten, dass immer alles gutgeht, und es fehlen auch die Möglichkeiten, unterschiedliche Prozesse zu überwachen.