RE:NODE

Fronty v Laravelu s Valkey: workery, opakování, neúspěšné úlohy

Text: Tým RE:NODE
21 min čtení


Laravel komunikuje s Valkey přes svůj queue ovladač redis, takže nastavení jsou čtyři řádky v .env a jeden dlouho běžící proces: php artisan queue:work redis. Ovladač, pravidla opakování a tabulka neúspěšných úloh jsou ta snazší půlka. Ta druhá, která lidi stojí úlohy, je souhra tří čísel - timeout úlohy, retry_after připojení a doba, po kterou worker skutečně žije - a otázka, co worker restartuje, když skončí, na což v jediném kontejneru neexistuje zjevná odpověď. Tento článek pokrývá všechno v pořadí, v jakém to budeš potřebovat.

Záměrně neopakuje dva dřívější články. Deploy Laravelu do produkce popisuje základní deploy a první queue:work s ovladačem database. Fronty úloh s Valkey porovnává BullMQ, Celery, RQ a Laravel a vysvětluje nastavení serveru, na kterém fronta závisí. Tady najdeš hloubku specifickou pro Laravel: konfigurační klíče, chování při opakování, zacházení s neúspěšnými úlohami a dohled nad procesy.

Proč přesunout frontu z databáze do Valkey#

Ovladač database je dobrá výchozí volba a pro malý web ta správná: úloha se zapíše ve stejné transakci jako data, která ji vyvolala, nic dalšího neběží a frontu můžeš prohlížet pomocí SQL. Začne to bolet ze tří důvodů. Workery se ptají cyklicky, takže každý nečinný worker spouští dotaz každých pár sekund. Mnoho workerů soupeří o stejné řádky. A tabulka úloh sdílí disk a připojení se zbytkem aplikace.

Valkey odebere provoz s úlohami z relační databáze. Vložení je jediná operace nad seznamem, worker čekající na práci drží blokující spojení místo cyklického dotazování a nahromaděné tisíce úloh stojí paměť, ne nafouklou tabulku. Vzdáváš se ale transakční záruky: s ovladačem redis může být úloha vložena dřív, než se potvrdí databázová transakce, která vytvořila její data, a rychlý worker ji pak spustí nad řádky, které ještě neexistují. Řeší to níže oddíl „Dispatch uvnitř transakcí“ a je to první věc ke kontrole, když úloha selhává jen občas.

Valkey zvol, když ho už máš pro cache nebo sessions, když je objem úloh tak velký, že cyklické dotazování je vidět v zátěži databáze, nebo když chceš Horizon. U database zůstaň, když je objem nízký a správnost kolem transakcí je důležitější než propustnost. Background jobs on a small server obhajuje jednoduchou variantu poctivě.

Nastavení ovladače fronty redis#

Podpora Redisu v Laravelu se s Valkey používá beze změny, protože Valkey mluví stejným protokolem. Nejdřív potřebuješ klientskou knihovnu. Laravel podporuje rozšíření PHP phpredis i čistě PHP balíček predis, vybírané přes REDIS_CLIENT. Zjisti, co má tvoje PHP:

bash
$ php -m | grep -i redis$ composer require predis/predis

Pokud php -m vypíše redis, použij REDIS_CLIENT=phpredis. Pokud ne, predis nepotřebuje žádné rozšíření a funguje stejně, jen o něco pomaleji. Pak nastav údaje o připojení v .env:

.env
QUEUE_CONNECTION=redisREDIS_CLIENT=phpredisREDIS_HOST=203.0.113.20REDIS_PORT=6380REDIS_PASSWORD=a-long-generated-passwordREDIS_PREFIX=shop-

REDIS_HOST, REDIS_PORT a REDIS_PASSWORD jsou hodnoty, které ti dá poskytovatel Valkey. Místo nich můžeš předat jediné REDIS_URL ve tvaru redis://:password@host:port; má přednost před samostatnými klíči. REDIS_PREFIX stojí za to nastavit ručně. Ve výchozím stavu Laravel sestaví prefix z APP_NAME, takže přejmenování aplikace změní prefix a osiří všechny úlohy čekající pod starým.

