almessadi.
Retour à l'index

Node.js Gèle Toujours Lorsque Vous Mettez du Travail CPU sur le Fil Principal_

Node est excellent pour l'I/O non-bloquant, mais les opérations synchrones lourdes en CPU bloquent toujours la boucle d'événements. C'est cette distinction qui est à l'origine de nombreux incidents en production.

Publié14 août 2024
Temps de lecture8 min read

Node.js est performant en matière de concurrence d'I/O. Il n'est pas performant à faire semblant que le travail CPU est asynchrone simplement parce que la fonction gestionnaire utilise `async`.

Cette distinction est importante car de nombreux incidents de performance ne proviennent pas de bases de données lentes ou de réseaux lents. Ils proviennent d'une requête effectuant trop de travail synchrone pendant que chaque autre requête attend derrière.

## L'Exemple Trompeur

Cette route semble inoffensive à première vue :

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

L'appel à la base de données est asynchrone. JSON.parse ne l'est pas.

Si le payload est énorme, l'analyse se fait sur le fil principal, et la boucle d'événements ne peut pas continuer à gérer d'autres requêtes entrantes jusqu'à ce que ce travail soit terminé.

Ce Qui Bloque Réellement

Dans les services Node réels, les coupables habituels sont :

  • des appels énormes à JSON.parse et JSON.stringify
  • la génération d'images ou de PDF
  • le travail cryptographique effectué au mauvais endroit
  • de grands travaux de système de fichiers synchrones
  • des boucles regex ou de transformation coûteuses

La solution n'est pas "rendre tout asynchrone." La solution consiste à déplacer ou remodeler le travail.

Meilleures Options

Lorsque cela est possible :

  • utilisez des flux au lieu de mettre en mémoire tampon de gros payloads
  • utilisez des travailleurs pour les tâches lourdes en CPU
  • déplacez les transformations coûteuses hors des chemins de requêtes critiques

Par exemple, les threads de travail constituent souvent une frontière plus propre pour les opérations lourdes en calcul :

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);
  });
}

Cela ne rend pas le travail moins cher. Cela permet de garder la boucle d'événements réactive pendant que le travail s'effectue ailleurs.

Le Compromis

Node est toujours un bon choix pour de nombreux systèmes backend. Vous devez simplement respecter le modèle d'exécution. Si votre service passe la plupart de son temps à attendre sur l'I/O, Node s'intègre naturellement. S'il passe la plupart de son temps à traiter des données sur le CPU, vous aurez besoin d'une isolation plus délibérée.

Lectures Complémentaires