3 Punkte von GN⁺ 2023-12-08 | 1 Kommentare | Auf WhatsApp teilen
  • FLAME zielt auf feingranulare elastische Skalierung, indem Teile bestehenden Anwendungscodes in Funktionen gekapselt und in einer temporären Kopie der Anwendung ausgeführt werden, ohne Code in eine separate Runtime zu verschieben
  • Der klassische FaaS-Ansatz kann selbst bei Aufgaben wie dem Erzeugen von Video-Thumbnails Strukturen erzeugen, in denen HTTP, S3, SQS, API Gateway, Encoder/Decoder und Workflow-Orchestrierung zunehmen
  • Die Elixir-Bibliothek flame delegiert Funktionsausführung mit FLAME.call an einen Remote-Runner; Fly.ios FLAME.FlyBackend bootet mit demselben Docker-Image eine neue Fly Machine und verbindet sie in etwa 3 Sekunden mit dem Parent-Node
  • In Entwicklung und Tests läuft der Code mit LocalBackend in derselben Runtime; in Produktion steuern min, max, max_concurrency und idle_shutdown_after Scale-to-zero und kurzes Warmhalten im Hot-Zustand
  • FLAME ersetzt Job-Queues nicht, sondern trennt Durability-Garantien von elastischer Ausführung: Die Queue übernimmt Dispatch, Commit und Retry, während CPU-intensive Aufgaben innerhalb eines FLAME-Aufrufs verarbeitet werden

Das Problem, auf das das FLAME-Pattern zielt

  • Elastisches Autoscaling reduziert den Aufwand für Serververwaltung und verspricht nutzungsbasierte Kosten; mit FaaS können jedoch zugleich separate Queues, Speicher, Glue Code sowie höhere Komplexität in Entwicklung, Tests und CI entstehen
  • FLAME steht für Fleeting Lambda Application for Modular Execution: Dabei wird die gesamte Anwendung wie eine Lambda behandelt, während nur bestimmte Modulaufgaben auf kurzlebiger Infrastruktur ausgeführt werden
  • Die Ziele lassen sich in drei Punkten zusammenfassen
    • Serververwaltung über bestehende Deployment-Flows wie fly deploy, git push heroku oder kubectl reduzieren
    • Nur bestimmte Teile des Anwendungscodes feingranular on demand skalieren
    • Die Anwendung nicht neu schreiben und keine Teile des Codes in eine proprietäre Runtime verschieben

Bestehende Funktionen mit FLAME.call auslagern

  • Das Beispiel ist eine Funktion generate_thumbnails in einer Elixir-Anwendung, die ein hochgeladenes Video mit ffmpeg in Thumbnails umwandelt
  • Die bestehende Funktion erstellt ein temporäres Verzeichnis, führt ffmpeg aus, speichert die erzeugten Thumbnails in dauerhaftem Storage und schreibt die URLs mit Repo.insert_all in die DB
  • CPU-intensives Video-Transcoding kann in Produktion den gesamten Dienst ausbremsen; statt den gesamten Code nach FaaS oder in einen Microservice zu verschieben, wird der Funktionsrumpf mit FLAME.call(MyApp.FFMpegRunner, fn -> ... end) umschlossen
  • FLAME.call nimmt den Namen des Runner-Pools und eine Funktion entgegen, findet oder bootet eine neue Kopie der gesamten Anwendung und führt nur diese Funktion aus
    • Variablen wie die eingefangene %Video{}-Struct und interval werden automatisch übertragen
    • Der FLAME-Runner verbindet sich nach dem Booten mit dem Parent-Node, erhält die auszuführende Funktion und gibt das Ergebnis an den Aufrufer zurück
    • Je nach Konfiguration wartet der Runner auf weitere Aufgaben und fährt im Idle-Zustand herunter oder beendet sich sofort
  • Da die gesamte Anwendung inklusive DB-Verbindung läuft, kann Repo.insert_all wie im ursprünglichen Code unverändert verwendet werden