Samotný ovladač fronty se nastavuje v config/queue.php:

config/queue.php
'redis' => [    'driver' => 'redis',    'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),    'queue' => env('REDIS_QUEUE', 'default'),    'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),    'block_for' => null,    'after_commit' => false,],
KlíčVýchozíCo dělá
connectiondefaultKterý záznam v config/database.php pod redis se použije
queuedefaultFronta, do které úloha jde, když žádná není uvedena
retry_after90Sekundy, po kterých se rezervovaná úloha vrátí zpět
block_fornullSekundy, po které worker blokovaně čeká na úlohu
after_commitfalsePřed vložením počkat na otevřené transakce

Dvě z nich si zaslouží rozhodnutí. block_for nastavené na 5 způsobí, že nečinný worker čeká na blokující pop až pět sekund místo cyklického dotazování, což snižuje latenci i provoz na Valkey. Nech null jen z dobrého důvodu. A after_commit je oprava transakcí popsaná dál.

Než začneš psát skutečné úlohy, ověř, že všechno funguje od začátku do konce:

bash
$ php artisan tinker --execute="dispatch(fn () => logger('queue ok'));"$ php artisan queue:work redis --once$ tail -n 3 storage/logs/laravel.log

--once zpracuje jedinou úlohu a skončí, což je správný způsob testování. Pokud se řádek v logu objeví, ovladač, přihlašovací údaje i prefix jsou v pořádku.

Co Laravel ukládá do Valkey#

Pomůže vědět, co v databázi je, když něco nevypadá správně. Pro každý název fronty drží Laravel pod tvým prefixem tři struktury:

KlíčTypObsahuje
queues:defaultseznamÚlohy připravené ke spuštění
queues:default:delayedsorted setÚlohy s časem spuštění v budoucnu
queues:default:reservedsorted setÚlohy, které worker převzal, ale nedokončil

Worker vyjme úlohu ze seznamu a ve stejném atomickém kroku ji přidá do množiny rezervovaných se skóre „teď plus retry_after“. Když úloha doběhne, odstraní se z množiny rezervovaných. Úlohu, jejíž skóre vypršelo, přesune zpět do seznamu další worker, který se podívá. Tento jediný mechanismus je celé chování retry-after a proto je důležitý další oddíl.

Můžeš se na to podívat přímo. Varianta s --scan se vyhne hádání prefixu:

bash
$ valkey-cli -h 203.0.113.20 -p 6380 --askpass --scan --pattern '*queues:*'$ valkey-cli -h 203.0.113.20 -p 6380 --askpass LLEN shop-queues:default$ valkey-cli -h 203.0.113.20 -p 6380 --askpass ZCARD shop-queues:default:reserved

Rostoucí počet v reserved při prázdném seznamu znamená, že workery úlohy berou a nedokončují. Rostoucí seznam s konstantním reserved znamená, že nepracuje vůbec nic. Pokud Laravel běží s jiným REDIS_PREFIX, než jaký prohlížíš, uvidíš prázdnou databázi a usoudíš, že fronta je rozbitá. Není.

Timeouty a retry_after: chyba, kvůli které se úlohy spouštějí dvakrát#

Každé hlášení o chybě fronty v Laravelu, které říká „moje úloha proběhla dvakrát“, je jedna ze dvou věcí. Buď úloha opravdu selhala v půlce a byla zopakována, což je správné chování při doručení alespoň jednou, nebo je retry_after kratší než úloha. To druhé je chyba konfigurace.

Existují tři časové limity a musí do sebe správně zapadat:

  1. Vlastní limit úlohy: vlastnost $timeout ve třídě úlohy, nebo --timeout u workeru (výchozí 60 sekund). Worker zabije úlohu, která běží déle, pomocí rozšíření pcntl.
  2. retry_after připojení (výchozí 90 sekund). Po této době se rezervovaná úloha považuje za ztracenou a je vrácena jinému workeru.
  3. --max-time workeru, po jehož uplynutí worker skončí mezi úlohami.

