Стварэнне надзеяных систем фонавых заданняў з BullMQ і Redis
Выучыце, як проекаваць стойкія пайплайны фонавых заданняў у Node.js з выкарыстоўваннем BullMQ і Redis, што включае павторныя спробы, канкурэнцію, ідемпотентнасць і манітарынг.
Адрасаванне піштовай листа, стварэнне звярнення, обрабоцка платежа — багато задач у фонавай частыні не патрабуе завершэння перш, чым вы адпавядаеце на корыстніка. У гэтым кансалтатэ разбіраецца, як стварыць надзяйныя системы фонавых задач с викорыстаннем BullMQ у поўнасою з’едначэнні з Redis.
Уявіце сабе фонавую частыню, дзе практычна кожная задача выконвалася безпосередзе ў цыклі HTTP-запита. Патрэбна піштовая лист? Адрадзейце ёй там жа. Патрэбна стварыць PDF? То ж самае. Патрэбна обрабоцка якіх-небудзь дадзеных у фонавай частыні? Зрабіце гэта таксама тут жа.
Такі падход спачатку працуе добра. А потым перестае працаваць.
API пачынае сповольняцца. Запиты пачынаюць таймаутаваць. А якшо якаясь зовнішняй сервіс зупініцца, весь запит можа таксама зазнаць няудачы.
Самэ гэткі момент, калі фонавыя задачы стаюць значнымі.
У замест на тое, каб прымусіць API завершыць кожны крок перад адпаведзеннем, можна паклаць цю роботу ў очэрэдгу і дазволіць спецыяльнаму працоўніку адмахнуцца яе окрема.
Хорашы варант у экасістэме Node.js — BullMQ, які падтрымваецца Redis для зберагчэння. Ось як усе гармоніюе разам.
1. Што такое фонавая задача?
Фонавая задача — гэта будзь-якае заведамасць роботы, якая не павінна выконвацца сінхрональна як частка HTTP-запыту.
Разглянем тыповы процес рэўістраціі. Калі хтось стварае аблік, API можа патрабаваць:
- Стварыць запис пра пользователя
- Апраўляць пісьмо з прыветам
- Склапаць PDF-файл з прыветам
- Апраўляць паведамленне
- Адкорэгаваць іншую систему
Можна спробаваць выконваць усё гэта адразу перад адпаведзеннем:
Client
↓
API
↓
Create User
↓
Send Email
↓
Generate PDF
↓
Send Notification
↓
Response
Але гэта прымусвае пользователя чакаць, пакуль завершыцца кожны з эўх крокаў.
Лепшы падход выглядае так:
Client
↓
API
↓
Create User
↓
Add Job to Queue
↓
Response
І окремае:
Queue
↓
Worker
↓
Send Email
↓
Done
Пакалькі API больш не павінна выканаць усе заведамыя перш чым адпавядаць, таму яна адпавядае набліжна шырэй.
2. Чаму нам патрабуецца канэ?
Падазроўваючы, што адправка пісьма займае 1 секунду, стварэнне PDF — 2 секунды, а вызов іншага API — 1 секунду. Ваша тэрміналная точка можа застацца у стане чакання калькі секунд прычымоўна да таго, калі адправіць адпаведзь — гэта паслаблены досвід для пользователя.
Ёшча горэ, што будзе, якщо прадаўцу электронных пісьма не можна будзе дасягнуць? Запрос можа зазнаць неудачы, нават якшо стварэнне пользователя фактычна успела. Гэта непатрэбная залежнасць між двума несувязанымі аспектамі.
Канэ разрыхляе гэтая зв’язок:
┌──────────────┐
│ Node API │
└──────┬───────┘
↓
Add Job
↓
┌──────────────┐
│ Redis │
│ Queue │
└──────┬───────┘
↓
┌──────────────┐
│ Worker │
└──────┬───────┘
↓
Email / PDF / API / etc.
З такім раздзеленнем API і фонавая задача кожная маюць сваю адзіну адпаведальнасць.
3. Чаму ёсць BullMQ?
BullMQ — это бібліятэка для очакоў у Node.js, яка выкорыстоўвае Redis для зберагачвання та коордынацыі задань. Їе архітектура, у загальных рысах, выглядае так:
Producer
↓
Queue
↓
Worker
↓
Job Processing
Продюсер — гэта тое, што стварае заданні. Очак зберагачвае іх. Рабочы процес — гэта тое, што фактычна ўпрацоўвае іх.
Напрыклад:
await emailQueue.add("welcome-email", {
userId: user.id,
email: user.email
});
По суты, API кажа:
"Є калькі роботы, якія трэба адрабатваць."
Ён сам не павінен выканаць гэтую роботу.
4. Стварэнне очака
Ось як выглядае мінімальна наладка очака BullMQ:
import { Queue } from "bullmq";
const connection = {
host: "localhost",
port: 6379
};const emailQueue = new Queue("email", {
connection
});
Звярху можна дадаць заданні ў яго:
await emailQueue.add("welcome-email", {
userId: "123",
email: "user@example.com"
});
Redis адпавядае за зберагачванне всіх станоў, related да очака, у тылу. Канцэптуальна можна яго спрацаваць так:
email queue
Job 1
Job 2
Job 3
Job 4
Job 5
Потым працоўнік бере і выкананае гэтыя заведаменні.
5. Стварэнне працоўніка
Працоўнік — гэта та частка, якая фактычна выкананае работу:
import { Worker } from "bullmq";
const worker = new Worker(
"email",
async (job) => {
console.log("Processing:", job.name); await sendWelcomeEmail(
job.data.email
);
},
{
connection
}
);
Якща з’едыніць усё разам, тады прабег справ працюе так:
API
↓
emailQueue.add()
↓
Redis
↓
Worker
↓
sendWelcomeEmail()
API не павінен чакаць, калі будзе завершана адправка паведамлення — і гэта ўсё галоўная перадчыннае заведаменні.
6. Што выходзіць, калі заведамленне не выкананае?
Самэль тут кола пачынае паказваць свою справжню перадчыннае пра звычны вызов службы.
Уявіце сабе такую наладку:
API
↓
Email Service
↓
ERROR
Калі вы вызываеце API безпосередна, вас прымушваюць негайна прыняць рашэнне ў зв’язку з неудачай.
Кола дае вам іншую альтернатыву: заведаменне можна проста спробаваць зноў.
Ось прыклад:
await emailQueue.add(
"welcome-email",
{
email: "user@example.com"
},
{
attempts: 3
}
);
За такой настройкі прабоўкі виканання задачы дазволеныя прыбліжна колькісць разоў, перш чым будзе здана.
Візуальна схема выглядае так:
Attempt 1
↓
Failed
↓
Attempt 2
↓
Failed
↓
Attempt 3
↓
Success
Этот патэрн ўсёлякі час корыстны, калі працюеце з ненадзвежнымі трэцімі сторонамі.
Але прабоўкі не должны быць безмежнымі чы абдуманымі.
Утримайцеся ад ситуацыі, калі задача будзе прабоўваць виканацца без канца без жадных меж.
7. Прабоўкі з адстаўкай
Падазроўваю, што зовнішняй сервіс тымчасава не працуе.
Трэба утримацца ад ситуацыі, падобнай да гэтай:
FAIL
RETRY IMMEDIATELY
FAIL
RETRY IMMEDIATELY
FAIL
RETRY IMMEDIATELY
Негаразды з сервісам можна толькі пагаршыць, якшо адразу прабаваць виканаць задачу знову і знову.
Рашэння — адстаўка між прабоўкамі.
Напрыклад:
await emailQueue.add(
"welcome-email",
{
email: "user@example.com"
},
{
attempts: 5,
backoff: {
type: "exponential",
delay: 5000
}
}
);
Гэта як гэта выглядае канцэптуальна:
Attempt 1 → Fail
↓
5 sec
↓
Attempt 2 → Fail
↓
10 sec
↓
Attempt 3 → Fail
↓
20 sec
↓
Attempt 4 → Success
Точны час залежыць ад таго, як вы настаўілі стратэгію прабоўкаў і адстаўкі.
Але основная ідея застаецца тая ж:
Дазвольце тымчасовым неудачам відновіцца, прытым як прабаваць зноў.
8. Заданні з адкладэнным выконанням
Не кожна задача павінна выконвацца негайнаа пасля стварэння.
Напрыклад:
Адправіць нагадванне чераз 24 гадзіны пасля рэўістрацыі.
BullMQ дазваляе запланаваць задачу на выконанне пазней.
await emailQueue.add(
"reminder",
{
userId: "123"
},
{
delay: 24 * 60 * 60 * 1000
}
);
Канцэптуальна:
Create Job
↓
Wait 24 hours
↓
Worker processes job
Этот шаблон выклікаецца ў такіх ситуацыях:
- Нагадванневыя электронныя лісты
- Запланаваныя паведамленні
- Заканчэнне прыбутковага періяду
- Нагадванні пра платеж
- Паслядуючыя поведамленні
9. Калькольвыя працоўнікі
Тепер уявіце сястэму, якая прымае тысячы задач у хвіліну.
Одны працоўнік можа не справіцца з такым обсягам.
Можна расшырыць магчымасці, запускаючы калькольвых працоўнікаў адразу:
Redis Queue
↓
┌──────────┼──────────┐
↓ ↓ ↓
Worker 1 Worker 2 Worker 3
↓ ↓ ↓
Jobs Jobs Jobs
Кожны з іх незалежна адчыняе заданні з очакальнай лісты.
Напрыклад, якшто:
1000 email jobs
Можна пабачыць нечацага такога:
Worker 1 → Job 1, 4, 7...
Worker 2 → Job 2, 5, 8...
Worker 3 → Job 3, 6, 9...
Дадзенне большай колькасці працоўнікаў — адна з способаў падвышэння працэсавой здатнасці.
Але будзьце абераглівы:
Дадзенне большай колькасці працоўнікаў да задачы не завжды ёсць парадны выкарыстоўванне.
Ваша база дадзенаў, ваш прадавець электранякі, ваша CPU, ваша память і будзь-якія наступныя сервісы адпаведна маюць свае ліміты ўместнасці.
10. Канкурантнасць
Пашчырэй за запуск кальколькі працоўнічых процэсаў, BullMQ таксама дазволяе насталяваць, сколькі заданніў можа адразу обрабляць адзін працоўнік.
Напрыклад:
const worker = new Worker(
"email",
async (job) => {
await sendEmail(job.data.email);
},
{
connection,
concurrency: 5
}
);
Гэта дазволяе аднаму працоўніку паралельна обрабляць кальколькі заданніў.
Канцэптуальна:
Worker
├── Job 1
├── Job 2
├── Job 3
├── Job 4
└── Job 5
Вышэйшая канкурантнасць можа падвысіць працэсавую здатнасць.
Але не трэба проста падняць рэгулярнасць адзначэння задач да 100 без рашучага разважэння.
Якщо кожная задача запрашвае вашу базу дадзэння, высока рэгулярнасць можа лёгкая перавантажыць яе.
Наладжэнні рэгулярнасці трэба налаштаваць так, каб яны падходзілі таму, што насправды можа вытрымаць ваша навантажэння.
11. Обмежэнне частоты запрашоў
Інодзе вузькія месца зовсім не ў вашай сэрвызе — яны ў стороннім сервісе, ад каго вы залежыце.
Сказаўм, ваш прадавець электранякі обмежывае вас фіксаваным колькістю запрашоў у секунду.
Якщо раптам у вас будзе:
10,000 jobs
вы не хачаце атрымваць усія запрашанні заодно.
Очэквальнік можа зменшыць швал, яким обрабоўваюцца задачы.
Рэзультатныя архітектуры выглядаюць так:
10,000 Jobs
↓
Queue
↓
Rate Limit
↓
Worker
↓
External API
Гэта набагато безпечней, чым атрымваць тыячы адночасовых запрашоў у прадавца.
12. Ідэмпотентнасць задач мае значэнне
Эя наступная ідея ўваходзіць у склад найважлівейшых прынцыпаў обробкі заданняў у фоне.
Разглядзім задання на обробку платежаў:
Process Payment
Рабочы процес яго запускае.
Платеж адбываецца успешна.
Але прычаму до таго, як рабочы процес пазначае його як завершаны, працэс зламваецца.
Очакальнік, выкананыя адпаведна да свайго парадку, перапрыбліжвае выкананне задання.
Без захоўнічых мераў можа выйсці так, што кліенту будзе выкладзены платэж во два разы.
Это справжняя і дорогая проблема.
Ёй запобiec трэба, калі толькі гэта можліва, прабаваць напісаць задання так, каб яны былі ідэмпатныя.
На практыцы гэта значыць, што два разы запуск адно і тое ж задання не должна ствараць небажанных дублікатных наследків.
Аднам з распашчых падходаў ёсць викорыстанне унікальнага ідэнтыфікатора платежа:
payment:order_123
Потым, перш чым працаваць, трэба пераканацца:
Has this payment already been completed?
↓
Yes → Don't charge again
↓
No → Process payment
Сам BullMQ не мае вбудованага механізма для гэтаго.
Забезпечыць ідэмпотентнасць трэба ў коде вашай аплікацыі.
13. Неудачныя заданні трэбуе стратэгіі
Не всі неудачы є роўныя, і не кожна з іх варта перапрыбутку.
Разглянем калькі прыкладоў:
Invalid email
Invalid user ID
Missing database record
Invalid payment information
Пры запуску гэтых заданняў знову пяць разоў нічога не будзе выправлена.
Дапамагае раздзеліць неудачы на два катэгорыі:
Тымчасовыя неудачы
Да іх належаць такія прыклады:
- Тайма-аут сеті
- Залежнасць, якая на час не ў доступе
- Разырванне з’язку з базай дадзеных
Гэта тыя проблемы, калі перапрыбутка пазней даскледзе сэнсу.
Пастаяныя неудачы
Да іх належаць такія прыклады:
- Некоректныя даны вхіду
- Рэсурс, на які ўказваецца, больш не існуе
У такіх случаях прабавы перадзванаць беспрацэйныя — задача трэба без адзінаго разу направіць на шлях адрабаткі нявыпалоў.
Якісна спроектаваная структура очаквання не проста дазволяе следаваць загальнаму правілу:
Retry everything
У замене ёна выкорыстоўвае болей продуманы прайсупутак:
Understand why it failed
↓
Temporary?
/ \
YES NO
↓ ↓
Retry Handle failure
14. Адрабатка нявыпалоў задач
Незалежна ад таго, насколькі вы астарожны, дзеянні будуць нявыпаліцца так, што іх не можна будзе парадзвануць. Вам трэба бачыць гэтыя задачы, каб яны не проста зніклі.
Напрыклад, можа выйсці ў такім стане:
Failed Jobs
──────────────
Job 101 → Email invalid
Job 102 → Payment failed
Job 103 → API timeout
Калі вы зможаце бачыць гэтыя нявыпалы, у вас будуць варыянты:
- Запісаць прычыну нявыпалоў для пазнейшага аналізу
- Паведаміць вашую команду
- Даць каму-небудзь можлівасць перадзвануць задачу ручна
- Выправіць некоректныя даны, якія сталі прычыной
Тое, як саме паблікуваць гэта заданне, залежыць ад патрэбаў вашай системы. Найважлівейшым є аднолькі прынцып:
Неудачныя заданні ніколі не должны зникнуць без следа.
15. Очакальнік проты заданняў Cron
Лёгка сплутаць гэтыя два элементы, але яны рашаюць разныя проблемы.
Заданнеў Cron мета — сказаць:
"Выконай гэты заведамы тады.
Заданнеў очакальніка мета — сказаць:
"Апрацаваць гэты элемент работы.
У практыцы гэтыя два інструменты часта добра працуюць разам. Напрыклад:
Cron
↓
Find users whose trial expires today
↓
Create jobs
↓
Queue
↓
Workers
↓
Send emails
Гэта дазволяе выдзеліць логіку планавання ад логіки обробкі. Зазвычай гэта чыстейшы дизайн, чым калі адна процэса Cron прабуе сама выконаць усю работу.
16. Падзеі в очакальніку і манітарынг
Калі вы запускаеце гэта ў працоўнай сіткі, вам патрэбна можлівасць бачыць, што на самай працэ ўнутрь очакальнага ряду.
Паметкі, якія варта стежыць, включаюць:
- Заданні, якія чакаюць на обробку
- Заданні, якія зараз обрабоўваюцца
- Заданні, якія былі успешна завершаны
- Заданні, якія зазналі неудачы
- Час, які трэбуецца на обробку
- Колькі разоў адбываецца праказка
- Аб’ём очакальнага ряду
Уявіце панель керування, якая раптам паказвае ўдзейсненне чагоś такога:
Waiting Jobs
Normal: 50
Current: 25,000
Такі стрымкі перапад ёсць сігналам апавяшэння. Гэта можа значыць:
- Вашы працоўнікі перасталі работаць
- Зовнішня API спавільнілася
- Ваша база дадзенаў перабоўлена
- Навантажэння зрасла
- Няўдалы запуск прывёз баг
Якщо вы не стежыце за сваёй очэрэдзю, гэтыя проблемы можу накаплівацца непазорна, пакуль корыстнікі не пачнуць з’являць, што ўсё не так.
17. Не ставіце все ў очэрэдзь
Няветка BullMQ не значыць, што кожная аперацыя павінна выконвацца у фоновай задачы.
Возьмімо прыклад:
GET /profile
У гэтым случае корыстнік чакае на даны свайго прафіля негараздо. Перадача гэтага у фоновую очэрэдзь не мала б сенсу — гэта толькі дадзе непатрэбны затрымкі.
Очэрэдзь мае сенс, калі:
- Робота выконваецца за дзвільны час
- Роботу можна выконваць асінхроно
- Можа знадобіцца практыкаваць роботу знову
- Робота вимагае большых ресурсоў
- Робота залежыць ад зовнішняйых служб, якія не ўсё час адпаведны
- Рэзультат не павінен быць частью негараздо адпаведнай адпаведзі
Корыстны вопыт, які трэба задаць:
Чы гэты рэзультат даскліваўся корыстніку прытаму, перш чым вы адправляеце адпаведны HTTP-адказ?
Якщо няма, варта рассмотрзець можлівасць выканання гэтай роботы у фонавым режыме.
18. Архітэктура у стыле працоўнай среды
Якшто аднаць усё гэта разам, тыповая наладка выглядае так:
Client
↓
Node.js API
↓
┌──────┴──────┐
↓ ↓
PostgreSQL Redis
↓
Queue
↓
┌──────────┼──────────┐
↓ ↓ ↓
Worker 1 Worker 2 Worker 3
↓ ↓ ↓
Email PDF Notifications
Слой API адпаведае за тое, што патрэбна зрабіць негаю. PostgreSQL (альбо іншая выбраная вам база дадзенаў) храніць вашы стойкія бізнес-дадзеныя. Redis падтрымлівае інфраструктуру канвою і іншыя короткакалендарныя завадзі, дзе ён паслужыць эфектыва. Рабочыя процесы адпаведаюць за всё, што можа выканацца асінхронна.
Такое раздзелэнне абавясцей значна спрыяе масштабаванню цэлай системы.
19. Памылкі, якіх варта утрымацца
Памылка 1: Выкананне всьго ўнутры HTTP-запыту
Гэта прыводзіць да API, якія ўсё адночасна є повольнымі і кранкавымі.
Памылка 2: Павтарэнне спроб без лімітаў
Дзеяныя неудачы проста не будуць рашаныя, незалежна ад таго, сколькі разоў вы прабуеце.
Памылка 3: Ігнараванне ідэмпотентнасці
Якщо задача выконваецца два разы, гэта можа спрычыніць дублікатныя паслядковыя эфекты, якіх вы не хацелі.
Памылка 4: Дазвол на необмежаную канкуранцію
Без лімітаў існуе рызык перавантажыць системы, ад яых залежаць вашы задачы.
Памылка 5: Ігнараванне монітарынгу
Чэрга, якая продовжвае растаць без контролю, — это проблема керування, якая жадае рашэння.
Памылка 6: Іспользованне Redis як системы зберагача данных
Стан чэргі і основныя бізнес-данные служа разным цылям і не трэба ўзаўме плутаць.
Памылка 7: Асінхроннае веданне всьго
Дзеяныя операцыі сапраўды патрабуюць завершэння перш, чым можна будзе адправіць адпаведны ўтвар.
20. Кращая ментальная модэль
Перш чым зрозумець, што такое очэры, прыродны інстынкт падказвае думаць пра обробку запыта так:
Request
↓
Do everything
↓
Response
Адносна корыстнейшыя модэлі выглядаюць так:
Request
↓
Do what must happen immediately
↓
Queue what can happen later
↓
Response
Пасля чаго:
Queue
↓
Worker
↓
Process
↓
Retry if appropriate
↓
Complete / Fail
Гэта перакіранне — адделенне таго, што павінна выконацца зараз, ад таго, што можа выконацца пазней, — являеся галоўной ідеяй усьго гэтага.
Заключны вывад
BullMQ ёсць корыстны не толькі таму, што яўляецца шырока вжываемай бібліятэкай для Node.js. Ён корыстны таму, што обробка фонавых заданняяў задовольняе рэальную архітектурную потрэбу.
Якщо якае-небудзь заведама:
- Павольна выконваецца
- Можа быць перапрыбутае
- Не павінна блакануць адпаведзь
- Залежыць ад зовнішняй службы
- Трохі ресурсаў
тады яго, верагодна, не трэба размешчаць у цыкле HTTP-запытаў.
Чарга дазволяе заданню існаваць у пэўным месца. Redis забезпечвае базовую інфраструктуру. BullMQ керуе адправленням заданняў. Рабочыя процесы выконваюць сама обробка. Перапрыбуткі дапамагаюць распрацаваць тымчасовыя неудачы. Наладкі канкурэнцыі дапамагаюць контролаваць праходзячую спроможнасць. Монітарынг паведамляе пра тое, калі ўсё не так. А рэштрыкованыя прыемлі аплявацыі гарантуюць, што заданні можа безпечна выконваліцца больш чым раз, калі гэта стане неабходным.
Галоўны урок тут такі:
Не кожна проблема павінна быць рашаная ў межах цыклу запит-адказ.
Інодзе правы адказ, які трэба адправіць, ёсць проста:
"Я прийняў заданне. Мы распрацуем усё рэшта."
Спадневана літэратура
- Проектаванне бэкендаў для чату у рэальным часе: камнеры, зберагачванне дадзейнаў і масштабаванне — Дазвольце вам дазнацца, як спроектаваць бэкенд для чату у рэальным часе з выкарыстоўваннем Socket.IO, PostgreSQL і Redis, уключаючы камнеры, порядак зберагачвання паведамленняў, статус прысутнасці і масштабаванне на калькі сервераў.
- Асновы кэшавання ў Redis: шаблоны, проблемы і праблемы пад час адбору на работу — Дазвольце вам дазнацца, як працюе кэшаванне ў Redis у дапытках Node.js, ад методаў кэшавання пабоку і TTL да захадоў проты атак, правіла вывядзу дадзейнаў і распашчытных пытанняў пад час адбору на работу.