6 Punkte von GN⁺ 2023-09-25 | 1 Kommentare | Auf WhatsApp teilen
  • Bevor man ein separates Queue-System einführt, kann man mit dem bereits genutzten Postgres oft eine Queue für Hintergrundjobs aufbauen und so stark von operativer Einfachheit und der Vertrautheit des Teams profitieren
  • Seit Postgres 9.5 lösen NOTIFY/LISTEN und FOR UPDATE SKIP LOCKED gemeinsam die Benachrichtigung über neue Jobs und die Vermeidung doppelter Verarbeitung zwischen Workern
  • Redis, Kafka, RabbitMQ und Amazon SQS sind leistungsfähige Optionen, aber jede neue Abhängigkeit bringt Fehlermodi und Lernaufwand in Entwicklungs-, Test- und Betriebsumgebungen
  • Auch eine Postgres-Queue ist kein Allheilmittel: Push/Pop kann langsamer sein als bei Redis, und es können betriebliche Kosten entstehen, etwa durch eine separate Queue-Datenbank oder einen eigenen Server sowie häufigeres VACUUM
  • Der Code für Hintergrundjobs sollte so abstrahiert werden, dass er nicht an ein bestimmtes Queue-Backend gebunden ist; andere Queue-Technologien sollte man erst dann prüfen, wenn die vertraute Technik die Anforderungen nicht erfüllt

Wie eine Postgres-Queue funktioniert

  • Eine Postgres-Queue-Technik besteht aus zwei Elementen
    • Pub/Sub zum Ankündigen und Empfangen neuer Jobs
    • Zeilensperren, damit nicht mehrere Worker denselben Job verarbeiten
  • Beide Funktionen sind seit Postgres 9.5, erschienen 2016, standardmäßig verfügbar
  • Mit NOTIFY und LISTEN zusammen lässt sich Pub/Sub an die Anwendung anbinden
  • FOR UPDATE SKIP LOCKED setzt Zeilensperren auf passende Datensätze und überspringt bereits gesperrte Datensätze
    • Auf Job-Datensätze angewandt lassen sich damit Queue-Abfragen wie SELECT * FROM jobs ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1 erstellen
  • SKIP LOCKED liefert eine „inkonsistente“ Sicht auf die Daten, aber genau diese Eigenschaft ist für Queues nützlich
    • Bereits in Bearbeitung befindliche Jobs sind wegen der Zeilensperre für andere Worker nicht sichtbar
    • Dieses Verhalten ermöglicht verteilte gegenseitige Ausschließung
  • Wenn NOTIFY neue Jobs an Worker sendet, die LISTEN verwenden, lassen sich sowohl periodische Batch-Verarbeitung als auch Echtzeitverarbeitung aufbauen

Warum Redis zum Standard für Hintergrundjobs wurde

  • Die Frage, wie lang laufende Jobs verarbeitet werden sollen, ist eine Technologieentscheidung mit vielen Trade-offs
  • Häufig genutzte Optionen für Queue- und Message-Broker sind:
    • Redis: ein In-Memory-Datenspeicher und Backend vieler Bibliotheken für Hintergrundjobs
    • Apache Kafka: eine von der Apache Foundation betreute verteilte Event-Streaming-Plattform
    • RabbitMQ: ein weit verbreiteter Message-Broker
    • Amazon SQS: ein skalierbarer Queue-SaaS-Dienst von Amazon
  • Unter dem GitHub-Thema background jobs topic basieren die fünf populärsten Bibliotheken alle auf Redis
    1. Sidekiq — Ruby
    2. resque — Ruby
    3. rq — Python
    4. Hangfire — C#
    5. asynq — Go
  • Redis speichert Daten im Arbeitsspeicher, ist dadurch schnell bei Einfügen und Abrufen und eignet sich mit seiner Pub/Sub-API sowie den Datentypen list und set gut für Queue-Implementierungen
  • Die Skalierbarkeit von Redis macht es für viele Entwickler zur Standardwahl, und Standards haben bei Technologieentscheidungen große Wirkung