FaaS-Komplexität und der Unterschied zu FLAME

  • FaaS stellt Bausteine zur Lösung des Problems bereit; FLAME ist eher ein Ansatz, der eine separate Kommunikationsschicht selbst reduziert
  • Selbst wenn man im Video-Thumbnail-Beispiel mit einer einfachen AWS Lambda Function URL beginnt, entsteht bald Komplexität
    • Um Thumbnails per HTTP zurück in die App zu streamen, müssen auf beiden Seiten eigene Encoder und Decoder geschrieben werden
    • Wenn Video-Transcoding oder Upload länger als 15 Minuten dauert, müssen wegen des harten Lambda-Timeouts Videos in Chunks aufgeteilt und weitere Lambdas eingesetzt werden
    • Workflow-Orchestrierung und zusätzliche Services wie SQS und S3 werden notwendig
  • Eine FaaS-basierte Implementierung erzeugt typischerweise folgende Kostenpunkte
    • Lambda-Trigger über HTTP endpoint, S3 und API Gateway
    • Eine maßgeschneiderte Lambda für Video-Transcoding schreiben
    • Thumbnail-Ergebnisse in SQS speichern
    • Einen SQS consumer auf App-Seite schreiben
    • In der DB speichern und einen Weg einrichten, Events an aktive Abonnenten zurückzuschicken, die mit anderen Instanzen verbunden sind
  • FLAME lässt internen Anwendungscode sowie bestehende DB-, PubSub- und Plattformfunktionen unverändert nutzen und kann so separate Services und Speicher zur Ergebnisabholung reduzieren

Fly.io-Backend und lokale Ausführung

  • Die Elixir-flame library ist eine Implementierung des FLAME-Patterns und bietet standardmäßig LocalBackend und FlyBackend
  • FLAME.FlyBackend bootet auf der Fly.io-Infrastruktur eine Anwendungskopie auf einer neuen Machine und kann sie in etwa 3 Sekunden mit dem Parent-Node verbinden, um Arbeit entgegenzunehmen
  • Da Fly.io Anwendungen als paketierte Docker-Images ausführt, wird die Fly API gebeten, eine neue Machine mit demselben Image wie die aktuelle App zu booten
  • Auf der Fly.io-Infrastruktur können FLAME-Runner in derselben Region wie der Parent gestartet werden, was die Latenz zwischen Parent und Runner reduziert
  • FLAME.FlyBackend umfasst inklusive Dokumentation weniger als 200 LOC; die einzige Bibliotheksabhängigkeit ist der HTTP-Client req
  • In Entwicklung und Tests wird der entsprechende Code mit LocalBackend in der bestehenden Runtime auf dem Laptop oder CI-Server ausgeführt

Dateiübertragung und Vereinfachung von Entwicklung und Tests

  • FLAME kann Business-Logik, DB-Konfiguration, PubSub und Plattformfunktionen wiederverwenden, ohne Code außerhalb der Anwendung zu schreiben
  • In Elixir kann dank der verteilten Funktionen der Erlang VM ein Dateistream vom Parent-Node an die entfernte FLAME-Anwendung gesendet werden
    • Auf dem Parent-Node wird ein Dateistream zum Videopfad geöffnet
    • Im FLAME-Child wird ein temporärer Dateistream geöffnet und der Parent-Stream dorthin kopiert
    • Anschließend erzeugt ffmpeg Thumbnails mit der temporären Datei auf dem Remote-Runner als Eingabe
  • Bei diesem Ansatz kann die Datei an den FLAME-Server übergeben werden, ohne S3 oder eine HTTP-Schnittstelle separat einzurichten
  • Separate Service-Deployments, Endpoint-Verwaltung, Ergebnisabholung über S3/SQS sowie Abhängigkeiten für Entwicklung, Tests und CI werden reduziert

