2 Punkte von GN⁺ 2024-11-04 | 1 Kommentare | Auf WhatsApp teilen
  • Die in Web-Services übliche Kombination aus PostgreSQL+Redis ist praktisch, aber einige Aufgaben wie Background-Job-Queues, verteilte Locks und Pub/Sub lassen sich auch allein mit PostgreSQL umsetzen
  • FOR UPDATE SKIP LOCKED aus PostgreSQL 9.5 überspringt gesperrte Zeilen, statt auf sie zu warten, und eignet sich so für die Implementierung von Queues, damit nicht mehrere Worker denselben Job aufnehmen
  • Verteilte Locks auf Anwendungsebene lassen sich mit den Advisory Locks von PostgreSQL umsetzen, indem die interne Lock-Engine für anwendungsspezifische Zwecke wiederverwendet wird
  • LISTEN/NOTIFY aus PostgreSQL 9 unterstützt das Abonnieren beliebiger String-Kanäle und das Senden von Benachrichtigungen, sodass es als Pub/Sub-Schicht genutzt werden kann; auch Rails ActionCable unterstützt PostgreSQL standardmäßig
  • Redis bleibt weiterhin stark bei TTL-Caching und der Verarbeitung temporärer Daten, aber manche Systeme können ihre Redis-Abhängigkeit verringern und so Betriebskosten und Entwicklungsaufwand senken

Redis-Rollen, die PostgreSQL übernehmen kann

  • Typische Web-Services verwenden PostgreSQL als Datenspeicher und Redis für die Koordination von Background-Job-Queues oder begrenzte atomare Operationen
  • Redis ist für sich genommen nützlich, aber einige seiner Aufgaben in dieser Kombination lassen sich auch nur mit PostgreSQL-Funktionen ersetzen

Background-Job-Queue

  • Redis wird in Web-Services häufig für die Koordination von Job-Queues verwendet, um Aufgaben an einen Pool von Background-Workern zu übergeben
    • Es hält fest, welche Background-Jobs auszuführen sind und welche Eingabedaten sie benötigen
    • Es muss sichergestellt werden, dass von mehreren Workern nur einer den jeweiligen Job übernimmt
    • Redis eignet sich dafür gut, weil es viele atomare Operationen auf Datenstrukturen bietet
  • Seit PostgreSQL 9.5 kann in SELECT ... FOR ...-Anweisungen die Option SKIP LOCKED verwendet werden
    • Mit dieser Option ignoriert PostgreSQL Zeilen, bei denen es auf das Freigeben eines Locks warten müsste
    • Mit FOR UPDATE SKIP LOCKED wird implizit ein Lock auf Zeilenebene für die zurückgegebenen Zeilen erworben
    • Dank SKIP LOCKED besteht keine Gefahr, durch Locks anderer Transaktionen blockiert zu werden
    • Falls andere Jobs zur Verarbeitung vorhanden sind, wird stattdessen einer davon zurückgegeben
    • Selbst wenn mehrere Worker denselben Befehl ausführen, erhalten sie wegen der Zeilen-Locks nicht dieselbe Zeile
  • Der grundlegende Ablauf besteht darin, innerhalb einer Transaktion einen Job im Status pending auszuwählen, ihn auf running zu setzen und dann zurückzugeben
    • BEGIN
    • SELECT ... FOR UPDATE SKIP LOCKED
    • UPDATE jobs SET status = 'running' ... RETURNING jobs.*
    • COMMIT
  • Zu beachten ist, dass die Kosten für das Durchsuchen der Queue und den Versuch, Locks zu erwerben, steigen können, wenn es sowohl viele Worker als auch viele Jobs gibt
    • Bei den meisten Apps, mit denen tatsächlich gearbeitet wurde, gab es weniger als 12 Background-Worker, daher war diese Belastung wahrscheinlich nicht groß

Anwendungs-Locks

  • In Routinen wie der Synchronisierung mit Drittanbieter-Diensten kann es nötig sein, sicherzustellen, dass für einen bestimmten Nutzer über alle Serverprozesse hinweg nur eine Instanz läuft
  • Solche verteilten Locks sind ein weiterer häufiger Anwendungsfall für Redis
  • PostgreSQL kann denselben Zweck mit Advisory Locks erfüllen
    • Advisory Locks erlauben es, die von PostgreSQL intern verwendete Lock-Engine für anwendungsdefinierte Zwecke zu nutzen