Kriterien, die wichtiger sein können als Skalierbarkeit

  • Die Fixierung der Tech-Branche auf „Scale“ kann Einfachheit, Wartbarkeit und eine geringere kognitive Last für Entwickler in den Hintergrund drängen
  • Viele Anwendungen brauchen keine Skalierung auf dem Niveau von Google, Facebook oder Uber
  • Beim Start eines Projekts oder Unternehmens sollte man daher nicht von Anfang an nur auf Skalierbarkeit optimieren, sondern zuerst auf Folgendes achten
    • Technologien, die das Team gut kennt
    • Technologien, die gut genug sind, um die Anforderungen der Nutzer zu erfüllen
    • die Lösung mit dem geringsten Aufwand, passend zu den Fähigkeiten des Teams
  • Auch Postgres lässt sich in der Praxis gut skalieren, ist aber für Queue-Anwendungsfälle nicht das am stärksten skalierende spezialisierte Queue-System
  • Als Generalist unter den Softwaresystemen erledigt Postgres viele Aufgaben ziemlich gut, und Queues sind eine davon

Entscheidungen mit „langweiliger Technologie“ treffen

  • Die wichtigste Frage bei der Wahl einer Queue-Technologie lautet, welche Technik bereits im Einsatz ist und wirklich gut verstanden wird
  • Technik, die man schon nutzt und gut kennt, belastet den Software-Stack deutlich weniger
  • Wenn bereits eine relationale Datenbank im Einsatz ist und es sich dabei um Postgres handelt, kann man eine Postgres-Queue vor anderer Software in Betracht ziehen
  • „Langweilige Technologie“ ist keine feste Liste, sondern hängt davon ab, was gerade schon genutzt wird
    • Für nachrichtenorientierte Anwendungen kann RabbitMQ die langweilige Technologie sein
    • Für cache-orientierte Anwendungen kann Redis die langweilige Technologie sein
    • Für Anwendungen mit vielen relationalen Daten kann Postgres die langweilige Technologie sein
  • Wenn Redis, Kafka, RabbitMQ oder SQS nur für Hintergrundjobs neu eingeführt werden, steigen die Kosten deutlich
    • Neue Systemabhängigkeiten kommen in Entwicklungs-, Test- und Produktionsumgebungen hinzu
    • Entwickler, DBAs und SREs müssen die Fehlermodi und Konfiguration des neuen Systems später beherrschen
    • Wissen über Disaster Recovery, Fehlerdiagnose und Performance-Monitoring wird nötig
    • Unknown unknowns, die das Team noch nicht kennt, bleiben als Risiko bestehen

Kosten und Auswahlkriterien für eine Postgres-Queue

  • Auch langweilige Technologie ist kein Allheilmittel, und Postgres bildet keine Ausnahme
  • Wer sich für eine Postgres-Queue entscheidet, gewinnt Vertrautheit, bekannte Fehlermodi und verteilte Kosten, zahlt dafür aber möglicherweise einen Preis
    • Push/Pop ist bei einer Postgres-Queue deutlich langsamer als bei Redis
    • Statt einer einzelnen relationalen Datenbank können eine Anwendungsdatenbank und eine separate Queue-Datenbank nötig sein
    • Um Hintergrundjobs unabhängig zu skalieren, kann ein eigener Datenbankserver erforderlich sein
    • Häufigeres VACUUM kann notwendig werden und zusätzliche Performance-Kosten verursachen
  • Keines von Postgres, Redis, Kafka, RabbitMQ oder SQS sollte ein automatischer Standard sein
  • Technologieentscheidungen bleiben fortlaufende Trade-offs und müssen anhand der Anforderungen von Team und Anwendung getroffen werden
  • Im Zweifel kann man die Regel anwenden: „Alternativen erst dann in Betracht ziehen, wenn nachgewiesen ist, dass die langweilige Technologie die Anforderungen nicht erfüllt“

Eine Architektur für Hintergrundjobs, bei der die Queue austauschbar bleibt

  • Der Verarbeitungscode für Hintergrundjobs sollte nicht von einer bestimmten Queue-Technologie abhängen
  • Mit dem Wachstum einer Anwendung können bei Bedarf Technologien wie memcached oder Redis hinzukommen; mit der Zeit können auch sie für das Team zur vertrauten „langweiligen Technologie“ werden
  • Wenn die Queue-Technologie abstrahiert wird, können Nutzer die passende Queue für ihre Aufgabe auswählen
  • Von den populärsten Bibliotheken für Hintergrundjobs auf GitHub bietet mit Ausnahme von Hangfire keine einen Ausstiegspfad weg von Redis zu anderen Queue-Technologien
    • In einer solchen Struktur muss beim Wechsel der Queue der Anwendungscode neu geschrieben werden
  • Neoq ist eine Go-Bibliothek, die nach dieser Philosophie entwickelt wurde
    • Als Queue-Backend können In-Memory, Postgres und Redis verwendet werden
    • Nutzer können mit einem anderen Queue-Backend initialisieren, ohne den Anwendungscode zu ändern
    • Die Implementierungen für In-Memory und Postgres stammen aus erster Hand, die Redis-Implementierung nutzt asynq
  • Redis, Kafka, RabbitMQ und SQS können hervorragende Technologien sein, sind aber nicht immer die passende Queue oder die richtige Komplexität für die jeweilige Aufgabe

