Piscine de rendu V8
Le pool de rendu V8 est le sous-système qui exécute réellement exécute rendu par le serveur JavaScript — pages PRISM, routes Next.js/RSC et applications de gestionnaire de récupération — à l'intérieur des isolats V8. Il s'agit de la dernière étape du Pipeline de rendu PRISM et de la partie la plus sensible du point de vue opérationnel du serveur : un isolement coincé peut bloquer tous les sites qui le partagent.
Cette page couvre l'architecture — topologie, planification, résilience. Pour les boutons de configuration à titre de référence, voir Moteur de rendu V8 ; pour les symptômes d'exécution, voir mise à l'échelle et dépannage.
Topologie#
En production, V8 s'exécute dans un pool global hors processus partagé sur chaque site — SubprocessPool dans bext-v8/src/subprocess.rs, installé en tant que Arc<dyn EvalBackend> :
actix / tokio HTTP front (num_cpus worker threads)
│ per SSR request → spawn_blocking
│ route match → compile bundle → send eval frame → recv_timeout
│
│ framed, big-endian, over Unix socketpairs
├───────┬───────┬───────┬─── … ───┬───────
worker0 worker1 worker2 worker7
1 OS process · 1 V8 isolate · 1 eval thread each
└─ multiplex::drive(): N concurrent requests share the isolate
Deux faits déterminent tout le reste :
- Le pool est global et indifférencié. Par défaut un La route de streaming SSE et une route de page à rendu lourd se disputent les mêmes isolats. (L'isolation de la charge de travail est facultative – voir voies de diffusion.)
- La concurrence est à deux niveaux. Les demandes de plannings directeurs parmi les travailleurs ; à l'intérieur de chaque travailleur, le pilote multiplex exécute plusieurs requêtes ** sur un seul isolat de manière coopérative.
Les backends de la piscine#
| Back-end | Déposer | Activation | Forme |
|---|---|---|---|
SubprocessPool |
bext-v8/src/subprocess.rs |
BEXT_V8_POOL=1 + BEXT_V8_POOL_SIZE=N |
N processus enfants, isolés en cas de crash, la topologie de production. |
En cours de processus PrismPool |
bext-v8/src/prism_pool.rs |
par défaut ( BEXT_PRISM_WORKERS, par défaut 2) |
Threads du système d’exploitation dans le processus maître, isolats de longue durée. |
| Filetage d'évaluation unique | bext-v8/src/eval.rs |
retomber | Un thread bext-v8-eval-worker sérialisant tous les rendus. |
| Piscine héritée par site React | bext-v8/src/pool.rs |
Sites Next.js | V8RenderPool, ensemble de travailleurs par site. |
BEXT_V8_POOL_SIZE est limité à 1–16 ; séries de production 8. Le mode sous-processus existe pour l'isolation des crashs : un SIGSEGV V8 supprime un travailleur, pas le maître. (Il est également porteur de mémoire - une première variante en cours de processus a divulgué un tas d'isolement 'static qui a gonflé le maître à environ 30 Go, donc le mode sous-processus ne doit jamais revenir silencieusement au pool en cours.)
À l’intérieur d’un travailleur : le pilote multiplex#
multiplex::drive() ( bext-v8/src/multiplex.rs, fermé sur
BEXT_V8_MULTIPLEX=1 ) est une boucle possédant un isolat qui entrelace de nombreux rendus en vol :
- drainer les requêtes nouvellement arrivées d'un canal mpsc,
- résoudre les récupérations de bouclage terminées (
drain_async_fetches), - pomper les microtâches et les minuteries JS (
pump_timers_and_microtasks), - parc piloté par les événements sur un canal de sillage (
DRIVER_WAKE_TX, poussé parwake_driver()à la fin de la mise en file d'attente/de la récupération) jusqu'àMAX_PARK_MS = 50 ms.
C'est ce qui permet à 8 isolats de servir bien plus de 8 requêtes simultanées : pendant qu'un rendu attend le fetch d'un loader, le pilote en avance d'autres sur le même isolat.
Contextes chaleureux et instantanés#
Le travail à froid du V8 est amorti à deux niveaux ( bext-v8/src/eval.rs ) :
- Contextes chauds —
prism_contexts/page_contexts/api_contextsconserve le contexte d'un bundle évalué (saisi parprism_cache_key(shell, bundle)), répétez donc les rendus d'un saut d'itinéraire en réévaluant le bundle. - Instantané de démarrage —
snapshot.rspeut intégrer React + polyfills dans un Blob de démarrage V8 (SNAPSHOT_BLOB) afin qu'un nouveau contexte démarre pré-initialisé ; le blob est transmis pour sous-traiter les enfants.
Hygiène par isolement : les travailleurs PRISM bouchent le tas ( BEXT_PRISM_HEAP_MB, 64 par défaut) et émettent un
low_memory_notification tous les rendus BEXT_V8_GC_EVERY_REQUESTS (100 par défaut).
Planification, admission et contre-pression#
L'admission est imposée par le maître avant l'envoi d'une trame ( subprocess.rs ) :
- Casquette de vol globale —
BEXT_V8_POOL_QUEUE_DEPTH(par défaut 64, bridé 1–4096).try_admit()CAS-incrémente un compteur global ; au-delà du plafond, la demande est rejetée et comptabiliséebext_v8_pool_rejections_total. - Plafond par travailleur —
BEXT_V8_POOL_PER_WORKER_CAP(par défaut 8 non multiplex / 16 multiplex). Les travailleurs à leur plafond sont ignorés par le planificateur. - Sélection du moindre en vol —
pick_and_claim_workersélectionne le travailleur vivant et non mis en quarantaine avec le moins de demandes en vol dans la voie appropriée. - Capacité dynamique (Phase 4.5) — nœuds de calcul dépassant une limite souple de tas ou les bouchons en dur sont filtrés, chacun avec son propre compteur de motifs de rejet.
Un rejet devient un 503 propre avec Retry-After: 1 plutôt qu'une connexion bloquée. Avec BEXT_PRISM_EARLY_SHED=1, cette perte se produit avant l'écriture de la compilation + du wrapper ( prism.rs::pool_admission_saturated ), donc une requête destinée au rejet ne brûle pas d'abord le processeur de compilation - la réponse est transmise
x-bext-pool-shed: queue-full.
Voies de diffusion#
Par défaut, les demandes de rendu et de streaming partagent tous les travailleurs, ce qui est exactement la façon dont un seul long flux SSE peut affamer les rendus de pages. Paramètre
BEXT_V8_POOL_STREAMING_LANE=N réserve les N derniers emplacements en tant que voie de streaming uniquement ( Lane::Streaming ) ; les premiers emplacements pool_size − N deviennent uniquement en rendu ( Lane::Render ). La division est stricte – aucun déversement dans les deux sens – donc un flux bloqué ne peut jamais revendiquer un emplacement de rendu.
Surveillance et résilience#
Trois mécanismes de sécurité indépendants protègent contre un isolement coincé.
1. Le délai d'attente d'évaluation / chien de garde SIGKILL (côté maître)#
Le maître attend recv_timeout pour chaque trame :
SUBPROCESS_EVAL_TIMEOUT_SECS = 30 pour les rendus,
SUBPROCESS_STREAMING_TIMEOUT_SECS = 300 pour les flux (constantes de temps de compilation). En cas de violation, il marque l'ouvrier WORKER_DEAD,
SIGKILL indique le PID, enregistre le délai d'attente et le superviseur par emplacement le réapparaît (budget : 5 crashs / 60 s → 60 s de temps de recharge).
Il s'agit d'un instrument contondant - il mesure l'aller-retour du fil, pas la progression du processeur, et il tue l'isolat entier du (et chaque requête multiplexée dessus). Cette franchise est à l’origine du gel ci-dessous ; les deux mécanismes suivants existent pour éviter de jamais l'atteindre.
2. La date limite de rendu dans la V8 ( render_deadline.rs )#
BEXT_RENDER_DEADLINE_MS (par défaut 0 = désactivé ; recommandé 28000, c'est-à-dire 2 s sous le SIGKILL) arme un thread de surveillance qui interroge tous les
WATCHDOG_TICK_MS = 50 ms et appelle isolate.terminate_execution() sur un rendu synchrone calé sur le processeur au prochain point de sécurité V8. Il s'agit d'un multiplex-safe : seul le process_request l'arme, il ne met donc jamais fin au travail d'un co-locataire. Le fil d'évaluation appelle
cancel_terminate_execution() après déroulement pour que l'isolat reste réutilisable - une suppression gracieuse d'un rendu au lieu d'un SIGKILL du travailleur.
3. Le coupe-circuit (quarantaine)#
BEXT_V8_POOL_CIRCUIT_BREAKER=1 permet à un créneau expiré de mettre en quarantaine pour BEXT_V8_POOL_QUARANTINE_MS (par défaut 5 000, limité entre 500 et 120 000). Le planificateur dépriorise les créneaux mis en quarantaine (toujours utilisable en dernier recours si tous les créneaux sont mis en quarantaine, comptés via
quarantine_fallbacks). L'état de quarantaine est saisi par l'index d'emplacement, il survit donc à la réapparition SIGKILL + du processus sous-jacent.
Variables d'environnement#
Une référence complète se trouve dans Moteur de rendu V8 ; le sous-ensemble pertinent pour l'architecture :
| Variable d'environnement | Défaut | Effet |
|---|---|---|
BEXT_V8_POOL |
désactivé | Activez le pool de sous-processus (défini avec POOL_SIZE ). |
BEXT_V8_POOL_SIZE |
1 (production 8) | Nombre de processus de travail ; serré 1–16. |
BEXT_V8_MULTIPLEX |
éteint (prod 1) | Chauffeur multiplex coopératif par travailleur. |
BEXT_V8_POOL_QUEUE_DEPTH |
64 | Plafond mondial d’admission en vol (1 à 4 096). |
BEXT_V8_POOL_PER_WORKER_CAP |
8 / 16 | Plafond de simultanéité par travailleur. |
BEXT_V8_POOL_CIRCUIT_BREAKER |
désactivé | Emplacements bloqués en quarantaine. |
BEXT_V8_POOL_QUARANTINE_MS |
5000 | Refroidissement de la quarantaine (500-120 000). |
BEXT_V8_POOL_STREAMING_LANE |
0 | Réservez les N derniers emplacements en streaming uniquement. |
BEXT_RENDER_DEADLINE_MS |
0 | Délai In-V8 terminate_execution. |
BEXT_V8_EVAL_TIMEOUT_SECS |
30 | Délai d'expiration du thread d'évaluation en cours (≤600). |
BEXT_PRISM_EARLY_SHED |
désactivé | 503 avant la compilation lorsque la file d'attente est pleine. |
BEXT_PRISM_COMPILE_OFFTHREAD |
désactivé | Compilez sur spawn_blocking, hors du thread actix. |
BEXT_TURBOPACK_COMPILE_SINGLEFLIGHT |
sur | Déduplication de compilation par hachage de contenu. |
BEXT_TURBOPACK_SCOPED_INVALIDATION |
désactivé | Expulsez uniquement le site modifié, pas tous les sites. |
BEXT_TURBOPACK_PIPE_POOL_SIZE |
1 | Nombre de travailleurs tsc-rs --pipe. |
Les variables d'environnement nécessitent un redémarrage complet. Un échange --swap-only sans temps d'arrêt hérite de l'ancien environnement — voir Mises à niveau sans temps d'arrêt. Utilisez le chemin --legacy-restart lorsque vous modifiez l'une des variables ci-dessus.
Activez BEXT_RENDER_DEADLINE_MS=28000 (2 s sous le chien de garde SIGKILL) en production. Sans cela, un rendu synchrone calé sur le processeur ne peut être récupéré que par le SIGKILL brutal de 30 s - qui tue également chaque requête multiplexée sur ce travailleur.
Métrique#
Exposé sur /__bext/metrics (voir surveillance ) :
| Métrique | Signification |
|---|---|
bext_v8_pool_in_flight |
Demandes actuelles en cours à travers le pool. |
bext_v8_pool_admission_max |
Plafond global configuré. |
bext_v8_pool_rejections_total |
Rejets d'admission → 503s. |
bext_v8_pool_rejections_by_reason_total{reason} |
Répartition : admission_full, per_worker_cap_filtered, hard_cap_filtered, heap_pressure_filtered, no_parked_slot_filtered. |
bext_v8_pool_wedge_timeouts_total |
Slots SIGKILLed pour un délai d'attente du coin. Maintenu non nul = coincement des travailleurs. |
bext_v8_pool_quarantined_workers |
Machines à sous actuellement en période de réflexion. |
bext_v8_pool_quarantine_fallbacks_total |
Requêtes acheminées vers un emplacement en quarantaine (il ne reste plus d'emplacement sain). Escalade = décrochage systémique. |
bext_v8_subprocess_crashes_total / _respawns_total |
Compteurs de crash + réapparition ; crash ≫ réapparition signifie que le superviseur limite le débit. |
bext_v8_in_isolate_dispatch_total{outcome} |
Résultats de court-circuit de récupération de bouclage isolé. |
Le gel de l’ensemble de la piscine du 10/06/2026#
La conception de résilience ci-dessus a été en grande partie expédiée en réponse à un incident. Il vaut la peine d’être lu comme une étude de cas sur la façon dont un pool partagé et indifférencié échoue.
Que s’est-il passé (23 : 47-23 : 59). Les 8 travailleurs d'évaluation ont déclenché le chien de garde de 30 s à quelques secondes d'intervalle, sur des itinéraires totalement indépendants ( /, une page de comparaison dynamique, une page de gestion, un exemple ISR et un flux de widget SSE). Une tempête de réapparition a suivi – 7 SIGKILL en 26 s – et la piscine s'est auto-guérison à 00h00.
Pourquoi une chose lente a tout détruit – cinq causes structurelles :
- Le pool est global et indifférencié. Un flux SSE a épinglé un isoler qui rend le trafic également nécessaire.
- Le planificateur ne connaissait pas l'état de calage. Il classe uniquement les travailleurs
par décompte en vol ; un travailleur saturé de CPU semblait identique à un travailleur en bonne santé jusqu'à ce que son
recv_timeoutse déclenche 30 s plus tard - le maître a donc continué à nourrir tous les travailleurs jusqu'à ce qu'ils déclenchent ensemble. - Le chien de garde des années 30 est un SIGKILL brutal. Une requête lente a tué le
travailleur et chaque demande multiplexée dessus. Le gracieux in-V8
terminate_executionexistait maisBEXT_RENDER_DEADLINE_MSn'était pas défini dans la production. - Le chien de garde mesure l'horloge murale et non la progression du processeur. Sous hôte saturation, les rendus légitimes ont duré plus de 30 s et sont devenus impossibles à distinguer des rendus coincés → cluster SIGKILL → réapparitions à froid, rechargement de l'instantané + bundle → plus de pression CPU → tempête.
- Une tempête de recompilation a été le déclencheur. Un script a réécrit deux
Modules
*.generated.ts; chaque réécriture a immédiatement expulsé le module et ses dépendants du registre, et N requêtes simultanées pour le même module à haut sort, chacune compilée à froid indépendamment - sérialisée via un seul canal tsc-rs, sur les threads de demande - augmentant le processeur exactement pendant qu'un flux SSE détenait un isolat.
Les correctifs (tous livrés le 11/06/2026, chacun derrière son propre indicateur d'environnement pour un déploiement indépendant) : le disjoncteur (#2), la date limite de rendu dans la V8 rendue déployable (#3), les voies de streaming (#1), la compilation en un seul vol + l'invalidation de portée + la compilation hors thread + un pool de canaux multi-slots (#5) et la suppression anticipée + l'application de la connexion maximale du proxy. (décharge de charge). Aucun n'est activé par défaut ; ils sont activés par déploiement.
La leçon codée dans l'architecture : , un pool de rendu partagé a besoin d'une isolation de la charge de travail, d'une planification tenant compte des coins, de suppressions gracieuses par rendu et d'un amortissement des tempêtes de compilation. – tuer sans ambages les isolats lors d'un délai d'attente d'horloge murale est un dernier recours, pas une stratégie.
Références croisées#
- Pipeline de rendu PRISM — ce qui produit les bundles exécutés par ce pool.
- Moteur de rendu V8 — la configuration complète référence.
- Cycle de vie des demandes — où le l'étape de rendu se trouve dans le pipeline HTTP.
- Mise à l'échelle · Surveillance · Dépannage — exploitation de la piscine.
- Mises à niveau sans temps d'arrêt — pourquoi des changements d'environnement sont nécessaires un redémarrage complet.