FLAME außerhalb von Elixir

  • Elixir passt mit Prozess-Supervision und verteiltem Messaging gut zum FLAME-Modell, aber auch Sprachen mit vernünftigen Concurrency-Primitiven können dieses Pattern nutzen
  • Ein JavaScript-Proof-of-Concept führt Anwendungsfunktionen auf Fly Machines auf einer anderen Machine aus: fly-run-this-function-on-another-machine
  • Der typische Ablauf eines JavaScript-basierten FLAME-Aufrufs besteht darin, den Modulausführungsteil in eine neue Datei zu verschieben und ihn in einem Runner-Pool auszuführen
  • Wenn Argumente JSON-serialisierbar sind, ähnelt der gesamte Ablauf dem Elixir-Beispiel: Anwendungscode wird auf einer kurzlebigen Instanz ausgeführt
  • Eine vollständige FLAME-Bibliothek müsste Folgendes behandeln
    • Logik für elastisches Scale-up und Scale-down des Pools
    • Pool-Verwaltung unter Berücksichtigung von Hot Startup und Cold Startup
    • Monitoring entfernter Runner zur Vermeidung orphaned resources
    • Einen Weg, Deployments aktuell zu halten

Beziehung zu Background-Job-Queues

  • FLAME funktioniert auch innerhalb von Background-Job-Prozessoren, überschneidet sich aber teilweise mit der Rolle von Job-Queues
  • Job-Queues werden meist eingesetzt, wenn Durability-Garantien benötigt werden; die Queue kann so angepasst werden, dass sie bei Laständerungen mehr Jobs verarbeitet
  • Dauerhafte Jobs und elastische Ausführung sind getrennte Anliegen
    • Wenn eine Queue nur zum einfachen Offload von Ausführung genutzt wird, braucht es Glue Code, um Daten in den Job zu legen und Ergebnisse an den Aufrufer oder das Nutzergerät zurückzugeben
    • Wenn nach einem Video-Upload sichergestellt werden muss, dass die Thumbnail-Erzeugung erfolgreich ist, kann die Queue Dispatch-, Commit- und Retry-Mechanismen übernehmen
    • Das eigentliche Transcoding kann als FLAME-Aufruf innerhalb des Jobs laufen, um Durability und skalierte Ausführung zu trennen
  • Aufgaben ohne Durability-Bedarf, etwa Videovorschauen vor dem Speichern oder die Ausführung eines ML-Modells, nachdem der Nutzer die App bereits verlassen hat, passen möglicherweise nicht zu einer Struktur, in der Jobs in dauerhaftem Storage abgelegt werden

Runner-Pools für elastische Skalierung

  • Die Elixir-FLAME-Implementierung definiert einen elastischen Pool von Runnern und unterstützt damit Scale-to-zero zusammen mit Concurrency-Limits
  • Die Beispielkonfiguration fügt FLAME.Pool zu start/2 der Anwendung hinzu
    • min: 0 erlaubt Scale-to-zero
    • max: 10 bootet maximal 10 Runner
    • max_concurrency: 5 unterstützt 5 ffmpeg-Jobs pro Runner
    • idle_shutdown_after: 30_000 fährt bei 30 Sekunden ohne Aufrufe in den Idle-Zustand herunter
  • Abhängig davon, ob ein FLAME-Parent vorhanden ist, startet der Phoenix-Webserver nur bedingt
    • In FLAME-Runnern, die keinen Web-Traffic verarbeiten, muss der Webserver nicht gestartet werden
    • MyApp.Repo für die DB bleibt unverändert, da es innerhalb des FLAME-Runners genutzt werden muss
  • Mit min: 1 kann ab Anwendungsstart mindestens ein ffmpeg-Runner im Hot-Zustand gehalten werden

Platzierung zustandsbehafteter Prozesse

  • Zustandsbehaftete Teile von Elixir-Anwendungen sind rund um leichtgewichtige Prozessprimitive mit Message-Mailbox aufgebaut
  • FLAME.call und FLAME.cast eignen sich für relativ stateless Code; FLAME.place_child startet bestehende Prozess-Spezifikationen auf einem FLAME-Runner statt lokal
  • FLAME.place_child kann an Stellen verwendet werden, die Interfaces wie Task.Supervisor.start_child oder DynamicSupervisor.start_child nutzen
  • Im Beispiel der Thumbnail-Erzeugung während eines LiveView-Uploads werden Upload-Chunks an den Prozess ThumbnailGenerator übergeben, der mit ffmpeg kommuniziert
    • Wenn in ffmpeg stdout ein PNG-Delimiter gefunden wird, sendet er eine Bildnachricht an den LiveView-Prozess
    • LiveView nimmt die Nachricht in handle_info entgegen und fügt der UI ein neues Bild hinzu
  • Ersetzt man den bestehenden Aufruf DynamicSupervisor.start_child(@sup, spec) durch FLAME.place_child(Thumbs.FFMpegRunner, spec), läuft der Prozess ThumbnailGenerator auf einem FLAME-Runner
  • Prozesse können unabhängig von ihrem Ort Nachrichten austauschen; wenn der Prozess durch Upload-Ende oder Schließen des Browser-Tabs endet, erkennt der FLAME-Server die Beendigung und fährt herunter, wenn keine anderen Aufgaben anstehen

