Zum Inhalt springen

Ihre Queue-Worker laufen mit dem Code der letzten Woche

Ein Worker ist ein langlebiger PHP-Prozess, der Ihre Anwendung einmal geladen und nie wieder nachgesehen hat. Deployments erreichen ihn nicht - die Fehler sind behoben und trotzdem da.

4 Min. Lesezeit

Eine Entwicklerin behebt einen Fehler in einem Job, lässt ihn prüfen, führt ihn zusammen, sieht das Deployment grün werden – und zehn Minuten später steht derselbe Fehler wieder im Log. Sie deployt erneut. Es passiert wieder. Irgendwo beim dritten Versuch kommt der Verdacht auf, dass die Produktion nicht den Code aus dem Repository ausführt, und der Verdacht stimmt.

Warum ein Deployment einen Worker nicht erreicht

Der Lebenszyklus einer Anfrage in PHP macht das überraschend. Jede HTTP-Anfrage bootet das Framework, erledigt ihre Arbeit und endet – neuer Code wird also per Definition übernommen, weil die nächste Anfrage die neuen Dateien lädt und kein alter Prozess da war, der die alten festhielt.

Ein Queue-Worker ist das Gegenteil. php artisan queue:work bootet das Framework einmal und läuft dann in einer Schleife, holt Jobs von der Queue, solange er lebt. Die beim Boot geladenen Klassen bleiben geladen. Tauschen Sie darunter jede Datei aus, und der laufende Prozess weiß es weder noch kümmert es ihn: Er hält seine eigene kompilierte Kopie Ihrer Anwendung, von dem Moment an, in dem er gestartet ist.

Nach einem Deployment haben Sie also eine Web-Schicht mit dem neuen Release und eine Queue-Schicht mit dem, was aktuell war, als diese Prozesse zuletzt gestartet sind – vielleicht das vorige Release, vielleicht eines von vor drei Wochen.

Wie das aussieht, wenn es schiefgeht

Die Fehlerbilder sind wiedererkennbar, sobald man weiß, wonach man sucht.

Ein behobener Fehler tritt weiter auf, ausschließlich in Hintergrundarbeit. Ein Job ruft eine Methode auf, die es im laufenden Code nicht gibt, oder ruft eine gerade hinzugefügte nicht auf. Eine Migration fügt eine Spalte hinzu, der Controller schreibt fröhlich hinein, und der Job, der dasselbe Modell liest, wirft eine Exception, weil seine Kopie des Schemas älter ist als die Spalte.

Am schlimmsten ist die Versionsspaltung. Starten Sie Worker nach und nach neu – oder lassen Sie sie an ihren eigenen Speichergrenzen durchwechseln – und eine Zeit lang laufen manche Prozesse mit dem neuen und manche mit dem alten Code. Jobs werden willkürlich verteilt. Sie haben jetzt Fehler, die bei etwa der Hälfte der Versuche auftreten und bei keinem reproduzierbar sind, was die teuerste Form ist, die ein Fehler annehmen kann.

Die Behebung ist eine Zeile im Deploy-Skript

php artisan queue:restart

Es tötet nichts. Es schreibt einen Zeitstempel in den Cache, und jeder Worker prüft diesen Zeitstempel zwischen zwei Jobs; ein Worker, der davor gestartet ist, beendet seinen aktuellen Job und geht. Ihre Prozessüberwachung – systemd, Supervisor, was die Plattform bietet – sieht das Ende und startet einen frischen Worker, der den neuen Code bootet.

Zwei Dinge müssen dafür zutreffen, und beide werden oft genug übersehen, dass sie erwähnt gehören.

Der Cache-Speicher muss gemeinsam genutzt werden. Der Zeitstempel landet im Cache. Mit dem Treiber array geht er in den Speicher des Prozesses, der Artisan ausgeführt hat, und das ist nicht der Worker. Mit file und Workern auf einer anderen Maschine passiert dasselbe. Redis, Memcached oder die Datenbank – irgendetwas, das beide Seiten lesen können.

