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.
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:restartEs 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:restartStarten 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.
