Queue-ներ և background worker-ներ
Դանդաղ աշխատանքն արեք ավելի ուշ, հուսալիորեն, առանց օգտատերերին սպասեցնելու։
30 վայրկյանում
Queue-ը (հերթը) ձեր backend-ի անելիքների ցուցակն է։ Վեբ հարցումը ավելացնում է job (ուղարկիր այս email-ը, համաժամեցրու այս պատվերը) և անմիջապես պատասխանում է. առանձին worker պրոցեսները վերցնում են job-երը և կատարում դրանք՝ ձախողման դեպքում կրկնելով։ Queue-ները հավելվածներն օգտատերերի համար դարձնում են ավելի արագ և ավելի կայուն, երբ արտաքին ծառայությունները դանդաղ են կամ անհասանելի։
Ինչու եմ օգտագործում
- Queue-ներ օգտագործում եմ, որովհետև email-ները, SMS-ները, վճարումների callback-ները, ERP sync-ը և հաշվետվությունները երբեք չպետք է արգելափակեն հաճախորդի հարցումը։
- Retry-ներ backoff-ով օգտագործում եմ, որովհետև արտաքին API-ները անընդհատ ձախողվում են, և ձախողումների մեծ մասը ժամանակավոր է։
- Queue-ներ օգտագործում եմ webhook-ների համար երկու ուղղությամբ էլ, որպեսզի գործընկերոջ outage-ը երբեք իր հետ չտանի իմ API-ն։
- Job-երը դարձնում եմ idempotent, որպեսզի retry-ը երբեք երկու անգամ գումար չգանձի կամ նույն SMS-ը երկու անգամ չուղարկի։
Ինչպես է աշխատում
01Ինչու queue-ներ. producer-ներ, consumer-ներ և broker-ներ
Պատկերացրեք ռեստորանի պատվերների ձողը։ Մատուցողը (producer) ամրացնում է պատվերի թերթիկը և վերադառնում հաճախորդների մոտ. խոհարարները (consumer) վերցնում են թերթիկները, երբ ազատ են։ Եթե տասը պատվեր միանգամից է գալիս, թերթիկները սպասում են ձողի վրա, իսկ մատուցողը չի «սառչում»։ Ծրագրային ապահովման մեջ այդ ձողը broker-ն է՝ Redis, տվյալների բազայի աղյուսակ, RabbitMQ կամ SQS։ Ձեր վեբ հավելվածը producer-ն է, worker պրոցեսները՝ consumer-ները։ Առավելությունները՝ արագ պատասխաններ, տրաֆիկի գագաթների հարթեցում, դանդաղ ծառայությունների ձախողումների մեկուսացում և worker-ների մասշտաբավորում վեբ սերվերներից անկախ։ Գինը՝ աշխատանքը դառնում է ասինխրոն, ուստի պետք է մտածել retry-ների, կրկնօրինակների, հերթականության և մոնիթորինգի մասին։
- Job. փոքր հաղորդագրություն՝ տեսակով և տվյալներով, իդեալում՝ միայն id-ներ, ոչ թե ամբողջական օբյեկտներ։
- Queue-ների անունները բաժանում են առաջնահերթությունները՝ critical, default, emails, reports։
- Concurrency. քանի job է worker-ը միաժամանակ կատարում։
02Retry-ներ, backoff և dead-letter queue-ներ
Job-երը ձախողվում են. վճարման API-ի timeout է լինում, տվյալների բազան deadlock է ընկնում, երրորդ կողմը վերադարձնում է 503։ Կրկնեք, բայց զգույշ։ Exponential backoff-ը յուրաքանչյուր անգամ ավելի երկար է սպասում (օրինակ՝ 10 վայրկյան, 1 րոպե, 5 րոպե)՝ մյուս կողմին վերականգնվելու ժամանակ տալով. jitter-ը ցրում է retry-ները, որպեսզի դրանք բոլորը միաժամանակ չհարվածեն։ Սահմանափակեք փորձերի քանակը։ Տարբերակեք սխալները. timeout-ը արժե կրկնել, իսկ validation սխալը («հաճախորդը գոյություն չունի») ընդմիշտ կձախողվի, ուստի այն անմիջապես նշեք որպես failed։ Վերջին փորձից հետո job-ը տեղափոխեք dead-letter queue (Laravel-ում՝ failed_jobs աղյուսակ, BullMQ-ում՝ failed set)։ Ինչ-որ մեկը պետք է նայի դրան. alert դրեք ձախողումների քանակի վրա և ստեղծեք միջոց՝ պատճառը շտկելուց հետո job-երը դիտելու և նորից գործարկելու համար։
class SyncOrderToErp implements ShouldQueue
{
use Queueable;
public int $tries = 5;
public int $timeout = 60;
public function __construct(public int $orderId) {}
public function backoff(): array
{
return [10, 60, 300, 900];
}
public function handle(ErpClient $erp): void
{
$order = Order::findOrFail($this->orderId);
$erp->upsertOrder($order); // idempotent by order id on the ERP side
}
}
03Idempotent job-եր. անվտանգ է երկու անգամ գործարկել
Queue-ների մեծ մասը երաշխավորում է at-least-once առաքում. job-ը կարող է երկու անգամ գործարկվել, օրինակ, եթե worker-ը crash է լինում աշխատանքն անելուց հետո, բայց մինչև acknowledge անելը։ Ուստի յուրաքանչյուր job պետք է լինի idempotent, այսինքն՝ նորից գործարկելը լրացուցիչ ազդեցություն չունենա։ Տեխնիկաներ՝ օգտագործեք բնական unique key (ստեղծեք 123 պատվերի հաշիվ-ապրանքագիրը միայն եթե այն գոյություն չունի), պահեք «մշակված է» նշիչ unique constraint-ով, insert-ների փոխարեն օգտագործեք upsert-ներ և վճարային պրովայդերներին փոխանցեք idempotency key։ Գործելուց առաջ ստուգեք վիճակը («արդեն ուղարկվե՞լ է»)։ Exactly-once առաքումը համակարգերի միջև հիմնականում առասպել է. exactly-once ազդեցությունը գալիս է idempotent handler-ներից։ Խուսափեք նաև դասական race-ից. job-երը dispatch արեք միայն տվյալների բազայի transaction-ի commit-ից հետո, այլապես worker-ը կարող է վերցնել job մի տողի համար, որը դեռ գոյություն չունի։
04BullMQ-ն Node-ում
BullMQ-ն Node-ի համար Redis-ի վրա հիմնված ստանդարտ queue-ն է։ Queue-ն ավելացնում է job-եր, Worker-ը մշակում է դրանք սահմանված concurrency-ով, իսկ option-ները կառավարում են փորձերը, backoff-ը, ուշացումները, առաջնահերթությունը և մաքրումը։ Կրկնօրինակումից խուսափելու համար սահմանեք jobId. արդեն գոյություն ունեցող id-ով job ավելացնելն անտեսվում է։ Կրկնվող աշխատանքի համար օգտագործվում են job scheduler-ներ (upsertJobScheduler)՝ cron pattern-ով կամ interval-ով. BullMQ v6-ում հին repeat option-ը հանվել է scheduler-ների օգտին։ Worker-ները գործարկեք API-ից առանձին պրոցեսներով, որպեսզի ծանր job-երը երբեք չդանդաղեցնեն հարցումները, և յուրաքանչյուր queue-ին տվեք իր concurrency-ն։ Redis-ը պետք է կարգավորված լինի maxmemory-policy noeviction-ով, այլապես Redis-ը կարող է լուռ ջնջել job-երը։
import { Queue, Worker } from 'bullmq';
const connection = { host: '127.0.0.1', port: 6379 };
export const webhooks = new Queue('webhooks', { connection });
await webhooks.add('deliver', { eventId }, {
jobId: 'evt-' + eventId, // dedupe
attempts: 8,
backoff: { type: 'exponential', delay: 5_000 },
removeOnComplete: 1000,
removeOnFail: 5000,
});
new Worker('webhooks', async (job) => deliverWebhook(job.data.eventId), {
connection,
concurrency: 10,
});
05Scheduling և worker-ների աշխատեցում production-ում
Պլանավորված աշխատանքը (գիշերային հաշվետվություններ, բաժանորդագրությունների ժամկետի ավարտ, ժամային sync-եր) աշխատում է timer-ով։ Դասական cron-ը հրամանը գործարկում է ֆիքսված ժամերին. Laravel-ի scheduler-ին պետք է մեկ cron գրառում, իսկ մնացածը սահմանվում է կոդում. BullMQ-ի scheduler-ները ժամանակացույցերը պահում են Redis-ում։ Պլանավորված job-երը դարձրեք անվտանգ համընկնման համար կամ բացահայտ կանխեք համընկնումը, և գործարկեք դրանք միայն մեկ սերվերի վրա։ Worker-ները երկարատև պրոցեսներ են, ուստի process manager-ը պետք է դրանք կենդանի պահի. supervisor կամ systemd՝ Laravel worker-ների համար, pm2 կամ systemd՝ Node-ի համար, կամ Docker-ում մեկ container յուրաքանչյուր worker տեսակի համար։ Worker-ները վերագործարկեք յուրաքանչյուր deploy-ի ժամանակ, որպեսզի նոր կոդը բեռնեն, և տվեք նրանց graceful shutdown, որպեսզի ընթացքի մեջ գտնվող job-ը կարողանա ավարտվել։
; /etc/supervisor/conf.d/app-worker.conf
[program:app-worker]
command=php /var/www/app/artisan queue:work redis --queue=critical,default --sleep=3 --tries=3 --max-time=3600
process_name=%(program_name)s_%(process_num)02d
numprocs=2
user=app
autostart=true
autorestart=true
stopwaitsecs=3600
stdout_logfile=/var/log/app-worker.log
06Մուտքային և ելքային webhook-ներ
Մուտքային webhook-ներ (վճարային պրովայդերներից, ERP-ներից, առաքման ծառայություններից)՝ ստուգեք ստորագրությունը, պահեք raw իրադարձությունը, արագ պատասխանեք 200 և մշակեք այն job-ում։ Պրովայդերները կրկնում են, ուստի կրկնօրինակները հեռացրեք ըստ նրանց event id-ի։ Գումարի հարցում երբեք միայն payload-ին մի վստահեք. վճարման կարգավիճակը նորից ստացեք պրովայդերի API-ից։ Ելքային webhook-ներ (ձեր գործընկերներին)՝ իրադարձությունը գրեք outbox աղյուսակում նույն transaction-ի ներսում, ինչ բիզնես փոփոխությունը, թողեք, որ worker-ը այն առաքի, body-ն ստորագրեք HMAC-SHA256-ով և timestamp-ով, որպեսզի ստացողները կարողանան ստուգել այն և մերժել replay-ները, կրկնեք backoff-ով մեկ օր կամ ավելի, և գործընկերոջը ցույց տվեք առաքման log-երը։ Այս outbox pattern-ը երաշխավորում է, որ երբեք չեք ծանուցի rollback արված բանի մասին և չեք կորցնի commit արված իրադարձությունը։
Օգտագործեք, երբ
- Email-ներ, SMS-ներ, push notification-ներ և ամեն ինչ, որ կանչում է երրորդ կողմի API։
- Ծանր աշխատանք՝ export-ներ, import-ներ, պատկերների մշակում, հաշվետվությունների գեներացում։
- Համաժամեցում ERP, հաշվապահական կամ ֆիսկալ համակարգերի հետ, որոնք կարող են դանդաղ կամ offline լինել։
- Webhook-ների ստացում և ուղարկում, ինչպես նաև domain event-ների fan-out։
Մի՛ օգտագործեք, երբ
- Երբ օգտատիրոջը շարունակելու համար արդյունքը հենց հիմա է պետք (ստուգել կուպոնը, ցույց տալ գինը)։
- Փոքրիկ, արագ գործողություններ, որտեղ queue-ի overhead-ը և բարդությունը ոչինչ չեն ավելացնում։
- Որպես տվյալների բազայի transaction-ի փոխարինող, երբ քայլերը պետք է միասին հաջողվեն կամ ձախողվեն։
Այլընտրանքներ
- Laravel queues + Horizon
Ներկառուցված queue-ներ Redis, database կամ SQS driver-ներով. Horizon-ը dashboard է ավելացնում։
- RabbitMQ
Լիարժեք message broker՝ routing-ով և acknowledgement-ներով. լավ է բազմաթիվ ծառայությունների համար։
- Amazon SQS
Ամբողջությամբ managed queue. ոչ մի սերվեր, վճարում ըստ հարցման։
- Kafka
Event log բարձր throughput-ով streaming-ի և replay-ի համար. ավելին, քան պետք է պրոդուկտների մեծ մասին։
Լավագույն պրակտիկաներ
- Job-երում փոխանցեք id-ներ, ոչ թե ամբողջական օբյեկտներ, և handler-ում բեռնեք թարմ տվյալներ։
- Յուրաքանչյուր job դարձրեք idempotent. ենթադրեք, որ այն մեկից ավելի անգամ կգործարկվի։
- Կրկնեք exponential backoff-ով և սահմանաչափով. մշտական սխալների դեպքում արագ ձախողվեք։
- Job-երը dispatch արեք տվյալների բազայի transaction-ի commit-ից հետո։
- Queue-ները բաժանեք ըստ առաջնահերթության և worker-ները գործարկեք process manager-ի ներքո։
- Alert դրեք failed job-երի և queue-ի խորության վրա. ստեղծեք replay գործիք։
- Ստորագրեք ելքային webhook-ները և ստուգեք մուտքայինները։
Տարածված սխալներ
- Ոչ idempotent job-եր, որոնք retry-ի ժամանակ կրկնակի գանձում կամ կրկնակի ուղարկում են անում։
- Անվերջ կրկնել առանց backoff-ի՝ «ծեծելով» արդեն ընկած ծառայությունը։
- Ամիսներով ոչ ոք չի նայում failed jobs աղյուսակին։
- Մոռանալ deploy-ի ժամանակ worker-ները վերագործարկել։
- Redis՝ eviction policy-ով, որը հիշողության ճնշման տակ ջնջում է հերթում սպասող job-երը։
Խորը վերլուծություն
07Ինչպես է job-ը «ամրագրվում»՝ visibility timeout-ներ, lock-եր և stalled job-եր
Երբ worker-ը վերցնում է job-ը, broker-ը այն չի ջնջում։ Այն որոշ ժամանակով թաքցնում է job-ը և սպասում հաստատման։ Եթե հաստատումը ժամանակին չի գալիս, broker-ը ենթադրում է, որ worker-ը մահացել է, և job-ը հանձնում է ուրիշին։ SQS-ում սա visibility timeout-ն է։ Laravel-ում դա retry_after-ն է config/queue.php-ում. եթե job-ը դեռ աշխատում է այդքան վայրկյանից հետո, մեկ այլ worker այն նորից է սկսում, ուստի job-ի timeout-ը միշտ պետք է կարճ լինի retry_after-ից, այլապես երկար job-երը զուգահեռ երկու անգամ կաշխատեն։ BullMQ-ն օգտագործում է lock, որը worker-ը անընդհատ թարմացնում է մշակման ընթացքում. եթե lock-ի ժամկետն անցնում է, օրինակ՝ որովհետև CPU-ծանր կոդը արգելափակել է event loop-ը, job-ը նշվում է որպես stalled և վերադարձվում waiting, իսկ չափից շատ stall-երից հետո ձախողվում է։ Երեք դիզայններն էլ տալիս են at-least-once առաքում, և հենց դա է պատճառը, որ idempotency-ն պարտադիր է։
// config/queue.php
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 960, // longer than the slowest job's timeout
'block_for' => 5,
'after_commit' => true, // push only after the DB transaction commits
],
// Slow export job: public $timeout = 900; run it on a worker
// started with --timeout=900, on its own queue.
08Զուգահեռության կառավարում՝ եզակիություն, համընկնում և rate limit-ներ
Retry-ներն ու զուգահեռ worker-ները նոր խնդիրներ են ստեղծում. նույն հաճախորդի երկու sync, որոնք մրցում են իրար հետ, կամ job-երի պոռթկում, որը բախվում է գործընկերոջ rate limit-ին։ Laravel-ը ձեզ տալիս է կառուցողական բլոկներ։ ShouldBeUnique-ը անտեսում է նոր dispatch-ը, քանի դեռ նույնական job-ը, ըստ uniqueId-ի, սպասման մեջ է։ WithoutOverlapping job middleware-ը թույլ է տալիս մեկ key-ի համար միաժամանակ աշխատել միայն մեկ job-ի, իսկ մյուսներին վերադարձնում է queue։ RateLimited-ը job-երի վրա կիրառում է անվանված rate limiter, իսկ ThrottlesExceptions-ը կրկնվող ձախողումներից հետո դանդաղեցնում է՝ գործելով որպես circuit breaker անկայուն API-ի համար. զուգակցեք այն retryUntil-ի հետ, որպեսզի retry-ները սահմանափակի ժամանակը, ոչ թե փորձերի քանակը։ Unique և overlap lock-երին պետք է cache store ատոմար lock-երով, օրինակ՝ Redis-ը կամ բազան։ BullMQ-ում deduplication տարբերակը անտեսում է նույն id-ով կրկնվող job-երը, իսկ worker limiter-ը սահմանափակում է job-երը ժամանակային պատուհանում։
class SyncCustomerToCrm implements ShouldQueue, ShouldBeUnique
{
use Queueable;
public function __construct(public int $customerId) {}
public function uniqueId(): string
{
return (string) $this->customerId;
}
public function middleware(): array
{
return [
new WithoutOverlapping('crm-'.$this->customerId),
new ThrottlesExceptions(10, 5 * 60), // 10 errors, then pause 5 min
];
}
public function retryUntil(): DateTime { return now()->addHours(6); }
}
09Workflow-ներ՝ chain-եր, batch-եր և parent-child flow-եր
Իրական աշխատանքը հաճախ կախվածություններով մի քանի job է։ Chain-ը job-երը գործարկում է մեկը մյուսի հետևից և կանգ է առնում, եթե մեկը ձախողվում է. գեներացնել հաշիվ-ապրանքագիրը, ապա ուղարկել այն, ապա տեղեկացնել հաշվապահին։ Batch-ը շատ job-եր գործարկում է զուգահեռ և callback է կանչում, երբ բոլորն ավարտվում են, ինչը հարմար է հազարավոր փոքր job-երի բաժանված import-ների և export-ների համար. Laravel-ը առաջընթացին հետևում է job_batches աղյուսակում, իսկ դրա job-երն օգտագործում են Batchable trait-ը։ BullMQ-ում FlowProducer-ը ստեղծում է parent-child ծառեր, որտեղ parent-ը աշխատում է միայն բոլոր child-երի ավարտից հետո և կարող է կարդալ դրանց արդյունքները։ Յուրաքանչյուր քայլ նախագծեք idempotent և փոքր, որպեսզի մեկ քայլի retry-ը չկրկնի ամբողջ workflow-ը։ Երբ ավելի ուշ քայլը ձախողվում է այն բանից հետո, երբ նախորդները փոխել են արտաքին համակարգեր, ձեզ պետք է բացահայտ փոխհատուցում, օրինակ՝ ամրագրման չեղարկում, որովհետև queue-ն չի կարող հետ գլորել վճարումը։
$batch = Bus::batch(
$orderIds->map(fn (int $id) => new ExportOrderRow($exportId, $id))->all()
)
->name('orders-export-'.$exportId)
->allowFailures()
->then(fn (Batch $batch) => BuildExportFile::dispatch($exportId))
->catch(fn (Batch $batch, Throwable $e) => report($e))
->finally(fn (Batch $batch) => CleanupExportChunks::dispatch($exportId))
->onQueue('reports')
->dispatch();
// Progress for the UI: Bus::findBatch($batch->id)->progress()
10Թողունակություն և observability՝ խորություն, տարիք և Little-ի օրենք
Քանի՞ worker է ձեզ պետք։ Little-ի օրենքը տալիս է առաջին պատասխանը. ընթացքի մեջ գտնվող job-երի միջին քանակը հավասար է ժամանման արագությանը՝ բազմապատկած մշակման միջին ժամանակով։ Եթե վայրկյանում 30 job է գալիս, և յուրաքանչյուրը տևում է 200 մվ, ձեզ պետք է առնվազն 6 զուգահեռ slot՝ պարզապես հետ չմնալու համար, և ավելին՝ պիկերի ու retry-ների համար։ Միայն queue-ի խորությունը մոլորեցնող է. 10,000 փոքրիկ job կարող է մաքրվել վայրկյանների ընթացքում, մինչդեռ 50 դանդաղը կարող է ժամերով հետ մնալ։ Ամենաօգտակար մեկ մետրիկան յուրաքանչյուր queue-ի ամենահին սպասող job-ի տարիքն է՝ ահազանգով թիրախի նկատմամբ, օրինակ՝ «email-ները 60 վայրկյանի ընթացքում»։ Հետևեք նաև թողունակությանը, ձախողումների մակարդակին և մշակման ժամանակի պերցենտիլներին։ Horizon-ը կարող է ավտոմատ հավասարակշռել worker-ները queue-երի միջև, իսկ php artisan queue:monitor-ը կարող է event արձակել, երբ queue-ն աճում է շեմից ավելի։ Multi-tenant համակարգերում հետևեք արդարությանը. մեկ tenant-ի import-ը չպետք է ուշացնի բոլորի կտրոնները։
- Worker-ները autoscale արեք ըստ ամենահին job-ի տարիքի, ոչ թե CPU-ի։
- Առանձին queue-եր ըստ ուշացման թիրախի՝ critical, default, bulk։
- Սահմանափակեք զուգահեռությունը ըստ tenant-ի ծանր job տեսակների համար։
Իրական իրավիճակներ
Վճարման webhook-ը մեկ պատվերը երկու անգամ կատարեց
Խնդիր
Հաճախորդից գումարը գանձվել է մեկ անգամ, բայց նա ստացել է երկու առաքում։ Լոգերը ցույց են տալիս, որ վճարային provider-ը նույն «payment succeeded» webhook-ը ուղարկել է երկու անգամ՝ մի քանի վայրկյան տարբերությամբ, և երկուսն էլ մշակվել են։
Ինչպես լուծել
Provider-ները webhook-ները հասցնում են առնվազն մեկ անգամ, ուստի կրկնօրինակները նորմալ են։ Յուրաքանչյուր մուտքային event պահեք provider-ի event id-ով unique index-ի տակ՝ նախքան queue դնելը. կրկնակի insert-ը ձախողվում է, և դուք վերադարձնում եք 200 առանց մշակման։ Job-ում lock արեք պատվերի տողը կամ օգտագործեք պայմանական update, օրինակ՝ status-ը դարձնել paid, որտեղ status-ը pending է, և շարունակեք միայն եթե տող է փոխվել։ Առաքումը նույնպես դարձրեք idempotent. մեկ առաքում մեկ պատվերի համար՝ ապահովված unique constraint-ով։ Գործելուց առաջ նորից վերցրեք վճարման կարգավիճակը provider-ի API-ից։ Վերջում ավելացրեք թեստ, որը նույն webhook-ը գործարկում է երկու անգամ և պնդում, որ առաքումը մեկն է։
ERP-ն վեց ժամ անհասանելի էր
Խնդիր
ERP sync-ի հետևում կանգնած հաշվապահական համակարգը մի ամբողջ կեսօր offline էր։ Հազարավոր sync job-եր ձախողվեցին, retry արվեցին և հայտնվեցին failed jobs աղյուսակում, և թիմը ստիպված էր տվյալները ձեռքով ուղղել։
Ինչպես լուծել
Նախագծեք խափանումների համար, այլ ոչ թե արձագանքեք։ Օգտագործեք ThrottlesExceptions կամ circuit breaker, որպեսզի կրկնվող ձախողումներից հետո job-երը դադար վերցնեն, այլ ոչ թե այրեն փորձերը, և retry-ները սահմանափակեք ժամանակով retryUntil-ով, օրինակ՝ 24 ժամ, այլ ոչ թե փոքր թվով փորձերով։ Backoff-ը պահեք աճող՝ վերին սահմանով։ Job-երը դարձրեք idempotent upsert-ներ՝ ըստ պատվերի id-ի, որպեսզի կրկին գործարկելն անվտանգ լինի։ Ավելացրեք health check, որը դադարեցնում է sync queue-ն, երբ ERP-ն անհասանելի է, և ահազանգում ինչ-որ մեկին։ Վերականգնումից հետո ազդված queue-ի համար կրկին գործարկեք php artisan queue:retry-ով և ստուգեք գումարները համադրման հաշվետվությամբ, որը համեմատում է երկու համակարգերը։
Մեծ export-ը ուշացնում է գաղտնաբառի վերակայման email-ները
Խնդիր
Երբ ադմինը սկսում է ապրանքների մեծ export, հաճախորդները քսան րոպե դադարում են ստանալ գաղտնաբառի վերակայման և պատվերի հաստատման email-ներ։
Ինչպես լուծել
Ամեն ինչ աշխատում է մեկ default queue-ով և worker-ների մեկ խմբով, ուստի export-ի հազարավոր կտորներ կանգնած են email-ների առջև։ Queue-երը բաժանեք ըստ ուշացման թիրախի. critical՝ նույնականացման email-ների և կտրոնների համար, default՝ սովորական աշխատանքի համար, reports՝ export-ների և import-ների համար։ Critical-ի համար գործարկեք առանձին worker-ներ, որպեսզի այն երբեք չմնա առանց ռեսուրսի, և սահմանափակեք reports queue-ի զուգահեռությունը, որպեսզի այն չսպառի նաև բազան։ Horizon-ով յուրաքանչյուր queue խմբի համար սահմանեք supervisor-ներ՝ իրենց պրոցեսների քանակով։ Ավելացրեք ահազանգ critical queue-ի ամենահին job-ի տարիքի վրա, օրինակ՝ 60 վայրկյանից բարձր, որպեսզի հաջորդ regression-ը արագ բռնվի։
Արագ ուղեցույց
- Retry անել Laravel-ի բոլոր ձախողված job-երը
php artisan queue:retry all- Retry անել մեկ queue-ի ձախողված job-երը
php artisan queue:retry --queue=emails- Ցուցակել ձախողված job-երը
php artisan queue:failed- Ջնջել 48 ժամից հին ձախողված job-երը
php artisan queue:prune-failed --hours=48- Event արձակել, երբ queue-ն չափից երկար է
php artisan queue:monitor redis:default,redis:emails --max=100- Վերագործարկել worker-ը՝ հիշողության աճը սահմանափակելու համար
php artisan queue:work --max-jobs=1000 --max-time=3600- Մշակել queue-ն և դուրս գալ (CI, միանգամյա container-ներ)
php artisan queue:work --stop-when-empty- BullMQ՝ անտեսել կրկնվող job-երը
await queue.add('sync', data, { deduplication: { id: 'customer-' + id } })- BullMQ՝ cron ժամանակացույց
await queue.upsertJobScheduler('nightly-report', { pattern: '0 2 * * *' }, { name: 'report' })- Redis-ը չպետք է evict անի job-երը
maxmemory-policy noeviction
Իմ իրական նախագծերում
- Multi-tenant POS SaaS-ում domain event-ները transactional outbox-ի միջով անցնում են BullMQ (մոտ 20 queue), իսկ HMAC-ով ստորագրված webhook-ները առաքվում են queue-ի միջոցով։
- Կենդանիների ապրանքների պլատֆորմում ERP և հաշվապահական sync-ը աշխատում է որպես պլանավորված, feature flag-ով կառավարվող job-եր՝ health alert-ներով և գնի հետ վերադարձով։
- Մարզասրահի CRM-ում պլանավորված job-երն աշխատում են ֆիքսված, չհամընկնող հերթականությամբ (unfreeze, expire, activate), իսկ SMS-ը կրկնվում է միայն միացման ձախողման դեպքում, որպեսզի ոչ մի հաղորդագրություն երկու անգամ չուղարկվի։
- Production սերվերներում Laravel worker-ները աշխատում են supervisor-ի ներքո, Node հավելվածները՝ pm2-ի, իսկ scheduler-ները շարժում է cron-ը։
Հարցեր և պատասխաններ
Q1Ի՞նչ է նշանակում idempotent job-ի համար, և ինչպե՞ս եք դրան հասնում։
Idempotent job-ը տալիս է նույն արդյունքը՝ անկախ նրանից, այն մեկ անգամ է գործարկվում, թե հինգ։ Դա կարևոր է, որովհետև queue-ները առաքում են at least once. worker-ը կարող է crash լինել աշխատանքն անելուց հետո, բայց մինչև acknowledge անելը, և job-ը նորից կգործարկվի։ Դրան հասնում եմ unique key-երով և վիճակի ստուգումներով. ստեղծում եմ հաշիվ-ապրանքագիրը միայն եթե այս պատվերի համար այն դեռ չկա, օգտագործում եմ upsert-ներ, մշակված event id-ները գրանցում եմ unique constraint-ի ներքո և idempotency key-եր եմ փոխանցում վճարային ու հաղորդագրման պրովայդերներին։ Նաև side effect-ները պահում եմ job-ի վերջում և transaction-ների միջոցով խուսափում մասնակի գրառումներից։
Q2Ինչպե՞ս եք նախագծում retry-ները արտաքին API կանչող job-ի համար։
Նախ դասակարգում եմ սխալները։ Timeout-ները, միացման սխալները, 429 և 5xx պատասխանները սովորաբար ժամանակավոր են, ուստի կրկնում եմ դրանք։ 4xx validation սխալները մշտական են, ուստի անմիջապես ձախողում եմ և գրանցում պատճառը։ Ժամանակավոր սխալների համար օգտագործում եմ exponential backoff՝ jitter-ով և փորձերի սահմանափակ քանակով՝ հարգելով Retry-After header-ները։ Յուրաքանչյուր կանչ ունի timeout, որը կարճ է job-ի timeout-ից։ Վերջին փորձից հետո job-ը հայտնվում է failed կամ dead-letter պահոցում՝ ամբողջական context-ով, alert է գործարկվում, և կա միջոց այն նորից գործարկելու, երբ պատճառը շտկված է։ Job-ը պետք է idempotent լինի, որպեսզի retry-ներն անվտանգ լինեն։
Q3Ի՞նչ է dead-letter queue-ն։
Դա այն վայրն է, ուր հաղորդագրությունները գնում են բոլոր retry-ներում ձախողվելուց հետո, որպեսզի դադարեն արգելափակել կամ ռեսուրսներ այրել, բայց չկորչեն։ Laravel-ում դա failed_jobs աղյուսակն է, BullMQ-ում՝ failed set-ը, SQS-ում կամ RabbitMQ-ում՝ առանձին queue։ Dead-letter queue-ն օգտակար է միայն այն դեպքում, եթե ինչ-որ մեկը հետևում է դրան, ուստի նոր գրառումների վրա alert եմ դնում և հետևում քանակին։ Յուրաքանչյուր գրառում պետք է ներառի payload-ը, սխալը և փորձերի պատմությունը։ Բագը շտկելուց կամ outage-ը սպասելուց հետո job-երը նորից գործարկում եք։ Failed job-երի օրինաչափությունները նաև ինտեգրացիոն խնդիրների հիանալի ազդանշան են։
Q4Ինչու՞ օգտագործել outbox pattern, այլ ոչ թե job-ն ուղղակիորեն dispatch անել։
Որովհետև տվյալների բազայում գրելը և հաղորդագրություն հրապարակելը երկու առանձին համակարգեր են, և մեկը կարող է ձախողվել մյուսի հաջողվելուց հետո։ Եթե dispatch եք անում transaction-ի ներսում, և transaction-ը rollback է լինում, worker-ը մշակում է մի իրադարձություն, որը երբեք տեղի չի ունեցել։ Եթե dispatch եք անում commit-ից հետո, և պրոցեսն արանքում crash է լինում, իրադարձությունը կորչում է։ Outbox-ի դեպքում իրադարձությունը insert եք անում աղյուսակում նույն transaction-ում, ինչ բիզնես փոփոխությունը, այնպես որ կամ երկուսն էլ commit են լինում, կամ ոչ մեկը։ Այնուհետև relay-ը կարդում է outbox-ը և հրապարակում queue-ում՝ տողերը նշելով որպես ուղարկված։ Consumer-ները մնում են idempotent, որովհետև առաքումը at least once է։
Q5Ինչպե՞ս եք աշխատեցնում և մոնիթորինգ անում worker-ները production-ում։
Worker-ները երկարակյաց պրոցեսներ են, ուստի աշխատում են process manager-ի ներքո՝ supervisor կամ systemd Laravel-ի համար, pm2 կամ systemd Node-ի համար, կամ առանձին container-ներ։ Queue-ները բաժանում եմ ըստ առաջնահերթության և concurrency-ն չափում յուրաքանչյուր queue-ի համար, որպեսզի մեծ export-ը չկարողանա «սովամահ անել» գաղտնաբառի վերականգնման email-ները։ Worker-ները վերագործարկվում են յուրաքանչյուր deploy-ի ժամանակ և graceful կերպով անջատվում՝ ավարտելով ընթացիկ job-ը։ Մոնիթորինգը ներառում է queue-ի խորությունը, job-ի տարիքը, ձախողումների տոկոսը և մշակման ժամանակը, alert-ներով, երբ ամենահին job-ը չափազանց հին է։ Horizon-ը կամ BullMQ dashboard-ը օգնում են դիտարկման հարցում, բայց հենց alert-ներն են ձեզ արթնացնում, երբ ինչ-որ բան խրված է։
Սենիոր մակարդակի հարցեր
S1Ինչպե՞ս եք երաշխավորում հերթականությունը ըստ entity-ի, երբ queue-ն մշակում են շատ worker-ներ։
Զուգահեռ worker-ներով գլոբալ հերթականությունն անհնար է առանց զուգահեռությունից հրաժարվելու, ուստի ես հարցնում եմ, թե իրականում ինչին է պետք հերթականություն։ Սովորաբար դա մեկ entity-ի event-ներն են. պատվերի կարգավիճակի փոփոխությունները կամ մեկ հաճախորդի հաշվեկշռի թարմացումները։ Տարբերակներ. entity-ի բոլոր job-երն ուղղորդել նույն partition-ին կամ խմբին և յուրաքանչյուր խումբ մշակել հաջորդաբար, ինչը natively աջակցում են Kafka-ի partition-ները, SQS FIFO message group-երը և BullMQ Pro-ի group-երը։ Պարզ Laravel կամ BullMQ queue-երի դեպքում ես օգտագործում եմ lock ըստ entity-ի, օրինակ՝ WithoutOverlapping՝ entity id-ով, գումարած version համար յուրաքանչյուր event-ում, որպեսզի handler-ը անտեսի պահված վիճակից ավելի հին ամեն ինչ։ Հաճախ ամենապարզ ամուր դիզայնն այն է, որ handler-ները ընթացիկ վիճակը կարդան բազայից, այլ ոչ թե վստահեն event-ի payload-ին, և այդ դեպքում հերթականությունն ավելի քիչ է կարևոր։
S2Redis queue-եր, RabbitMQ, SQS թե՞ Kafka. ինչպե՞ս եք ընտրում։
Սկսեք աշխատանքից, ոչ թե գործիքից։ Մեկ պրոդուկտի ներսում ֆոնային աշխատանքի համար, ինչպիսիք են email-ները, sync-երն ու export-ները, Redis-ի վրա հիմնված queue-երը, օրինակ՝ Laravel queue-երը կամ BullMQ-ն, պարզ են, արագ և արդեն stack-ում են. փոխզիջումն այն է, որ հուսալիությունը կախված է Redis-ի persistence կարգավորումներից և հիշողությունից։ SQS-ը լիովին managed է և շատ հուսալի՝ ներկառուցված visibility timeout-ներով և dead-letter queue-երով, բայց ունի ավելի բարձր ուշացում և AWS lock-in։ RabbitMQ-ն ավելացնում է հարուստ routing, exchange-ներ և ըստ հաղորդագրության acknowledgement-ներ, ինչը հարմար է հրամաններ փոխանակող բազմաթիվ ծառայությունների համար։ Kafka-ն կրկին գործարկելի event log է partition-ներով և consumer group-երով, իդեալական բարձր թողունակության stream-ների, event sourcing-ի և բազմաթիվ անկախ սպառողների համար, բայց ավելի ծանր է շահագործման մեջ։ Ես ընտրում եմ Redis queue-եր, մինչև հայտնվի կոնկրետ կարիք՝ replay, միջթիմային stream-ներ կամ հսկայական թողունակություն։
S3Ինչպե՞ս եք որոշում worker-ների խմբի չափը և autoscale անում այն։
Ես չափում եմ ժամանման արագությունը և մշակման ժամանակը ըստ job տեսակի և հիմք ընդունում Little-ի օրենքը. անհրաժեշտ զուգահեռությունը հավասար է վայրկյանում job-երի քանակին՝ բազմապատկած մեկ job-ի միջին վայրկյաններով, գումարած պաշար պիկերի և retry-ների համար։ Այնուհետև ստուգում եմ, թե ինչից են կախված worker-ները, որովհետև ավելի շատ worker-ները կարող են պարզապես bottleneck-ը տեղափոխել բազա կամ rate limit ունեցող գործընկերոջ API։ Autoscaling-ը պետք է արձագանքի ամենահին սպասող job-ի տարիքին կամ խորությանը՝ բաժանած թողունակությանը, ոչ թե CPU-ին, քանի որ շատ job-եր I/O-bound են։ Յուրաքանչյուր queue ստանում է իր ուշացման թիրախը և worker-ների իր խումբը, իսկ ծանր queue-երի վրա սահմանափակումներ կան, որպեսզի դրանք չխեղդեն կրիտիկականները։ Scale-in-ը պետք է նուրբ լինի. worker-ները դադարում են job վերցնել, ավարտում են ընթացիկը և դուրս գալիս grace period-ի ընթացքում, այլապես job-երը retry են արվում։
S4100,000 տողի batch import-ը մասամբ ձախողվեց։ Ինչպե՞ս պետք է վարվի համակարգը։
Այն պետք է ավարտի այն, ինչ կարող է, հստակ հաղորդի ձախողվածը և թույլ տա անվտանգ կրկին գործարկում։ Ես import-ը բաժանում եմ կտորների կամ տողերի խմբի համար մեկ job-ի՝ batch-ի ներսում, որը թույլ է տալիս ձախողումներ, այնպես որ մեկ վատ տողը չի չեղարկում մնացածը։ Վավերացման սխալները մշտական են, ուստի դրանք գրանցվում են ըստ տողի՝ պատճառով, և երբեք retry չեն արվում. անցողիկ սխալները, օրինակ՝ timeout-ները, retry են արվում backoff-ով։ Յուրաքանչյուր գրառում upsert է՝ բնական id-ով, այնպես որ ամբողջ import-ը կամ միայն ձախողված կտորները կրկին գործարկելը երբեք չի կրկնօրինակում տվյալները։ Երբ batch-ն ավարտվում է, օգտատերը ստանում է ամփոփում՝ ներմուծված, բաց թողնված և ձախողված, ներբեռնելի սխալների ֆայլով, և կարող է ուղղել ու կրկին վերբեռնել միայն այդ տողերը։ Փոխզիջումն ավելի շատ հաշվառումն է, բայց այլընտրանքը՝ «ամբողջը կամ ոչինչ» import-ները, մեծ ֆայլերի բեռնումը դարձնում է գրեթե անհնար։
Բառարան
- Visibility timeout
- Որքան ժամանակ է վերցված job-ը թաքնված մնում մյուս worker-ներից, նախքան broker-ը ենթադրի ձախողում և կրկին հասցնի այն։
- At-least-once delivery
- Երաշխիք, որ հաղորդագրությունը կհասցվի, հնարավոր է՝ մեկից ավելի անգամ, ուստի handler-ները պետք է հանդուրժեն կրկնօրինակները։
- Idempotency key
- Գործողության հետ ուղարկվող եզակի id, որպեսզի ստացողը կարողանա ճանաչել և անտեսել կրկնությունները։
- Poison message
- Job, որն ամեն անգամ ձախողվում է իր բովանդակության պատճառով և առանց retry-ի սահմանի կարգելափակեր կամ հավերժ կպտտվեր։
- Stalled job
- BullMQ-ում job, որի worker-ը դադարել է թարմացնել lock-ը. այն վերադարձվում է waiting կամ failed։
- Job batch
- Միասին հետևվող job-երի խումբ՝ callback-ներով, երբ բոլորը հաջողվում են, որևէ մեկը ձախողվում է կամ բոլորն ավարտվում են։
- Little-ի օրենք
- Համակարգում job-երի միջին քանակը հավասար է ժամանման արագությանը՝ բազմապատկած մեկ job-ի միջին ժամանակով. օգտագործվում է worker-ների քանակը որոշելու համար։
- Circuit breaker
- Պաշտպանիչ, որը որոշ ժամանակով դադարեցնում է ձախողվող ծառայության կանչերը՝ նրան վերականգնվելու ժամանակ տալով։
- Արդարություն (fairness)
- Երաշխիք, որ մեկ tenant կամ job-ի տեսակ չի կարող մենաշնորհել worker-ները և ուշացնել մնացած բոլորին։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։