@nodefony/devkit 10.0.0-beta.3 → 10.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -4
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +24 -11
- package/dist/nodefony/config/config.js +16 -137
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controllers/DevkitController.js +7 -14
- package/dist/nodefony/controllers/McpController.js +25 -226
- package/dist/nodefony/controllers/ProfileSessionController.js +47 -0
- package/dist/nodefony/mcp/observe.js +235 -0
- package/dist/nodefony/mcp/profileReport.js +242 -0
- package/dist/nodefony/mcp/profileRoute.js +521 -0
- package/dist/nodefony/mcp/shotClient.js +84 -0
- package/dist/nodefony/mcp/shutdown.js +44 -0
- package/dist/nodefony/mcp/tools.js +696 -0
- package/dist/nodefony/service/DevkitService.js +93 -29
- package/dist/nodefony/src/errors/DevkitError.js +1 -2
- package/dist/nodefony/src/profileSession.js +14 -0
- package/dist/types/index.d.ts +15 -1
- package/dist/types/nodefony/config/config.d.ts +1 -1
- package/dist/types/nodefony/controllers/McpController.d.ts +11 -26
- package/dist/types/nodefony/controllers/ProfileSessionController.d.ts +31 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +22 -1
- package/dist/types/nodefony/mcp/observe.d.ts +83 -0
- package/dist/types/nodefony/mcp/profileReport.d.ts +138 -0
- package/dist/types/nodefony/mcp/profileRoute.d.ts +87 -0
- package/dist/types/nodefony/mcp/shotClient.d.ts +53 -0
- package/dist/types/nodefony/mcp/shutdown.d.ts +25 -0
- package/dist/types/nodefony/mcp/tools.d.ts +109 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +41 -4
- package/dist/types/nodefony/src/profileSession.d.ts +12 -0
- package/docs/index.md +160 -11
- package/package.json +16 -11
- package/skills/nodefony-add-crud/SKILL.md +5 -0
- package/skills/nodefony-add-service/SKILL.md +3 -3
- package/skills/nodefony-authentication/SKILL.md +5 -5
- package/skills/nodefony-browser/SKILL.md +20 -0
- package/skills/nodefony-browser/references/socket.md +13 -4
- package/skills/nodefony-browser/scripts/inspect.mjs +1 -1
- package/skills/nodefony-browser/scripts/lib/probes.mjs +53 -0
- package/skills/nodefony-browser/scripts/socket.mjs +70 -4
- package/skills/nodefony-browser/scripts/watch.mjs +101 -0
- package/skills/nodefony-dev/SKILL.md +31 -1
- package/skills/nodefony-dev/scripts/docs.mjs +3 -3
- package/skills/nodefony-devops/SKILL.md +85 -64
- package/skills/nodefony-devops/references/dimensionnement.md +122 -0
- package/skills/nodefony-devops/references/frontal.md +277 -47
- package/skills/nodefony-devops/references/kubernetes.md +41 -14
- package/skills/nodefony-devops/references/secrets.md +13 -2
- package/skills/nodefony-devops/references/variables.md +14 -12
- package/skills/nodefony-protect-route/SKILL.md +6 -4
- package/skills/nodefony-security-review/scripts/review-diff.mjs +4 -4
- package/skills/nodefony-troubleshoot/SKILL.md +86 -12
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nodefony-devops
|
|
3
3
|
metadata:
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
description: >
|
|
6
|
-
Exploite CETTE application Nodefony en conteneur : image, compose, frontal,
|
|
7
|
-
Kubernetes, Podman. N'enseigne pas Docker — il
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
Exploite CETTE application Nodefony en conteneur : image, compose, frontal (nginx, HAProxy, h2c,
|
|
7
|
+
Ingress), secrets, variables, Kubernetes, Podman, nombre de pods. N'enseigne pas Docker — il
|
|
8
|
+
énonce le CONTRAT d'exploitation, chiffré : sondes, durée du drain, code de sortie, identifiant,
|
|
9
|
+
dossiers écrits, ce qui casse quand une variable manque. À charger AVANT d'écrire un manifeste,
|
|
10
|
+
un compose, une configuration de frontal, de dimensionner les pods, ou de diagnostiquer un
|
|
11
|
+
conteneur qui redémarre.
|
|
12
12
|
Déclencheurs : "déployer mon application en production", "mettre mon app en production",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
13
|
+
"docker compose", "derrière nginx", "derrière HAProxy", "Ingress", "h2c",
|
|
14
|
+
"ma WebSocket se coupe derrière le proxy", "SSE derrière un proxy", "combien de pods ?", "dimensionner un pod",
|
|
15
|
+
"plusieurs pods en temps réel", "Kubernetes", "mon pod redémarre", "mon pod perd des requêtes",
|
|
16
|
+
"arrêt gracieux", "politique Restricted", "secrets", "variables d'environnement", "Podman".
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
# nodefony-devops — exploiter cette application
|
|
@@ -28,19 +28,21 @@ Tu sais faire du Docker. Ce que tu ne sais pas encore, c'est ce que **cette** ap
|
|
|
28
28
|
Ce que l'application tient, sans que tu aies rien à coder. Chiffré, parce qu'un ordonnanceur se
|
|
29
29
|
règle avec des nombres.
|
|
30
30
|
|
|
31
|
-
| Ce que tu veux | Ce qu'elle fait
|
|
32
|
-
| --------------------------------- |
|
|
33
|
-
| **Savoir si le process vit** | `GET /livez` → `200`. Reste à `200` **pendant tout l'arrêt** : un exemplaire qui draine est vivant, le tuer ferait exactement ce qu'on veut éviter.
|
|
34
|
-
| **Savoir s'il peut servir** | `GET /readyz` → `503` avant d'être prêt, `200` ensuite, et `503` **dès la première milliseconde du SIGTERM**. C'est lui qui te retire du répartiteur.
|
|
35
|
-
| **Ne pas être cru mort à tort** | Les deux sondes **court-circuitent le pipeline avant la limitation de débit**. Un agent d'ordonnancement limité recevrait `429`, qu'il lirait comme un échec de sonde.
|
|
36
|
-
| **Un arrêt propre** | `SIGTERM` → refus des nouvelles requêtes, on laisse finir celles en vol, borné à **15 s**. Sous les 30 s qu'un kubelet accorde par défaut.
|
|
37
|
-
| **Laisser le répartiteur suivre** | Un délai configurable **avant** le début du drain, le temps que ton frontal retire l'exemplaire de sa table.
|
|
38
|
-
| **Vérifier que l'arrêt a marché** | Code de sortie **`0`**. Un **`137`** signifie qu'il a été tué : les requêtes en vol sont perdues, à chaque déploiement, **sans une ligne de journal**.
|
|
39
|
-
| **Un utilisateur non root** | `USER 1000:1000` — **numérique**, donc utilisable tel quel par un `runAsUser`. Un `USER nom` ne l'est pas, et certains ordonnanceurs le refusent.
|
|
40
|
-
| **Des secrets montés** |
|
|
41
|
-
| **Identifier l'exemplaire** | `NF_POD_NAME` — à alimenter depuis `metadata.name`. Elle dérive l'identité d'origine du bus temps réel ; sans elle, plusieurs exemplaires se confondent.
|
|
42
|
-
| **Un port** | `5151`, déclaré par `EXPOSE`. Au-dessus de 1024, donc aucune capacité privilégiée n'est requise.
|
|
43
|
-
| **
|
|
31
|
+
| Ce que tu veux | Ce qu'elle fait |
|
|
32
|
+
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| **Savoir si le process vit** | `GET /livez` → `200`. Reste à `200` **pendant tout l'arrêt** : un exemplaire qui draine est vivant, le tuer ferait exactement ce qu'on veut éviter. |
|
|
34
|
+
| **Savoir s'il peut servir** | `GET /readyz` → `503` avant d'être prêt, `200` ensuite, et `503` **dès la première milliseconde du SIGTERM**. C'est lui qui te retire du répartiteur. |
|
|
35
|
+
| **Ne pas être cru mort à tort** | Les deux sondes **court-circuitent le pipeline avant la limitation de débit**. Un agent d'ordonnancement limité recevrait `429`, qu'il lirait comme un échec de sonde. |
|
|
36
|
+
| **Un arrêt propre** | `SIGTERM` → refus des nouvelles requêtes, on laisse finir celles en vol, borné à **15 s**. Sous les 30 s qu'un kubelet accorde par défaut. |
|
|
37
|
+
| **Laisser le répartiteur suivre** | Un délai configurable **avant** le début du drain, le temps que ton frontal retire l'exemplaire de sa table. |
|
|
38
|
+
| **Vérifier que l'arrêt a marché** | Code de sortie **`0`**. Un **`137`** signifie qu'il a été tué : les requêtes en vol sont perdues, à chaque déploiement, **sans une ligne de journal**. |
|
|
39
|
+
| **Un utilisateur non root** | `USER 1000:1000` — **numérique**, donc utilisable tel quel par un `runAsUser`. Un `USER nom` ne l'est pas, et certains ordonnanceurs le refusent. |
|
|
40
|
+
| **Des secrets montés** | Les variables du catalogue acceptent le suffixe `_FILE` : `NF_CSRF_SECRET_FILE=/run/secrets/csrf` lit le contenu du fichier — URL de base et de cache comprises (`NF_DATABASE_URL_FILE`). |
|
|
41
|
+
| **Identifier l'exemplaire** | `NF_POD_NAME` — à alimenter depuis `metadata.name`. Elle dérive l'identité d'origine du bus temps réel ; sans elle, plusieurs exemplaires se confondent. |
|
|
42
|
+
| **Un port** | `5151`, déclaré par `EXPOSE`. Au-dessus de 1024, donc aucune capacité privilégiée n'est requise. |
|
|
43
|
+
| **HTTP/2 vers un frontal** | En option, un port **h2c** (HTTP/2 en clair, `5153`) : `NF__APP__SERVERS__H2C__ENABLED=true`. Ni sondes ni WebSocket dessus — elles restent sur `5151`. |
|
|
44
|
+
| **Des WebSocket qui survivent** | Un battement de cœur ping/pong réclame les sockets mortes ; à l'arrêt, chaque client reçoit `1001` et se reconnecte ailleurs. Le frontal doit tolérer ≥ 300 s d'inactivité. |
|
|
45
|
+
| **Un contrôle de santé natif** | `HEALTHCHECK` sur `/readyz`, déjà dans l'image. |
|
|
44
46
|
|
|
45
47
|
**Où l'application écrit — la question qui décide de tout le reste.** Deux dossiers, et deux
|
|
46
48
|
seulement : **`tmp/`** et **`var/`**. Tout le reste du système de fichiers peut être scellé.
|
|
@@ -74,9 +76,14 @@ Ce que l'application ne peut pas deviner. Chacun de ces oublis produit une dégr
|
|
|
74
76
|
**silencieuse** — pas une erreur, pas une alerte.
|
|
75
77
|
|
|
76
78
|
- **`NF__HTTP__TRUSTPROXY`** dès qu'un frontal termine le TLS. Sans lui, l'application constate un
|
|
77
|
-
schéma `http`
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
schéma `http` : **toute connexion par mot de passe répond `403`** (le secret est reçu « en
|
|
80
|
+
clair »), le cookie de session perd son préfixe `__Host-`, et l'adresse cliente devient celle du
|
|
81
|
+
frontal — donc une vague de reconnexions WebSocket épuise **un seul** quota. Sans le moindre
|
|
82
|
+
message. Détail et preuve → [`references/frontal.md`](references/frontal.md).
|
|
83
|
+
- **Une inactivité de frontal ≥ 300 s.** Plus courte que le battement, elle coupe des WebSocket et
|
|
84
|
+
des flux SSE **vivants**. `proxy:generate` la dérive ; un contrôleur d'entrée Kubernetes, non.
|
|
85
|
+
- **Un bus temps réel partagé dès deux exemplaires** : `NF_REALTIME_DRIVER=redis`. Sans lui, un
|
|
86
|
+
message n'atteint que les clients du pod qui l'émet → [`references/dimensionnement.md`](references/dimensionnement.md).
|
|
80
87
|
- **Deux volumes inscriptibles**, sur `/app/tmp` et `/app/var`, si tu scelles la racine.
|
|
81
88
|
- **Une fenêtre d'arrêt ≥ 30 s**, strictement supérieure au drain de 15 s.
|
|
82
89
|
- **Pas de limite processeur.** Une limite mémoire tue proprement et l'exemplaire redémarre ; une
|
|
@@ -88,20 +95,22 @@ Ce que l'application ne peut pas deviner. Chacun de ces oublis produit une dégr
|
|
|
88
95
|
|
|
89
96
|
Chaque ligne a la commande qui la **prouve**. Une case cochée sans commande est une supposition.
|
|
90
97
|
|
|
91
|
-
| # | Le point | La preuve
|
|
92
|
-
| --- | --------------------------------------------------------- |
|
|
93
|
-
| 1 | L'image se construit | `docker build -t <app> .`
|
|
94
|
-
| 2 | Elle n'embarque **aucun secret** | `npx nodefony image:check <app>` — lit les **couches**, pas l'image aplatie (§5)
|
|
95
|
-
| 3 | Elle démarre et se déclare prête | `docker run -d --name t <app>` puis `curl -fsS localhost:5151/readyz`
|
|
96
|
-
| 4 | Elle s'arrête **proprement** | `docker stop t; docker inspect t --format '{{.State.ExitCode}}'` → doit rendre **`0`**
|
|
97
|
-
| 5 | Les variables requises sont posées | `npx nodefony doctor --env production`
|
|
98
|
-
| 6 | Les secrets sont **montés**, pas dans l'environnement | Suffixe `_FILE` → [`references/secrets.md`](references/secrets.md)
|
|
99
|
-
| 7 | Les migrations passent **avant** les nouveaux exemplaires | `kubectl apply -f deploy/migrate-job.yaml`, attendre la fin
|
|
100
|
-
| 8 | Le frontal est **dérivé**, pas écrit à la main | `npx nodefony proxy:generate nginx`
|
|
101
|
-
| 9 | Le schéma constaté derrière le frontal est le bon | Une requête réelle pose un cookie `__Host-` → [`references/frontal.md`](references/frontal.md)
|
|
102
|
-
| 10 | Un redéploiement ne perd **aucune** requête | Charge constante pendant un `rollout restart` ; zéro `5xx`
|
|
103
|
-
| 11 |
|
|
104
|
-
| 12 |
|
|
98
|
+
| # | Le point | La preuve |
|
|
99
|
+
| --- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
100
|
+
| 1 | L'image se construit | `docker build -t <app> .` |
|
|
101
|
+
| 2 | Elle n'embarque **aucun secret** | `npx nodefony image:check <app>` — lit les **couches**, pas l'image aplatie (§5) |
|
|
102
|
+
| 3 | Elle démarre et se déclare prête | `docker run -d --name t <app>` puis `curl -fsS localhost:5151/readyz` |
|
|
103
|
+
| 4 | Elle s'arrête **proprement** | `docker stop t; docker inspect t --format '{{.State.ExitCode}}'` → doit rendre **`0`** |
|
|
104
|
+
| 5 | Les variables requises sont posées | `npx nodefony doctor --env production` |
|
|
105
|
+
| 6 | Les secrets sont **montés**, pas dans l'environnement | Suffixe `_FILE` → [`references/secrets.md`](references/secrets.md) |
|
|
106
|
+
| 7 | Les migrations passent **avant** les nouveaux exemplaires | `kubectl apply -f deploy/migrate-job.yaml`, attendre la fin |
|
|
107
|
+
| 8 | Le frontal est **dérivé**, pas écrit à la main | `npx nodefony proxy:generate nginx` |
|
|
108
|
+
| 9 | Le schéma constaté derrière le frontal est le bon | Une requête réelle pose un cookie `__Host-` → [`references/frontal.md`](references/frontal.md) |
|
|
109
|
+
| 10 | Un redéploiement ne perd **aucune** requête | Charge constante pendant un `rollout restart` ; zéro `5xx` |
|
|
110
|
+
| 11 | Une WebSocket **traverse** le frontal et y survit | Ouverte à travers lui, muette plus d'une minute : toujours ouverte |
|
|
111
|
+
| 12 | Le nombre de pods et leur mémoire sont **calculés** | Débit mesuré sur **ton** application → [`references/dimensionnement.md`](references/dimensionnement.md) |
|
|
112
|
+
| 13 | La racine du conteneur est **scellée** | `--read-only` + volumes sur `/app/tmp` et `/app/var` |
|
|
113
|
+
| 14 | Tu sais ce que tu **ne** peux **pas** garantir | Podman sans privilèges perd l'adresse du client (§7) — à énoncer, pas à masquer |
|
|
105
114
|
|
|
106
115
|
## 5. Symptôme → cause → correction
|
|
107
116
|
|
|
@@ -112,7 +121,13 @@ Le tableau qu'on lit en urgence.
|
|
|
112
121
|
| L'exemplaire redémarre en boucle, journaux muets | La sonde de vivacité tombe avant la fin du démarrage | Sonde de démarrage sur `/readyz`, dimensionnée sur le **pire** cas |
|
|
113
122
|
| Le conteneur sort en **137** à l'arrêt | Il a été tué : la fenêtre d'arrêt est ≤ au drain | Fenêtre ≥ 30 s (Podman : `--stop-timeout 20`, son défaut est **10 s**) |
|
|
114
123
|
| Des `5xx` **pendant** chaque déploiement | Le répartiteur envoie encore du trafic quand le drain a commencé | Un délai avant drain, et un court arrêt avant terminaison |
|
|
115
|
-
|
|
|
124
|
+
| Le cookie de session n'a pas le préfixe `__Host-` en production | Schéma constaté `http` derrière le frontal | `NF__HTTP__TRUSTPROXY` |
|
|
125
|
+
| La WebSocket se ferme seule au bout d'une minute | L'inactivité du frontal est plus courte que le battement | Régénérer le frontal ; sur un contrôleur d'entrée, `proxy-read-timeout` ≥ 300 s → [`references/frontal.md`](references/frontal.md) |
|
|
126
|
+
| Un message temps réel n'arrive qu'à une partie des clients | Plusieurs exemplaires, bus local : chaque pod ne diffuse qu'à ses propres clients | `NF_REALTIME_DRIVER=redis` + `NF_REDIS_URL` → [`references/dimensionnement.md`](references/dimensionnement.md) |
|
|
127
|
+
| Depuis l'activation de h2c, le pod n'est jamais prêt | La sonde vise le port h2c (5153), qui refuse HTTP/1.1 | Sonder le port HTTP (5151) |
|
|
128
|
+
| Depuis l'activation de h2c, les WebSocket ne s'ouvrent plus | Le frontal les envoie au port h2c | Router `Upgrade: websocket` vers le port HTTP → [`references/frontal.md`](references/frontal.md) §8 |
|
|
129
|
+
| `DB_SQLITE_FALLBACK` en production alors que la base est posée | Le nom de la variable est faux d'un caractère, ou le Secret n'est pas monté dans CE conteneur | `npx nodefony doctor --env production` nomme la variable lue (`NF_DATABASE_URL` ou `NF_DATABASE_URL_FILE`) |
|
|
130
|
+
| Le pod est tué (`OOMKilled`) avec peu de connexions | La contre-pression : des clients lents retiennent jusqu'à 4 Mio chacun | Baisser `maxBackpressure` sur `websocket` **et** `websocketSecure` → [`references/dimensionnement.md`](references/dimensionnement.md) |
|
|
116
131
|
| Toutes les connexions répondent `403` « doit passer par HTTPS » | Même cause : le mot de passe arrive sur une requête que l'application croit en clair | `NF__HTTP__TRUSTPROXY` — jamais `allowInsecureCredentials` |
|
|
117
132
|
| Sortie **78** au démarrage, une clé ou une variable nommée | Faute de configuration : clé inconnue (ou retirée par une mise à jour), valeur invalide, secret requis absent (`NF_CSRF_SECRET`, `NF_JWT_KEYSET`) | Lire la ligne qui la nomme ; `npx nodefony doctor --env production` annonce AVANT les variables requises (pas `NF_JWT_KEYSET`, exigée seulement si l'app émet des jetons) |
|
|
118
133
|
| Les journaux d'audit montrent tous la **même adresse** | L'adresse du client n'est pas propagée, ou le frontal n'est pas de confiance | `trustProxy` ; en Podman sans privilèges, **c'est irréparable** (§7) |
|
|
@@ -131,21 +146,26 @@ Service et l'Ingress ne le sont pas — tu les écris, et les §1 et §3 te dise
|
|
|
131
146
|
kubectl apply -f deploy/migrate-job.yaml # les migrations AVANT les exemplaires
|
|
132
147
|
```
|
|
133
148
|
|
|
134
|
-
**Le manifeste rendu
|
|
135
|
-
|
|
149
|
+
**Le manifeste rendu passe la politique Restricted.** Recopie-en le contexte de sécurité dans ton
|
|
150
|
+
Deployment — sauf son `readOnlyRootFilesystem: false`, juste pour un travail éphémère, pas pour un
|
|
151
|
+
exemplaire qui sert du trafic.
|
|
136
152
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
satisfaisables :
|
|
153
|
+
Aucun de ces champs n'est « fourni par l'image » : ce sont des champs de **manifeste**, c'est toi
|
|
154
|
+
qui les poses. Ce que l'image apporte, c'est de les rendre satisfaisables :
|
|
140
155
|
|
|
141
|
-
| Le champ | Ce qu'il te coûte
|
|
142
|
-
| ------------------------------------- |
|
|
143
|
-
| `runAsNonRoot: true` | rien
|
|
144
|
-
| `runAsUser` / `runAsGroup` numériques | rien — l'image déclare `USER 1000:1000`, tu reportes `1000`
|
|
145
|
-
| `allowPrivilegeEscalation: false` | rien
|
|
146
|
-
| `capabilities.drop: ["ALL"]` | rien — l'application n'écoute qu'au-dessus de 1024
|
|
147
|
-
| `seccompProfile: RuntimeDefault` | rien — mais
|
|
148
|
-
| `readOnlyRootFilesystem: true` | **les deux volumes du §3** — sans eux le conteneur ne démarre pas
|
|
156
|
+
| Le champ | Exigé par Restricted ? | Ce qu'il te coûte |
|
|
157
|
+
| ------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `runAsNonRoot: true` | **oui** | rien |
|
|
159
|
+
| `runAsUser` / `runAsGroup` numériques | non nul **s'il est posé** | rien — l'image déclare `USER 1000:1000`, tu reportes `1000` |
|
|
160
|
+
| `allowPrivilegeEscalation: false` | **oui** | rien |
|
|
161
|
+
| `capabilities.drop: ["ALL"]` | **oui** | rien — l'application n'écoute qu'au-dessus de 1024 |
|
|
162
|
+
| `seccompProfile: RuntimeDefault` | **oui** | rien — mais la politique traite l'**absence** de profil comme une violation, pas comme un défaut permissif |
|
|
163
|
+
| `readOnlyRootFilesystem: true` | non — durcissement en plus | **les deux volumes du §3** — sans eux le conteneur ne démarre pas |
|
|
164
|
+
|
|
165
|
+
**Sondes sur le port HTTP (5151)**, même avec h2c actif. Combien d'exemplaires, quelle mémoire, et
|
|
166
|
+
le bus partagé dès qu'il y en a deux → [`references/dimensionnement.md`](references/dimensionnement.md).
|
|
167
|
+
L'Ingress (ingress-nginx est retiré depuis mars 2026), la Gateway API et leurs délais →
|
|
168
|
+
[`references/frontal.md`](references/frontal.md) §10.
|
|
149
169
|
|
|
150
170
|
**La base de données n'est pas dans le cluster**, et ça a trois conséquences dures →
|
|
151
171
|
[`references/kubernetes.md`](references/kubernetes.md).
|
|
@@ -173,15 +193,16 @@ de la politique, ni l'Ingress, ni un déploiement progressif. Ce qu'il ne prouve
|
|
|
173
193
|
|
|
174
194
|
## 8. Les références
|
|
175
195
|
|
|
176
|
-
| Fichier
|
|
177
|
-
|
|
|
178
|
-
| [`references/image.md`](references/image.md)
|
|
179
|
-
| [`references/compose.md`](references/compose.md)
|
|
180
|
-
| [`references/variables.md`](references/variables.md)
|
|
181
|
-
| [`references/secrets.md`](references/secrets.md)
|
|
182
|
-
| [`references/frontal.md`](references/frontal.md)
|
|
183
|
-
| [`references/kubernetes.md`](references/kubernetes.md)
|
|
184
|
-
| [`references/
|
|
196
|
+
| Fichier | Quand l'ouvrir |
|
|
197
|
+
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
198
|
+
| [`references/image.md`](references/image.md) | Modifier le `Dockerfile` : étages, arguments, bases candidates, ce qui ne se touche pas |
|
|
199
|
+
| [`references/compose.md`](references/compose.md) | Services, profils, volumes, réseaux, le profil de topologie de production |
|
|
200
|
+
| [`references/variables.md`](references/variables.md) | Le catalogue, la grammaire des noms, `_FILE`, **ce qui casse si absent** |
|
|
201
|
+
| [`references/secrets.md`](references/secrets.md) | Mots de passe, rotation, couches d'image, ce qui n'entre jamais |
|
|
202
|
+
| [`references/frontal.md`](references/frontal.md) | nginx et HAProxy dérivés, `trustProxy`, SSE, **h2c**, Envoy, Traefik, Ingress, Gateway API |
|
|
203
|
+
| [`references/kubernetes.md`](references/kubernetes.md) | Ton Deployment, Restricted, sondes, arrêt, la base administrée |
|
|
204
|
+
| [`references/dimensionnement.md`](references/dimensionnement.md) | Combien de pods, quelle mémoire, WebSocket et SSE, plusieurs exemplaires |
|
|
205
|
+
| [`references/podman.md`](references/podman.md) | Les six écarts, et celui qui ne se répare pas |
|
|
185
206
|
|
|
186
207
|
## 9. Passer la main
|
|
187
208
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Dimensionner un pod — HTTP, WebSocket, SSE
|
|
2
|
+
|
|
3
|
+
> **Maintenance** : vérité courante. Éditer en place. Les constantes du §2 se recalent sur la page
|
|
4
|
+
> de performance publiée, jamais de mémoire.
|
|
5
|
+
|
|
6
|
+
## 1. Le modèle — un process, une boucle, un cœur
|
|
7
|
+
|
|
8
|
+
Un process Node exécute ton code sur **une seule** boucle d'événements. Ce qui borne un pod n'est
|
|
9
|
+
donc pas le nombre de cœurs de la machine, c'est la part de temps où cette boucle travaille :
|
|
10
|
+
l'**ELU** (_event loop utilization_, de 0 à 1). À ELU 1, le pod ne sert pas plus vite : il met en
|
|
11
|
+
file, et la latence monte.
|
|
12
|
+
|
|
13
|
+
| Conséquence | Le geste |
|
|
14
|
+
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
+
| Un pod n'utilise **qu'un cœur** pour ton code | `requests.cpu: "1"`. Davantage ne sert qu'au ramasse-miettes et aux E/S de fond |
|
|
16
|
+
| Une **limite** processeur ralentit sans tuer | pas de `limits.cpu` (voir le §3 de la page principale) |
|
|
17
|
+
| Monter en charge, c'est **ajouter des pods** | horizontal, par l'orchestrateur ; `nodefony cluster -w N` n'est que pour une machine sans orchestrateur |
|
|
18
|
+
| Plusieurs workers dans un pod | à éviter : la diffusion `broadcast()` ne touche que les clients du **même** worker, et le cluster Node répartit des **connexions** — avec les rares connexions h2c d'un proxy, un worker peut tout recevoir |
|
|
19
|
+
|
|
20
|
+
## 2. Les constantes mesurées — un ordre de grandeur, pas ta capacité
|
|
21
|
+
|
|
22
|
+
Mesurées sur Nodefony **10.0.0**, en **production**, Node 26.10, **un** process, route de banc
|
|
23
|
+
**triviale**, charge de 1 Ko. Chaque débit est ramené à ELU 1 (débit observé ÷ ELU), donc c'est un
|
|
24
|
+
**plafond**. Source et calculateur :
|
|
25
|
+
[peut-on partir en production ?](https://nodefony.github.io/nodefony-core/performance/latest/).
|
|
26
|
+
|
|
27
|
+
| Constante | Valeur | Ce qu'elle dit |
|
|
28
|
+
| ------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------- |
|
|
29
|
+
| Tas par socket WebSocket — TLS terminé au frontal | **≈ 10 Ko** | le coût d'une connexion **ouverte et muette** ; pente linéaire sur 0 → 800 sockets |
|
|
30
|
+
| Tas par socket — TLS terminé par Node (`wss`) | ≈ 12 à 14 Ko | mesure non concordante d'un run à l'autre : retenir le haut de la fourchette |
|
|
31
|
+
| Écho WebSocket 1:1 | ≈ 35 000 à 46 000 msg/s | messages reçus **et** renvoyés par seconde, à ELU 1 |
|
|
32
|
+
| Diffusion à 100 abonnés | ≈ 197 000 livraisons/s | une livraison = un message remis à un abonné ; c'est elle qui coûte, pas l'émission |
|
|
33
|
+
| HTTP/1.1, route triviale | ≈ 15 000 req/s (TLS ≈ 12 000) | p99 ≈ 0,3 ms à vide — **ton** contrôleur, ta base et tes services dominent ce chiffre |
|
|
34
|
+
| Mémoire au repos (RSS) | **à mesurer chez toi** | dépend des modules chargés ; `docker stats` sur un conteneur prêt et sans trafic |
|
|
35
|
+
|
|
36
|
+
🔴 **Ce que ces constantes ne disent pas.** Une route réelle lit une base, signe un jeton, rend une
|
|
37
|
+
vue : elle coûte dix à cent fois la route de banc. Le coût d'un **flux SSE** ouvert n'est pas
|
|
38
|
+
mesuré. Ces nombres servent à vérifier un ordre de grandeur, puis à **mesurer ton application**
|
|
39
|
+
(§5) — jamais à remplir directement un manifeste.
|
|
40
|
+
|
|
41
|
+
## 3. La formule
|
|
42
|
+
|
|
43
|
+
Trois plafonds, et le pod est borné par le **premier** atteint :
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
pods = max( ⌈ C / C_pod ⌉ , ⌈ M / (D_msg × u) ⌉ , ⌈ R / (D_req × u) ⌉ ) + 1
|
|
47
|
+
|
|
48
|
+
C connexions simultanées (WebSocket + flux SSE) au pic
|
|
49
|
+
M livraisons temps réel par seconde au pic (un message diffusé à N abonnés = N livraisons)
|
|
50
|
+
R requêtes HTTP par seconde au pic
|
|
51
|
+
D_msg livraisons/s d'UN pod à ELU 1 — mesurées sur TON application (§5)
|
|
52
|
+
D_req requêtes/s d'UN pod à ELU 1 — idem
|
|
53
|
+
u ELU visée : 0,6 — la marge absorbe une rafale et la reconnexion d'un déploiement (§4)
|
|
54
|
+
+1 le pod qui draine pendant un déploiement, ou celui qu'on perd
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`C_pod` se déduit de la **mémoire** :
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
limits.memory ≥ RSS_repos + C_pod × coût_socket + L × maxBackpressure + marge GC (≈ 30 %)
|
|
61
|
+
|
|
62
|
+
coût_socket ≈ 10 Ko de tas (§2) + les tampons noyau et TLS, hors tas
|
|
63
|
+
L clients LENTS simultanés tolérés
|
|
64
|
+
maxBackpressure file d'envoi par socket avant de jeter : 4 Mio par défaut
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> 🔴 **Le terme qui fait exploser la mémoire n'est pas le nombre de sockets, c'est la
|
|
68
|
+
> contre-pression.** Dix mille sockets muettes tiennent en ≈ 100 Mo de tas ; cent clients mobiles
|
|
69
|
+
> qui ne lisent plus, à 4 Mio chacun, en réservent 400. Pour des canaux d'**état** (seul le dernier
|
|
70
|
+
> instantané compte), baisser `maxBackpressure` — et le régler sur `websocket` **et**
|
|
71
|
+
> `websocketSecure`, régler l'un ne règle pas l'autre :
|
|
72
|
+
>
|
|
73
|
+
> ```ts
|
|
74
|
+
> use("@nodefony/http", {
|
|
75
|
+
> websocket: { maxBackpressure: 262144, backpressureCloseAfterDrops: 50 },
|
|
76
|
+
> websocketSecure: {
|
|
77
|
+
> maxBackpressure: 262144,
|
|
78
|
+
> backpressureCloseAfterDrops: 50,
|
|
79
|
+
> },
|
|
80
|
+
> });
|
|
81
|
+
> ```
|
|
82
|
+
>
|
|
83
|
+
> Au-delà du solde de refus, le client est fermé en `1013` et se reconnecte de lui-même.
|
|
84
|
+
|
|
85
|
+
**Exemple.** 20 000 connexions, 2 000 messages/s diffusés à 50 abonnés en moyenne (100 000
|
|
86
|
+
livraisons/s), 300 req/s. Ton application mesure `D_msg = 60 000`, `D_req = 1 500`, RSS au repos
|
|
87
|
+
250 Mo, et tu tolères 200 clients lents avec `maxBackpressure` à 256 Kio :
|
|
88
|
+
|
|
89
|
+
- mémoire par pod pour 10 000 connexions : 250 + 10 000 × 0,01 + 200 × 0,25 = 400 Mo, + 30 % ≈ **520 Mo** ;
|
|
90
|
+
- connexions : ⌈20 000 / 10 000⌉ = 2 ;
|
|
91
|
+
- temps réel : ⌈100 000 / (60 000 × 0,6)⌉ = 3 ;
|
|
92
|
+
- HTTP : ⌈300 / (1 500 × 0,6)⌉ = 1 ;
|
|
93
|
+
- **pods = 3 + 1 = 4**, `limits.memory: 768Mi`. C'est le temps réel qui dimensionne, pas les connexions.
|
|
94
|
+
|
|
95
|
+
## 4. Ce qui change dès qu'il y a plus d'un pod
|
|
96
|
+
|
|
97
|
+
| Fait | Conséquence, et le geste |
|
|
98
|
+
| ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
99
|
+
| Un canal est **local au pod** par défaut | deux utilisateurs sur deux pods ne se voient pas, **sans erreur**. Bus partagé : `NF_REALTIME_DRIVER=redis` et `NF_REDIS_URL`, plus un `namespace` explicite si le Redis est mutualisé — sinon deux applications se parlent. Le canal doit aussi **demander** à traverser : skill `nodefony-add-realtime-channel` |
|
|
100
|
+
| Chaque exemplaire doit être **identifiable** | `NF_POD_NAME` depuis `metadata.name` — sans elle, l'identité d'origine du bus se confond entre pods |
|
|
101
|
+
| Un déploiement **ferme** les WebSocket du pod qui draine (`1001`) | tous ses clients se reconnectent **en même temps** sur les pods restants : `maxUnavailable: 0`, `maxSurge: 1`, et la marge `u = 0,6` du §3 |
|
|
102
|
+
| La limitation de débit compte **par adresse cliente** | derrière un frontal sans `NF__HTTP__TRUSTPROXY`, tous les clients ont l'adresse du frontal : une vague de reconnexions épuise un seul quota et répond `429` à tout le monde |
|
|
103
|
+
| Une connexion WebSocket **ne migre pas** | après un ajout de pods, les anciens gardent leurs connexions jusqu'à ce qu'elles se rouvrent : l'équilibre ne revient qu'avec les reconnexions. Côté HAProxy, `balance leastconn` répartit mieux des connexions longues que le tourniquet |
|
|
104
|
+
| Le CPU est un mauvais signal de mise à l'échelle pour des connexions longues | un pod chargé de sockets muettes a peu de CPU et beaucoup de mémoire. L'application **n'expose aucune métrique Prometheus** : une mise à l'échelle sur le nombre de connexions demande de la produire toi-même |
|
|
105
|
+
| Chaque connexion est un **descripteur de fichier**, deux à travers un frontal sur la même machine | vérifier `ulimit -n` de l'hôte ou du conteneur avant de viser des dizaines de milliers de connexions par pod |
|
|
106
|
+
|
|
107
|
+
## 5. Mesurer TON application — la seule source de `D_msg` et `D_req`
|
|
108
|
+
|
|
109
|
+
1. **Un pod**, en production (`NODE_ENV=production`), à la taille prévue, avec ses vraies
|
|
110
|
+
dépendances (base, Redis). Pas de limite processeur.
|
|
111
|
+
2. Une charge qui ressemble au trafic réel : tes routes, tes canaux, tes tailles de message.
|
|
112
|
+
`h2load` (HTTP/1.1 et h2c avec le même instrument), `k6` ou `artillery` pour le WebSocket.
|
|
113
|
+
3. Monter par paliers jusqu'à ce que le CPU du conteneur plafonne autour d'**un** cœur
|
|
114
|
+
(`kubectl top pod`, `docker stats`) ou que le p99 décroche : le débit à ce palier est ton plafond.
|
|
115
|
+
4. Rejouer **trois fois** et garder la médiane. Un écart de plus de 10 % entre runs ne tranche
|
|
116
|
+
rien : le décor bouge (autre charge sur la machine, base froide).
|
|
117
|
+
5. Mesurer la mémoire **à connexions ouvertes et muettes**, par paliers (0, 1 000, 2 000…) : la
|
|
118
|
+
pente est ton `coût_socket`. Une mesure à un seul point mélange le repos et le coût.
|
|
119
|
+
|
|
120
|
+
> Le CPU d'un conteneur additionne la boucle, le ramasse-miettes et les fils d'E/S : il surestime
|
|
121
|
+
> un peu l'ELU. C'est sans gravité pour dimensionner — la marge `u = 0,6` le couvre — mais deux
|
|
122
|
+
> mesures ne se comparent qu'avec le même instrument.
|