1 Punkte von GN⁺ 2024-08-11 | 1 Kommentare | Auf WhatsApp teilen
  • Screenshotbot verwendet ohne separate DB den RAM-Zustand des Webserver-Prozesses als Datenspeicher und stellt den Zustand nach Ausfällen mit Snapshots und Transaktionslogs wieder her
  • Durch weniger SQL-Serialisierung, DB-Roundtrips und separate Background-Job-Services wird die Entwicklererfahrung eines Single-Process-Systems vereinfacht; genutzt werden In-Memory-Indizes und threadbasierte Verarbeitung
  • Hohe Verfügbarkeit wird über den Raft-Konsensalgorithmus erreicht, indem das Transaktionslog auf drei Server repliziert wird; fällt der Leader aus, übernimmt innerhalb weniger Sekunden ein neuer Leader die Anfragen
  • Die konkrete Implementierung kombiniert Common Lisp, bknr.datastore, das Open-Source-Projekt bknr.cluster, Baidu Braft und EFS; Serverneustarts erfolgen normalerweise nur etwa einmal alle 1–2 Monate
  • Selbst wenn die CI eines bekannten Enterprise-Kunden bei jedem Commit und Pull Request Hunderte API-Aufrufe ausführt, liegt die CPU-Auslastung auf einer Maschine mit 4 Kernen und 16 GB RAM bei höchstens 20 %; der Großteil entsteht durch Bildverarbeitung

Eine Architektur, die RAM-Zustand wie eine Datenbank behandelt

  • Bei klassischen Web-Services war es üblich, ein Web-Framework wie Rails, Django oder Node zusammen mit einer Datenbank wie MySQL, PostgreSQL oder MongoDB zu wählen
  • In den vergangenen zehn Jahren haben sich die Rahmenbedingungen so verändert, dass sich dieser Ansatz neu bewerten lässt
    • Festplatten sind mit NVMe schneller geworden
    • Datenträger wie EBS/EFS sind robuster geworden
    • RAM ist günstiger geworden, sodass die Daten der meisten Startups vollständig in den Arbeitsspeicher passen
    • Maschinen mit Hunderten von Kernen können gemietet werden
    • 2014 wurde der Raft-Konsensalgorithmus veröffentlicht, und die Zahl robuster Implementierungen ist gestiegen
  • Diese Architektur vereint Web-Service und Datenbankinstanz und nutzt den Speicherzustand des Prozesses wie eine Datenbank
  • Anders als bei SQLite werden Daten nicht serialisiert gespeichert, sondern Objekte und Felder im RAM direkt als Anwendungszustand behandelt

Explore: Wie frühe Produktiteration vereinfacht wird

  • Wenn alle Daten im RAM liegen, müssen sie nicht per SQL-Abfrage serialisiert werden, und auch die Struktur mit mehreren Frontend-Servern an einer einzigen DB wird reduziert
  • Steigt die Last, kann man mit größeren Servern mit mehr RAM und CPU reagieren
  • Für Indizes können statt auf Festplattenlatenz ausgelegter B-Bäume Strukturen wie In-Memory-Hash-Tabellen verwendet werden
    • Bei Screenshotbot waren Indizes auf Basis funktionaler Collections wichtig für die Skalierbarkeit
  • Da Datenabfragen RAM-Lesezugriffe sind, braucht es weder Spezialstrukturen zur Reduktion von DB-Roundtrips noch Async-IO
  • Background-Jobs werden zu Threads innerhalb desselben großen Prozesses, und Nebenläufigkeit lässt sich meist mit In-Memory-Mutexen und Condition Variables lösen

Fehlerbehebung: Snapshots und Transaktionslogs

  • Um Prozessabstürze abzufangen, wird der vollständige RAM-Zustand regelmäßig als Snapshot gespeichert
  • Änderungen seit dem letzten Snapshot werden zunächst als Transaktionslog auf Festplatte geschrieben, bevor der Speicherzustand verändert wird
    • foo.setBar(2) protokolliert zuerst eine Transaktion, dass das Feld bar von foo auf 2 gesetzt wurde, und ändert erst danach das tatsächliche Feld
    • new Foo() protokolliert zuerst eine Transaktion über die Erzeugung eines Foo-Objekts und gibt dann das neue Objekt zurück
  • Beim Neustart wird zuerst der Snapshot geladen und danach das Transaktionslog abgespielt, um den Zustand wiederherzustellen
  • Indexänderungen müssen nicht ins Transaktionslog aufgenommen werden
    • Hat etwa das Feld bar von Foo einen Index, aktualisiert setBar den Index, und diese Aktualisierung geschieht auch beim Laden des Snapshots oder beim Wiedergeben der Transaktionen erneut

