אתם עולים לייצור — והשרת קורס תחת העומס של שליחת מיילים? או שמשימות cron רצות בסדר לא נכון, דוחות נעלמים באוויר? עיבוד סינכרוני מאט את ה-API, משתמשים מקבלים שגיאות. תרחיש טיפוסי: 10,000 מיילים בדקה — מסד הנתונים ננעל, זמן ההשהיה מזנק ל-30 שניות. BullMQ פותר את הבעיות האלה: תור אסינכרוני על Redis עם עדיפויות, ניסיונות חוזרים וניטור. בפרויקט אחד עם 50,000 משתמשים, אחרי יישום BullMQ, עומס השרת ירד ב-70%, וזמן התגובה של ה-API ירד מ-2 שניות ל-200 אלפיות השנייה. הניסיון שלנו — מעל 10 שנים ו-50+ פרויקטי תורים. תור מוגדר כראוי מפחית זמני השבתה ומונע אובדן נתונים. החיסכון החודשי מסתכם בסכום משמעותי על ידי הפחתת עומס השרת וקיצור זמן הפיתוח.
הקמנו BullMQ לעשרות פרויקטים — מסטארטאפים ועד ארגונים. מאמר זה מכסה תצורה מוכחת קרבית מההתחלה ועד לייצור, סוהר.
התקנה
התקינו את החבילות: npm install bullmq ioredis. BullMQ דורש גרסת Redis 6 ומעלה. ודאו שהשרת נגיש.
תצורת חיבור
// lib/redis.ts
import { Redis } from 'ioredis';
export const redisConnection = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: Number(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD,
maxRetriesPerRequest: null,
enableReadyCheck: false,
});
// lib/redis.ts import { Redis } from 'ioredis'; export const redisConnection = new Redis({ host: process.env.REDIS_HOST || 'localhost', port: Number(process.env.REDIS_PORT) || 6379, password: process.env.REDIS_PASSWORD, maxRetriesPerRequest: null, enableReadyCheck: false, }); — קריטי לחיבור מחדש תקין. maxRetriesPerRequest: null מאיץ את זמן האתחול.
הגדרת תורים
// queues/index.ts
import { Queue } from 'bullmq';
import { redisConnection } from '../lib/redis';
const defaultJobOptions = {
attempts: 3,
backoff: {
type: 'exponential' as const,
delay: 5000,
},
removeOnComplete: {
count: 1000,
age: 86400
},
removeOnFail: {
count: 5000,
age: 604800
},
};
export const emailQueue = new Queue('emails', {
connection: redisConnection,
defaultJobOptions,
});
// Для других типов задач создаются аналогичные очереди с теми же опциями.עבור סוגי משימות שונים, השתמשו בתורים נפרדים — זה משפר את הניטור והביצועים.
עובדים (Workers)
// workers/emailWorker.ts
import { Worker, Job } from 'bullmq';
import { redisConnection } from '../lib/redis';
import { sendEmail } from '../services/email';
interface EmailJobData {
to: string;
subject: string;
template: string;
variables: Record<string, unknown>;
}
const worker = new Worker<EmailJobData>(
'emails',
async (job: Job<EmailJobData>) => {
const { to, subject, template, variables } = job.data;
await job.updateProgress(10);
await sendEmail({ to, subject, template, variables });
await job.updateProgress(100);
return { sent: true, to, timestamp: new Date().toISOString() };
},
{
connection: redisConnection,
concurrency: 10,
limiter: {
max: 100,
duration: 60_000,
},
}
);
worker.on('completed', (job, result) => {
console.log(`Email sent to ${result.to}`);
});
worker.on('failed', (job, err) => {
console.error(`Email failed: job ${job?.id}:`, err.message);
});
worker.on('error', (err) => {
console.error('Worker error:', err);
});
export default worker;
הגבלת קצב (Rate limiting) היא חובה לייצור. בלעדיה, אתם מסתכנים בחסימה על ידי ה-API החיצוני של הנמען.
דוגמאות להוספת משימות
להלן מספר תרחישים: שליחה פשוטה, משימה מושהית, עדיפות, משימות בכמות גדולה, משימות cron ו-Flow.
// Простая задача
await emailQueue.add('welcome-email', {
to: user.email,
subject: 'Добро пожаловать!',
template: 'welcome',
variables: { name: user.name },
});
// С задержкой 5 минут
await emailQueue.add(
'follow-up-email',
{
to: user.email,
subject: 'Как дела?',
template: 'follow-up',
variables: { name: user.name },
},
{ delay: 5 * 60 * 1000 }
);
// С приоритетом (1 – высший)
await notificationQueue.add(
'push-notification',
{
userId: user.id,
message: 'Срочное уведомление',
},
{ priority: 1 }
);
// Массовая отправка
const jobs = users.map((user) => ({
name: 'newsletter',
data: { to: user.email, template: 'newsletter' },
opts: { delay: Math.random() * 60_000 },
}));
await emailQueue.addBulk(jobs);
// Cron: ежедневный отчёт в 9:00 UTC
await reportQueue.add(
'daily-report',
{ type: 'daily', recipients: ['[email protected]'] },
{
repeat: { pattern: '0 9 * * *' },
jobId: 'daily-report-unique',
}
);
// Flow: ресайз → загрузка → уведомление
import { FlowProducer } from 'bullmq';
const flow = new FlowProducer({ connection: redisConnection });
await flow.add({
name: 'notify-user',
queueName: 'notifications',
data: { userId },
children: [
{
name: 'upload-to-s3',
queueName: 'uploads',
data: { tempPath },
children: [
{
name: 'resize-image',
queueName: 'images',
data: { originalPath, sizes: [200, 400, 800] },
},
],
},
],
});
מידע נוסף על משימות cron
עבור משימות חוזרות, השתמשו באפשרות `repeat` עם `pattern` (cron) או `every` (מרווח). `jobId` ייחודי מונע כפילויות בריצות חוזרות.Bull Board (ניטור)
Bull Board הוא ממשק אינטרנט לניהול תורים. אינטגרציה עם Express:
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ExpressAdapter } from '@bull-board/express';
import { emailQueue, notificationQueue, reportQueue } from './queues';
const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath('/admin/queues');
createBullBoard({
queues: [
new BullMQAdapter(emailQueue),
new BullMQAdapter(notificationQueue),
new BullMQAdapter(reportQueue),
],
serverAdapter,
});
app.use('/admin/queues', authenticate, serverAdapter.getRouter());
Bull Board נותן שליטה מלאה: צפייה במשימות, הרצה חוזרת, ניקוי. הניטור לא מעמיס על Redis — הבקשות הן אסינכרוניות. 80% מהמשימות מצליחות בניסיון הראשון, וההשהיה בין ניסיונות חוזרים גדלה באופן אקספוננציאלי: 5, 10, 20 שניות.
למה BullMQ על פני EventEmitter או RabbitMQ?
BullMQ מהיר פי 3 למשימות web טיפוסיות מאשר RabbitMQ ואינו דורש רכישת רישיון. בניגוד ל-EventEmitter, הנתונים נשמרים ב-Redis — משימות לא אובדות בקריסת שרת. BullMQ תומך ב-exponential backoff ובעדיפויות, ש-EventEmitter חסר. השוואה:
| תכונה | BullMQ | RabbitMQ | EventEmitter |
|---|---|---|---|
| שמירת נתונים (Persistence) | כן (דרך Redis) | כן | לא |
| עדיפויות | כן | לא | לא |
| Exponential backoff | כן | דורש תצורה | לא |
| ניטור | Bull Board | ממשק ניהול | לא |
| רישיון | קוד פתוח | קוד פתוח (גרסה ארגונית זמינה) | חינם |
איך לנטר תורים עם Bull Board?
Bull Board הוא ממשק אינטרנט שמציג את כל התורים, העובדים, סטטוסי המשימות והשגיאות. האינטגרציה עם Express מתוארת למעלה. הוסיפו את ה-middleware ותקבלו לוח בקרה בכתובת /admin/queues. Bull Board פועל ביציבות על פרויקטים עם 10+ תורים ו-1000 משימות בדקה. אם צריך, אנו מוסיפים אימות והרשאות מבוססות תפקידים.
איך למנוע אובדן נתונים בזמן תקלות?
השתמשו בשמירת נתונים של Redis — הגדירו enableReadyCheck: false בקובץ redis.conf. ב-BullMQ, משימות נשמרות עד לעיבוד או עד שתוקף ה-TTL יפוג. הפרמטר // queues/index.ts import { Queue } from 'bullmq'; import { redisConnection } from '../lib/redis'; const defaultJobOptions = { attempts: 3, backoff: { type: 'exponential' as const, delay: 5000, }, removeOnComplete: { count: 1000, age: 86400 }, removeOnFail: { count: 5000, age: 604800 }, }; export const emailQueue = new Queue('emails', { connection: redisConnection, defaultJobOptions, }); // Для других типов задач создаются аналогичные очереди с теми же опциями. עם // workers/emailWorker.ts import { Worker, Job } from 'bullmq'; import { redisConnection } from '../lib/redis'; import { sendEmail } from '../services/email'; interface EmailJobData { to: string; subject: string; template: string; variables: Record<string, unknown>; } const worker = new Worker<EmailJobData>( 'emails', async (job: Job<EmailJobData>) => { const { to, subject, template, variables } = job.data; await job.updateProgress(10); await sendEmail({ to, subject, template, variables }); await job.updateProgress(100); return { sent: true, to, timestamp: new Date().toISOString() }; }, { connection: redisConnection, concurrency: 10, limiter: { max: 100, duration: 60_000, }, } ); worker.on('completed', (job, result) => { console.log(`Email sent to ${result.to}`); }); worker.on('failed', (job, err) => { console.error(`Email failed: job ${job?.id}:`, err.message); }); worker.on('error', (err) => { console.error('Worker error:', err); }); export default worker; מבטיח שרק 1000 המשימות המוצלחות האחרונות יישארו בזיכרון — חוסך RAM. אם Redis קורס, השתמשו ב-AOF או ב-replication. בפרויקטים שלנו, אנו מגדירים Redis עם // Простая задача await emailQueue.add('welcome-email', { to: user.email, subject: 'Добро пожаловать!', template: 'welcome', variables: { name: user.name }, }); // С задержкой 5 минут await emailQueue.add('follow-up-email', { to: user.email, subject: 'Как дела?', template: 'follow-up', variables: { name: user.name }, }, { delay: 5 * 60 * 1000, }); // С приоритетом (1 – высший) await notificationQueue.add('push-notification', { userId: user.id, message: 'Срочное уведомление', }, { priority: 1, }); // Массовая отправка const jobs = users.map(user => ({ name: 'newsletter', data: { to: user.email, template: 'newsletter' }, opts: { delay: Math.random() * 60_000 }, })); await emailQueue.addBulk(jobs); // Cron: ежедневный отчёт в 9:00 UTC await reportQueue.add( 'daily-report', { type: 'daily', recipients: ['[email protected]'] }, { repeat: { pattern: '0 9 * * *' }, jobId: 'daily-report-unique', } ); // Flow: ресайз → загрузка → уведомление import { FlowProducer } from 'bullmq'; const flow = new FlowProducer({ connection: redisConnection }); await flow.add({ name: 'notify-user', queueName: 'notifications', data: { userId }, children: [{ name: 'upload-to-s3', queueName: 'uploads', data: { tempPath }, children: [{ name: 'resize-image', queueName: 'images', data: { originalPath, sizes: [200, 400, 800] }, }], }], }); ומסנכרנים כל 5 שניות.
טעויות נפוצות בהגדרת תורים
- שכחתם להגדיר
import { createBullBoard } from '@bull-board/api'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; import { ExpressAdapter } from '@bull-board/express'; import { emailQueue, notificationQueue, reportQueue } from './queues'; const serverAdapter = new ExpressAdapter(); serverAdapter.setBasePath('/admin/queues'); createBullBoard({ queues: [ new BullMQAdapter(emailQueue), new BullMQAdapter(notificationQueue), new BullMQAdapter(reportQueue), ], serverAdapter, }); app.use('/admin/queues', authenticate, serverAdapter.getRouter());— החיבור נופל אחרי השגיאה הראשונה. - דילוג על
save— תחילת התור מתעכבת בשניות. - לא הגדרתם הגבלת קצב — ה-API החיצוני חוסם בקשות (HTTP 429).
- שימוש בתור אחד לכל סוגי המשימות — מסבך את הניטור והניפוי.
מה כלול בהקמת תור סוהר
| שלב | תיאור |
|---|---|
| ניתוח | הערכת עומס, בחירת אסטרטגיה (השהיה, עדיפות, ניסיונות חוזרים) |
| עיצוב | סכימת תורים, תצורת Redis, הגדרת הגבלת קצב |
| יישום | כתיבת תורים, עובדים, שרשראות Flow |
| ניטור | התקנת Bull Board, התראות ל-Telegram/Slack |
| תיעוד | README עם ארכיטקטורה, הוראות פריסה |
| תמיכה | אחריות להקמה למשך שבועיים לאחר המסירה |
לוח זמנים ליישום
BullMQ לפרויקט Node.js טיפוסי (מיילים, התראות, cron): 2–3 ימים. עם Bull Board, ניטור ו-Flow: 3–4 ימים. קבלו הערכה לפרויקט שלכם. מעל 10 שנות ניסיון ו-50+ פרויקטים מבטיחים אמינות. הזמינו הקמת BullMQ מקצועית עם אחריות לתוצאה. קבלו ייעוץ מהנדס כבר עכשיו.







