Asynchrone Aufgaben mit Node.js und BullMQ verwalten
Webanwendungen haben Anfrage-/Antwortzyklen. Wenn Sie eine URL besuchen, sendet der Browser eine Anfrage an den Server, der eine Anwendung ausführt, die Daten verarbeitet oder Abfragen in der Datenbank ausführt. Währenddessen wartet der Nutzer darauf, dass die Anwendung eine Antwort zurückgibt. Einige Aufgaben können schnell beantwortet werden; zeitintensive Aufgaben wie Bildverarbeitung, Datenanalyse, Berichtserstellung oder das Versenden von E-Mails benötigen jedoch länger und können den Anfrage-/Antwortzyklus verlangsamen.
Herausforderungen bei CPU-intensiven Aufgaben
Angenommen, Sie haben eine Anwendung, bei der Nutzer Bilder hochladen können. In diesem Fall müssen Sie möglicherweise das Bild verkleinern, komprimieren oder in ein anderes Format konvertieren, um den Speicherplatz Ihres Servers zu schonen, bevor Sie das Bild dem Nutzer anzeigen. Die Bildverarbeitung ist eine CPU-intensive Aufgabe, die einen Node.js-Thread blockieren kann, bis die Aufgabe abgeschlossen ist. Dies kann einige Sekunden oder Minuten dauern. Die Nutzer müssen warten, bis die Aufgabe abgeschlossen ist, um eine Antwort vom Server zu erhalten.
Einführung in BullMQ für Hintergrundaufgaben
Um den Anfrage-/Antwortzyklus nicht zu verlangsamen, können Sie BullMQ verwenden, eine verteilte Aufgabenwarteschlange, die es ermöglicht, zeitaufwändige Aufgaben von Ihrer Node.js-Anwendung auszulagern. BullMQ entlastet den Anfrage-/Antwortzyklus, indem es Aufgaben asynchron im Hintergrund und unabhängig von Ihrer Anwendung ausführt. Um Aufgaben zu verfolgen, verwendet BullMQ Redis, um eine kurze Beschreibung jeder Aufgabe in einer Warteschlange zu speichern. Ein BullMQ-Worker nimmt dann die Aufgaben aus der Warteschlange und führt sie aus, wobei er sie nach Abschluss als erledigt markiert.
BullMQ zur Verbesserung der Anwendungsreaktionsfähigkeit
In diesem Artikel verwenden Sie BullMQ, um eine zeitintensive Aufgabe in den Hintergrund auszulagern. Dadurch kann Ihre Anwendung schnell auf Nutzeranfragen reagieren. Zunächst erstellen Sie eine Anwendung mit einer zeitintensiven Aufgabe ohne BullMQ. Anschließend nutzen Sie BullMQ, um die Aufgabe asynchron auszuführen. Abschließend installieren Sie ein visuelles Dashboard, um BullMQ-Aufgaben in einer Redis-Warteschlange zu verwalten.
Voraussetzungen
- Eine eingerichtete Node.js-Entwicklungsumgebung.
- Redis auf Ihrem System installiert.
- Vertrautheit mit Promises und async/await-Funktionen.
- Grundlegende Kenntnisse zur Verwendung von Express.
- Vertrautheit mit Embedded JavaScript (EJS).
- Grundlegendes Verständnis der Bildverarbeitung mit Sharp.
Schritt 1: Projektverzeichnis einrichten
In diesem Schritt erstellen Sie ein Verzeichnis und installieren die notwendigen Abhängigkeiten für Ihre Anwendung. Die Anwendung, die Sie in diesem Tutorial erstellen, ermöglicht es Nutzern, ein Bild hochzuladen, das dann mit der Sharp-Bibliothek verarbeitet wird. Da die Bildverarbeitung zeitintensiv ist und den Anfrage-/Antwortzyklus verlangsamen kann, ist dies eine ideale Aufgabe für BullMQ. Die Technik, die Sie verwenden, funktioniert auch für andere zeitintensive Aufgaben.
Beginnen Sie, indem Sie ein Verzeichnis namens image_processor
erstellen und in das Verzeichnis wechseln:
mkdir image_processor && cd image_processor
Initialisieren Sie das Verzeichnis anschließend als npm-Paket:
npm init -y
Der Befehl erstellt eine package.json
-Datei. Die Option -y
weist npm an, alle Standardwerte zu akzeptieren.
Die Ausgabe nach dem Ausführen des Befehls sieht wie folgt aus:
{
"name": "image_processor",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Diese Ausgabe bestätigt, dass die package.json
-Datei erstellt wurde. Wichtige Eigenschaften sind der Name Ihrer App (name
), die Versionsnummer der Anwendung (version
) und der Einstiegspunkt Ihres Projekts (main
). Wenn Sie mehr über die anderen Eigenschaften erfahren möchten, können Sie die npm package.json-Dokumentation lesen.
Die Anwendung, die Sie in diesem Tutorial erstellen, benötigt die folgenden Abhängigkeiten:
express
: ein Webframework zur Erstellung von Webanwendungen.express-fileupload
: ein Middleware-Tool, das es ermöglicht, Dateien über Formulare hochzuladen.sharp
: eine Bibliothek zur Bildverarbeitung.ejs
: eine Template-Sprache zur Erstellung von HTML-Markup mit Node.js.bullmq
: eine verteilte Aufgabenwarteschlange.bull-board
: ein Dashboard zur Überwachung der Aufgaben in BullMQ mit einer grafischen Benutzeroberfläche.
Um alle diese Abhängigkeiten zu installieren, führen Sie den folgenden Befehl aus:
npm install express express-fileupload sharp ejs bullmq @bull-board/express
Zusätzlich zu den installierten Abhängigkeiten verwenden Sie später in diesem Tutorial ein Bild:
Verwenden Sie curl
, um das Bild an einem Ort Ihrer Wahl auf Ihrem lokalen Computer herunterzuladen:
curl -O https://deved-images.nyc3.cdn.digitaloceanspaces.com/CART-68886/underwater.png
Sie haben nun die notwendigen Abhängigkeiten installiert, um eine Node.js-Anwendung ohne BullMQ zu erstellen. Im nächsten Schritt wird diese Anwendung entwickelt.
Schritt 2: Implementierung einer zeitintensiven Aufgabe ohne BullMQ
In diesem Schritt erstellen Sie eine Anwendung mit Express, die es Nutzern ermöglicht, Bilder hochzuladen. Die Anwendung startet eine zeitintensive Aufgabe mit Sharp, um das Bild in mehrere Größen zu ändern, die dem Nutzer nach der Antwort angezeigt werden. Dieser Schritt hilft Ihnen, zu verstehen, wie sich zeitintensive Aufgaben auf den Anfrage-/Antwortzyklus auswirken.
Erstellen der Datei index.js
Erstellen Sie mit nano
oder Ihrem bevorzugten Texteditor die Datei index.js
:
nano index.js
Importieren der notwendigen Abhängigkeiten
Fügen Sie in der Datei index.js
den folgenden Code hinzu, um die Abhängigkeiten zu importieren:
const path = require("path");
const fs = require("fs");
const express = require("express");
const bodyParser = require("body-parser");
const sharp = require("sharp");
const fileUpload = require("express-fileupload");
In der ersten Zeile importieren Sie das path
-Modul, um Dateipfade mit Node zu berechnen. In der zweiten Zeile importieren Sie das fs
-Modul, um mit Verzeichnissen zu interagieren. Anschließend importieren Sie das express
-Webframework. Sie importieren das body-parser
-Modul, um Middleware hinzuzufügen, die Daten in HTTP-Anfragen analysiert. Danach importieren Sie das sharp
-Modul zur Bildverarbeitung und express-fileupload
für den Umgang mit Uploads aus HTML-Formularen.
Middleware in der App implementieren
Implementieren Sie als Nächstes Middleware in Ihrer App:
const app = express();
app.set("view engine", "ejs");
app.use(bodyParser.json());
app.use(
bodyParser.urlencoded({
extended: true,
})
);
Zuerst setzen Sie die Variable app
auf eine Instanz von Express. Mit der Methode set()
konfigurieren Sie Express so, dass die ejs
-Vorlagensprache verwendet wird. Dann fügen Sie mit der Methode use()
die Middleware von body-parser
hinzu, um JSON-Daten in HTTP-Anfragen in Variablen umzuwandeln, die mit JavaScript zugänglich sind. In der nächsten Zeile tun Sie dasselbe mit URL-codierten Eingaben.
Hinzufügen von Middleware für Datei-Uploads und statische Dateien
Fügen Sie als Nächstes Middleware hinzu, um Datei-Uploads zu behandeln und statische Dateien bereitzustellen:
app.use(fileUpload());
app.use(express.static("public"));
Sie fügen Middleware hinzu, um hochgeladene Dateien mit der Methode fileUpload()
zu analysieren. Außerdem legen Sie ein Verzeichnis fest, in dem Express nach statischen Dateien wie Bildern und CSS sucht und diese bereitstellt.
Erstellen einer Route für den Bild-Upload
Erstellen Sie nun eine Route, die ein HTML-Formular für den Bild-Upload anzeigt:
app.get("/", function (req, res) {
res.render("form");
});
Hier verwenden Sie die Methode get()
des Express-Moduls, um die Route /
anzugeben und die Callback-Funktion zu definieren, die ausgeführt wird, wenn der Nutzer die Startseite oder Route /
aufruft. Im Callback rufen Sie res.render()
auf, um die Datei form.ejs
im views
-Verzeichnis zu rendern. Dieses Verzeichnis und die Datei form.ejs
haben Sie noch nicht erstellt.
Erstellen des Verzeichnisses und der Datei
Um sie zu erstellen, speichern und schließen Sie zunächst Ihre Datei. Geben Sie in Ihrem Terminal den folgenden Befehl ein, um das Verzeichnis views
im Stammverzeichnis Ihres Projekts zu erstellen:
mkdir views
Wechseln Sie in das Verzeichnis views
:
cd views
Erstellen Sie die Datei form.ejs
in Ihrem Editor:
nano form.ejs
In der Datei form.ejs
erstellen Sie ein HTML-Formular für den Upload. Diese Implementierung und die Verarbeitung des Formulars werden im nächsten Abschnitt detailliert behandelt.
Schritt 3: Verwendung von BullMQ zur asynchronen Verarbeitung
In diesem Schritt verwenden Sie BullMQ, um eine zeitintensive Aufgabe in den Hintergrund auszulagern. Dadurch wird der Anfrage-/Antwortzyklus freigegeben, und Ihre App kann sofort auf Nutzeranfragen reagieren, während die Aufgabe im Hintergrund ausgeführt wird.
Dazu erstellen Sie eine kurze Beschreibung der Aufgabe und fügen sie mit BullMQ in eine Warteschlange ein. Eine Warteschlange ist eine Datenstruktur, die ähnlich wie eine Warteschlange im echten Leben funktioniert: Die erste Person in der Warteschlange wird zuerst bedient. Weitere Personen reihen sich am Ende der Warteschlange ein und werden der Reihe nach bedient. BullMQ nutzt das Prinzip „First-In, First-Out“ (FIFO), wobei der erste Eintrag in der Warteschlange auch zuerst entfernt (dequeued) wird.
Speicherung von Warteschlangeneinträgen in Redis
In BullMQ ist die Warteschlange in Redis gespeichert. Wenn Sie eine Aufgabe beschreiben und sie in die Warteschlange einfügen, erstellt man ein Eintrag für die Aufgabe in der Redis-Warteschlange. Eine Aufgabenbeschreibung kann ein String oder ein Objekt mit minimalen Daten oder Verweisen auf Daten sein, die BullMQ später zur Ausführung benötigt. Sobald Sie die Funktionalität zum Hinzufügen von Aufgaben definiert haben, verschieben Sie den zeitintensiven Code in eine separate Funktion. BullMQ ruft diese Funktion dann mit den in der Warteschlange gespeicherten Daten auf, sobald die Aufgabe entnommen wird. Nach Abschluss der Aufgabe markiert BullMQ sie als abgeschlossen, zieht eine weitere Aufgabe aus der Warteschlange und führt sie aus.
Erstellen der Datei index.js
Öffnen Sie die Datei index.js
in Ihrem Editor:
nano index.js
Hinzufügen einer Warteschlange in Redis mit BullMQ
Fügen Sie in Ihrer Datei index.js
die folgenden Zeilen hinzu, um eine Warteschlange in Redis mit BullMQ zu erstellen:
const { Queue } = require("bullmq");
const redisOptions = { host: "localhost", port: 6379 };
const imageJobQueue = new Queue("imageJobQueue", {
connection: redisOptions,
});
async function addJob(job) {
await imageJobQueue.add(job.type, job);
}
Funktionsweise der Warteschlange
Hier extrahieren Sie die Klasse Queue
aus BullMQ, mit der Sie eine Warteschlange in Redis erstellen. Sie legen die Variable redisOptions
als Objekt mit Eigenschaften fest, die die Verbindung zu Redis definieren. Die Eigenschaft host
hat den Wert localhost
, da Redis lokal ausgeführt wird. Die Eigenschaft port
hat den Standardwert 6379
.
Anschließend instanziieren Sie die Klasse Queue
mit dem Namen imageJobQueue
und übergeben als zweites Argument ein Objekt mit der Verbindungskonfiguration. Nach der Instanziierung wird eine Warteschlange namens imageJobQueue
in Redis erstellt.
Die Funktion addJob()
fügt der Warteschlange eine Aufgabe hinzu. Die Funktion nimmt ein Parameterobjekt job
, das die Informationen zur Aufgabe enthält. Mit der Methode add()
der Warteschlange wird der Name der Aufgabe und die zugehörigen Daten übergeben.
Hinzufügen einer Aufgabe in die Warteschlange
Fügen Sie nun den Code hinzu, um die Funktion addJob()
aufzurufen und eine Aufgabe hinzuzufügen:
app.post("/upload", async function (req, res) {
const { image } = req.files;
if (!image) return res.sendStatus(400);
await addJob({
type: "processUploadedImages",
image: {
data: image.data.toString("base64"),
name: image.name,
},
});
res.redirect("/result");
});
Verarbeitung der Aufgabe
Hier rufen Sie die Funktion addJob()
mit einem Objekt auf, das die Aufgabe beschreibt. Die Eigenschaft type
gibt den Namen der Aufgabe an. Die Eigenschaft image
enthält ein Objekt mit den hochgeladenen Bilddaten. Da die Bilddaten als Puffer (Binärdaten) vorliegen, wandeln Sie diese mit toString()
in einen String um, der in Redis gespeichert werden kann.
Warnung: Da Redis eine In-Memory-Datenbank ist, vermeiden Sie es, große Datenmengen in der Warteschlange zu speichern. Wenn eine Aufgabe große Dateien benötigt, speichern Sie die Datei auf der Festplatte oder in der Cloud und speichern Sie nur den Link in der Warteschlange.
Sie haben nun die Funktionalität definiert, um eine Warteschlange in Redis zu erstellen und Aufgaben hinzuzufügen. Als Nächstes implementieren Sie einen Worker, der Aufgaben aus der Warteschlange abarbeitet.
Schritt 4: Hinzufügen eines Dashboards zur Überwachung der BullMQ-Warteschlangen
In diesem Schritt verwenden Sie das bull-board
-Paket, um die Aufgaben in der Redis-Warteschlange über ein visuelles Dashboard zu überwachen. Dieses Paket erstellt automatisch eine Benutzeroberfläche (UI), die die Informationen über die in der BullMQ-Warteschlange gespeicherten Aufgaben organisiert und anzeigt. Über Ihren Browser können Sie Aufgaben überprüfen, die abgeschlossen sind, noch warten oder fehlgeschlagen sind, ohne das Redis-CLI im Terminal öffnen zu müssen.
Öffnen Sie die Datei index.js
in Ihrem Texteditor:
nano index.js
Fügen Sie den folgenden Code hinzu, um bull-board
zu importieren:
const { createBullBoard } = require("@bull-board/api");
const { BullMQAdapter } = require("@bull-board/api/bullMQAdapter");
const { ExpressAdapter } = require("@bull-board/express");
In diesem Code importieren Sie die Methode createBullBoard()
aus bull-board
. Sie importieren außerdem den BullMQAdapter
, der bull-board
Zugriff auf BullMQ-Warteschlangen ermöglicht, sowie den ExpressAdapter
, der die Funktionalität bietet, das Dashboard in einer Express-App anzuzeigen.
Verbinden Sie nun bull-board
mit BullMQ:
const serverAdapter = new ExpressAdapter();
const bullBoard = createBullBoard({
queues: [new BullMQAdapter(imageJobQueue)],
serverAdapter: serverAdapter,
});
serverAdapter.setBasePath("/admin");
Zunächst setzen Sie die Variable serverAdapter
auf eine Instanz von ExpressAdapter
. Anschließend rufen Sie createBullBoard()
auf, um das Dashboard mit den BullMQ-Warteschlangendaten zu initialisieren. Die Methode akzeptiert ein Objekt mit zwei Eigenschaften:
queues
: Ein Array mit den BullMQ-Warteschlangen, hierimageJobQueue
.serverAdapter
: Eine Instanz des Express-Adapters.
Mit der Methode setBasePath()
legen Sie fest, dass das Dashboard unter dem Pfad /admin
zugänglich ist.
Fügen Sie nun die Middleware für die Route /admin
hinzu:
app.use("/admin", serverAdapter.getRouter());
Die vollständige Datei index.js
sollte nun wie folgt aussehen:
const path = require("path");
const fs = require("fs");
const express = require("express");
const bodyParser = require("body-parser");
const fileUpload = require("express-fileupload");
const { Queue } = require("bullmq");
const { createBullBoard } = require("@bull-board/api");
const { BullMQAdapter } = require("@bull-board/api/bullMQAdapter");
const { ExpressAdapter } = require("@bull-board/express");
const redisOptions = { host: "localhost", port: 6379 };
const imageJobQueue = new Queue("imageJobQueue", {
connection: redisOptions,
});
async function addJob(job) {
await imageJobQueue.add(job.type, job);
}
const serverAdapter = new ExpressAdapter();
const bullBoard = createBullBoard({
queues: [new BullMQAdapter(imageJobQueue)],
serverAdapter: serverAdapter,
});
serverAdapter.setBasePath("/admin");
const app = express();
app.set("view engine", "ejs");
app.use(bodyParser.json());
app.use(
bodyParser.urlencoded({
extended: true,
})
);
app.use(fileUpload());
app.use(express.static("public"));
app.use("/admin", serverAdapter.getRouter());
app.get("/", function (req, res) {
res.render("form");
});
app.get("/result", (req, res) => {
const imgDirPath = path.join(__dirname, "./public/images");
let imgFiles = fs.readdirSync(imgDirPath).map((image) => {
return `images/${image}`;
});
res.render("result", { imgFiles });
});
app.post("/upload", async function (req, res) {
const { image } = req.files;
if (!image) return res.sendStatus(400);
await addJob({
type: "processUploadedImages",
image: {
data: Buffer.from(image.data).toString("base64"),
name: image.name,
},
});
res.redirect("/result");
});
app.listen(3000, function () {
console.log("Server running on port 3000");
});
Starten Sie nun die Datei index.js
:
node index.js
Öffnen Sie Ihren Browser und besuchen Sie http://localhost:3000/admin
. Das Dashboard lädt und zeigt den Status Ihrer Warteschlangen, einschließlich wartender, abgeschlossener oder fehlgeschlagener Aufgaben.
Sie können nun das bull-board
-Dashboard nutzen, um Warteschlangen zu überwachen und Aufgaben in Echtzeit nachzuverfolgen.
Schlussfolgerung
In diesem Artikel haben Sie eine zeitintensive Aufgabe mit BullMQ in eine Aufgabenwarteschlange ausgelagert. Zuerst haben Sie eine Anwendung ohne BullMQ erstellt, bei der eine zeitintensive Aufgabe den Anfrage-/Antwortzyklus verlangsamt. Anschließend haben Sie BullMQ verwendet, um die Aufgabe asynchron auszuführen, was den Anfrage-/Antwortzyklus beschleunigt hat. Danach haben Sie Bull-Board genutzt, um ein Dashboard zu erstellen, mit dem Sie BullMQ-Aufgaben in Redis überwachen können.
Sie können die BullMQ-Dokumentation besuchen, um mehr über nicht behandelte Funktionen wie das Planen, Priorisieren oder Wiederholen von Aufgaben sowie die Konfiguration von Concurrency-Einstellungen für Worker zu erfahren. Sie können auch die Bull-Board-Dokumentation besuchen, um mehr über die Dashboard-Funktionen zu erfahren.