Codeformen, die ein Single-Process-Modell ermöglichen

  • Da alle Anfragen im selben Prozess verarbeitet werden, können Closures im Serverspeicher gehalten und zur Verarbeitung von Seitenanfragen genutzt werden
  • URLs im Format https://screenshotbot.io/n/nnnnnnn von Screenshotbot werden auf interne Closures abgebildet
  • Weil Closures Objektreferenzen halten, müssen bei Seitenwechseln seltener Objekt-IDs übergeben oder Objekte serialisiert werden
  • Auch Debugging, Profiling und Monitoring konzentrieren sich auf einen einzigen Service
    • Es gibt keine separaten Beobachtungspunkte einer Datenbank wie ein MySQL slow query log
    • Fällt dieser eine Service aus, ist zwar auch die Website down, aber wenn Service und Server jeweils nur einmal vorhanden sind, wird die Ausfallwahrscheinlichkeit als geringer angesehen
    • Stirbt der Server, kann AWS innerhalb weniger Minuten einen neuen Server hochfahren
  • Auch Tests werden einfacher, weil keine Datenbank gemockt werden muss

Expand: Hohe Verfügbarkeit mit Raft

  • Wenn Kunden mit hohem Risiko 99,999 % Verfügbarkeit verlangen, reicht eine Single-Server-Architektur allein nicht aus
    • Es kann mehrere Minuten dauern, bis AWS nach einem Serverausfall wieder eine Instanz hochfährt
    • Auch das Wiederherstellen von Festplatten-Snapshots durch den Prozess kann mehrere Minuten dauern
    • Bei einem Redeploy kann der Server durch den Service-Neustart mehrere Minuten nicht verfügbar sein
  • Der Raft-Konsensalgorithmus repliziert das Transaktionslog des Webserver-/Datenbank-Systems als endlichen Zustandsautomaten
  • Bei Replikation auf drei Maschinen wird bei Ausfall des Leaders innerhalb weniger Sekunden ein neuer Leader gewählt, der Anfragen weiterbearbeitet
  • Ohne die Art, wie Entwickler Code schreiben, wesentlich zu verändern, kann so aus einem einfachen Service eine hochverfügbare Datenbank werden
  • In einer Raft-basierten Konfiguration sind auch Rolling Deployments ohne Abschaltung der Server möglich

Extract: Sharding und erwartete Flaschenhälse

  • In einer Phase mit mehr großen Kunden kann Sharding eingeführt werden, ähnlich wie große Unternehmen es bei Datenbanken tun
  • Jeder Shard kann ein eigener Cluster sein
  • Screenshotbot bietet bereits heute pro Enterprise-Kunde dedizierte Cluster an
  • Meta ist zur Handhabung der Replikation von MySQL-Clustern auf Raft umgestiegen, und Screenshotbot nutzt einen ähnlichen Ansatz ohne separate Datenbank
  • Ein zentraler möglicher Flaschenhals ist die Skalierung des Commit-Threads
    • Lese-Threads lassen sich gut parallelisieren
    • Ein einzelner Commit-Thread wendet Transaktionen nacheinander an
    • Da Raft mehrere Transaktionen gemeinsam auf Festplatte committed, ist Festplattenlatenz nicht entscheidend
    • Problematisch würde es, wenn die CPU-Kosten für das Anwenden von Transaktionen die Leistung eines einzelnen Kerns übersteigen
    • In diesem Fall kann man die Commit-Kosten profilieren, Teile der Arbeit aus dem Transaktionsthread auslagern oder Sharding prüfen

Der tatsächliche Stack von Screenshotbot

  • Screenshotbot verwendet Common Lisp
  • Die erste Implementierung nutzte MySQL, wurde aber wegen der schwierigen Handhabung von Nebenläufigkeit durch bknr.datastore ersetzt
  • bknr.datastore ist eine Bibliothek, die die im Explore-Abschnitt beschriebene Struktur für Common Lisp bereitstellt
  • In dieser Architektur verarbeiten Threads innerhalb eines einzelnen Prozesses Webanfragen, daher ist starkes Multithreading wichtig
    • Aus diesem Grund werden Ruby und Python als ungeeignet angesehen
  • Da Closures im Serverspeicher erhalten bleiben, ist häufiges Neustarten des Servers schwierig
    • Beim Neustart gehen die Closures verloren
    • Stattdessen wird der Code im laufenden Prozess per Hot Reloading aktualisiert
    • Common Lisp bietet bei Änderungen an Klassendefinitionen Standardfunktionen wie reinitialize-instance, die bestehende Objekte aktualisieren