Irgendetwas muss den beendeten Worker neu starten. queue:restart stoppt Worker. Es startet sie nicht. Ohne eine Überwachung dahinter hinterlässt ein Deployment still und leise gar keine Worker, und die Jobs stauen sich, bis jemand die Queue-Tiefe bemerkt.

Unter Horizon lautet der Aufruf php artisan horizon:terminate, und Horizon bringt seine eigene Überwachung mit – aber gesagt bekommen muss es das trotzdem, denn Ihr Deployment sieht es nicht.

Die Reihenfolge zählt mehr als der Befehl

Wo der Neustart im Skript steht, entscheidet, ob das Fenster zwischen altem und neuem Code gefährlich ist:

# neuer Code zuerst an Ort und Stelle
php artisan migrate --force
php artisan config:cache
php artisan queue:restart

Starten Sie neu, bevor der neue Code auf der Platte liegt, booten die frischen Worker das alte Release – genau der Fehler, den Sie beheben wollten. Lassen Sie Migrationen nach dem Neustart laufen, treffen neue Worker auf ein Schema, das sich noch nicht geändert hat.

Der schwierigere Fall ist eine Migration, die etwas entfernt. Eine gelöschte Spalte bricht alte Worker sofort, und alte Worker wird es so lange geben, wie ihre aktuellen Jobs dauern. Jede Schemaänderung, die etwas wegnimmt, will auf zwei Deployments aufgeteilt werden: aufhören es zu benutzen, ausliefern, dann löschen.

Bestätigen, dass es gewirkt hat

Vertrauen Sie dem Skript nicht. Fragen Sie die Worker:

ps -eo lstart,cmd | grep "[q]ueue:work"

Startzeiten, die älter sind als das Deployment, heißen, dass der Neustart sie nicht erreicht hat, und das ist jetzt besser zu wissen als beim nächsten Vorfall. In Horizon steht dieselbe Antwort im Dashboard unter der Prozessliste des Supervisors.

Eine Zeile in einem Deploy-Skript, und eine ganze Fehlerklasse, die komplette Nachmittage frisst, hört auf zu existieren.

Das ist einer von vier Schritten, die darüber entscheiden, ob ein Deployment unsichtbar bleibt; die anderen drei stehen hier. Wenn die Queue selbst das ist, dem Sie nicht trauen können, mit Jobs, die verschwinden, sich verdoppeln oder dort scheitern, wo niemand hinsieht, übernehmen wir das als eigenes Projekt.

Verwandte Fragen

Tötet queue:restart laufende Jobs?
Nein, und genau das ist der Sinn. Es setzt ein Flag mit einem Zeitstempel; jeder Worker prüft dieses Flag zwischen zwei Jobs und beendet sich sauber, wenn er vor diesem Zeitstempel gestartet ist. Bereits begonnene Arbeit läuft zu Ende. Den Prozess stattdessen abzuschießen ist das, was einen halbfertigen Job verliert.
Wir nutzen Horizon. Ist das damit erledigt?
Horizon überwacht Worker und startet sie bei terminate neu, was die Mechanik gut abdeckt. Es weiß nur nichts von einem Deployment, solange das Deployment es ihm nicht sagt - der Artisan-Aufruf gehört also weiterhin ins Skript, nur heißt der Befehl anders.
Warum sind nur manche Jobs gescheitert?
Weil nur manche Worker ersetzt wurden. Eine Queue mit sechs Prozessen, die neu starten, sobald sie zufällig fertig werden, läuft ein Zeitfenster lang mit zwei Versionen Ihrer Anwendung gleichzeitig, und welche Version ein Job erwischt, ist Zufall. Deshalb sehen die Fehler auch sprunghaft und nicht reproduzierbar aus.
Kommt daher die Meldung, dass eine Spalte nicht existiert?
Sehr wahrscheinlich. Eine Migration, die eine Spalte hinzufügt, läuft sofort gegen die Datenbank; ein Worker mit dem Modell des vorigen Releases weiß nichts davon. Der umgekehrte Fall ist schlimmer: eine Migration, die eine Spalte entfernt, während alte Worker noch hineinschreiben.

← Zurück zu allen Artikeln

Anrufen+1 848 272 7583WhatsApp+90 850 308 5436E-Mailinfo@codefacture.comKontaktseite