almessadi.
Zur Übersicht

Node.js friert weiterhin ein, wenn Sie CPU-Arbeit im Haupt-Thread ausführen_

Node ist hervorragend im nicht blockierenden I/O, aber synchrone CPU-intensive Operationen blockieren weiterhin den Ereignis-Loop. Diese Unterscheidung ist der Ursprung vieler Vorfälle in der Produktion.

Veröffentlicht14. August 2024
Lesezeit8 min read

Node.js ist gut in der I/O-Konkurrenz. Es ist nicht gut darin, CPU-Arbeit als asynchron darzustellen, nur weil die Handler-Funktion async verwendet.

Diese Unterscheidung ist wichtig, denn viele Leistungsprobleme stammen nicht von langsamen Datenbanken oder langsamen Netzwerken. Sie entstehen durch eine Anfrage, die zu viel synchrone Arbeit verrichtet, während jede andere Anfrage dahinter wartet.

Das Irreführende Beispiel

Diese Route sieht auf den ersten Blick harmlos aus:

app.post("/webhook", async (req, res) => {
  const payload = JSON.parse(req.body.raw);
  await database.save(payload);
  res.send("ok");
});

Der Datenbankaufruf ist asynchron. JSON.parse ist es nicht.

Wenn die Nutzlast groß ist, erfolgt das Parsen im Haupt-Thread, und der Ereignis-Loop kann andere eingehende Anfragen nicht bedienen, bis diese Arbeit abgeschlossen ist.

Was Tatsächlich Blockiert

In realen Node-Diensten sind die üblichen Übeltäter:

  • große JSON.parse und JSON.stringify Aufrufe
  • Bild- oder PDF-Erstellung
  • Krypto-Arbeiten an der falschen Stelle
  • große synchrone Dateioperationen
  • teure Regex- oder Transformationsschleifen

Die Lösung ist nicht "alles asynchron machen". Die Lösung besteht darin, die Arbeit zu verlagern oder umzugestalten.

Bessere Optionen

Wann immer möglich:

  • streamen Sie anstelle von Puffern großer Nutzlasten
  • verwenden Sie Worker für CPU-intensive Aufgaben
  • verlagern Sie teure Transformationen aus heißen Anforderungswegen

Zum Beispiel sind Worker-Threads oft die sauberere Grenze für rechenintensive Operationen:

import { Worker } from "node:worker_threads";

export function runHeavyTask(input: unknown) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(new URL("./worker.js", import.meta.url), {
      workerData: input,
    });

    worker.once("message", resolve);
    worker.once("error", reject);
  });
}

Das macht die Arbeit nicht günstiger. Es hält den Ereignis-Loop reaktionsschnell, während die Arbeit woanders geschieht.

Der Kompromiss

Node ist immer noch eine gute Wahl für viele Backendsysteme. Sie müssen nur das Laufzeitmodell respektieren. Wenn Ihr Dienst die meiste Zeit mit dem Warten auf I/O verbringt, passt Node natürlich. Wenn er die meiste Zeit mit der Verarbeitung von Daten auf der CPU verbringt, benötigen Sie eine überlegte Isolation.

Weitere Lektüre