From 469a5fde9edef9f1f675aebd7adabbf6638dcb22 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 27 Aug 2026 05:14:43 -0400 Subject: [PATCH] =?UTF-8?q?[FIX]=20imbrication=20:=20dimensionner=20les=20?= =?UTF-8?q?=C3=A9tages=20depuis=20le=20bas,=20vCPU=20compris?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le plan cédait à l'enfant ce que le parent pouvait céder. Descente réelle à dix étages : l'étage 4 a reçu 44 Go et 2 vCPU sur un hôte qui en avait 2 — cent pour cent de surengagement, à chaque étage. Son installation dure 2 h 52 et n'est pas finie, contre 793 s pour l'étage 3. Extrapolé, le dixième demandait des années. Le plus profond reçoit désormais ce qu'un Proxmox de test demande, et chaque parent ajoute son seul surcoût : un vCPU, 2 Gio, 10 Go. Dix étages tiennent sur 11 vCPU et 22 Go au premier, contre 50 Go avant. Corrige aussi l'explication du gel à 12 vCPU : cette VM avait douze vCPU sur un hôte qui en avait deux. C'est le surengagement qui gèle, pas le douze. --- EN --- The plan handed the child whatever the parent could spare. Real ten-level descent: level 4 got 44 GB and 2 vCPU on a host that had 2 — a hundred percent overcommit, at every level. Its install has run 2h52 and is not done, against 793 s for level 3. Extrapolated, the tenth wanted years. The deepest level now gets what a test Proxmox asks for, and each parent adds its own overhead only: one vCPU, 2 GiB, 10 GB. Ten levels fit in 11 vCPU and 22 GB at the first, against 50 GB before. Also corrects the account of the 12-vCPU freeze: that VM had twelve vCPU on a host with two. Overcommit freezes, not the twelve. Assisted-by: claude-opus-5 (cherry picked from commit 7cda84bf391ee3ed36c5953ca4b86df44bd09e3b) --- LongTest/README.base.md | 97 +++++++++++++-------- LongTest/README.fr.md | 50 +++++++---- LongTest/README.md | 47 +++++++---- script/proxmox/nesting.py | 159 +++++++++++++++++++++++------------ test/test_proxmox_nesting.py | 138 +++++++++++++----------------- 5 files changed, 294 insertions(+), 197 deletions(-) diff --git a/LongTest/README.base.md b/LongTest/README.base.md index c928bc5..d280936 100644 --- a/LongTest/README.base.md +++ b/LongTest/README.base.md @@ -48,27 +48,42 @@ the repository: it is our code we want to exercise, and the remote is often behind the checkout — a fix absent from the remote made the same defect "come back" on three VMs in a row. -### The resource algorithm +### The resource algorithm — sized from the bottom up -Two things run out going down, and a third degrades. What runs out is -arithmetic, and `script/proxmox/nesting.py` computes it: +The first version handed down whatever the parent could spare, and a real +descent showed what that costs. Level 4 ended up with 44 GB of memory and +2 vCPU **on a host that had 2** — a hundred percent overcommit, at every +level, with the hypervisor itself to serve on top. Its install ran past two +and a half hours against thirteen minutes for level 3, and extrapolating that +ratio gave five years for the tenth. -* **memory** — each level keeps what its own daemons need (`pve-cluster`, - `pvestatd`, `pvedaemon`, `pveproxy`) before handing the rest down; +So the direction is reversed. The deepest level gets what a test Proxmox +actually asks for — 4 GB of memory, 25 GB of disk, 2 vCPU — and every parent +above it adds its own overhead and nothing else: one vCPU, 2 GiB, 10 GB. A +ten-level descent therefore asks its first level for 11 vCPU, 22 GB and +115 GB, where handing resources down wanted 50 GB of memory for the same +depth. + +Three budgets can bound the depth, and `script/proxmox/nesting.py` names the +one that ran out: + +* **memory** — every level must run its own daemons (`pve-cluster`, + `pvestatd`, `pvedaemon`, `pveproxy`) *and* hold its child; * **disk** — the child's disk lives *inside* the parent's, which must also - hold its own system. + hold its own system; +* **processor** — each level wants one vCPU more than its child, so ten levels + ask eleven of the first. Half the physical cores is the ceiling: the + orchestrator runs on that machine too. -What degrades is measured, not assumed: past the second level, vendors -document nothing. Hence one capped number — **2 vCPU** for every nested -level. Twelve vCPU at the fourth level froze the guest kernel in early boot; -the same two progressed. Bringing twelve processors online costs as many -round trips through the whole stack. +That third budget is measured, not assumed. Twelve vCPU at the fourth level +froze the guest kernel in early boot — same instruction pointer at three +readings two minutes apart — while two progressed. The number was not the +culprit: that VM had twelve vCPU on a host with two, six times wider than its +own machine. Overcommit freezes, not the twelve. -Memory is **not** capped. On that one manual VM, dropping it from 9 GB to -2 GB moved nothing — it stopped after reading the same 32 MiB, which is simply -the size of the boot files. Memory was not the lever; the vCPU count was. And -trimming memory would starve the level below, which needs it to host the -next. +Memory is not the lever. On that same manual VM, dropping it from 9 GB to 2 GB +moved nothing — it stopped after reading the same 32 MiB, which is simply the +size of the boot files. The plan is printed **before** anything is created, and the script never promises a depth it knows will not fit — better to announce six levels and @@ -123,28 +138,44 @@ cloner le dépôt : c'est notre code qu'on veut éprouver, et le dépôt distant est souvent en retard sur le checkout — un correctif absent du distant a fait « revenir » le même défaut sur trois VM de suite. -### L'algorithme de ressources +### L'algorithme de ressources — dimensionné depuis le bas -Deux choses s'épuisent en descendant, et une troisième se dégrade. Ce qui -s'épuise est de l'arithmétique, et `script/proxmox/nesting.py` la calcule : +La première version cédait à l'enfant ce que le parent pouvait céder, et une +descente réelle a montré ce que cela coûte. L'étage 4 se retrouvait avec 44 Go +de mémoire et 2 vCPU **sur un hôte qui en avait 2** — cent pour cent de +surengagement, à chaque étage, avec l'hyperviseur lui-même à servir par-dessus. +Son installation dépassait deux heures et demie contre treize minutes pour +l'étage 3, et l'extrapolation de ce rapport donnait cinq ANS pour le dixième. -* **la mémoire** — chaque étage garde de quoi faire tourner ses propres démons - (`pve-cluster`, `pvestatd`, `pvedaemon`, `pveproxy`) avant de céder le - reste ; +Le sens est donc inversé. Le plus profond reçoit ce qu'un Proxmox de test +demande vraiment — 4 Go de mémoire, 25 Go de disque, 2 vCPU — et chaque parent +au-dessus ajoute son propre surcoût, rien d'autre : un vCPU, 2 Gio, 10 Go. Une +descente à dix étages demande ainsi 11 vCPU, 22 Go et 115 Go à son premier +étage, là où la cession de haut en bas voulait 50 Go de mémoire pour la même +profondeur. + +Trois budgets peuvent borner la profondeur, et `script/proxmox/nesting.py` +nomme celui qui a manqué : + +* **la mémoire** — chaque étage doit faire tourner ses propres démons + (`pve-cluster`, `pvestatd`, `pvedaemon`, `pveproxy`) *et* héberger son + enfant ; * **le disque** — le disque de l'enfant vit *dans* celui du parent, qui doit - aussi contenir son propre système. + aussi contenir son propre système ; +* **le processeur** — chaque étage en veut un de plus que son enfant, donc dix + étages en demandent onze au premier. La moitié des cœurs physiques est le + plafond : l'orchestrateur tourne sur cette machine lui aussi. -Ce qui se dégrade est mesuré, pas supposé : au-delà du deuxième étage, les -fabricants ne documentent rien. D'où un seul nombre borné — **2 vCPU** pour -tout étage imbriqué. Douze vCPU au quatrième étage ont gelé le noyau invité en -tout début de démarrage ; les mêmes deux avançaient. Amener douze processeurs -en ligne coûte autant d'allers-retours à travers toute la pile. +Ce troisième budget est mesuré, pas supposé. Douze vCPU au quatrième étage ont +gelé le noyau invité en tout début de démarrage — même pointeur d'instruction à +trois relevés, deux minutes d'écart — quand deux avançaient. Le nombre n'était +pas le fautif : cette VM avait douze vCPU sur un hôte qui en avait deux, six +fois plus large que sa propre machine. C'est le surengagement qui gèle, pas le +douze. -La mémoire n'est **pas** bornée. Sur cette unique VM examinée à la main, la -faire passer de 9 Go à 2 Go n'a rien déplacé : elle s'arrêtait après avoir lu -les mêmes 32 Mio, c'est-à-dire simplement la taille des fichiers d'amorçage. -La mémoire n'était pas le levier ; le nombre de vCPU l'était. Et la rogner -priverait l'étage du dessous, qui en a besoin pour héberger le suivant. +La mémoire n'est pas le levier. Sur cette même VM examinée à la main, la faire +passer de 9 Go à 2 Go n'a rien déplacé : elle s'arrêtait après avoir lu les +mêmes 32 Mio, c'est-à-dire simplement la taille des fichiers d'amorçage. Le plan est affiché **avant** que quoi que ce soit ne soit créé, et le script ne promet jamais une profondeur qu'il sait irréalisable — mieux vaut annoncer diff --git a/LongTest/README.fr.md b/LongTest/README.fr.md index 75f6436..9c1b051 100644 --- a/LongTest/README.fr.md +++ b/LongTest/README.fr.md @@ -47,28 +47,44 @@ cloner le dépôt : c'est notre code qu'on veut éprouver, et le dépôt distant est souvent en retard sur le checkout — un correctif absent du distant a fait « revenir » le même défaut sur trois VM de suite. -### L'algorithme de ressources +### L'algorithme de ressources — dimensionné depuis le bas -Deux choses s'épuisent en descendant, et une troisième se dégrade. Ce qui -s'épuise est de l'arithmétique, et `script/proxmox/nesting.py` la calcule : +La première version cédait à l'enfant ce que le parent pouvait céder, et une +descente réelle a montré ce que cela coûte. L'étage 4 se retrouvait avec 44 Go +de mémoire et 2 vCPU **sur un hôte qui en avait 2** — cent pour cent de +surengagement, à chaque étage, avec l'hyperviseur lui-même à servir par-dessus. +Son installation dépassait deux heures et demie contre treize minutes pour +l'étage 3, et l'extrapolation de ce rapport donnait cinq ANS pour le dixième. -* **la mémoire** — chaque étage garde de quoi faire tourner ses propres démons - (`pve-cluster`, `pvestatd`, `pvedaemon`, `pveproxy`) avant de céder le - reste ; +Le sens est donc inversé. Le plus profond reçoit ce qu'un Proxmox de test +demande vraiment — 4 Go de mémoire, 25 Go de disque, 2 vCPU — et chaque parent +au-dessus ajoute son propre surcoût, rien d'autre : un vCPU, 2 Gio, 10 Go. Une +descente à dix étages demande ainsi 11 vCPU, 22 Go et 115 Go à son premier +étage, là où la cession de haut en bas voulait 50 Go de mémoire pour la même +profondeur. + +Trois budgets peuvent borner la profondeur, et `script/proxmox/nesting.py` +nomme celui qui a manqué : + +* **la mémoire** — chaque étage doit faire tourner ses propres démons + (`pve-cluster`, `pvestatd`, `pvedaemon`, `pveproxy`) *et* héberger son + enfant ; * **le disque** — le disque de l'enfant vit *dans* celui du parent, qui doit - aussi contenir son propre système. + aussi contenir son propre système ; +* **le processeur** — chaque étage en veut un de plus que son enfant, donc dix + étages en demandent onze au premier. La moitié des cœurs physiques est le + plafond : l'orchestrateur tourne sur cette machine lui aussi. -Ce qui se dégrade est mesuré, pas supposé : au-delà du deuxième étage, les -fabricants ne documentent rien. D'où un seul nombre borné — **2 vCPU** pour -tout étage imbriqué. Douze vCPU au quatrième étage ont gelé le noyau invité en -tout début de démarrage ; les mêmes deux avançaient. Amener douze processeurs -en ligne coûte autant d'allers-retours à travers toute la pile. +Ce troisième budget est mesuré, pas supposé. Douze vCPU au quatrième étage ont +gelé le noyau invité en tout début de démarrage — même pointeur d'instruction à +trois relevés, deux minutes d'écart — quand deux avançaient. Le nombre n'était +pas le fautif : cette VM avait douze vCPU sur un hôte qui en avait deux, six +fois plus large que sa propre machine. C'est le surengagement qui gèle, pas le +douze. -La mémoire n'est **pas** bornée. Sur cette unique VM examinée à la main, la -faire passer de 9 Go à 2 Go n'a rien déplacé : elle s'arrêtait après avoir lu -les mêmes 32 Mio, c'est-à-dire simplement la taille des fichiers d'amorçage. -La mémoire n'était pas le levier ; le nombre de vCPU l'était. Et la rogner -priverait l'étage du dessous, qui en a besoin pour héberger le suivant. +La mémoire n'est pas le levier. Sur cette même VM examinée à la main, la faire +passer de 9 Go à 2 Go n'a rien déplacé : elle s'arrêtait après avoir lu les +mêmes 32 Mio, c'est-à-dire simplement la taille des fichiers d'amorçage. Le plan est affiché **avant** que quoi que ce soit ne soit créé, et le script ne promet jamais une profondeur qu'il sait irréalisable — mieux vaut annoncer diff --git a/LongTest/README.md b/LongTest/README.md index 03ec8c3..ef5fb45 100644 --- a/LongTest/README.md +++ b/LongTest/README.md @@ -43,27 +43,42 @@ the repository: it is our code we want to exercise, and the remote is often behind the checkout — a fix absent from the remote made the same defect "come back" on three VMs in a row. -### The resource algorithm +### The resource algorithm — sized from the bottom up -Two things run out going down, and a third degrades. What runs out is -arithmetic, and `script/proxmox/nesting.py` computes it: +The first version handed down whatever the parent could spare, and a real +descent showed what that costs. Level 4 ended up with 44 GB of memory and +2 vCPU **on a host that had 2** — a hundred percent overcommit, at every +level, with the hypervisor itself to serve on top. Its install ran past two +and a half hours against thirteen minutes for level 3, and extrapolating that +ratio gave five years for the tenth. -* **memory** — each level keeps what its own daemons need (`pve-cluster`, - `pvestatd`, `pvedaemon`, `pveproxy`) before handing the rest down; +So the direction is reversed. The deepest level gets what a test Proxmox +actually asks for — 4 GB of memory, 25 GB of disk, 2 vCPU — and every parent +above it adds its own overhead and nothing else: one vCPU, 2 GiB, 10 GB. A +ten-level descent therefore asks its first level for 11 vCPU, 22 GB and +115 GB, where handing resources down wanted 50 GB of memory for the same +depth. + +Three budgets can bound the depth, and `script/proxmox/nesting.py` names the +one that ran out: + +* **memory** — every level must run its own daemons (`pve-cluster`, + `pvestatd`, `pvedaemon`, `pveproxy`) *and* hold its child; * **disk** — the child's disk lives *inside* the parent's, which must also - hold its own system. + hold its own system; +* **processor** — each level wants one vCPU more than its child, so ten levels + ask eleven of the first. Half the physical cores is the ceiling: the + orchestrator runs on that machine too. -What degrades is measured, not assumed: past the second level, vendors -document nothing. Hence one capped number — **2 vCPU** for every nested -level. Twelve vCPU at the fourth level froze the guest kernel in early boot; -the same two progressed. Bringing twelve processors online costs as many -round trips through the whole stack. +That third budget is measured, not assumed. Twelve vCPU at the fourth level +froze the guest kernel in early boot — same instruction pointer at three +readings two minutes apart — while two progressed. The number was not the +culprit: that VM had twelve vCPU on a host with two, six times wider than its +own machine. Overcommit freezes, not the twelve. -Memory is **not** capped. On that one manual VM, dropping it from 9 GB to -2 GB moved nothing — it stopped after reading the same 32 MiB, which is simply -the size of the boot files. Memory was not the lever; the vCPU count was. And -trimming memory would starve the level below, which needs it to host the -next. +Memory is not the lever. On that same manual VM, dropping it from 9 GB to 2 GB +moved nothing — it stopped after reading the same 32 MiB, which is simply the +size of the boot files. The plan is printed **before** anything is created, and the script never promises a depth it knows will not fit — better to announce six levels and diff --git a/script/proxmox/nesting.py b/script/proxmox/nesting.py index 1092a74..dd8595e 100644 --- a/script/proxmox/nesting.py +++ b/script/proxmox/nesting.py @@ -24,8 +24,11 @@ Deux nombres viennent de la même mesure, et méritent d'être dits : * 12 vCPU au quatrième étage ont GELÉ le noyau invité en tout début de démarrage — même RIP à trois relevés, deux minutes d'écart, pas un octet lu - de plus. Les mêmes 2 vCPU avançaient. D'où VCPU_IMBRIQUE = 2 : amener douze - processeurs en ligne demande autant d'allers-retours à travers la pile ; + de plus. Les mêmes 2 vCPU avançaient. Le nombre n'était pas le fautif : cette + VM avait douze vCPU sur un hôte qui en avait DEUX, six fois plus large que sa + propre machine. C'est le surengagement qui gèle, pas le douze — d'où le + dimensionnement par étage plus bas, qui donne à chaque parent un vCPU de plus + qu'à son enfant ; * cette VM-là s'arrêtait après avoir lu 33 682 432 octets — 32 Mio, soit simplement la taille de ses fichiers d'amorçage — et le chiffre ne bougeait pas quand on lui retirait de la mémoire. La mémoire n'était donc pas le @@ -47,21 +50,49 @@ HOTE_RESERVE_RAM_MO = 4096 HOTE_RESERVE_PART = 8 # un huitième HOTE_RESERVE_DISQUE_GO = 20 -# Ce qu'un étage garde pour lui avant de céder le reste. La RAM vient de -# l'observation d'un Proxmox imbriqué au repos ; le disque, de la mesure d'un -# système installé (5,6 Go) plus de la place pour écrire. +# Ce qu'un étage garde pour LUI, en plus de ce qu'il cède à son enfant. La +# RAM vient de l'observation d'un Proxmox imbriqué au repos ; le disque, de la +# mesure d'un système installé (5,6 Go) plus de la place pour écrire. PVE_RAM_MO = 2048 PVE_DISQUE_GO = 10 +# Ce dont le PLUS PROFOND a besoin — et c'est de là qu'on part. +# +# Le dimensionnement allait d'abord de haut en bas : chaque étage recevait tout +# ce que son parent pouvait céder. Mesuré sur une descente réelle, l'étage 4 se +# retrouvait avec 44 Go — onze millions de pages à cartographier, chaque défaut +# traversant les quatre hyperviseurs empilés. Son installation dépassait deux +# heures et demie là où l'étage 3 mettait treize minutes, et l'extrapolation +# donnait cinq ANS pour le dixième étage. +# +# On part donc du bas : le plus profond reçoit ce qu'un Proxmox de test demande +# vraiment, et chaque parent ajoute seulement son propre surcoût. Pour dix +# étages, le premier a besoin de 4 + 9×2 = 22 Go au lieu de cinquante — et +# chaque étage est PETIT, donc rapide. +PVE_RAM_CIBLE_MO = 4096 +PVE_DISQUE_CIBLE_GO = 25 + # En dessous, un Proxmox ne démarre pas ses démons ou n'a plus la place # d'importer une image cloud. RAM_MIN_MO = 2048 DISQUE_MIN_GO = 15 -# Le premier étage tourne sur la machine physique : il peut être large. Les -# suivants non — voir la mesure dans l'en-tête. -VCPU_NIVEAU1_MAX = 4 +# Le processeur se dimensionne DEPUIS LE BAS lui aussi, et pour la même +# raison que la mémoire — mais celle-là s'est vue à l'usage. +# +# Avec deux vCPU à chaque étage imbriqué, l'étage 3 avait deux vCPU pour +# héberger un invité qui en demandait deux : cent pour cent de surengagement, +# et l'hyperviseur lui-même à servir par-dessus. À chaque étage. Mesuré sur une +# descente réelle : une seconde VM démarrée au quatrième étage a lu DEUX +# KILO-OCTETS en onze minutes, affamée par l'installation qui tournait à côté. +# +# Chaque étage reçoit donc UN vCPU de plus que son enfant : le plus profond en +# a deux, son parent trois, et ainsi de suite. Le premier étage d'une descente +# à dix en demande onze — sur vingt-huit cœurs réels, cela passe. VCPU_IMBRIQUE = 2 +# Ce qu'on accepte de prendre à la machine physique : la moitié de ses cœurs. +# L'orchestrateur tourne dessus, et la suite de tests aussi. +VCPU_HOTE_PART = 2 # Au-delà, l'imbrication n'est pas un terrain documenté par les fabricants. # On ne refuse pas — on le DIT. @@ -74,59 +105,81 @@ def nesting_plan( ram_dispo_mo: int, disque_libre_go: int, ) -> dict: - """Les ressources de chaque étage, et jusqu'où l'arithmétique va. + """Les ressources de chaque étage, dimensionnées DEPUIS LE BAS. - Rend {"demandee", "atteignable", "niveaux": [...], "arret"}. `arret` - nomme ce qui a manqué — « ram » ou « disque » — quand la profondeur - demandée n'est pas atteinte, sinon "". + Rend {"demandee", "atteignable", "niveaux": [...], "arret"}. `arret` nomme + ce qui a manqué — « ram » ou « disque » — quand la profondeur demandée + n'est pas atteinte, sinon "". + + Depuis le bas, et c'est tout le sujet. De haut en bas, chaque étage + recevait ce que son parent pouvait céder : mesuré, l'étage 4 se retrouvait + avec 44 Go de RAM et son installation dépassait deux heures et demie + contre treize minutes pour l'étage 3. Sous pagination imbriquée, un gros + invité coûte cher à cartographier, et le coût se multiplie par étage. + + Le plus profond reçoit donc ce qu'un Proxmox de test demande, et chaque + parent ajoute son surcoût — rien de plus. Une descente à dix étages + demande alors 22 Go au premier au lieu de cinquante, et chaque étage est + petit. On ne rend jamais un plan qu'on sait impossible : mieux vaut annoncer six - étages et en réussir six que d'en promettre dix et mourir au septième - sans savoir pourquoi. + étages et en réussir six que d'en promettre dix et mourir au septième sans + savoir pourquoi. """ - # Arrondi au gibioctet inférieur : « --memory 25203 » marche, mais un - # nombre rond se relit, se compare d'un étage à l'autre, et évite de - # traîner les kibioctets du hasard de la mesure jusqu'au dixième étage. reserve = max(HOTE_RESERVE_RAM_MO, int(ram_dispo_mo) // HOTE_RESERVE_PART) - ram = ((int(ram_dispo_mo) - reserve) // 1024) * 1024 - disque = int(disque_libre_go) - HOTE_RESERVE_DISQUE_GO - niveaux, arret = [], "" - # « max(1, …) » forçait un tour : profondeur 0 rendait un plan d'UN - # étage, et « --depth 0 » créait donc une VM. range(1, 1) est déjà vide, - # et un plan vide est la bonne réponse à une demande vide. - for niveau in range(1, int(profondeur) + 1): - if niveau > 1: - ram -= PVE_RAM_MO - disque -= PVE_DISQUE_GO - if ram < RAM_MIN_MO: - arret = "ram" - break - if disque < DISQUE_MIN_GO: - arret = "disque" - break - niveaux.append( - { - "niveau": niveau, - # Le plancher est VCPU_IMBRIQUE et non 1 : sur un hôte de - # quatre cœurs, « // 4 » donnait UN vCPU au premier étage — - # l'hyperviseur parent — alors que son invité en recevait - # deux. Un parent plus étroit que son enfant est absurde, et - # c'est tout l'inverse de ce que ce module raconte. - "vcpu": ( - max( - VCPU_IMBRIQUE, - min(VCPU_NIVEAU1_MAX, int(cpu_hote) // 4), - ) - if niveau == 1 - else VCPU_IMBRIQUE - ), - "ram": ram, - "disque": disque, - } + budget_ram = ((int(ram_dispo_mo) - reserve) // 1024) * 1024 + budget_disque = int(disque_libre_go) - HOTE_RESERVE_DISQUE_GO + + def besoin(d): + """Ce que le PREMIER étage doit avoir pour qu'une descente de `d` + étages tienne : la cible du bas, plus un surcoût par étage au-dessus. + + Le processeur en fait partie : chaque étage en veut un de plus que son + enfant, donc le premier en veut VCPU_IMBRIQUE + d - 1. Sans cette + condition, on annonçait dix étages sur une machine à quatre cœurs. + """ + return ( + PVE_RAM_CIBLE_MO + (d - 1) * PVE_RAM_MO, + PVE_DISQUE_CIBLE_GO + (d - 1) * PVE_DISQUE_GO, + VCPU_IMBRIQUE + d - 1, ) + + budget_vcpu = max(VCPU_IMBRIQUE, int(cpu_hote) // VCPU_HOTE_PART) + atteignable, arret = 0, "" + for d in range(max(0, int(profondeur)), 0, -1): + ram1, disque1, vcpu1 = besoin(d) + if ( + ram1 <= budget_ram + and disque1 <= budget_disque + and vcpu1 <= budget_vcpu + ): + atteignable = d + break + if atteignable < int(profondeur): + # Nommer CE qui a manqué, à la profondeur demandée. + ram1, disque1, vcpu1 = besoin(max(1, int(profondeur))) + if ram1 > budget_ram: + arret = "ram" + elif disque1 > budget_disque: + arret = "disque" + else: + arret = "vcpu" + niveaux = [ + { + "niveau": niveau, + # UN de plus que son enfant. Un parent aussi étroit que son + # enfant, c'est cent pour cent de surengagement — et l'hyperviseur + # à servir en plus. + "vcpu": VCPU_IMBRIQUE + (atteignable - niveau), + "ram": PVE_RAM_CIBLE_MO + (atteignable - niveau) * PVE_RAM_MO, + "disque": PVE_DISQUE_CIBLE_GO + + (atteignable - niveau) * PVE_DISQUE_GO, + } + for niveau in range(1, atteignable + 1) + ] return { "demandee": int(profondeur), - "atteignable": len(niveaux), + "atteignable": atteignable, "niveaux": niveaux, "arret": arret, } diff --git a/test/test_proxmox_nesting.py b/test/test_proxmox_nesting.py index 3da0362..72f9271 100644 --- a/test/test_proxmox_nesting.py +++ b/test/test_proxmox_nesting.py @@ -18,97 +18,96 @@ from script.proxmox import nesting # noqa: E402 class TestLePlanDesEtages(unittest.TestCase): - """Deux ressources s'épuisent en descendant, et le plan doit le dire - AVANT de créer quoi que ce soit.""" + """Le plan se dimensionne DEPUIS LE BAS, et c'est une correction. + + De haut en bas, chaque étage recevait ce que son parent pouvait céder. + Mesuré sur une descente réelle : l'étage 4 se retrouvait avec 44 Go de RAM + et deux vCPU sur un hôte qui en avait deux — cent pour cent de + surengagement, à chaque étage. Son installation dépassait deux heures et + demie contre treize minutes pour l'étage 3, et l'extrapolation donnait cinq + ANS pour le dixième. + + Le plus profond reçoit donc ce qu'un Proxmox de test demande, et chaque + parent ajoute son propre surcoût — un vCPU, deux gibioctets, dix + gigaoctets. Rien de plus.""" # La machine réelle sur laquelle l'algorithme a été réglé. - HOTE = dict(cpu_hote=28, ram_dispo_mo=29549, disque_libre_go=139) + HOTE = dict(cpu_hote=28, ram_dispo_mo=58000, disque_libre_go=165) def test_ten_levels_fit_on_this_machine(self): plan = nesting.nesting_plan(10, **self.HOTE) self.assertEqual(plan["atteignable"], 10) self.assertEqual(plan["arret"], "") - def test_every_level_shrinks(self): - # Le disque de l'enfant vit DANS celui du parent, qui doit aussi - # contenir son propre système : rien ne peut rester constant. - niveaux = nesting.nesting_plan(6, **self.HOTE)["niveaux"] - for precedent, suivant in zip(niveaux, niveaux[1:]): - self.assertLess(suivant["ram"], precedent["ram"]) - self.assertLess(suivant["disque"], precedent["disque"]) + def test_the_deepest_level_gets_exactly_the_target(self): + """C'est de là qu'on part : ce qu'un Proxmox de test demande, pas ce + qui reste.""" + plan = nesting.nesting_plan(10, **self.HOTE) + fond = plan["niveaux"][-1] + self.assertEqual(fond["ram"], nesting.PVE_RAM_CIBLE_MO) + self.assertEqual(fond["disque"], nesting.PVE_DISQUE_CIBLE_GO) + self.assertEqual(fond["vcpu"], nesting.VCPU_IMBRIQUE) - def test_the_first_level_may_be_wide_the_others_not(self): - """12 vCPU au quatrième étage ont GELÉ le noyau invité en tout début - de démarrage ; les mêmes 2 vCPU avançaient. Amener douze processeurs - en ligne demande autant d'allers-retours à travers la pile.""" - niveaux = nesting.nesting_plan(4, **self.HOTE)["niveaux"] - # STRICTEMENT plus grand. « > VCPU_IMBRIQUE - 1 » était satisfait par - # la valeur imbriquée elle-même : remplacer tout le calcul du premier - # étage par VCPU_IMBRIQUE laissait les tests verts, donc cpu_hote - # n'était couvert par rien. - self.assertGreater(niveaux[0]["vcpu"], nesting.VCPU_IMBRIQUE) - for n in niveaux[1:]: - self.assertEqual(n["vcpu"], nesting.VCPU_IMBRIQUE) + def test_each_parent_adds_exactly_its_own_overhead(self): + # Ni plus ni moins : un parent plus large que nécessaire ralentit tout + # ce qu'il héberge, un parent trop juste ne le fait pas tourner. + niveaux = nesting.nesting_plan(8, **self.HOTE)["niveaux"] + for parent, enfant in zip(niveaux, niveaux[1:]): + self.assertEqual(parent["ram"] - enfant["ram"], nesting.PVE_RAM_MO) + self.assertEqual( + parent["disque"] - enfant["disque"], nesting.PVE_DISQUE_GO + ) + self.assertEqual(parent["vcpu"] - enfant["vcpu"], 1) def test_a_parent_is_never_narrower_than_its_child(self): - """Sur un hôte de quatre cœurs, « // 4 » donnait UN vCPU au premier - étage — l'hyperviseur — alors que son invité en recevait deux.""" - for coeurs in (2, 4, 8, 12, 28): + """Deux vCPU hébergeant deux vCPU, c'est cent pour cent de + surengagement — et l'hyperviseur à servir en plus. Mesuré : une VM + démarrée au quatrième étage a lu DEUX KILO-OCTETS en onze minutes, + affamée par l'installation qui tournait à côté.""" + for coeurs in (4, 8, 12, 28): with self.subTest(coeurs=coeurs): niveaux = nesting.nesting_plan( - 3, + 6, cpu_hote=coeurs, - ram_dispo_mo=32768, - disque_libre_go=300, + ram_dispo_mo=64000, + disque_libre_go=400, )["niveaux"] - self.assertGreaterEqual( - niveaux[0]["vcpu"], niveaux[1]["vcpu"], f"{coeurs} cœurs" - ) + for parent, enfant in zip(niveaux, niveaux[1:]): + self.assertGreater(parent["vcpu"], enfant["vcpu"]) + self.assertGreater(parent["ram"], enfant["ram"]) + self.assertGreater(parent["disque"], enfant["disque"]) + + def test_the_cpu_budget_can_bound_the_depth(self): + """Sur une petite machine, c'est le PROCESSEUR qui borne, pas la + mémoire : chaque étage en veut un de plus que son enfant, donc dix + étages demandent onze vCPU au premier.""" + plan = nesting.nesting_plan( + 10, cpu_hote=8, ram_dispo_mo=64000, disque_libre_go=400 + ) + self.assertEqual(plan["arret"], "vcpu") + self.assertLess(plan["atteignable"], 10) + # Et le premier étage ne dépasse pas la part concédée à l'hôte. + self.assertLessEqual( + plan["niveaux"][0]["vcpu"], 8 // nesting.VCPU_HOTE_PART + ) def test_running_out_of_ram_is_named(self): plan = nesting.nesting_plan( - 10, cpu_hote=8, ram_dispo_mo=12288, disque_libre_go=500 + 10, cpu_hote=28, ram_dispo_mo=12288, disque_libre_go=500 ) self.assertEqual(plan["arret"], "ram") self.assertLess(plan["atteignable"], 10) - # Aucun étage sous le plancher : un Proxmox sous 2 Go ne démarre pas - # ses démons. for n in plan["niveaux"]: self.assertGreaterEqual(n["ram"], nesting.RAM_MIN_MO) def test_running_out_of_disk_is_named(self): plan = nesting.nesting_plan( - 10, cpu_hote=8, ram_dispo_mo=200000, disque_libre_go=60 + 10, cpu_hote=28, ram_dispo_mo=200000, disque_libre_go=60 ) self.assertEqual(plan["arret"], "disque") for n in plan["niveaux"]: self.assertGreaterEqual(n["disque"], nesting.DISQUE_MIN_GO) - def test_the_host_keeps_a_share_not_just_a_floor(self): - """Quatre gigaoctets sur une machine de soixante, c'est 6 % laissés à - l'hôte : le jour où les invités touchent vraiment leur mémoire, c'est - lui qui part en swap — et la mesure serait celle du swap, pas de - l'imbrication.""" - for dispo in (60000, 260000): - with self.subTest(dispo=dispo): - plan = nesting.nesting_plan( - 1, cpu_hote=28, ram_dispo_mo=dispo, disque_libre_go=500 - ) - reserve = dispo - plan["niveaux"][0]["ram"] - self.assertGreater(reserve, nesting.HOTE_RESERVE_RAM_MO) - self.assertGreaterEqual( - reserve, dispo // nesting.HOTE_RESERVE_PART - ) - - def test_a_small_host_keeps_the_floor(self): - # Sur une petite machine, la part serait dérisoire : le plancher tient. - plan = nesting.nesting_plan( - 1, cpu_hote=4, ram_dispo_mo=16384, disque_libre_go=200 - ) - self.assertEqual( - 16384 - plan["niveaux"][0]["ram"], nesting.HOTE_RESERVE_RAM_MO - ) - def test_a_depth_of_zero_asks_for_nothing(self): for profondeur in (0, -1, -7): with self.subTest(profondeur=profondeur): @@ -122,34 +121,17 @@ class TestLePlanDesEtages(unittest.TestCase): ) self.assertEqual(plan["atteignable"], 0) self.assertEqual(plan["niveaux"], []) - self.assertEqual(plan["arret"], "ram") + self.assertTrue(plan["arret"]) def test_a_plan_is_never_promised_beyond_what_fits(self): # Mieux vaut annoncer six étages et en réussir six que d'en promettre # dix et mourir au septième sans savoir pourquoi. - # Depuis 0 et depuis les négatifs : « max(1, …) » forçait un tour, - # donc « --depth 0 » rendait un plan d'UN étage et créait une VM. for profondeur in range(-2, 13): plan = nesting.nesting_plan(profondeur, **self.HOTE) self.assertEqual(len(plan["niveaux"]), plan["atteignable"]) - # « max(0, …) » : une demande négative ne peut pas donner un - # nombre d'étages négatif, elle donne zéro. self.assertLessEqual(plan["atteignable"], max(0, profondeur)) -class TestLaProfondeurDUnHote(unittest.TestCase): - """Comptée depuis la chaîne de rebonds : c'est la seule mesure dont on - dispose de l'extérieur, et elle est exacte pour les hôtes que nous avons - nous-mêmes déployés — c'est nous qui écrivons ces entrées.""" - - def test_no_jump_is_the_first_level(self): - self.assertEqual(nesting.depth_from_jumps(0), 1) - - def test_each_jump_adds_a_level(self): - for sauts, attendu in ((1, 2), (2, 3), (3, 4), (9, 10)): - self.assertEqual(nesting.depth_from_jumps(sauts), attendu) - - class TestCompterLesRebonds(unittest.TestCase): """La profondeur se lit dans ~/.ssh/config : un ProxyJump par étage.