Pub/Sub

  • Auch zum Pushen von Events an aktive Clients wird Redis oft verwendet
    • Um Nutzer darüber zu informieren, dass neue Nachrichten gelesen werden können
    • Um Daten an Clients zu streamen, sobald sie bereitstehen
    • Typischerweise bilden WebSockets die Schicht für die Ereignisübertragung und Redis fungiert als Pub/Sub-Engine
  • Seit PostgreSQL 9 bietet PostgreSQL mit den Anweisungen LISTEN und NOTIFY Pub/Sub-Funktionalität
    • PostgreSQL-Clients können mit LISTEN bestimmte Nachrichtenkanäle abonnieren, die aus beliebigen Strings bestehen
    • Wenn ein anderer Client ein NOTIFY an diesen Kanal sendet, erhalten alle abonnierenden Clients die Benachrichtigung
    • Optional kann eine kleine Nachricht mitgesendet werden
  • Wer Rails mit ActionCable verwendet, hat PostgreSQL-Unterstützung standardmäßig dabei

Bereiche, in denen Redis weiterhin sinnvoll ist

  • Redis deckt andere Bereiche ab als PostgreSQL und ist stark bei Aufgaben, auf die PostgreSQL nicht abzielt
    • Daten-Caching mit TTL
    • Speicherung und Verarbeitung temporärer Daten
  • PostgreSQL hat einen größeren Funktionsumfang, als es ein einfaches SQL-Datenbanksystem oder eine undurchsichtige Komponente hinter einem ORM vermuten lässt
  • Einige Aufgaben, die Redis übernimmt, könnten auch gut zu PostgreSQL passen
  • Der Verzicht auf Redis kann eine Möglichkeit sein, Betriebskosten und Entwicklungsaufwand zu senken, die durch die Abhängigkeit von mehreren Datendiensten entstehen