Pravidlo zní, že timeout úlohy musí být kratší než retry_after, s rezervou. S výchozími hodnotami je to 60 proti 90, což je v pořádku. Pokud zvýšíš $timeout úlohy na 300 kvůli exportu reportu a necháš retry_after na 90, druhý worker převezme stejnou úlohu po 90 sekundách, zatímco první ještě běží. Oba doběhnou. Zákazník dostane dva reporty.

app/Jobs/BuildExport.php
class BuildExport implements ShouldQueue{    use Queueable;    public int $timeout = 300;    public bool $failOnTimeout = true;    // config/queue.php retry_after must be larger than 300, say 330}

Tomu odpovídá nastavení REDIS_QUEUE_RETRY_AFTER=330 v .env. $failOnTimeout = true označí úlohu po timeoutu jako neúspěšnou místo jejího opakování, což je obvykle to, co chceš u úlohy, která už běžela pět minut.

Plýtvavá alternativa je jeden obrovský retry_after pro všechno, třeba hodinu. Je bezpečný proti duplicitám a hrozný při zotavení: pokud je worker zabit uprostřed úlohy, úloha leží neviditelná v množině rezervovaných celou hodinu, než ji kdokoli zopakuje. Lepší návrh je držet retry_after střídmý a skutečně pomalé úlohy dát do vlastní fronty s vlastním workerem spuštěným s delším --timeout, přičemž retry_after je nastaven nad tímto timeoutem. Dvě připojení v config/queue.php, redis a redis-long, každé s vlastním retry_after, jsou legitimní způsob, jak přesně tohle udělat.

Workery také potřebují rozšíření PHP pcntl, aby timeouty vůbec vynucovaly. Pokud úloha visí donekonečna a --timeout nic nedělá, zkontroluj php -m | grep pcntl.

Tries, backoff a middleware#

Kolikrát se úloha zkusí a jak dlouho se mezi pokusy čeká, lze nastavit na workeru nebo na úloze. Pokud jsou přítomna obě, vyhrává úloha.

app/Jobs/SendInvoice.php
class SendInvoice implements ShouldQueue{    use Queueable;    public int $tries = 5;    public int $maxExceptions = 3;    public function backoff(): array    {        return [10, 60, 300, 900];    }    public function handle(Mailer $mailer): void    {        $mailer->send($this->invoiceId);    }}

Atributy, na kterých záleží:

Vlastnost nebo volbaVýznam
$tries / --triesMaximální počet pokusů včetně prvního
$maxExceptionsSelhat po tolika výjimkách, i když zbývají pokusy
backoff() / --backoffSekundy před opakováním; pole se zvyšuje s každým pokusem
retryUntil()Termín místo počtu
$timeout / --timeoutZabít úlohu po tolika sekundách

Několik bodů, na kterých lidé klopýtají. --tries=0 znamená neomezeně, což u úlohy, která vždy selže, je nekonečná smyčka, která se nikdy nedostane do failed_jobs. Úloha uvolněná přes $this->release(30) se počítá jako pokus. $tries spolu s retryUntil() znamená, že platí limit, který se dosáhne dřív. A úloha opakovaná proto, že její worker byl zabit, spotřebuje pokus, i když tvůj kód nikdy nevyvolal výjimku, a proto existuje $maxExceptions.

Middleware drží logiku opakování mimo tělo úlohy. Tři stojí za znalost. WithoutOverlapping vezme zámek, aby dvě úlohy pro stejný klíč nikdy neběžely současně. RateLimited zadržuje úlohy, když je pojmenovaný limiter vyčerpán, což je správný způsob, jak respektovat kvótu API třetí strany. ThrottlesExceptions ustupuje po opakovaných selháních nespolehlivé služby, místo aby spálil všechny pokusy za minutu. První dva používají cache, takže na sdíleném cache úložišti záleží, pokud někdy provozuješ víc než jeden proces workeru na oddělených strojích; Valkey, které už máš, je dobrá volba.

php
public function middleware(): array{    return [(new WithoutOverlapping($this->invoiceId))->releaseAfter(60)];}

Pro práci, která se nikdy nesmí dít dvakrát současně, ShouldBeUnique na třídě úlohy zabrání zařazení druhé kopie, dokud jedna čeká. Také spoléhá na zámek v cache.

Dispatch uvnitř transakcí#

Tohle je selhání, které se projeví jen pod zátěží. Controller otevře databázovou transakci, vytvoří objednávku, odešle ProcessOrder a o pár milisekund později potvrdí. Worker na Valkey vyjme úlohu za méně než milisekundu, dotáže se na objednávku 812 a nic nenajde, protože commit ještě neproběhl. Úloha vyhodí ModelNotFoundException a zopakuje se; při druhém pokusu řádek existuje a projde. V lozích zůstane neúspěšný pokus bez vysvětlení.

Existují tři opravy, seřazené podle preference:

