So stellen Sie Ihre Rails-Jobs sicher bereit

Illustration von Bailey McGinn

Jeden Tag bei Doctolib wird mindestens eine Version in der Produktion bereitgestellt. Wir möchten, dass jede Bereitstellung für unsere Millionen von Benutzern reibungslos funktioniert und keine Ausfallzeiten entstehen. In zahlreichen Artikeln wird bereits beschrieben, wie Code- oder Datenbankänderungen implementiert werden. Daher konzentrieren wir uns auf eine weniger dokumentierte Seite der Gleichung: das sichere Implementieren von Änderungen in Bezug auf Hintergrundjobs. Wenn wir Änderungen an unseren Jobs bereitstellen, möchten wir, dass alle Jobs vor, während und nach der Bereitstellung fehlerfrei verarbeitet werden.

Bei Doctolib verwenden wir Unicorn als Webserver und Resque als Job Worker.

Drei Schritte im Bereitstellungsprozess, die für eine erfolgreiche Bereitstellung von Jobs ohne Ausfallzeiten entscheidend sind:

  1. Datenbank migrieren (falls erforderlich)
  2. Starten Sie die Web-Worker nacheinander neu
  3. Starten Sie die Arbeiter nacheinander neu

Was könnte schiefgehen?

Angenommen, die N-Codebasis ist die aktuelle Codebasis, die vor der Bereitstellung in der Produktion ausgeführt wird, und die N + 1-Codebasis, die wir bereitstellen.

Fall 1 - Bereitstellung läuft

Das erste Problem, mit dem Sie konfrontiert werden, tritt beim Neustart von Web-Workern und vor dem Neustart von Job-Workern auf. Die (auf der N + 1-Codebasis ausgeführten) Web-Worker geben Jobs mit N + 1-Nutzdaten an Job-Worker weiter, die auf der N-Codebasis ausgeführt werden.

Fall 2 - Bereitstellung fast abgeschlossen

Zweites Problem: Jobs mit N Nutzdaten wurden möglicherweise in die Warteschlange gestellt und werden nach dem Neustart der jetzt auf N + 1-Codebasis ausgeführten Job-Worker noch verarbeitet.

In beiden Situationen können Aufträge fehlschlagen, da die beiden Codebasen nicht kompatibel sind. Dies kann auftreten, wenn sich die Nutzlast der Jobs ändert, beispielsweise wenn:

  • Der N + 1-Codebasis wird ein neuer obligatorischer Parameter hinzugefügt, der eine Ausnahme auslöst, wenn er von der N-Codebasis verarbeitet wird.
  • Ein Job wird gelöscht,
  • Ein Job wird umbenannt usw.

Zusammenfassend kann es sein, dass Sie auf ein Problem stoßen, sobald eines dieser drei Elemente nicht die gleiche Version wie die beiden anderen hat:

  • Code senden von Aufträgen
  • Jobs Nutzlast
  • Code-Verarbeitungsjobs

Vereinfachte Wiedergabe des Problems

Angenommen, dies ist der Code eines Jobs, der in der Produktion ausgeführt wird:

# N Code Base
# Payload-Beispiel: {id: 42, Meldung: "Hallo!"}
Klasse UserConfirmationJob 

Und wir wollen einen neuen Parameter hinzufügen:

# N + 1 Codebasis
Beispiel für Nutzlast: {ID: 42, Nachricht: "Hallo!", Datum: "2017-12-24"}
Klasse UserConfirmationJob 

Dies geschieht während der Bereitstellung, wenn die Codebasis des neuen Jobs wie oben beschrieben bereitgestellt wird:

  • Zu Beginn werden Jobs mit N Nutzdaten in die Warteschlange gestellt und von Jobmitarbeitern verarbeitet, die auf Basis von N Codes ausgeführt werden.
  • Während und nach dem Neustart der Web-Worker werden Jobs mit N + 1-Nutzdaten in die Warteschlange gestellt und von Job-Workern verarbeitet, die auf Basis von N-Code ausgeführt werden (Fall 1).
    Die Jobs schlagen fehl: ArgumentError: Falsche Anzahl von Argumenten (3 angegeben, 2 erwartet).
  • Nach dem Neustart von Job Workern verbleiben möglicherweise einige Jobs in der Warteschlange mit N Nutzdaten und werden von Job Workern verarbeitet, die auf der Basis von N + 1 Code ausgeführt werden (Fall 2).
    Die Jobs schlagen fehl: ArgumentError: Falsche Anzahl von Argumenten (2 angegeben, 3 erwartet).

Ein Platzhalterproblem ist aufgetreten: eine schnelle und schmutzige Lösung

Fall 1 - Bereitstellung läuft