1 Kommentare

 
GN⁺ 2024-11-04
Meinungen auf Hacker News
  • Weil alle auf übermäßig verteilten Architekturen beharren, übersehen viele die echten Stärken von Redis. Wenn es auf derselben Maschine wie die Anwendung läuft, kann es deutlich schneller als in 1 Millisekunde antworten, und dadurch lassen sich in der Anwendung Dinge tun, die mit Postgres schwierig wären.
    Postgres ist zweifellos großartig, läuft aber nicht im Arbeitsspeicher derselben Maschine wie die Anwendung. Wenn man nur so etwas wie eine Queue braucht, ist ein In-Memory-Key-Value-Store vielleicht nicht nötig. Der Kern eines In-Memory-Key-Value-Stores ist, Dinge zu tun, die die Leistungsmerkmale von RAM benötigen, und über eine Netzwerkverbindung hinweg bekommt man diese Eigenschaften nicht.

    • Wenn nur ein lokaler Prozess Redis auf der Maschine als In-Memory-Cache verwenden soll, ist es besser, einfach die Datenstrukturen der verwendeten Programmiersprache zu nutzen.
    • Mich würde interessieren, wie stark sich der Overhead von Postgres und Redis unterscheidet, wenn beide lokal laufen. Ich verstehe auch nicht, warum man annimmt, Postgres laufe nicht lokal.
      Postgres hat keine besondere Magie; es ist wie Redis einfach ein Programm, das in einem anderen Prozess läuft. Bei lokalen Verbindungen nutzt es schnelle Pipes, um Latenz zu reduzieren, und es kann auch schnellere Verfahren für Massendatentransfers verwenden. Ich habe das mehrfach so eingesetzt.
    • Django hat einen eingebauten Cache mit Redis-Unterstützung, und es gibt auch eine Option für einen In-Memory-Cache, die aber als „nicht für Produktion“ gekennzeichnet ist. Der Grund ist, dass bei mehreren Django-Instanzen jeder In-Memory-Cache voneinander abweicht.
      Bei internen Business-Tools kann man jedoch eine einzelne Instanz lange hochskalieren, und dieser In-Memory-Cache macht sie sehr schnell. django-cachalot ist eine Bibliothek, die Cache-Invalidierung automatisch behandelt, sobald in Tabellen geschrieben wird. Das ist etwas grob, bringt aber fast ohne Aufwand Leistungsgewinne; interne Business-Apps mit seltenen Updates laufen praktisch aus dem RAM und fallen nur bei Cache-Misses auf normale Datenbankabfragen zurück.
      https://github.com/noripyt/django-cachalot
    • Schneller als Redis ist in dieser Konfiguration eine Hashmap. Es gibt auch keinen Grund, warum Postgres nicht auf demselben Server wie die App laufen könnte; tatsächlich ist das eine recht verbreitete Konfiguration.
    • Overengineering und vorschnelle Verteilung sind reale Probleme, aber Redis steht für „Remote Dictionary Server“. Der ursprüngliche Zweck ist keineswegs, es lokal laufen zu lassen. Natürlich kann lokales Ausführen trotzdem eine legitime Designentscheidung sein, etwa wenn das Standard-Dictionary der Sprache keine Range Queries unterstützt.
  • In dieser Diskussion gibt es viele defensive Argumente aus Redis-Perspektive, aber natürlich gibt es bestimmte Bereiche, in denen Redis besser ist.
    Ich denke aber nicht, dass das der Kern des Artikels ist. Der Kernsatz lässt sich so zusammenfassen: „PostgreSQL hat weit mehr Funktionen, als man erwarten würde, wenn man es nur als einfache SQL-Datenbank oder als mysteriöse Entität hinter einem ORM betrachtet.“ Wenn man eine Datenbank nur hinter einem ORM verwendet, verpasst man bei jeder Datenbank wahrscheinlich viele Funktionen. Wenn man einen weiteren Dienst wie Redis hinzufügen müsste, kann es besser sein, die bereits konfigurierte Datenbank zu nutzen, statt eine neue Abhängigkeit einzuführen.

  • Zu verstehen, was Postgres alles kann, ist gut. Es ist eine leistungsfähige Datenbank.
    Das Gegenargument ist, dass die Einstiegshürde für Redis sehr niedrig ist und man dafür hohe Performance, breite Library-Unterstützung und Entlastung der primären Datenbank bekommt. Zum Beispiel kann man einen API-Response-Cache mit Postgres bauen. TTL lässt sich umsetzen, indem ein cron-Job alte Cache-Werte aufräumt. Oder man nimmt einfach Redis.
    Advisory Locks sind elegant und nützlich, aber wenn man etwas wie PgBouncer verwendet, können Probleme zwischen Session-Advisory-Locks und Transaction Interleaving entstehen. Ein separates System hat Nachteile wie Netzwerkaufrufe, Verfügbarkeit und nötiges Domänenwissen, aber der Kompromiss bei Redis ist vergleichsweise klein.

    • Ein Vorteil ist, dass man in Produktion eine Sache weniger verwalten muss. Mit PostgreSQL anfangen und erst dann spezialisierte Systeme hinzufügen, wenn Performance, Skalierung oder Kosten es nötig machen.
  • Der Artikel ist schon ziemlich alt, aber inzwischen ist das ein sehr verbreitetes Muster. 90 % der Projekte, die nur eine Job-Queue für E-Mail-Versand oder Berichtserstellung brauchen, verarbeiten keine Millionen Nachrichten pro Sekunde; daher lohnt es sich, eine Vereinfachung des Stacks zu prüfen.
    Ich habe dieses Muster oft genutzt, um Probleme mit Celery zu umgehen, und es schließlich in ein eigenes Framework ausgelagert: https://github.com/TkTech/chancy Feedback willkommen.
    Es gibt viele solcher Tools, und einige davon sind kommerzielle Dienste, was auf eine klare Nachfrage hinweist.
    https://worker.graphile.org/ (Node.js)
    https://riverqueue.com/ (Go)
    https://github.com/acaloiaro/neoq (Go)
    https://github.com/contribsys/faktory (Go)
    https://github.com/sorentwo/oban (Elixir)
    https://github.com/procrastinate-org/procrastinate (Python)

    • Ich war auf der Suche nach einer einfachen Job-Queue. Huey ist auch in Ordnung, aber wir verwenden bereits Postgres, und die Jobs werden etwa einmal pro Stunde in die Queue gestellt, daher fühlte sich eine Redis-basierte Lösung immer überdimensioniert an.
    • Für Node gibt es auch PG-Boss: https://github.com/timgit/pg-boss
  • PGQueuer nutzt FOR UPDATE SKIP LOCKED und LISTEN/NOTIFY von PostgreSQL, um Job-Queues, Locks und Echtzeitbenachrichtigungen bereitzustellen.
    Wenn man bereits PostgreSQL nutzt, ist es eine minimalistische Alternative ohne Redis.
    https://github.com/janbjorge/PGQueuer
    Nur zur Einordnung: Ich habe es gebaut.

  • Ich mag Postgres, aber es gibt einige Einschränkungen.
    Wenn man einen Key-Value-Store braucht, sollte man prüfen, ob man autovacuum versteht, die Grenzen des Connection-Pools kennt und ob einem Durchsatz oder Sicherheit wichtiger ist. Wenn man eine Queue braucht, sollte man schauen, ob es um sequenzielle Verarbeitung geht, ob es Rate-Limits gibt, ob Fan-out nötig ist oder ob eine Trennung nach Themen gewünscht ist. Wenn man Publish/Subscribe braucht, sollte man abwägen, ob doppelte Zustellung relevant ist, ob Nachrichtenverlust relevant ist und ob Replay benötigt wird. Wenn man Locks braucht, sollte man die Grenzen des Connection-Pools und statement_timeout kennen. Die meisten der oben genannten Probleme lassen sich lösen, aber so einfach ist es nicht.

    • Man sollte auch prüfen, ob die Implementierung nicht zusammenbricht, wenn lang laufende Transaktionen das Entfernen von Tupeln durch vacuum verhindern.
  • Ein großes Hindernis bei Publish/Subscribe in Postgres ist die maximale Nachrichtengröße von 8000 Byte.
    Der empfohlene Workaround besteht darin, die Daten in eine Tabelle zu schreiben und nur die ID zu senden. Wenn man diese Daten aber nicht dauerhaft behalten will, muss man dafür Garbage Collection machen, und pro Nachricht kommt zusätzliche Arbeit hinzu. Natürlich gibt es Fälle, in denen das in Ordnung ist, aber in vielen Redis-Anwendungsfällen ist diese Einschränkung ein Grund, warum man es kaum als gleichwertig ansehen kann.

  • Schauen wir, ob pgsql 15.000 Client-Verbindungen bewältigt.

    • Der Unterschied zwischen Postgres und MySQL ist hier wirklich erstaunlich groß.
      Jemand von Planetscale sagte in einem Podcast, dass GitHubs MySQL-Instanzen jeweils über 50.000 Verbindungen verarbeiten. Wenn man dagegen bei Postgres mehr als 100 Verbindungen braucht, ist PgBouncer praktisch schon notwendig.
    • Man kann Connection Pooling verwenden. Mit Diensten wie AWS RDS Proxy geht das mit ein paar Klicks.
  • Queues, Locks und Publish/Subscribe sind möglich. Aber Caching, der wichtigste Einsatzzweck von Redis, fehlt.
    Updates in Postgres sind berüchtigt dafür, teurer zu sein als Inserts; sie erzeugen Müll und benötigen vacuum. Für Caching wird das Schreiben auch wegen Haltbarkeitsgarantien, die dort nicht wichtig sind, deutlich langsamer. Automatisches Ablaufen ist sehr praktisch und reduziert Fehler.

    • Die meisten nicht benötigten Funktionen kann man abschalten. Zum Beispiel kann man eine Tabelle als unlogged anlegen: https://www.postgresql.org/docs/current/sql-createtable.html...
      Auch synchrone Commits, autovacuum usw. kann man deaktivieren. Natürlich wird Redis weiterhin schneller sein, aber der Unterschied ist möglicherweise keiner, um den sich ein durchschnittliches Unternehmen kümmern muss.
  • Klarer gesagt lautet die Kernaussage des Artikels: mit Postgres anfangen und bei Bedarf zu Redis wechseln.
    Es ist gut, die Zahl der beweglichen Teile möglichst gering zu halten.

    • Redis ist nicht besonders schwer bereitzustellen. Statt eine spätere Umstellung zu planen, bei der man nicht weiß, welche unerwarteten Nebenwirkungen auftreten, kann es auch sinnvoll sein, einfach von Anfang an Redis zu verwenden.