  1. Nastav 'after_commit' => true u připojení redis v config/queue.php, nebo REDIS_QUEUE_AFTER_COMMIT=true, pokud to tvoje konfigurace čte. Laravel pak drží dispatch, dokud se okolní transakce nepotvrdí, a zahodí ho, pokud se vrátí zpět.
  2. Zavolej ->afterCommit() u jednoho dispatche: ProcessOrder::dispatch($order)->afterCommit();
  3. Implementuj ShouldQueueAfterCommit na třídě úlohy.

Pro celou aplikaci dej přednost první variantě, protože zapomenutí druhé u jediného dispatche je způsob, jakým se občasná chyba vrací. Háček: se zapnutým after_commit se dispatch mimo jakoukoli transakci chová normálně, takže v běžném případě nestojí nic.

Neúspěšné úlohy a tabulka failed_jobs#

Když úloha vyčerpá své pokusy, Laravel ji zapíše do úložiště neúspěšných úloh. Toto úložiště je databázová tabulka bez ohledu na ovladač fronty, takže fronta redis stále potřebuje databázi pro svá selhání. V moderní instalaci migrace už existuje; jinak:

bash
$ php artisan make:queue-failed-table$ php artisan migrate --force

Příslušný blok v config/queue.php vybírá ovladač, obvykle database-uuids, a název tabulky:

config/queue.php
'failed' => [    'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),    'database' => env('DB_CONNECTION', 'mysql'),    'table' => 'failed_jobs',],

Každý řádek obsahuje připojení, frontu, serializovaný payload, výjimku se stack trace a čas selhání. Příkazy, které budeš používat:

PříkazCo dělá
php artisan queue:failedVypíše neúspěšné úlohy s jejich id
php artisan queue:retry <id>Vrátí jednu neúspěšnou úlohu zpět do její fronty
php artisan queue:retry allVrátí zpět všechny neúspěšné úlohy
php artisan queue:retry --queue=highZopakuje selhání z jedné fronty
php artisan queue:forget <id>Smaže jednu neúspěšnou úlohu
php artisan queue:flushSmaže všechny
php artisan queue:prune-failed --hours=168Smaže selhání starší než týden

Před queue:retry all si přečti, co selhalo. Pokud čtyřicet úloh selhalo, protože API třetí strany nebylo dostupné, jejich opakování je správné. Pokud čtyřicet selhalo kvůli chybě ve tvém kódu, nejdřív nasaď opravu, jinak budeš sledovat, jak všech čtyřicet znovu selže a skončí v tabulce s novými časovými značkami.

Mazání není automatické. Tabulka, kterou nikdo neprořezává, roste s každým incidentem a obsahuje serializované payloady, které mohou zahrnovat osobní údaje. Naplánuj úklid tak, aby proběhl, aniž by si na něj kdokoli musel vzpomenout:

routes/console.php
use Illuminate\Support\Facades\Schedule;Schedule::command('queue:prune-failed --hours=168')->daily();Schedule::command('queue:prune-batches --hours=48')->daily();

Pro reakce na úrovni jednotlivé úlohy definuj metodu failed. Běží jednou, po posledním pokusu, a je to místo, kde někoho upozorníš nebo vrátíš zpět napůl dokončený stav:

php
public function failed(?Throwable $exception): void{    Log::error('Invoice send gave up', ['invoice' => $this->invoiceId, 'error' => $exception?->getMessage()]);}

Pro upozorňování na úrovni celé aplikace se Queue::failing() v service provideru spustí při každém selhání v každé frontě. Upozorňuj na četnost, ne na jednotlivou událost: jedno selhání je opakování, které dělá svou práci; třicet za hodinu je incident. php artisan queue:monitor redis:default,redis:high --max=100 vyvolá událost QueueBusy, když fronta překročí práh, a naplánování každou minutu ti dá alarm na hromadění úloh bez dashboardu.

Spuštění workeru: přepínače, na kterých záleží#

Příkaz z prvního oddílu je úplný, ale každý přepínač má svůj důvod. Rozumný řádek pro produkci:

bash
$ php artisan queue:work redis --queue=high,default --tries=3 --backoff=10 \    --timeout=60 --max-time=3600 --max-jobs=1000 --memory=128 --sleep=3
PřepínačVýchozíProč ho nastavit
--queuequeue připojeníPořadí priorit; high se vyprázdní před default
--max-time0 (nikdy)Skončit po N sekundách, aby byly úniky paměti omezené
--max-jobs0 (nikdy)Skončit po N úlohách, ze stejného důvodu
--memory128Skončit, když worker překročí tolik MB
--sleep3Sekundy čekání, když je fronta prázdná
--rest0Sekundy pauzy mezi úlohami
--stop-when-emptyvypnutoSkončit, když se fronta vyprázdní; užitečné pro testy

Ukončení jsou záměrná. Worker v PHP je proces, který načte aplikaci jednou a pak v ní spouští úlohy, takže se hromadí každý únik v balíčku, každá statická cache a každý růst vazeb služeb v kontejneru. --max-time, --max-jobs a --memory mění pomalý únik v čisté ukončení jednou za čas a supervisor ho restartuje s novým procesem. Důsledek je zásadní: worker, který končí záměrně, funguje jen tehdy, když ho něco spustí znovu. To je další oddíl.

Totéž dlouhodobé chování je důvod, proč deploy musí restartovat workery. php artisan queue:restart zapíše časovou značku do cache; každý worker ji kontroluje mezi úlohami, dokončí aktuální úlohu a skončí. Potřebuje funkční cache úložiště a CACHE_STORE=array v produkci tiše způsobí, že příkaz nedělá nic.

Udržení workeru při životě v jednom kontejneru#

Na běžném linuxovém serveru bys použil systemd nebo Supervisor. V jednom kontejneru máš jeden hlavní proces a vše ostatní musí žít vedle něj nebo pod ním. Worker bez dohledu selhává zvláštním způsobem: skončí po --max-time, kvůli výjimce v balíčku nebo kvůli limitu paměti, webový proces dál obsluhuje stránky a úlohy se hromadí ve Valkey, zatímco web vypadá zdravě.

Na RE:NODE je každý server jeden kontejner s pevným limitem CPU a limitem paměti. Na limitu paměti se kontejner zastaví a čistě se restartuje místo swapování, takže worker, který vyroste za to, co jsi mu nechal, zabije celý server včetně webového procesu. Platforma také hlídá smyčky pádů: tři restarty za hodinu dají na stránku serveru varování a automaticky otevřou ticket, šest server pozastaví. Proto se vyplatí níže uvedené dimenzování a proto je worker, který záměrně skončí a sám se restartuje, lepší vzor než ten, který shodí kontejner.

Jsou tři poctivé možnosti, od nejjednodušší po nejschopnější.

Možnost 1: druhý server pro worker

Protože fronta žije ve Valkey a ne v kontejneru, worker nemusí sdílet kontejner s webovým procesem. Spusť stejný repozitář na druhém serveru Laravel / PHP, jehož jediným úkolem je queue:work, se stejnými hodnotami .env pro databázi a Valkey. Webový a úlohový provoz se pak nemohou vzájemně vyhladovět a každý má vlastní limit paměti a CPU. Dostaneš také nejčistší řešení dohledu: proces, který server spouští, je worker, a když skončí, platforma server restartuje. Plán Laravel / PHP aplikace začíná na $4 měsíčně a k serveru Valkey se obě strany připojují přes síť. Cenou je druhý .env, který je třeba udržovat v souladu, a startovací příkaz druhého serveru musí spouštět worker místo webového serveru - na záložce Startup zjisti, co ti tvůj plán dovolí změnit.

Možnost 2: smyčka ve skriptu vedle webového procesu

Když jeden server musí dělat obojí, malá shellová smyčka restartuje worker, kdykoli skončí, a obsluhuje signál zastavení, aby restart nepřeřízl úlohu v půlce.

bash
#!/bin/sh# worker-loop.sh - restart queue:work whenever it exits, stop cleanly on SIGTERMstop=0trap 'stop=1; kill -TERM "$pid" 2>/dev/null' TERM INTwhile [ "$stop" -eq 0 ]; do  php artisan queue:work redis --queue=high,default --tries=3 --backoff=10 \    --max-time=3600 --memory=128 &  pid=$!  wait "$pid"  [ "$stop" -eq 0 ] && sleep 3donewait "$pid"

První wait se vrátí, když worker skončí z jakéhokoli důvodu, a sleep 3 brání workeru, který selže hned, aby protáčel CPU. Když přijde signál zastavení, trap přepošle SIGTERM workeru a závěrečné wait mu nechá dokončit aktuální úlohu. Worker Laravelu zpracuje SIGTERM tak, že dokončí úlohu, na které je, a pak skončí. Skript je třeba spustit vedle webového procesu, což znamená, že tvůj startovací příkaz musí spouštět oba; kde ho změnit nemůžeš, tato možnost není k dispozici a odpovědí je Možnost 1.

Možnost 3: Supervisor uvnitř kontejneru

Pokud startovací příkaz kontejneru může spustit supervisord v popředí, jeden konfigurační soubor spouští a restartuje každý proces. Je to standardní produkční nastavení Laravelu a v kontejneru funguje stejně jako na serveru.

supervisord.conf
[supervisord]nodaemon=true[program:laravel-worker]process_name=%(program_name)s_%(process_num)02dcommand=php /app/artisan queue:work redis --queue=high,default --tries=3 --max-time=3600autostart=trueautorestart=truestopasgroup=truekillasgroup=truenumprocs=2redirect_stderr=truestdout_logfile=/app/storage/logs/worker.logstopwaitsecs=3600

autorestart=true restartuje worker při jakémkoli ukončení, včetně záměrného z --max-time. stopwaitsecs musí být delší než tvoje nejdelší úloha, jinak Supervisor při zastavení zabije rozdělanou úlohu. numprocs=2 spustí dva workery; víc neznamená lépe, jak ukáže další oddíl. Samotný Supervisor musí být nainstalován v image nebo přes správce balíčků a přidává proces, který také musíš udržet při životě - proto jsou Možnosti 1 a 2 na malém plánu často jednodušší.

Pokud je tvoje práce s frontou rozsáhlá, Laravel Horizon nahradí příkaz workeru příkazem php artisan horizon. Dohlíží na vlastní procesy workerů, rozděluje je mezi fronty a přidává dashboard, takže místo několika spouštíš jeden proces. Vyžaduje ovladač redis, takže má smysl až na Valkey. Spusť php artisan horizon:terminate ve skriptu deploye, na stejném místě jako queue:restart.

Graceful shutdown a health checky vysvětluje, proč na signálu zastavení záleží na každé platformě, a Průvodce správcem procesů PM2 pokrývá stejný problém dohledu pro Node.

Dimenzování workerů v malém kontejneru#

Worker v PHP drží celou kopii tvého frameworku. Holý worker Laravelu má běžně zhruba 40 až 80 MB, worker s těžkým balíčkem nebo velkou kolekcí v paměti několikrát tolik a skutečné číslo je to, co ti řekne ps na tvé vlastní aplikaci. Měř, nespoléhej na tabulku:

bash
$ ps -o pid,rss,cmd -C php | sort -k2 -n -r | head

rss je v kilobajtech. Vezmi největší worker, vynásob počtem workerů, přičti webový proces (workery FrankenPHP nebo PHP-FPM také drží každý vlastní kopii) a porovnej součet s limitem paměti plánu. Plán s 1 GB pohodlně pojme webový proces a dva workery pro běžné úlohy. Čtyři workery, z nichž každý dosáhne špičky 150 MB, jsou 600 MB dřív, než webový proces obslouží první požadavek.

Nastav --memory pod to, co si můžeš dovolit, ne nad to. Na limitu kontejneru se restartuje celý server a přijdeš o rozpracované webové požadavky i všechny běžící úlohy. Na vlastním limitu --memory workeru skončí a restartuje se jen ten worker a Laravel nejdřív dokončí aktuální úlohu.

CPU je druhý limit. CPU kontejneru je pevné omezení na podíl, který jsi koupil, takže plán s půl jádrem a dvěma workery náročnými na CPU druhým workerem nezrychlí; dostane stejné CPU rozdělené na dvě části. Fronta zmenšování obrázků chce tolik workerů, kolik máš jader. Fronta volání API a e-mailů čeká na síť, ne na CPU, a jeden proces jich může užitečně držet několik - v tu chvíli je víc procesů workeru způsob, jak v PHP získat souběžnost, protože worker spouští jednu úlohu najednou.

Umístění strany Valkey na vlastní server také znamená, že jeho paměť nemůžeš utrácet ty. Plán Valkey je samostatný server dostupný na vlastním hostu a portu, od 256 MB, chráněný heslem a ukládaný na disk pomocí AOF i snapshotů, takže čekající úlohy přežijí restart kterékoli strany. Nastavení, která rozhodují o tom, zda fronta ztrácí úlohy, především maxmemory-policy noeviction, jsou v článku Paměť Valkey a politiky vytlačování a režimy persistence v článku Persistence ve Valkey: AOF a RDB. Svoje nastavení zkontroluj pomocí valkey-cli ... CONFIG GET maxmemory-policy a pokud není noeviction, změň ho, nebo požádej toho, kdo server spravuje.

Deploymenty a fronta#

Deploy se fronty dotýká dvěma způsoby: nový kód se musí dostat k workerům a úlohy, které už ve Valkey jsou, se musí stále dát deserializovat proti novému kódu. To druhé lidé zapomínají.

Úloha ve frontě ukládá název své třídy a své vlastnosti. Pokud přejmenuješ třídu nebo odstraníš vlastnost, zatímco na ni čekají úlohy, worker je nedokáže deserializovat a skončí v failed_jobs. Bezpečné pořadí je nasadit nejdřív aditivní změny: přidej novou třídu nebo vlastnost, nech frontu vyprázdnit od starých úloh a starou odstraň až v pozdějším vydání. Pokud musíš přejmenovat, nech starou třídu na deploy nebo dva jako tenkou podtřídu.

Na RE:NODE ti s prvním problémem pomáhá průběh deploye. S nasazením z GitHubu při pushi push restartuje běžící server (a spustí zastavený), takže workery nastartují znovu s novým kódem; přepínače najdeš v průvodci deployi. Restart ale není pauza: čisté zastavení nechá běžící úlohu dokončit, kill ne, a takto přerušená úloha se vrátí do fronty po retry_after sekundách. To je opět chování „alespoň jednou“ a proto musí být úlohy bezpečné k opakování. php artisan queue:restart si ve skriptu deploye přesto nech; nic nestojí a pokrývá hostingy, kde deploy proces nerestartuje. Zbytek postupu deploye je v článku Deploy Laravelu do produkce.

Řešení potíží#

Úlohy se vkládají a nic se neděje. Neběží žádný worker, nebo je připojený k jiné frontě či prefixu. Spusť ručně php artisan queue:work redis --once a sleduj výstup. Pak porovnej QUEUE_CONNECTION a REDIS_PREFIX na webovém procesu a na workeru; nesoulad není z kódu vidět.

`Connection refused` nebo `NOAUTH Authentication required`. První je host nebo port, nebo firewall; druhé je chybějící REDIS_PASSWORD. Pozor, že cachovaná konfigurace opravené .env ignoruje: spusť php artisan config:clear.

Úloha běží dvakrát. retry_after je kratší než doba běhu úlohy. Zvyš ho nad $timeout úlohy, jak je popsáno výše, nebo přesuň úlohu do fronty s delším limitem.

`ModelNotFoundException` jen u některých úloh. Úloha byla vložena dřív, než se potvrdila její transakce. Nastav after_commit na true u připojení.

Worker po deployi běží se starým kódem. Nebyl restartován. Spusť php artisan queue:restart a ujisti se, že cache úložiště, na které spoléhá, funguje.

Paměť roste, dokud se kontejner nerestartuje. Únik v balíčku nebo neomezená kolekce. Přidej --max-jobs a --memory a skutečnou velikost zkontroluj pomocí ps. Velké sady výsledků streamuj přes lazy() nebo cursor() místo jejich načtení.

`MaxAttemptsExceededException` a úloha nikdy nespustila svůj kód pro selhání. Worker byl opakovaně zabit nebo vypršel timeout a pokusy se vyčerpaly. Zkontroluj, že $timeout je kratší než retry_after a že worker není zabíjen limitem paměti kontejneru.

`OOM command not allowed when used memory > 'maxmemory'`. Valkey je plné a noeviction odmítá zápisy. Workery nejedou, jsou příliš pomalé, nebo je queues:*:reserved plné uvízlých úloh. Oprav příčinu; nemeň politiku vytlačování, jen aby chyba zmizela.

FAQ#

Potřebuji Horizon ke spouštění front Laravelu na Valkey?

Ne. php artisan queue:work stačí a obyčejné workery se v malém kontejneru provozují snadněji. Horizon má smysl přidat, když chceš dashboard, vyvažování po frontách a metriky jako čas čekání a propustnost, aniž bys je psal sám.

Můžu tabulku neúspěšných úloh držet taky ve Valkey?

Podporovaným způsobem ne. Neúspěšné úlohy se ukládají přes blok failed v config/queue.php a jeho ovladače jsou database, database-uuids, dynamodb a file. Nech je v běžné databázi, kde je můžeš dotazovat a mazat.

Kolik workerů mám v jednom kontejneru spustit?

Začni s jedním nebo dvěma a měř. Každý worker spouští jednu úlohu najednou, takže u práce vázané na I/O druhý worker zdvojnásobí propustnost, ale paměť a CPU se sdílejí s webovým procesem a s počtem nerostou. Další worker přidej, až když je nejstarší čekající úloha starší, než můžeš akceptovat.

Proč mi queue:retry all vrací stejné úlohy zase jako neúspěšné?

Protože příčina stále existuje. Opakování spustí stejný payload proti aktuálnímu kódu a prostředí. Nejdřív oprav chybu nebo obnov závislost a teprve pak opakuj; před vrácením čehokoli zpět si přečti výjimku v queue:failed nebo ve sloupci exception.

Je bezpečné sdílet jedno Valkey pro frontu, cache a sessions?

Funguje to, ale je to špatný tvar. Cache chce vytlačovat staré klíče a fronta nesmí ztratit žádný. Dej frontě vlastní Valkey, nebo alespoň provozuj s noeviction a smiř se s tím, že plná cache pak odmítá zápisy. Sessions ve Valkey pokrývá stejný kompromis ze strany sessions.

Co se stane s čekajícími úlohami, když se kontejner restartuje?

Čekající úlohy jsou ve Valkey, ne v kontejneru, takže přežijí. Úlohy, které měl worker rezervované, se vrátí do fronty po retry_after sekundách a spustí se znovu, proto musí být úloha bezpečná k opakování. Ukládání Valkey na disk chrání zbytek, pokud se restartuje samotné Valkey.

Pomohlo vám to?