Die Neuronen Schmiede setzt sich in erster Linie mit Themen im Bereich Software Entwicklung auseinander. Ein Fokus dabei sind robuste Web-Anwendungen.

Widerstandsfähige Background Jobs in Webanwendungen mit PostgreSQL

Permalink

Sidekiq, eine populäre Ruby Lösung für Hintergrundverarbeitung, ist anfällig auf Datenverlust wenn sie laut Handbuch integriert wird. Sidekiq wird von einem Großteil von Ruby Webanwendungen für die Hintergrundverarbeitung verwendet. So auch Applikationen, die meine Firma edgy circle aus Salzburg entwickelt. Bis vor Kurzem habe ich das nicht weiter hinterfragt und Sidekiq wie empfohlen integriert.

Durch Brandur Leach bin ich darauf aufmerksam geworden, dass meine Verwendung von Sidekiq alles andere als robust ist. Die Auslagerung von Aufgaben in den Hintergrund ist ein wichtiger Bestandteil von Webanwendungen. Sidekiq ist also ein fundamentaler Baustein meiner Arbeit.

Für mich war jedenfalls schnell klar, dass sich etwas ändern muss. Dieser wackelige Baustein ist mit meinem Anspruch an Robustheit und Stabilität nicht vereinbar. Wie sieht eine bessere Lösung aus?

Kontext und Anforderungen an Hintergrundverarbeitung

Ohne Kontext haben die folgenden Anforderungen keine Aussagekraft und bieten zu viel Raum für Interpretation. Deshalb ein kurzer Blick auf meinen Kontext. Wie schon gesagt entwickeln wir Webanwendungen und Digitale Services. Unsere Auftraggeber sind dabei Unternehmen aus dem DACH Raum. Keine Silicon Valley Skalierung ohne Rücksicht auf Verluste. In unserem Umfeld geht es darum Geschäftsprozesse und Online-Dienste abzubilden. Dabei steht Korrektheit und Stabilität im Vordergrund.

In einem Softwaresystem gibt es Aufgaben die ohne direkte Aktion einer Benutzerin erledigt gehören. Der tägliche automatische Versand von Zahlungserinnerungen zum Beispiel.

Zusätzlich gibt es Situationen in denen bei einer Benutzer-Interaktion rechenintensive Anweisungen in den Hintergrund verlagert werden. Dieses Vorgehen ermöglicht es dem Benutzer schnell eine Antwort zu präsentieren, ohne dabei von der rechenintensiven Aufgabe ausgebremst zu werden. Ein klassisches Beispiel dafür ist der notorisch langsame und unzuverlässige Versand von E-Mails. Was sind die Anforderungen an solche asynchronen Anweisungen in einem System?

  • Wie der Name schon sagt, muss die Verarbeitung der asynchronen Anweisungen im Hintergrund passieren.
  • Bei Fehlern während der Verarbeitung darf die Anweisung nicht verloren gehen. Stattdessen muss die Verarbeitung kontrolliert wiederholt werden, bis sie gelingt oder ein Operator informiert werden muss.
  • Für Aufgaben die in der Zukunft liegen muss es bereits in der Gegenwart möglich sein die Verarbeitung für später anzusetzen.
  • Es muss garantiert sein, dass eine Anweisung mindestens einmal ausgeführt wird.
  • Anweisungen müssen innerhalb einer maximalen Zeit verarbeitet werden. Ob die Latenz zufriedenstellend ist, ist natürlich von der Gesamtmenge der ausstehenden Anweisungen sowie der zur Verfügung stehenden Ressourcen abhängig. Unter für uns typischen Rahmenbedingungen sollte die Lösung 170 Anweisungen pro Sekunde verarbeiten können. Ein Newsletter mit 100.000 Empfängern kann so innerhalb von 10 Minuten versandt werden.

Probleme von Sidekiq