1 Kommentare

 
GN⁺ 2023-09-25
Meinungen auf Hacker News
  • Ich habe bisher dreimal ein verteiltes Job-System gebaut, und die Faustregel, die ich seit Jahren empfehle, lautet: „Entwirf für das Zehnfache der aktuellen Größenordnung.“
    Wenn es 70 Requests pro Sekunde sind, plane für 700; wenn du 20 Server für Batch-Jobs brauchst, rechne mit 200. Selbst ein Startup, das jedes Jahr um 100 % wächst, ist nach drei Jahren erst bei der achtfachen Größe, also bleibt beim Wachsen noch Zeit, Dinge neu zu schreiben.
    Beim ersten System habe ich wegen „Skalierbarkeit“ SQL vermieden, bin aber über viele Randfälle gestolpert, in denen transaktionale Integrität nötig war.
    Die beiden danach gebauten verteilten Job-Systeme nutzten PostgreSQL als Koordinator und waren im Grunde rund um SELECT FOR UPDATE SKIP LOCKED aufgebaut. Eines steuert normalerweise 350 Worker, das andere wendet komplexe Prioritäten auf Tausende von Jobs an.
    Beide werden problemlos durchhalten, bis sie mehrere Millionen Dollar Jahresumsatz tragen; das System mit 350 Workern ließe sich zum Beispiel mit etwas Feinschliff auf etwa 2.000 CPUs skalieren.
    Hyperscale-Technologie wirkt ziemlich billig, bis man merkt, dass man Transaktionen braucht. Ab diesem Moment wird es zum Engineering-Albtraum, Transaktionssemantik auf einem eventual-consistency Store nachzubauen.
    Also sollte man sich hinsetzen und durchrechnen: Wie groß müsste das verteilte System werden, wenn das Unternehmen 100 Mio. Dollar Jahresumsatz macht? Lässt sich leicht eine PostgreSQL-Instanz bekommen, die diese Last bewältigt, oder reicht Sharding pro Client? Wenn ja, sollte man PostgreSQL ernsthaft in Betracht ziehen. Hundert kleine Dinge werden dadurch einfacher.

    • Ich habe ähnliche Erfahrungen gemacht. Mehrfach habe ich SQL-basierte Queues um ein Vielfaches im zweistelligen Bereich weiter getrieben als die Größenordnung, bei der Leute sagen: „Hier bricht SQL zusammen und eine verteilte Lösung ist zwingend.“
      SQL-Lösungen sind meist einfacher, verbrauchen weniger Rechenressourcen und sind leichter betrieblich zu betreuen.
      Damit sie funktionieren, muss man die Datenbank allerdings gut genug kennen, um überhaupt zu wissen, dass Funktionen wie SELECT FOR UPDATE SKIP LOCKED existieren. Heutzutage wachsen viele Engineers in Umgebungen auf, in denen schwergewichtige ORMs die tatsächlichen Fähigkeiten des DBMS verdecken, sodass dieses Wissen ziemlich selten wird.
    • Entwirf und teste für das Zehnfache der aktuellen Größenordnung, aber implementiere nur so viel, wie du jetzt brauchst.
      Systeme müssen Spitzenlast aushalten, und wenn man die Spitze nicht kennt, braucht man entweder Sicherheitsmarge oder einen Weg, Jobs bei Bedarf zu verwerfen oder nach hinten zu schieben.
      Alles ist ein Trade-off; optimiert werden sollte nur, was Optimierung braucht, und zu erkennen, was das ist, zeichnet gute Engineers aus.
    • Der Satz „Transaktionssemantik auf einem eventual-consistency Store nachzubauen ist ein Engineering-Albtraum“ ist wirklich wichtig.
      Falls das neu für dich ist, hast du gerade den Gegenwert einer Woche HN-Lesen während der Arbeitszeit bekommen; es lohnt sich, ihn auf Papier zu schreiben und auf deine Geldkarte zu kleben.
    • In PostgreSQL kann man auch Queues auf Basis von Advisory Locks bauen; das ist deutlich langsamer, hat aber ebenfalls Vorteile.
  • In mehreren Projekten habe ich einen einfacheren Ansatz gewählt: normale Tabellen und SELECT FOR UPDATE SKIP LOCKED, was direkt mit den ORMs/Query-DSL-Frameworks aller Sprachen funktioniert.
    https://www.pgcasts.com/episodes/the-skip-locked-feature-in-...
    Es ist kein „Web Scale“, aber meiner Erfahrung nach skaliert es problemlos bis zu Tausenden von Hintergrundjobs.

    • Ich habe es mangels Transaktionslogik auch schon ohne Locks noch einfacher gemacht: Eine Zeile auswählen, dann versuchen, ein Feld zu aktualisieren, das sagt „übernommen“. Wenn 1 Zeile betroffen ist, ist es mein Job; wenn 0 Zeilen betroffen sind, hat ihn jemand vorher genommen, also wählt man eine neue Zeile.
      Ich habe diesen Ansatz auch für Jobs in großen Organisationen ohne Probleme genutzt. Man braucht kein besonderes Deployment und keine neue Infrastruktur, sondern startet einfach ein paar Worker-Threads in der App. Man kann auch einen Thread haben, der aufgegebene Jobs zurücksetzt.
      In drei Jahren ist diese Situation allerdings nie tatsächlich eingetreten; alles lag in try/catch, bei einem Fehler wurde es wieder in die Queue gelegt, und auch die Java-App war sehr stabil.
    • Kürzlich wurde für PostgreSQL genau zu diesem Ansatz ein Manifest und ein Code-Schnipsel veröffentlicht.
      delete from task
      where task_id in
      ( select task_id
      from task
      order by random() -- use tablesample for better performance
      for update
      skip locked
      limit 1
      )
      returning task_id, task_type, params::jsonb as params
      [1] https://taylor.town/pg-task
    • Dass ein Queue-System nicht richtig skaliert, gehört zu den Problemen, die man auf keinen Fall erst ganz zum Schluss entdecken sollte. Denn wenn man erkannt hat, dass das Queue-System strukturell nicht skalierbar ist, lässt es sich ohne Datenverlust nur schwer reparieren.
      Es hieß „Tausende von Hintergrundjobs“, aber Queues sollte man in der Regel anhand von Littles Gesetz betrachten und Größen wie die durchschnittliche Job-Zuflussrate pro Sekunde und die durchschnittliche Job-Dauer angeben. Eine bloße Rohanzahl sagt nicht viel aus.
      Am Anfang reicht auch ein naives UPDATE ... SET, aber das hält zu viele Locks. Man kann mit einer SELECT-Subquery im UPDATE arbeiten oder mit SELECT FOR UPDATE SKIP LOCKED das Sperren beim Herausnehmen effizienter machen, doch am Ende können die Pull-Queries sich gegenseitig durch Locks blockieren und die Queue zum Stillstand bringen.
      Dann kann man zwar den Zufluss abschalten, um der DB Luft zu verschaffen, aber wegen des verlorenen Zuflusses entsteht Datenverlust, und meist ist das Kernproblem, dass sich die Pulls gegenseitig blockieren.
      Man kann die Job-Tabelle hastig sharden, um Locks zu vermeiden, aber das ist bei der Verteilung über mehrere Worker fragil und kann zu Datenverlust führen. Man kann auch einen Teil zufälliger Jobs verwerfen, aber das ist natürlich ebenfalls Datenverlust.
      Solche Optionen sind im laufenden Betrieb extrem stressig, und ohne eine Neuentwicklung von Grund auf ist eine Wiederherstellung sehr schwierig. Wenn man nur ein paar Kunden hat und sicher ist, für immer bei ein paar Dutzend Jobs pro Sekunde zu bleiben, mag das anders sein; seit ich diese Situation selbst erlebt habe, wähle ich nach Möglichkeit echte Queue-Technologie statt einer Datenbank.
    • Graphiles https://github.com/graphile/worker ist ungefähr auf diese Weise implementiert.
  • Kritik nach dem Muster „Ingenieure kopieren FAANG-Infrastruktur, um cool auszusehen, obwohl sie völlig andere Anforderungen haben“ sieht man oft, aber ich halte das zu einem großen Teil für ein Problem von Wissen und Dokumentation.
    Wenn man FAANG oder anderen Startups nacheifern und eine unbegrenzt skalierbare, Queue-basierte Architektur bauen will, gibt es Dutzende hochwertiger Guides, Tutorials und Whitepaper, mit denen man skalierbare Lösungen wie Redis oder SQS in wenigen Stunden einrichten kann.
    Die Wartungskosten sind höher, aber man kann mit Befehlen, Code und Konfigurationen aus angesehenen Quellen per Copy-and-paste loslegen.
    Wenn man dagegen PostgreSQLs NOTIFY verwenden möchte und nach „SQLALchemy notify listen postgres“ sucht, findet man nur ein paar unbeantwortete Stack-Overflow-Fragen und GitHub-Gists ohne Kontext.
    Ich würde diesen Ansatz in einem Nebenprojekt wirklich gern ausprobieren, habe aber nicht die Zeit, allein 2–3 Tage herauszufinden, wie es geht.
    Oberflächlich sieht es aus wie die Wahl zwischen „einfach, aber nicht skalierend, also nur PostgreSQL“ und „komplex, aber skalierend, also Redis/SQS usw.“; dann wirkt es so, als würde ich mich von cooler Technik und FAANG blenden lassen und unnötige Skalierbarkeit wählen.
    Berücksichtigt man aber auch Guides und Materialien, liegen die tatsächlichen Optionen eher bei „komplex und Skalierbarkeit unvorhersehbar, weil ich die Implementierung und Fallstricke nicht kenne“ und „einfach und skalierbar, so wie es alle in der Praxis machen“. Dann erscheint die Entscheidung eines Ingenieurs, FAANG zu folgen, deutlich rationaler.

    • Der Vorteil „langweiliger“ Technologien wie PostgreSQL ist die hervorragende Dokumentation. Wenn man NOTIFY verwenden will, liest man einfach https://www.postgresql.org/docs/current/sql-notify.html; besondere Suchfähigkeiten braucht man nicht.
    • Skalierbarkeit hat ihren Preis. Wenn man sie einbaut, obwohl man sie nicht braucht, sinkt die Flexibilität – und für ein Startup ist genau das unerwünscht.
      Nutzt man zum Beispiel PostgreSQL bei geringer Last, werden Schema-Migrationen, neue Constraints, Analysen usw. fast trivial.
      Mit SQS, Cassandra usw. gewinnt man Skalierbarkeit/Verfügbarkeit, aber wenn man feststellt, dass das ursprüngliche Design nicht passt, dauert eine Änderung deutlich länger. Etwa wenn das Business verlangt, einen Constraint wie „Nutzer vom Typ foo dürfen nicht gleichzeitig mit Wert bar kombiniert werden“ hinzuzufügen.
      Das lässt sich auch ohne PostgreSQL umsetzen, ist aber besonders bei Änderungsbedarf weder leicht noch einfach.
      Deshalb halte ich es für besser, PostgreSQL zu verwenden, um flexibel zu bleiben, und erst dann etwas zusätzlich auf PostgreSQL zu setzen, wenn man weiß, dass es sich nicht ändern wird. Natürlich kommen in diesem Fall Infrastruktur- und Wartungs-Overhead hinzu.
      Am Ende ist es immer ein Trade-off, und man muss wissen, was man wann gegen was eintauscht.
    • Dieser Artikel richtet sich eher an Library-Entwickler als an Anwendungsentwickler.
      Ich halte es nicht für ideal, wenn jeder in seiner Anwendung eigene, maßgeschneiderte Background-Job-Worker implementiert, egal ob mit PostgreSQL-Backend oder einer anderen Queue.
      Bei Background-Jobs gibt es viele subtile Punkte und Implementierungsdetails, bei denen man leicht Fehler macht; normalerweise sollte das besser eine umfassendere dedizierte Library oder ein Framework übernehmen.
      Wenn nicht jede Rails-Anwendung Sidekiq/Active Jobs hätte und stattdessen jede ihre eigenen Background-Worker implementieren würde, hätten Rails-Apps in Sachen Zuverlässigkeit vermutlich einen deutlich schlechteren Ruf als heute.
    • Der Punkt „höherer Wartungsaufwand“ bringt kleine Startups um. Natürlich bekommt man auch viel dafür, aber wenn das Unternehmen scheitert, bedeutet das nichts.
      Man sollte die Lösung wählen, die den größten Nutzen bringt, ohne sich zu weit vom Hauptziel zu entfernen.
      Ich habe einmal ein Startup gesehen, in dem Entwickler etwa 80 % ihrer Zeit damit verbrachten, mit Tools und Infrastruktur zu kämpfen. Die Runway betrug drei Monate, und heute ist am Ende nur noch ein großes Loch übrig. Allein der Gedanke daran lässt mich immer noch schaudern.
    • Wenn das Argument lautet, dass ein Design auf FAANG-Niveau Zeit spart, dann ist die zentrale Begründung nur, dass man Online-Guides finden kann.
      Mir scheint, dass es nicht genug Respekt vor Komplexität gibt.
  • Der größte Nachteil, wenn man PostgreSQL mit LISTEN/NOTIFY als Publish/Subscribe-Bus verwendet, ist, dass LISTEN eine Session-Funktion ist und daher nicht zu Connection-Pooling auf Statement-Ebene passt.
    Wenn man diesen Ansatz nutzt, sollte man Advisory Locks verwenden. Andere explizite Locks belasten die Datenbank stärker, aber Advisory Locks sind bewusst sehr leichtgewichtig gestaltet.
    Mein Lieblingsbeispiel für eine Implementierung ist que, das in mehrere Sprachen portiert wurde.

    • Einer der Gründe, warum ich NOTIFY/LISTEN nicht mag, ist, dass es bei Problemen schwer zu diagnostizieren ist.
      Erst kürzlich hatten wir ein Problem, bei dem nach einiger Zeit alle NOTIFY/LISTEN-Vorgänge stoppten und sich das nur durch einen Neustart der Datenbank beheben ließ; deshalb mussten wir die Nutzung einstellen. https://dba.stackexchange.com/questions/325104/error-could-n...
    • Ich frage mich, ob es eine Node-Version von Que gibt.
  • Skype nutzte vor Jahren PostgreSQL als Queue zusammen mit einem kleinen Plugin, um alle CDRs zu verarbeiten. Ich weiß nicht, ob sie es heute noch nutzen, aber gemessen an vor 10 Jahren war das „Web Scale“, und während im Internet darüber gestritten wurde, dass eine Datenbank als Queue ein Anti-Pattern sei, lief es einfach gut
    Transaktionen zu haben ist ziemlich praktisch
    https://wiki.postgresql.org/wiki/SkyTools
    Da ich es beruflich ziemlich viel genutzt habe, habe ich bei Sydpy auch ein paar Vorträge zu dem Thema gehalten. Wenn man PostgreSQL ohnehin schon gut betreibt und unterstützt, ist es nützlich
    Allerdings würde ich heutzutage wohl eine dedizierte Queue verwenden und RabbitMQ auf jeden Fall vermeiden

    • Es wäre gut, wenn du den Teil „RabbitMQ auf jeden Fall vermeiden“ näher erklären könntest. Mich interessieren auch die Erfahrungen oder Gedanken anderer
    • Ich stimme zu, dass ich heute eine dedizierte Queue verwenden würde. Der Hauptgrund ist, dass in einer Cloud-Umgebung die meisten Cloud-Anbieter sehr einfach zu nutzende und günstige gehostete Queue-Technologien anbieten
      Selbst wenn man Vendor Lock-in befürchtet, sind die grundlegenden Operationen einer Queue im Grunde nur push/pop und damit so klein, dass es relativ leicht ist, den Code bei Bedarf portierbar zu schreiben
    • Ich frage mich, ob du schlechte Erfahrungen mit RabbitMQ gemacht hast. Falls ja, würde mich interessieren, ob es an der Technik selbst lag oder an Begleitfaktoren wie Dokumentation oder Community
    • Eine Datenbank als Queue zu verwenden ist großartig, solange es funktioniert, aber die Arten, wie es kaputtgehen kann, führen in Zustände, die sich nur sehr schwer wiederherstellen lassen
      Wenn man die Queue nur deshalb in die Datenbank gelegt hat, weil man die Datenbank ohnehin schon betreibt, ist diese Logik ein zweischneidiges Schwert. Es ist kein Grund zum Feiern, aber das Queue-Chaos kann jetzt auch den primären Datenspeicher mit herunterziehen
  • Einer der größten Vorteile, PostgreSQL als Anwendungs-Queue zu verwenden, ist, dass alle eingeplanten asynchronen Jobs von Transaktionalität profitieren
    Wenn es zum Beispiel nach einer Registrierung eine komplexe Backend-Änderung gibt, die eine E-Mail versendet, und nach dem Einreihen des E-Mail-Jobs ein späterer Teil derselben Transaktion fehlschlägt und zurückgerollt wird, landet dieser E-Mail-Job überhaupt nicht erst in der Queue

    • Man sollte klarstellen, dass man bei der Anbindung an andere nicht-idempotente Systeme zwangsläufig zwischen At-least-once- und At-most-once-Semantik wählen muss
      Bei E-Mail muss man, wenn das Warten auf die Versandbestätigung fehlschlägt, entscheiden, ob man die Transaktion scheitern lässt und die Möglichkeit doppelter E-Mails in Kauf nimmt oder ob man fortfährt und die Möglichkeit verlorener E-Mails in Kauf nimmt
      Der große Vorteil entsteht bei Codepfaden, die die DB asynchron ändern. Da Job-Verbrauch und DB-Update in derselben Transaktion liegen, kann Exactly-once-Semantik vollständig transaktional umgesetzt werden
    • Das ist ein wirklich wichtiger Punkt. Ich nutze PostgreSQL und SQS häufig zusammen, weil SQS die automatische Skalierung von Job-Verarbeitungsclustern einfach macht
      In PostgreSQL gibt es eine Transaktionslog-Tabelle, die ausgelöste Events und die pg_current_xact_id() der jeweiligen Transaktion enthält. Man könnte auch das eingebaute xmin der Zeile verwenden, muss dann aber auf Transaction-Wraparound achten. Beim Einfügen in diese Zeile wird ein NOTIFY ausgelöst
      Ein Hintergrundprozess läuft wiederholt und wählt aus der Transaktionstabelle alle Zeilen mit Transaktions-IDs zwischen dem xmin des letzten Laufs und dem aktuellen pg_snapshot_xmin(pg_current_snapshot()) aus. Diese Events werden auf Jobs gemappt und an SQS übermittelt; anschließend wird xmin gespeichert und per LISTEN auf das nächste NOTIFY gewartet
    • Wir mildern dieses Problem auch etwas ab, indem wir das Hinzufügen zur Queue so planen, dass es nach dem Commit ausgeführt wird. Trotzdem bleiben unsichere Stellen, und wenn die RabbitMQ-Verbindung ausfällt, wird es unangenehm
    • Der Datenbank mitzuteilen, dass etwas verarbeitet wurde, und eine Nachricht an etwas wie RabbitMQ zu schicken, kann nicht zu 100 % transaktional sein. Genau das ist der wichtigste Grund für diesen Ansatz
      Bei einer PostgreSQL-Queue stimmt es: Wenn nach dem Einreihen des E-Mail-Versandjobs die Transaktion später fehlschlägt, landet die E-Mail nicht in der Queue
      Datenbankcode, der wirklich getrennt sein muss, sollte in separate Transaktionen aufgeteilt werden. Umgekehrt möchte man vielleicht keine Registrierungs-E-Mail senden, wenn der Benutzer nicht in der DB angelegt wurde; es hängt also von der Situation ab
    • Ein weiterer Vorteil ist, dass garantiert ist, dass die Transaktion abgeschlossen ist, bevor der Job abgeholt wird
      Bei Redis-basierten Queues oder praktisch jeder anderen Queue gerät man schnell in Situationen, in denen ein Job ausgeführt wird, der davon ausgeht, dass ein Datenbankdatensatz existiert, bevor die Transaktion committet wurde. Der Code, der das behebt, wird meist unbeholfen und komplex
  • Was ich an Kafka mag, ist, dass es einfach ein Append-only-Log ist und Clients im Wesentlichen nur Offsets halten
    Konzeptionell ist das sehr einfach zu verstehen, persistent und ziemlich robust gegenüber Ausfällen, weil man zu jedem Offset zurückgehen und erneut lesen kann
    Leider bringt Kafka aufgrund seines verteilten Charakters genug Komplexität mit, dass es sich für die meisten Anwendungsfälle am Ende nicht lohnt
    Persönlich fände ich etwas Ähnliches schön, das einfacher zu betreiben ist. Auf einem einzelnen Node könnte es Hunderte, vielleicht Tausende Events pro Sekunde verarbeiten, und ohne verteilte Komplexität wäre das ziemlich gut
    Theoretisch kann man das auch mit PostgreSQL machen, man darf nur niemals Zeilen löschen. Vielleicht ist das ja die Antwort

    • PostgreSQL hat mit Auto-Increment-IDs ein natives Gegenstück zu „Offsets“ und unterstützt auch Partitionierung nach Datum; ich halte es daher für einen sehr guten Kandidaten als einfache Kafka-Alternative
      Wenn man nicht den vollständigen Funktionsumfang wie Consumer Groups und Partitionen braucht, dürfte auch die Consumer-Konfiguration deutlich einfacher werden
    • Ich hatte gehofft, dass FasterLog irgendwann reif genug werden würde
  • Bei Windmill verwenden wir genau diesen Ansatz. Windmill ist eine Open-Source-Alternative zu Retool und eher ein modernes Airflow-ähnliches Tool, und wir lassen täglich Benchmarks laufen
    Wenn man auf einer ordentlichen GitHub-CI-Instanz einen Windmill-Worker und PostgreSQL in Containern ausführt, verarbeitet der Benchmark 1.200 Jobs pro Sekunde. Mit zusätzlichen Workern skaliert es stabil bis auf 5.000 Jobs pro Sekunde
    Wir prüfen den Einsatz von Citus, um auf einer Multi-Tenant-Instanz die Marke von 5.000 Jobs pro Sekunde zu überschreiten
    https://github.com/windmill-labs/windmill/tree/benchmarks

  • Zu der Zeit, als es um etwa 10 Nachrichten pro Sekunde ging, haben wir für einige Queues PostgreSQL verwendet, und es ließ sich durchaus skalieren. Aber ehrlich gesagt ist es in AWS, GCP und Azure extrem einfach, SQS oder einen anderen Queue-Stack einzurichten; diese Dienste sind genau für diesen Zweck gebaut und bringen Dinge wie DLQs bereits mit.
    Ich sehe keinen Grund, diesen Weg zu wählen und sich Sorgen zu machen, dass dieses System zusammenbricht und dadurch auch der Zustand der restlichen DB beeinträchtigt wird.
    Ich mag das Prinzip „verwende das einfachste Werkzeug“, aber manchmal gehen Engineers dabei zu weit und lassen am Ende nur noch das dümmste Werkzeug mit fragwürdigen Einschränkungen übrig, obwohl die etablierten Alternativen vergleichsweise günstig und einfach sind.

    • Der Ansatz, bei dem ich gelandet bin: Den Zustand der meisten Jobs in der DB speichern und die Job-Queue nur verwenden, um Worker aufzuwecken, damit sie diese Jobs verarbeiten.
      Wenn der Job-Zustand in der DB liegt, kann man ihn gut abfragen. Auch wenn das nicht unbedingt den exakten tatsächlichen Zustand zeigt, hilft es bei der Analyse von Betriebsstörungen. Vor allem löschen die meisten Job-Queues verarbeitete Records einfach.
      Und wenn man alle Background-Jobs idempotent macht, ist ein Ansatz wie „eine Nachricht an die Job-Queue senden, damit dieser Job verarbeitet wird“ fast immer sicher.
      Wenn man sich nur auf eine Message Queue verlässt, ist es bei Performance-Problemen oft schwer zu erkennen, was passiert. RabbitMQ kann zum Beispiel zwar die Queue-Größe melden, bietet aber kaum oder nur sehr eingeschränkte Möglichkeiten, die internen Daten zu inspizieren.
    • Es geht um Transaktionen und Datenkonsistenz. Das bekommt man mit SQS nicht.
    • Weil jede Software andere Anforderungen hat. Bei On-Premises-/B2B-Appliances kann es selbst ein Feature sein, keine externen Services außer PostgreSQL zu benötigen.
      Manche Software wird voraussichtlich nie über die Fähigkeiten von PostgreSQL hinauswachsen; und selbst wenn doch, kann man sie so bauen, dass ein Wechsel zu einem anderen Service einfach bleibt.
      Außerdem kann man ein transaktionales Job-System schlicht innerhalb von PostgreSQL aufbauen wollen.
  • Wir verwenden Oban für Elixir und verarbeiten damit innerhalb von PostgreSQL täglich Hunderttausende bis Millionen von Jobs.
    Transaktionssemantik rund um Background-Jobs zu haben – etwa E-Mails nur dann einzuplanen, wenn eine Transaktion erfolgreich war – ist enorm praktisch.
    Man muss autovacuum ein wenig tunen, aber nachdem wir das bereinigt hatten, hat es für uns sehr gut gepasst.