Wenn ein Job mit N + 1 Nutzdaten einen Fehler auslöst, nachdem er von einem Worker verarbeitet wurde, der auf N-Code-Basis ausgeführt wird, besteht die Möglichkeit, dass eine einfache Wiedergabe das Problem behebt. Warten Sie, bis die Bereitstellung abgeschlossen ist, und wiederholen Sie den Auftrag. Da wir Resque verwenden, ist es möglich, die Resque-Weboberfläche zu verwenden, um eine kleine Menge kleiner fehlgeschlagener Jobs zu lösen. Andernfalls können die fehlgeschlagenen Jobs über eine Rails-Konsole wiedergegeben werden.

Fall 2 - Bereitstellung fast abgeschlossen

Wenn das Gegenteil der Fall ist und ein Job mit N Nutzdaten einen Fehler auslöst, nachdem er von einem Worker verarbeitet wurde, der auf N + 1-Codebasis ausgeführt wird, kann er nicht wiederholt werden. Die beste Lösung besteht darin, die Bereitstellung zurückzusetzen, die fehlgeschlagenen Jobs erneut abzuspielen und den neuen Code erneut bereitzustellen. Achten Sie auf die Version der Worker und der Jobs: Die Worker müssen die Jobs derselben Version verarbeiten.

Aber es gibt einen besseren Weg!

Wie sicher bereitstellen?

Sie müssen sicherstellen, dass N + 1 Payloads mit N Payloads kompatibel sind.

Das beste Szenario wäre, nicht unterbrechende Änderungen vorzunehmen. In Fällen, in denen Sie jedoch eine unterbrechende Änderung implementieren müssen, sollten Sie diese in mehrere nicht unterbrechende Änderungen aufteilen und nacheinander implementieren.

Wenden wir dies auf unser vorheriges Beispiel an. Anstatt die gesamte Änderung bereitzustellen, werden wir diese Übergangsversion bereitstellen:

Klasse UserConfirmationJob 

Mit dieser temporären Version behandeln wir die beiden problematischen Fälle:

  • Wenn ein Job, der mit einer N + 1-Nutzlast in die Warteschlange gestellt wurde, von einem Job-Worker verarbeitet wird, der auf der Basis von N-Code ausgeführt wird (if-Klausel),
  • Wenn ein Job in einer Warteschlange mit N Nutzdaten von einem Job-Worker verarbeitet wird, der auf der Basis von N + 1-Code ausgeführt wird (else-Klausel).

Kurz nach der Bereitstellung befinden sich nur Jobs in der Warteschlange, deren N + 1-Nutzdaten von Jobmitarbeitern verarbeitet werden, die auf N + 1-Codebasis ausgeführt werden. Unsere else-Klausel ist jetzt unbrauchbar: Wir können die endgültige Version unserer Änderung bereitstellen:

Klasse UserConfirmationJob 

Und voilà, die Änderungen wurden während der Bereitstellung fehlerfrei bereitgestellt!

Dieses Beispiel bezieht sich auf das Hinzufügen eines Parameters, aber ein ähnlicher Ansatz kann für andere Änderungen verwendet werden:

Wenn ein Job gelöscht wird:

  • Löschen Sie den Code, der den Job in die Warteschlange stellt (nicht den Job selbst).
  • Bereitstellen
  • Der Job ist nicht mehr in der Warteschlange: Löschen Sie ihn und stellen Sie ihn erneut bereit

Wenn ein Job umbenannt wird (z. B. von OldJob nach NewJob):

  • Erstellen Sie einen neuen Job NewJob, der eine Kopie von (oder eine Referenz auf) OldJob ist.
    Behalten Sie OldJob bei und ändern Sie nicht den Code, mit dem der Job in die Warteschlange gestellt wird.
  • Bereitstellen
  • Ändern Sie den Code, der den Job in die Warteschlange stellt, um NewJob zu verwenden.
    OldJob nicht löschen
  • Bereitstellen
  • OldJob wird nicht mehr verwendet: Löschen Sie es und stellen Sie es erneut bereit

Die Fälle, mit denen Sie konfrontiert werden, sehen möglicherweise anders aus, aber die Lösung ist immer etwas Ähnliches wie oben.

Woher weißt du, dass dein Code Arbeiter kaputt macht?

Jedes Mal, wenn Sie den Code eines Jobs ändern, indem Sie einfach den Unterschied zwischen altem und neuem Code betrachten und wissen, dass dieses Problem auftreten kann, können Sie 80% der potenziellen Probleme erkennen.

Sechs Augen sind besser als zwei: Bei Doctolib führen wir mindestens zwei Codeprüfungen für jede Pull-Anfrage durch.

Die in diesem Artikel beschriebenen Tipps sind nicht auf Jobs beschränkt. Beim Upgrade einer API oder eines Webdienstes tritt möglicherweise das gleiche Problem der Abwärtskompatibilität auf: Es ist möglich, die oben genannten Empfehlungen in andere Anwendungsbereiche zu integrieren.

Bei Doctolib gibt es viele technische Herausforderungen! Wenn Sie an einer Zusammenarbeit mit uns interessiert sind, werfen Sie doch einen Blick auf unsere Karriereseite für Ingenieure :)