Remote-Monitoring und Fehlerbehandlung

  • Kurzlebige Infrastruktur benötigt Failsafes, um orphaned resources zu verhindern
  • Wenn ein Parent einen Runner startet, muss der Runner sich selbst herunterfahren, wenn keine Arbeit ansteht; wenn er den Parent-Node nicht mehr erreichen kann, muss er einen Failsafe-Shutdown durchführen
  • Wird der Parent durch ein neues Deployment ersetzt, müssen auch Runner beendet werden, um sicherzustellen, dass im Cluster derselbe Code ausgeführt wird
  • Aktive Aufrufer, die auf Runner-Ergebnisse warten, müssen berücksichtigen, dass der Runner aus beliebigen Gründen herunterfahren kann
  • Die von der Erlang VM bereitgestellten Primitive vereinfachen diese Implementierung
    • Monitoring und Supervision lokaler und entfernter Prozesse
    • Node Monitoring, das erkennt, wenn Nodes hoch- und herunterfahren
    • Shutdown-Flows, die die Start- und Stoppreihenfolge der Anwendung steuern und aktiven Runnern bei neuen Deployments Zeit geben, ihre Arbeit zu beenden
  • Interne Implementierungsdetails sollen in einem separaten Artikel folgen; aktuell kann man sich den flame source ansehen

Aktueller Stand und nächste Schritte

  • Die Elixir-FLAME-Bibliothek befindet sich noch in einem frühen Stadium, kann aber bereits ausprobiert werden
  • Für die Zukunft sind fortgeschrittenere Techniken für Pool-Wachstum und eine tiefere Analyse der Elixir-Implementierung geplant
  • Auch Diskussionen zur Implementierung des FLAME-Patterns in anderen Sprachen sind möglich