Cluster, Dateispeicherung und Betriebsgröße

  • Serverneustarts finden derzeit normalerweise nur etwa einmal alle 1–2 Monate statt
  • Wenn ein Neustart nötig ist, wird im Raft-Cluster ein Rolling Restart durchgeführt
  • Pro Installation wird ein Cluster aus drei Servern verwendet; diese Konfiguration toleriert den Ausfall eines Servers
  • Kubernetes wird derzeit nicht eingesetzt und noch nicht als notwendig angesehen
  • Die Raft-Implementierung ist eine eigene Bibliothek auf Basis von bknr.datastore
    • bknr.cluster wurde als Open Source veröffentlicht
    • Intern wird Braft von Baidu verwendet
    • Braft verarbeitet Snapshots im Hintergrund, sodass der Server auch während der Snapshot-Erstellung weiter Anfragen bedienen kann
  • Bilddateien oder Blobs, die nicht in den Datastore gehören, werden auf einem von allen drei Servern gemeinsam genutzten EFS gespeichert
    • EFS ist ein hochverfügbares NFS
    • Im Vergleich zu S3 ist die Arbeit damit einfacher, weil keine speziellen Fehlerbedingungen behandelt werden müssen
    • Da auf Festplatte geschrieben wird, ohne mit externen Servern zu interagieren, ist es auch besser testbar

Aktuelle Performance und Einsatzbereich

  • Screenshotbot unterstützt einige große Enterprise-Kunden sowie insbesondere einen sehr bekannten Kunden
  • Dort läuft es in der CI und erhält bei jedem Commit und Pull Request Hunderte API-Anfragen
  • Trotz dieser Last reicht für die Anfragenverarbeitung eine Maschine mit 4 Kernen und 16 GB RAM aus
    • Auch die Replikationsserver verwenden ähnliche Maschinen und sind meist im Leerlauf
    • Die CPU-Auslastung liegt bei maximal 20 %
    • Der größte Teil der CPU-Nutzung entfällt auf Bildverarbeitung
  • Wichtig ist, nicht für eine viel größere Skalierung zu entwerfen, als tatsächlich benötigt wird
  • Wer Common Lisp wählt, kann die zugehörige Konfiguration als Screenshotbot OSS nutzen