Sidekiq funktioniert gut und erfüllt fast alle Anforderungen. Es fehlt jedoch eine essenzielle Eigenschaft: kein Datenverlust.

Sidekiq speichert die asynchronen Anweisungen in Redis. Dadurch kann es bei der empfohlenen Verwendung zu Datenverlust kommen. Die Ursache liegt darin, dass sich eine PostgreSQL Transaktion nicht auf den Schreibvorgang in Redis auswirkt. Es kann deshalb zu folgenden Situationen kommen.

with_postgresql_transaction {
  postgresql_write_1()
  sidekiq_redis_write()
  postgresql_write_2()
}
with_postgresql_transaction {
  postgresql_write_1()
  postgresql_write_2()
}
sidekiq_redis_write()

Im ersten Pseudocode Beispiel befindet sich der Sidekiq Aufruf innerhalb einer Transaktion. Hier kann es passieren, dass durch postgresql_write_2() ein Abbruch der Transaktion ausgelöst wird. In diesem Fall würden die Daten fälschlicherweise weiterhin in Redis gespeichert sein. sidekiq_redis_write() ist nämlich unabhängig von der PostgreSQL Transaktion und wird nicht mit zurückgerollt.

Das zweite Pseudocode Beispiel zeigt ein ähnliches Problem. Hier befindet sich der sidekiq_redis_write() Aufruf außerhalb der Transaktion. Jetzt kann das Gegenteil zum vorherigen Szenario passieren. Nach einer erfolgreichen PostgreSQL Transaktion kann der sidekiq_redis_write() Aufruf einen Fehler werfen oder der gesamte Server Prozess abstürzen. In diesem Fall wären die Daten in der PostgreSQL Datenbank gespeichert, aber die asynchrone Anweisung in Redis wäre verloren gegangen.

Diese Eigenschaft von Sidekiq macht es uns unmöglich robuste Webanwendungen zu bauen.

Implementierung ohne Datenverlust

Um die beschriebenen Probleme zu umgehen, dürfen die asynchronen Anweisungen nicht in Redis gespeichert werden. Stattdessen müssen sie ebenfalls in PostgreSQL abgelegt werden. Auf diese Weise gelten die transaktionellen Garantien auch für unsere asynchronen Anweisungen. In Pseudocode sieht das wie folgt aus.

with_postgresql_transaction {
  postgresql_write_1()
  schedule_asynchronous_task_in_postgresql()
  postgresql_write_2()
}

Die Transaktion garantiert, dass Anwendungsdaten und asynchrone Anweisungen gemeinsam gespeichert oder zurückgerollt werden. Es kommt zu keinem Datenverlust unter widrigen Bedingungen.

Für eine Implementierung ohne Datenverlust kann auf eine Sidekiq Alternative (Delayed::Job, Que und queue_classic) zurückgegriffen werden die PostgreSQL statt Redis als Datenspeicher verwenden.

Mit einem zusätzlichen Prozess kann Sidekiq weiterhin verwendet werden. Die asynchronen Anweisungen werden zuerst in PostgreSQL gespeichert bevor sie durch eine weitere Hintergrundverarbeitung in Redis gespeichert werden.

Keiner dieser Wege überzeugt mich. Die Alternativen verwenden keine für PostgreSQL idealen Techniken und die Sidekiq Lösung holt eine weitere Abhängigkeit ins Boot.

Deshalb haben wir uns dazu entschieden eine eigene Lösung zu implementieren. Das legt die Verantwortung unmissverständlich in unsere Hände und hat folgende Vorteile.

  • Unsere Infrastruktur kann auf Redis verzichten. Neben der offensichtlichen Vereinfachung müssen wir nicht mehr lernen, wie Redis korrekt unter Last im Echtbetrieb verwendet wird.

  • Die PostgreSQL basierte Lösung garantiert, dass keine asynchronen Anweisungen mehr verloren gehen.

  • Ohne Sidekiq haben unsere Anwendungen eine große Abhängigkeit weniger. Das bedeutet schnellere Startzeiten und weniger Quellcode da unsere eigene Lösung weniger Features benötigt.

  • Für Backups ist es ausreichend sich mit der Datenbank zu beschäftigen. Ein PostgreSQL Dump inkludiert automatisch alle ausstehenden Anweisungen.

  • Wir können geeignete PostgreSQL Techniken verwenden, die uns die beste Performance liefern.