1 Kommentare

 
GN⁺ 2023-12-08
Hacker-News-Kommentare
  • Nachdem ich in den letzten vier Jahren den Schmerz und die Komplexität einer App mit über 100 Lambda-Funktionen erlebt habe, finde ich, dass dieser Artikel die Nachteile von FaaS-Serverless-Architekturen genau trifft.
    Am Anfang sieht man diese Nachteile kaum. Im Gegenteil: Bei geringer Nutzung sind die Vorteile offensichtlich, weil es fast kostenlos ist und kaum Wartung anfällt.
    Erst später, wenn die Lambda-Workflows immer stärker miteinander verflochten sind und durch gegenseitige Abhängigkeiten zunehmend starr werden, bereut man, nicht einfach einen Monolithen gebaut und ein paar Hundert Dollar mehr für den Eigenbetrieb ausgegeben zu haben. Heutzutage kann es bei Anbietern wie fly.io sogar weniger kosten.
    Ich frage mich, wie das aussieht, wenn man kein Elixir verwendet.

    • Ich habe immer wieder das Muster gesehen, dass motivierte Entwickler fast jeden Prozess oder jede Architektur etwa 18 Monate lang am Laufen halten können; inzwischen wirkt es fast wie eine Regel.
      Wenn die Lage schlechter wird, ist oft auch der Zeitpunkt gekommen, sich einen neuen Job zu suchen. Besonders dann, wenn man etwa ein Jahr nach dem Einstieg den Prozess bereut, den man selbst eingeführt hat. Ich habe das mehrfach bei schlechten Chefs, miserablen „Unit Tests“, Scrum usw. gesehen.
      Allerdings weiß ich nicht, ob Menschen die Ursache ihres Unbehagens am Arbeitsplatz klar erkennen oder es nur vage als „jetzt ist es Zeit zu gehen“ wahrnehmen. Wenn ich versucht habe, dem Unangenehmen einen Namen zu geben, gab es viel Gegenwehr; seit ich Good to Great gelesen habe, kostet mich das emotional deutlich weniger. Niemand will sagen: „Ach, das ist die Folge meines eigenen Handelns.“
      Die Leute, die das Rube-Goldberg-System, das ich betreue, im Wesentlichen geschaffen haben, waren die Ersten, die gegangen sind. Der Kapitän dieses Schiffs fragte einen Kollegen, ob wir unsere Engine Open Source machen sollten, und der Kollege antwortete, niemand werde ein System benutzen wollen, das ein bereits existierendes und besseres Rad neu erfindet. Innerhalb von 3 bis 4 Monaten ging diese Person freiwillig.
    • Wir bauen etwas, das dieses Problem in jeder Sprache lösen können sollte. Derzeit gibt es ein TypeScript SDK, und Java ist ebenfalls in Arbeit.
      Wenn man normalen serviceorientierten Code schreiben kann, in dem Lambdas einander aufrufen, muss man eine Anwendung nicht in Hunderte kurz laufender Einzelteile aufspalten. Das funktioniert allerdings nicht, wenn man auch für Wartezeiten zahlen muss. Wenn eine Lambda während I/O-Wartezeiten ihre Ausführung pausieren könnte, wäre das Problem gelöst. Deshalb halte ich durable execution für eine mögliche Antwort.
      In den letzten Wochen habe ich an einem Artikel gearbeitet, der das zeigt: https://restate.dev/blog/suspendable-functions-make-lambda-t...
    • Ein Abschnitt des Artikels behandelt FLAME auch außerhalb von Elixir. Kurz gesagt: Es ist ein Muster, das im Allgemeinen auf Sprachen mit einem vernünftigen Concurrency-Modell anwendbar ist.
      Es wird schwierig sein, die gesamte Ergonomie zu bekommen, die man in Elixir kostenlos erhält, etwa Funktionen mit Serialisierung erfasster Variablen. Aber in Sprachen wie JavaScript dürfte man zu etwa 90 % hinkommen, indem man statt eines Closure-Wrappings den auszuführenden Modulteil in eine neue Datei verschiebt.
      Wer eine FLAME-Bibliothek implementiert, muss außerdem Pooling, Monitoring und Remote-Kommunikation schreiben. Elixir liefert bei verteiltem Messaging und Monitoring vieles gratis. Die Funktionen zur Prozessplatzierung sind im Grunde ebenfalls Elixir-spezifisch.
    • Schon 12 Lambdas waren kaum auszuhalten. Die ursprüngliche App wurde von jemandem gebaut, der Wartung oder Deployment kaum bedacht hatte, und der Code war überall copy-pasted.
      Am Ende sind wir zu einem fetten Lambda-Monolithen gewechselt, bei dem eine einzelne Lambda mehrere Endpunkte verarbeitet.
    • Wenn einen Cold Starts nicht stark stören oder man sie in den Griff bekommt, kann man auch auf Lambda einen Monolithen bauen.
      Anders gesagt: Man kann auf einen Monolithen setzen und trotzdem die AWS-Kosten minimieren. Das ist keine Entweder-oder-Frage.
      Ich nutze derzeit asp.net, und selbst eine ziemlich große App, die ready-to-run inklusive optimiertem EF-Modell deployed wird, startet relativ schnell.
  • Ich bin der Autor. Ich freue mich, das endlich veröffentlichen zu können, und beantworte gern Fragen. Ich hoffe, dass sich ein paar Leute ausreichend inspiriert fühlen, das FLAME-Pattern in JavaScript, Go und anderen Sprachen umzusetzen.

    • Sieht gut aus. Ich hoffe, Microsoft schaut sich das an. Azure Functions sind bei Sicherheitseinstellungen und Deployment viel zu kompliziert und beruhen auf seltsamen Annahmen darüber, welchen Code man ausführen will.
    • Die natürlichste Implementierung dürfte so etwas wie vert.x auf der JVM sein.
      Dort gibt es bereits Mechanismen, um asynchrone Ausführung über Reactive Extensions, Futures und Coroutines auf einem Event Bus zu behandeln, und es unterstützt Datenserialisierung und Verteilung über den gesamten Cluster. Außerdem gibt es Event-Bus-Clients für mehrere populäre Sprachen, sodass man Anwendungen wohl auch aus mehreren Sprachen zusammensetzen könnte.
    • Ich würde die Übertreibung etwas zurückfahren.
      Wenn man das Problem als „…ein Schicksal schlimmer als der Tod“ darstellt und die Lösung so einfach und schmerzlos erscheinen lässt, dass jeder dumm wirkt, der nicht alle anderen Ansätze aufgibt, dann wird so ein Artikel leicht als reiner Sales-Pitch abgetan. Das ist die Methode von Schlangenölverkäufern.
      Am Anfang des Artikels wurde das Problem selbst gut herausgearbeitet; eine deutlich weniger emotionale objektive Gegenüberstellung hätte bei Leuten, die neugierig auf bessere Ansätze sind, vermutlich weniger Abwehr ausgelöst.
      Der eigentliche Inhalt des Artikels war interessant, aber die Art der Darstellung hat mich abgeschreckt. Bitte als konstruktives Feedback verstehen.
    • Der Artikel und das Video sind gut, und das Konzept ist sehr interessant. Ich freue mich auf eine JavaScript-Implementierung, aber es sieht nicht einfach aus.
      Und jetzt habe ich ein ganz kleines schlechtes Gewissen, dass ich mir ffmpeg.fly.dev gesichert habe.
    • Ich frage mich, worin sich das grundlegend davon unterscheidet, wie Sidekiq die Rails-Codebase instanziiert, um Hintergrundjobs auszuführen.
  • Interessant ist die Stelle: „Stell dir vor, du könntest einfach einen beliebigen Teil deines bestehenden App-Codes in eine Funktion einschließen, und er würde automatisch skalieren, wobei dieser Codeblock in einer temporären Kopie deiner App ausgeführt wird.“
    Das klingt, als hätte man das, was fork macht, für Serverless gebaut. Großartige Arbeit.

  • Vor ein paar Jahren habe ich tatsächlich einen Dienst genutzt, der so etwas gemacht hat. PiCloud wurde leider von Dropbox geschluckt, hatte davor aber genau dieses Modell, bei dem Aufgaben transparent auf Worker verteilt wurden. Der Code wurde gebündelt und auf Workern ausgeführt.
    Ein Beispiel gibt es hier. Man sieht, dass es genau dasselbe Modell ist: https://github.com/picloud/basic-examples/blob/master/exampl...
    Elixir habe ich nie benutzt, aber Erlang vor Jahrzehnten schon, und BEAM scheint sich im Kern nicht stark verändert zu haben. Da das ein zentraler Teil des Designs ist, dürfte es für solche Aufgaben deutlich besser geeignet sein. Ganz ohne Gratisessen ist es aber wohl trotzdem nicht, denn während man wartet, könnte ja der Hauptprozess sterben.

  • Ich stimme der Gesamtaussage zu. Wir haben bei https://www.windmill.dev einen anderen Ansatz gewählt: Wir betrachten die Abstraktionseinheit nicht auf Container-, sondern auf Quellcode-Ebene.
    Wir parsen die main-Funktion und die Imports, extrahieren Argumente und Abhängigkeiten und führen den Code dann unverändert in der gewünschten Runtime aus (TypeScript, Python, Go, Bash). Der Kerntrick besteht darin, den Cache effizient zu verwalten, sodass die Worker unabhängig von Imports immer warm bleiben.
    Dieser Ansatz ist nicht so stark in die Codebase integriert wie FLAME, richtet sich aber an andere Nutzer. Unsere Nutzer erstellen komplexe Workflows, Cronjobs oder einmalige Skripte mit automatisch generierter UI von Grund auf.
    Bei FLAME wirkt es so, als würde der gesamte Kontext als Snapshot genommen und auf der Ziel-VM wiederhergestellt. Ein anderer Ansatz wäre, eine Syntax einzuführen, mit der man angibt, welcher Kontext benötigt wird und welcher nicht, damit nur das Minimum geladen wird. Das untersuchen wir gerade, um Windmill besser in bestehende Codebases zu integrieren und nicht auf HTTP-Aufrufe angewiesen zu sein.

    • Was bei FLAME tatsächlich passiert, ist nicht ganz das. FLAME ruft lediglich über die eingebaute Clustering-Funktion von BEAM eine Funktion auf einem entfernten Node auf.
      Dabei wird implizit nur der benötigte Kontext übertragen. Im Artikel wird es so erklärt: „FLAME.call nimmt den Namen eines Runner-Pools und eine Funktion entgegen. Dann findet oder startet es eine neue Kopie der gesamten Anwendung und führt die Funktion dort aus. Variablen, die die Funktion per Closure erfasst hat, zum Beispiel die %Video{}-Struct und interval, werden automatisch mit übergeben.“
    • Da ist ein w zu viel. Für alle, die danach suchen, ist hier die URL: https://www.windmill.dev/
      Mir gefällt das Ziel des Projekts. Ich hoffe wirklich, dass Windmill eine bessere Open-Source-Alternative zu Retool/Airtable wird.
  • Mir gefällt, dass „bei FLAME die Entwicklungs- und Test-Runner einfach auf dem lokalen Backend laufen“. Serverless mit ordentlicher lokaler Developer Experience klingt gut.

    • Genau. Einer der Gründe, warum ich Serverless nicht mag, ist, dass die lokale Developer Experience im Vergleich zum Ausführen eines Monolithen viel schlechter ist.
  • Wenn „dann eine neue Kopie der gesamten Anwendung gefunden oder gestartet wird und die Funktion dort ausgeführt wird“, heißt das, dass bei jedem Flame.call der gesamte App-Prozess neu gestartet und der Ausführungskontext hineinkopiert wird?
    Aus Skalierungssicht ist das eine sehr einfache Lösung, aber sie dürfte auch Nachteile haben.
    Wenn die App-Startzeit um 10 ms steigt, kommen an jeder Flame.call-Stelle der Anwendung 10 ms dazu, und beim Speicher dürfte es ähnlich sein.
    Solche Bedenken sollte man bei der Nutzung dieses Systems wohl berücksichtigen.

    • FLAME.Pool weiter hinten im Artikel behandelt genau diesen Punkt. Runner werden gepoolt und für eine konfigurierbare gewünschte Dauer warm gehalten, bevor sie bei Inaktivität heruntergefahren werden.
      Unter Last ist der Pool bereits warm, sodass man kaum Cold-Start-Kosten zahlt. Als Nächstes kommen außerdem ausgefeiltere Pool-Wachstumsverfahren in die Elixir-Library, sodass man auch vermeiden kann, auf einen ausgelasteten Runner zu treffen und dann neu kalt starten zu müssen.
      Bei warmen Runnern besteht der Overhead nur aus der Latenz zwischen Parent und Child. Da sie im selben Rechenzentrum sein sollten, dürfte sie bei 1 ms oder darunter liegen.
  • Großartig. Es fühlt sich wie eine sehr leichtgewichtige, Elixir-spezifische Version dessen an, was wir bei https://www.inngest.com/ gebaut haben.
    Beide sind ähnlich darin, bestehenden Code mit etwas zu umhüllen, sodass er in serverlosen Funktionen genutzt und im Kern per Remote-RPC aufgerufen werden kann.
    Solcher Code läuft oft als Abfolge imperativer Schritte. Jeder Schritt kann seriell oder parallel als zusätzliche Lambda ausgeführt werden. Zwischen den Schritten gibt es aber impliziten Zustand, der in Variablen erfasst ist. Dadurch wird die Funktion zu einem Workflow. Im Inngest-Modell erfassen wir diesen Zustand und injizieren ihn wieder in die Funktion, um Dauerhaftigkeit zu erreichen.
    Aus Sicht der Dauerhaftigkeit sollten solche Prozesse auf einer Queue basieren. Das Schöne an diesem Modell ist, dass Queues billig sind. Wenn man eine Queue so billig macht wie eine Codezeile, wird alles einfacher. Jeder Entwickler kann zuverlässigen Code schreiben, ohne sich um Infrastruktur zu kümmern.
    Monitoring und Observability sind ebenfalls wichtig. Dead-Letter-Queues sind wirklich schrecklich, und man muss fehlgeschlagene Funktionen oder Schritte verwalten und erneut ausführen können.
    Es gibt auch Unterschiede zwischen FLAME und Inngest. Inngest ist Queue-basiert, eventgetrieben und kann in jeder Sprache per HTTP bereitgestellt werden. Da Inngest den Zustand extern speichert, kann man einen Workflow in Elixir schreiben, ihn dann in TypeScript neu schreiben und erneut deployen, und laufende Funktionen können ähnlich wie bei CRIU live über Backend-Sprachen hinweg migriert werden.
    Mit einem eventgetriebenen Ansatz ist auch Flow Control möglich. Debounce, Batching, Throttling und Fan-out lassen sich in jeder Runtime und Sprache handhaben. Zum Beispiel könnte eine Elixir-App auf Fly ein Event senden, das eine Funktion in TypeScript + Lambda ausführt.
    Ich bin gespannt, wohin FLAME sich entwickelt. Ich sehe da teilweise ähnliche Ziele.

    • Inngest sieht nach einem tollen Dienst aus. Der Artikel hat auch Job-Handler, Dauerhaftigkeit und Retries behandelt.
      Wenn man in Elixir Dauerhaftigkeit, Retries und Workflows braucht, verwendet man normalerweise Oban, und das wird auch hier weiterhin so sein. Ein Oban-Job ruft dann FLAME auf, um die elastische Ausführung zu übernehmen.
  • Das ist einer der Gründe, warum ich die von HN erzwungene amerikanische Titel-Großschreibung wirklich nicht mag. Serverless sieht so aus, als ginge es darum, das Unternehmen serverless.com mit großem S neu zu denken, und nicht darum, das Prinzip serverless mit kleinem s neu zu denken.
    Nebenbei: Ich wünschte, jemand würde Serverless einmal neu denken.

    • Ich glaube, die Groß- und Kleinschreibung wird nicht von HN erzwungen, sondern von der Person festgelegt, die den Beitrag einreicht.
      Ich weiß nicht, ob du https://sst.dev/ gesehen hast, aber es scheint, als hätten sie genau das getan. Zum Beispiel gibt es Live Lambda Development, wodurch lokale Entwicklung wirklich einfach wird, weil man den Feedback-Loop stark verkürzt, ohne Code in die Cloud hochladen und auf ein Deployment warten zu müssen.
  • Die Idee ist ziemlich cool, und die API ist auch hervorragend.
    Bei der Stelle „CPU-bound Aufgaben wie Video-Transcoding können in der Produktion schnell einen ganzen Dienst zum Stillstand bringen“: Könnte man nicht einfach automatisch nach CPU-Auslastung skalieren?

    • Diesen Gedanken wollte ich am Anfang des Artikels behandeln. Das Problem bei diesem Ansatz ist, dass man auf der falschen Betriebsebene skaliert.
      Um eine bestimmte heiße Aufgabe zu verarbeiten, skaliert man die gesamte Anwendung mitsamt Webserver. Was wir wollen – und der Grund, warum man sich FaaS ansieht – ist feingranulare elastische Skalierung. Die Idee hier ist, eine solche feingranulare Skalierung für bestehenden App-Code zu ermöglichen, statt wild auf den Skalierungsbutton für Webserver oder Worker zu drücken und zu hoffen, dass es gutgeht.
    • Jein. Der restliche Workload braucht möglicherweise nicht viel CPU. Vielleicht benötigen nur ein oder zwei Workloads diese Leistung, und man möchte verhindern, dass diese Aufgabe von anderen Dingen verdrängt wird.
      Oder man braucht vielleicht eine GPU.
      Für den Kerndienst reichen ein oder zwei Server, aber für eine Aufgabe, die vielleicht nur einmal am Tag anfällt, muss man bei Bedarf unter Umständen auf Dutzende, Hunderte oder Tausende Maschinen skalieren.