1 Kommentare

 
GN⁺ 2024-08-11
Meinungen auf Hacker News
  • Diese Architektur ähnelt grob der Art, wie HashiCorps Nomad, Consul und Vault gebaut wurden (ich bin einer der Nomad-Maintainer). Sie ist definitiv eine ungewöhnliche Architektur, aber sobald man sich daran gewöhnt hat, ist die Developer Experience ziemlich gut.
    Den In-Memory-State kann man in beliebiger Form halten, sodass man an die Anwendung angepasste Indizierungs- und Abfragefunktionen selbst bauen kann. Man könnte für die Raft-FSM auch :memory: SQLite verwenden, aber wenn man einen transaktionalen In-Memory-Store bauen oder finden kann (wir verwenden unser eigenes go-memdb), werden State-Lesezugriffe einfach zu Funktionsaufrufen. Auch veraltete Lesezugriffe oder Write-Skew zu verhindern, ist simpel. Da jedes geschriebene Objekt einen Raft-Index hat, kann man APIs bauen wie: „frage Objekt foo bei einem Follower ab, warte aber mindestens bis Index 123“. Das nimmt einem viel von der „Magie“ weg, die man normalerweise in ein RDBMS oder einen externen Store schiebt.
    Allerdings wäre ich vorsichtig, diese Architektur für ein neues Startup außerhalb des „Infrastruktur“-Bereichs zu wählen. Im Grunde baut man damit nämlich seine eigene Datenbank. Man muss grundlegende Bausteine wie RPC zwischen Nodes, Persistenz auf Disk und einen transaktionalen In-Memory-State-Store gut auswählen oder selbst schreiben. Upgrades sind besonders schwierig: Neuer Code könnte versuchen, Entitäten in das Raft-Log zu schreiben, die Nodes mit älteren Versionen nicht verstehen. Schlimmer noch: Die Verarbeitungslogik könnte sich ändern, sodass ältere Nodes sie falsch interpretieren. Es gibt kein kostenloses Mittagessen.

    • Der Teil „für die Raft-FSM kann man auch :memory: SQLite verwenden“ war in den ersten rund sieben Jahren das Grunddesign von rqlite[1]. rqlite ist jedoch auf diskbasiertes SQLite umgestiegen, und mit WAL-Modus sowie PRAGMA synchronous=OFF[2] war es genauso schnell wie Schreiben in RAM oder nahe genug dran. Dadurch ließen sich auch die Einschränkungen einer :memory:-SQLite-Datenbank vermeiden, darunter die maximale Größe von 2 GB. Wir hätten von Anfang an den Disk-Modus verwenden sollen; das ist mir erst jetzt klar geworden.
      Da rqlite dieselbe Raft-Bibliothek[3] wie Nomad verwendet, ist dir ein Teil davon vielleicht schon bekannt.
      Das Upgrade-Problem gibt es tatsächlich. Ich frage mich, ob man bei Nomad im Feld häufig darauf stößt. In den zehn Jahren Entwicklung von rqlite haben wir nur sehr selten einen neuen Raft-Entry-Typ eingeführt, und echte Nutzer sind nur einmal darauf gestoßen. Eine Möglichkeit damit umzugehen ist, zuerst eine Version auszurollen, die den neuen Typ versteht, ihn aber niemals schreibt, und erst nachdem diese Version vollständig ausgerollt ist, auf eine Version zu aktualisieren, die den neuen Typ tatsächlich verwendet. Praktisch habe ich das allerdings nie gemacht, und es erfordert auch Disziplin von den Endnutzern.
      [1] https://www.rqlite.io
      [2] Das mag riskant klingen, aber im aktuellen rqlite-Design wird die zugrunde liegende SQLite-Datenbank beim Start vollständig aus dem Raft-Log rekonstruiert (das Raft-Log wird bei jedem Schreibvorgang per fsync geschrieben). Wenn die SQLite-Datenbank also etwa durch einen Stromausfall beschädigt wird, ist das nicht sonderlich relevant, weil die SQLite-Datenbank nicht der maßgebliche Datenspeicher von rqlite ist.
      [3] https://github.com/hashicorp/raft
    • Ich bin für die Idee, Daten im Speicher zu halten, offener als viele andere Antworten hier. Als ich im Artikel den Teil über Common Lisp und Hot Reloading sah, dachte ich: „Dieses Team kann wohl machen, was es will. Aber nicht jeder arbeitet in diesem Team.“
    • „Upgrades sind besonders schwierig“ stimmt wirklich. Das gilt aber nicht für Startups in der Explore-Phase, die keine Replikation brauchen, und so haben auch wir lange gearbeitet. Genau in dieser Phase ist diese Architektur für Produktiteration am nützlichsten.
      Sobald man in der Expand-Phase mit Replikation beginnt, entstehen allerdings tatsächlich Engineering-Aufgaben. Trotzdem sind das alles lösbare Probleme. In Common Lisp kann man Code hot reloaden, was ebenfalls hilft, weil manche Migrationen dadurch deutlich einfacher werden.
  • Vor Jahrzehnten schrieb PG, dass Viaweb keine Datenbank verwendete, und dass es seltsam wirkte, dass Web-Apps zu Frontends für Datenbanken würden, während Desktop-Apps das nicht seien[0]. HN verwendet ebenfalls keine Datenbank.
    Da moderne Desktop- und Mobile-Apps jedoch häufig Datenbanken nutzen, meist SQLite, stimmt diese Aussage so nicht mehr. Denn es hat sich gezeigt, dass relationale Datenspeicherung und Abfragen für eine sehr breite Palette von Anwendungen ziemlich nützlich sind.
    [0] https://www.paulgraham.com/vwfaq.html

    • Wenn man den Link liest, scheint „Datenbank“ nicht für alle dasselbe zu bedeuten.
      In der vwfaq steht weiterhin, dass Daten von der Festplatte gelesen werden, und auch, dass „ein Prozess gestartet wird, der auf eine HTTP-Anfrage antwortet“. Mit „Datenbank“ scheint hier eine separate Server-Komponente zur Persistierung von Daten gemeint zu sein sowie eine Architektur, bei der man mit einem anderen Server kommunizieren muss, um diese Daten abzurufen.
      Nach dieser Definition fällt SQLite natürlich nicht unter Datenbanken. Und wenn man bereits Daten von der Festplatte liest, verwendet man entweder eine Datenbank oder hat eine ad hoc selbst gebaute Persistenzschicht implementiert. Die Frage ist, ob man es weiterhin als Nutzung einer Datenbank ansehen würde, wenn die App beim Start Daten aus SQLite lädt.
      Das Problem an dieser Denkweise ist, dass sie ignoriert, dass der Kern einer Datenbank darin liegt, Daten auf bequeme Weise zu speichern und abzurufen, ohne sich um Low-Level-Details kümmern zu müssen. Daten in einer Datenbank zu speichern heißt nicht, irgendwo eine Postgres-Instanz hochzufahren und die Daten übers Web abzurufen. Wenn man alle Daten im Speicher hält und einen Prozess hat, der Snapshots mit einer logstrukturierten Datenstruktur auf die Festplatte schreibt: Glückwunsch. Man hat gerade eine eigene Datenbank gebaut.
    • Es ist offensichtlich, dass PGs Text Einfluss hatte. Wir verwenden ebenfalls Common Lisp, und in diesem Bereich kommt man an PG schwer vorbei. Allerdings scheint Viaweb keine Transaktionslogs wie bknr.datastore verwendet zu haben, und dadurch wurde der Entwicklungsprozess deutlich reibungsloser.
    • Als PG Viaweb schrieb, war SQLite selbst noch nicht so verbreitet wie heute. Korrektur: Damals existierte SQLite noch gar nicht. Wenn es SQLite nicht gab und die Optionen praktisch nur aus Key-Value-Stores bestanden, hätte in den meisten Fällen auch das Dateisystem gereicht.
      Zweitens sind RDBMS-Abfragen in den letzten 20 Jahren deutlich einfacher geworden. Es gibt inzwischen alle möglichen ORMs und Row Mapper, die Boilerplate reduzieren.
      Außerdem sind fortgeschrittene Funktionen wie Volltextsuche hinzugekommen, die für Desktop- und Mobile-Apps nützlich sind. Heute ist der Einsatz eines RDBMS in einer Desktop-App eine gute Wahl.
    • HN verwendet keine Datenbank? Kannst du das etwas näher erklären? Das ist ziemlich überraschend.
    • Damals war es eine andere Zeit. Soweit ich weiß, bestand Viaweb aus mehreren Common-Lisp-Instanzen, und der gesamte Zustand der Nutzersitzungen lag im Speicher der jeweiligen Maschine. Ich erinnere mich, irgendwo gelesen zu haben, dass sie in der Produktion Bugs in Echtzeit gepatcht haben, während sie mit Nutzern telefonierten.
      Das Web ist viel größer geworden, und viele dieser Praktiken würden heute nicht mehr funktionieren. Wenn man mit dem heutigen Testaufwand während eines Kundentelefonats Live-Fixes direkt auf eine Produktionsmaschine schieben würde, würden hier viele an meinem Geisteszustand zweifeln.
  • Ich verstehe den Wunsch, etwas Interessantes auszuprobieren, aber das hier wirkt wie eine enorme Zeitverschwendung, nur um nicht die grundlegendsten Teile von MySQL oder Postgres lernen zu müssen. Man baut einfach darauf auf, fertig — besonders wenn es in einer Public Cloud läuft.
    Die Argumente zu zusätzlicher Round-Trip-Latenz oder Concurrency-Problemen überzeugen mich nicht. Für Letzteres gibt es einfache Lösungen wie grundlegendes Tuning oder das Isolieren lauter Kunden. In einem anderen Blogpost von ihnen ist von der Möglichkeit die Rede, 10 Millionen Zeilen pro Tag hinzuzufügen, und von Indexierungsaufgaben; das ist buchstäblich nichts. Selbst beim Zehnfachen wäre das meiner Ansicht nach kein Grund, eine maßgeschneiderte Lösung zu entwickeln.
    Bis es wirklich nötig wird, gilt „worse is better“, und wenn es so weit ist, merkt man es eindeutig. Dann kennt man auch den Bottleneck und kann klüger reagieren, statt von Anfang an zu viel zu bauen.

    • Bei einer serverbasierten Datenbank-Engine könnte man immer noch argumentieren, dass dadurch Netzwerkaufrufe reduziert werden. Fragwürdig, aber möglich.
      Dass der Blogger aber als Grund gegen SQLite anführt, es könne Funktionen geben, die man nicht braucht, ist befremdlich. Das ist absurd und rechtfertigt gar nichts.
      Der Text liest sich wie ein schlechter Ansatz für ein hypothetisches Problem, gefolgt von einem verzweifelten Versuch, mit gezwungenen Argumenten die naheliegende Lösung abzulehnen.
    • Was mich interessiert: Wäre ihr Geschäft erfolgreich gewesen, wenn sie keine Zeit damit verbracht hätten, das Rad neu zu erfinden? Schon öffentlich zu bauen und darüber zu bloggen macht das Produkt bekannt und demonstriert technisches Können. Wenn sie langweilige Technologien verwendet hätten, die zusammengesetzt gut funktionieren, gäbe es weniger zu erzählen — und damit vielleicht auch weniger PR-Effekt?
      Ich frage mich, ob ich damit falsch liege, oder ob dieser zusätzliche Aufwand, der kontrovers, aber unnötig wirkt, Teil des Produkts ist und genau die Art ist, wie man in diesem Bereich erfolgreich wird.
  • Es ist seltsam: Der Text beginnt mit „Ich meine nicht so etwas wie SQLite, wo Daten weiterhin serialisiert werden“, endet dann aber doch bei einem eigenen Transaktionslog, das Serialisierung braucht und repliziert werden muss. Datenbankreplikation funktioniert im Grunde genau so.
    Wenn die gesamte Last auf einem Server landet, sollte man einfach auf diesem Server eine Datenbank betreiben und Dinge wie „eine besondere Architektur zur Reduzierung von Datenbank-Roundtrips“ vergessen. Wenn alle Daten in den RAM passen, kann man für die Datenbank, wenn man möchte, eine RAM-Disk verwenden und mit Standardwerkzeugen auf persistenten Speicher replizieren. Das ist tatsächlich einfach.

    • Insgesamt fühlt es sich so an, als ließe es sich mit „wir haben unser eigenes SQLite + Raft-Replikation gebaut“ zusammenfassen. Nur ohne die erprobte Zuverlässigkeit von SQLite und ohne dessen Fähigkeit, Speicher effizient auf die Platte auszulagern.
      Damit ist es im Grunde so etwas wie https://litestream.io/. Durch die explizite Raft-Konfiguration könnte der Failover schneller sein. Ich bin kein Litestream-Nutzer und kenne die feinen Unterschiede nicht, aber es klingt sehr ähnlich.
      Abgesehen von dieser starken Vereinfachung gefällt mir die Idee an sich ziemlich gut, und ich finde, der Artikel vermittelt das Konzept recht überzeugend. Für viele Systeme dürfte es selbst bei ungewöhnlich großem Erfolg ausreichend skalieren, um den Großteil oder das gesamte Geschäft zu tragen, und die Performance wäre im Vergleich zu fast jeder Alternative absurd gut.
    • Eine RAM-Disk braucht man meiner Meinung nach auch nicht. Datenbanken cachen ohnehin alles im Speicher, und nur Schreibvorgänge erreichen die Platte.
      Man muss nur eine Datenbank kalt starten und dann ein ziemlich großes select zweimal ausführen.
    • Wichtig ist zu verstehen, dass jedes Startup drei Phasen durchläuft: Explore, Expand, Extract. Was in einer Phase einfach ist, ist in einer anderen nicht einfach.
      Eine transaktionale Datenbank ist in den Phasen Expand und Extract einfach, wird in der Explore-Phase aber zur zusätzlichen Last, weil sie den Fokus auf Infrastrukturprobleme statt auf das Produkt lenkt. In der Explore-Phase gibt es keine Kunden, also auch keine Daten, und Datenzuverlässigkeit ist nicht wichtig.
      Alles mit bknr.datastore im Speicher zu halten (ohne Replikation) ist in der Explore-Phase einfach. Kommt man jedoch in die Expand-Phase, entsteht operativer Aufwand, um Datenkonsistenz sicherzustellen.
      Bis man die Expand-Phase erreicht, hat man das Produkt aber bereits validiert und schon viel Code geschrieben. Das Ganze auf eine transaktionale Datenbank umzuschreiben ergibt keinen Sinn; einfacher ist es, obenauf Raft-Replikation zu ergänzen.
    • Stimme zu. Ein WAL neu zu bauen bedeutet, auch alle damit verbundenen Kopfschmerzen neu zu bauen oder zu ignorieren. Die Wiederherstellung aus dem Log scheint ziemlich lange zu dauern, daher sind sie vermutlich noch nicht einmal richtig bei Log-Checkpointing angekommen.
    • Trading-Systeme legen ganz offen alles in vorab allozierten Strukturen im RAM ab. Es hängt davon ab, welchen Kompromiss man wählt.
  • Die Logik dieses Artikels ist irritierend. Soll das eine Methode sein, zustandsbehaftete Anwendungen einfacher und schneller zu machen?
    Die Prämissen sind schwach und die Behauptungen überzogen. Der Autor übertreibt die Schwierigkeit der Serialisierung, um eine schwache These stärker wirken zu lassen.

    • Aus dem Blogbeitrag bleibt stark der Eindruck hängen: „Schaut, wie schlau wir sind.“
      Mit solchen Leuten ist die Zusammenarbeit normalerweise schwierig. Gut, dass ich ein Startup gefunden habe, das sie versenken können, damit ich mich nicht mit ihnen auseinandersetzen muss.
    • Und dann implementieren sie Serialisierung, um Transaktionen in ein Log zu schreiben und auf andere Nodes zu replizieren.
  • Wenn man ein neues Projekt startet, ist die Datenstruktur normalerweise eine „Liste von Einträgen mit Eigenschaften“. Ich schreibe zum Beispiel gerade eine Fitness-App; die Daten sind eine Liste von Übungen, und jede Übung hat einen Titel, eine Beschreibung, eine Video-URL und weitere Eigenschaften.
    Normalerweise beginne ich damit, diese Einträge als YAML-Dateien in ein data-Verzeichnis zu legen. In Wirklichkeit ist es ein eigener YAML-Dialekt ohne die seltsamen Eigenheiten des eigentlichen YAML. Jeder Wert ist ein String, es gibt keine magischen Typumwandlungen. Einen neuen Eintrag anzulegen heißt einfach, mit vim crunches.yaml Daten einzutragen; Bearbeiten und Löschen sind in dieser Datenstruktur ebenfalls sehr einfach.
    Wenn das Projekt wächst, erstelle ich normalerweise ein DB-Schema und verschiebe die Einträge nach MariaDB oder SQLite.
    Dieses Mal plane ich, die Einträge (Übungen) in eine JSON-Spalte einer SQLite-DB zu verschieben. Alle Eigenschaften eines Eintrags sollen in einem einzigen JSON-Feld gespeichert werden, und ich möchte einen kleinen DB-Browser schreiben, mit dem sich JSON-Felder wie YAML bearbeiten lassen. Damit will ich die Bequemlichkeit erhalten, menschenlesbare Daten zu bearbeiten.
    Der DB-Browser dürfte ziemlich geradlinig zu schreiben sein. Mit etwas ncurses durch Tabellen navigieren, eine auswählen, Zeilen durchsuchen sowie Zeilen einfügen und löschen. Beim Bearbeiten eines Feldes wird Vim geöffnet. Wenn das Feld JSON ist, wird es vor dem Übergang an Vim in YAML umgewandelt; wenn der Nutzer Vim beendet, wird es wieder zurück in JSON konvertiert.

  • Was am Anfang des Artikels beschrieben wird, ähnelt im Grunde der Funktionsweise von NUMA-Maschinen (z. B. SGI Altix oder UV). Die damals angeführten Vorteile waren außerdem niedrige Latenz und die Möglichkeit, Arbeit mit Multithreading über großen RAM zu parallelisieren. Clustering entstand als kostengünstige Alternative zu Maschinen, die über eine Million Dollar kosteten. Es gibt auch Ähnlichkeiten zur Persistenz von AS/400, bei der eine App einfach in den Speicher schreibt und dies transparent auf die Platte gemappt wird.
    Nun reist man mit günstiger Hardware gewissermaßen zurück zu den Vorteilen clusterartiger NUMA-Maschinen. Auf dem Weg gab es Verbesserungen, und der Artikel war interessant zu lesen.
    Eine weitere frühere Technik bestand darin, innerhalb des Clusters den TCP/IP-Stack zu entfernen und damit die entsprechenden Probleme loszuwerden. Lösungen wie Active Messages waren eine dünne Schicht über der Hardware. Es gibt auch Designs für Netzwerkrouter mit eingebauter starker Konsistenz. Damit lässt sich ziemlich viel machen.
    Wenn das größer wird, gibt es auch Hardware-Chancen. Auf der CPU-Seite hat SGI zwei Dinge getan: NUMA-Maschinen skalierten die Zahl der CPUs und den RAM in einem System, und FPGAs wurden direkt an den Speicherbus angeschlossen, um sie als kundenspezifische Beschleuniger zu nutzen. Schließlich haben einige Informatik-Paper den Prozessorbefehlssatz, das On-Chip-Netzwerk und anderes verändert, um Multithreading-Engpässe zu beseitigen oder zu reduzieren. Chips wie OpenPiton erhöhen mit offenen, anpassbaren Kernen die Zahl der Cores (z. B. 32).

  • „Stell dir vor, was für großartige Dinge man bauen könnte, wenn man Daten nicht in SQL-Abfragen serialisieren müsste“ – das gibt es bereits in ausreichend ausgereiften Implementierungen des Actor-Modells[0], zum Beispiel Akka Event Sourcing[1]. Auch das folgende Problem wird dort behandelt:
    „Der wichtige Teil ist aber, wie man sich erholt, wenn der Prozess crasht. Die Antwort ist einfach: Man erstellt regelmäßig Snapshots des gesamten RAM.“
    Das ist im Wesentlichen gelöst, ohne dass man eine „neue Architektur für die Webentwicklung“ bauen müsste. Open-Source-Versuche, das RAFT-Protokoll mit Actors zu erkunden, gibt es hier[2] und hier[3].
    0 - https://en.wikipedia.org/wiki/History_of_the_Actor_model
    1 - https://doc.akka.io/docs/akka/current/typed/persistence.html
    2 - https://github.com/Michael-Dratch/RAFT_Implementation
    3 - https://github.com/invkrh/akka-raft

  • Mein erster Gedanke war: „Ah, das ist die Art, wie ich es gemacht habe, als ich Common Lisp geschrieben habe; lustig, dass jemand diese Technik wiederentdeckt hat.“
    War es dann aber nicht – es gab einfach nur noch mehr Lisp-Leute.

  • „RAM ist sehr billig“ ist vielleicht eines der größten Missverständnisse unter Entwicklern.
    SSDs sind beim Durchsatz oder bei den IOPS um das 100- bis 10.000-Fache besser geworden, und die vCPU-Leistung pro Dollar ist ebenfalls um das 20- bis 50-Fache gestiegen. Von 45/32 nm sind wir inzwischen bei 5/3 nm angekommen, und auch der Durchsatz an Instruktionen pro Takt ist deutlich höher geworden.
    Aber die RAM-Preise sind längst nicht so stark gefallen wie bei CPUs oder SSDs. RAM mag viel schneller geworden sein, man kann dank dichterer Chips mehr Speicher einbauen, und die Zahl der Channels ist vielleicht von Dual-Channel auf 8 oder 12 gestiegen. Betrachtet man jedoch die DRAM-Spotpreise von 2008 bis 2022, lag der niedrigste DRAM-Preis dreimal auf demselben Niveau von etwa 2,8 Dollar/GB. Im selben Zeitraum schwankte er je nach Zyklus zwischen 6 und 8 Dollar/GB. Wenn man also in den letzten etwa 15 Jahren am Tiefpunkt oder am Hochpunkt DRAM gekauft hat, lag der Preis – ohne Inflation zu berücksichtigen – grob in einem ähnlichen Bereich von ±10–20 %.
    Erst Mitte 2022 wurde die Marke von 2,8 Dollar/GB durchbrochen, fiel bis in die Nähe von 1 Dollar/GB und pendelte sich dann bei DDR5 bei etwa 2 Dollar/GB ein.
    Heute kann man 4 TB RAM in einen Server stecken. Das heißt aber nicht, dass DRAM enorm billig ist. Durchschnittliche Entwickler oder Entwickler bei Big Tech verdienen heute viel mehr als 2010, und deshalb fühlt sich RAM deutlich erschwinglicher an. Tatsächlich ist der DRAM-Preis selbst am Tiefpunkt der letzten 15 Jahre höchstens um etwas mehr als das Doppelte gefallen. Und es ist gut möglich, dass die DRAM-Preise innerhalb von ein bis zwei Jahren wieder stark anziehen.

    • Eine andere Interpretation ist ebenfalls möglich: Die maximale RAM-Kapazität einzelner Nodes ist in den letzten Jahrzehnten drastisch gestiegen.
      Ein einfaches Beispiel: Wenn ein bestimmter Node vor 20 Jahren auf 16 GB RAM begrenzt war, hätte man 256 Nodes gebraucht, um 4 TB RAM im System zu haben (ohne den Overhead der jeweiligen Betriebssysteme).
      Heute kann ein einzelner Node die gesamten 4 TB in einem einzigen Chassis aufnehmen.
      Die Gesamtkosten der RAM-Chips selbst mögen sich nicht verändert haben, aber die Kosten, diesen RAM in einem physischen System tatsächlich zu nutzen, sind dramatisch gesunken.
    • Gleichzeitig greifen viele Entwickler viel zu schnell zu verteilten Systemen, obwohl sie einfach nur mehr RAM kaufen müssten. Das könnte auch das gewesen sein, was der Autor gemeint hat.
    • Danke für die Daten. Ich stimme der anderen Antwort zu. Statt „billig“ hätte man wohl sagen sollen, dass es einfacher geworden ist, Maschinen mit Hunderten GB RAM zu bekommen.