Eine selbst entwickelte Lösung hat natürlich nicht nur Vorteile. Die beiden größten Nachteile im Vergleich zu Sidekiq sind die geringeren Leistungswerte sowie fehlende Praxiserfahrung aus dem Echtbetrieb. Die geringere Leistung ist in unseren Webanwendungen wie Eingangs beschrieben kein Problem. Und die fehlende Erfahrung aus dem Betrieb können wir nur wettmachen, in dem wir es verwenden und lernen.

Die Implementierung besteht aus 7 Komponenten die einzeln in Isolation betrachtet werden können.

Datenbanktabelle

Alle asynchronen Anweisungen, die noch abzuarbeiten sind, werden in einer Datenbanktabelle gespeichert. Diese Tabelle muss sich in derselben Datenbank befinden wie die restlichen Anwendungsdaten. Ansonsten kann die Datenbank keine Garantien mit Transaktionen geben.

Anweisung

Damit in der Zukunft klar ist was zu tun ist, muss die Intention, also die Anweisung selbst alle nötigen Informationen haben. In der Regel ist das ein eindeutiger Name, mit dem die Anweisung identifiziert wird, sowie alle weiteren benötigten Daten.

Geschäftslogik

Von außen betrachtet gibt es eine einzelne Funktion, die die Geschäftslogik repräsentiert. Als Argument wird die Anweisung übergeben, um der Geschäftslogik die benötigten Daten zur Verfügung zu stellen.

Ausführbares Programm

Eine leichte Hülle um den Prozess der die Anweisungen ausführt. Wird während der Entwicklung lokal bzw. in Produktion auf dem Server gestartet. Reagiert auf Steuersignale des Betriebssystems und koordiniert die Prozesskomponenten.

Prozess um kontinuierlich Anweisungen abzuarbeiten

Ruft in einer Endlosschleife Anweisungen aus der Datenbank ab und übergibt diese an die Geschäftslogik. Stoppt auf Kommando und stellt sicher, dass die Datenbank nicht von Abrufen überlastet wird.

Transaktionaler Abruf einer Anweisung

Lädt mit einem sicheren Mechanismus die nächste Anweisung aus der Datenbank und gibt sie zurück. Bei einem erfolgreichen Abarbeiten wird die Anweisung abschließend aus der Datenbank gelöscht. Im Fehlerfall wird die Anweisung für eine erneute Abarbeitung in der Zukunft terminiert.

Zentrale Zuordnung von Anweisungen zu Geschäftslogik

Ein Dienst der weiß, welche Geschäftslogik Funktionen mit welchen Anweisungen aufgerufen werden. Als zentraler Punkt ebenfalls geeignet für übergreifende Funktionalität.

Abschließende Gedanken

Dieser Text ist nicht als Kritik an Sidekiq zu verstehen. Ich bin derjenige der seine Hausaufgaben nicht gemacht hat und eine Technologie eingesetzt hat, ohne die Konsequenzen vollständig zu verstehen. Da ich vermutlich nicht der einzige Unwissende bin, habe ich versucht meinen Wissensstand hier zusammenzufassen.

Die konkrete Ruby Implementierung zeige ich bewusst nicht. Im Moment ist der Quellcode noch direkt in die Anwendung eingebettet und enthält entsprechend viele Domänen spezifische Konzepte. Sobald das in eine eigene Bibliothek extrahiert wurde, gibt es hier ein Update.

Danke an Hannah Langhagel und Christoph Edthofer für Feedback und Korrekturen.