Blog ·
Pourquoi un 200 de Proxmox n’est pas un résultat
Proxmox répond à une demande de clonage par un UPID et vaque à ses affaires. Comment lire cet identifiant, l’interroger et distinguer fini de réussi.
Par Provibr
Vous envoyez une demande de clonage en POST à un nœud Proxmox. Vous recevez un HTTP 200 et une chaîne qui ressemble à du bruit de ligne. À cet instant, exactement une chose est vraie : la requête a été acceptée. Aucun disque n’a été copié, aucune machine n’existe, et rien n’a échoué non plus pour l’instant.
Presque tous les appels mutants de l’API Proxmox fonctionnent ainsi, et c’est une conception parfaitement raisonnable pour des opérations qui prennent des minutes. Cela ne devient un problème que lorsque le code appelant traite le 200 comme une issue, ce qui est le comportement par défaut de tous les clients HTTP jamais écrits.
Voici comment nous lisons ces tâches, et quelles parties se sont révélées plus tranchantes qu’elles n’en ont l’air.
La chaîne est un identifiant, et elle est structurée
Ce qui revient est un UPID : un identifiant séparé par des deux-points qui nomme une tâche en cours. Il porte, entre autres champs, le nœud sur lequel la tâche tourne, le type de tâche, la chose sur laquelle elle opère, et l’utilisateur qui l’a lancée.
Trois d’entre eux sont porteurs.
Le nœud est celui auquel vous devez demander l’état. Une tâche appartient au nœud qui l’exécute, et une question posée à l’échelle de la grappe n’est pas la même question. Prenez le nœud dans l’identifiant plutôt que dans la variable que vous aviez sous la main, car les deux peuvent différer et le cas où ils diffèrent est précisément celui que vous n’avez pas testé.
Le type vous dit ce qui a réellement démarré, ce qui n’est pas toujours ce que vous aviez demandé par son nom. Une sauvegarde lancée par le point d’entrée du nœud revient avec un type de tâche vzdump, et non avec quelque chose ayant la forme d’un invité.
La cible est l’identifiant de l’invité pour les opérations sur invité, et elle peut être vide. Ce n’est pas une corruption. Une tâche peut légitimement porter sur un nœud plutôt que sur un invité : le code qui analyse ce champ doit donc renvoyer « pas d’invité » au lieu de lever une exception ou de retomber sur zéro.
Notre analyseur est délibérément étroit sur ce qu’il rejette : il exige le préfixe, il exige au moins huit champs séparés par des deux-points, et il exige que le nœud et le type soient non vides. Tout le reste a le droit de manquer, car le format doit continuer de fonctionner pour des types de tâches que nous n’avons jamais vus.
Fini n’est pas la même chose que réussi
Demandez à un nœud l’état d’une tâche et vous obtenez deux champs qui répondent à deux questions différentes. L’un dit si la tâche tourne encore ou si elle s’est arrêtée. L’autre, présent seulement une fois qu’elle s’est arrêtée, dit OK ou porte une chaîne d’erreur.
Écrire cela comme deux prédicats distincts plutôt qu’un seul n’est pas du pédantisme. Un unique isDone() qui les confond produit le bogue où un clonage échoué est signalé comme un clonage terminé, et où la machine qui n’existe pas obtient une ligne dans votre base de données. Nous avons deux fonctions : l’une demande si la tâche s’est arrêtée, l’autre demande si elle s’est arrêtée et si l’état de sortie vaut exactement OK. Tout le reste est un échec, y compris les états de sortie que nous n’avons jamais vus, car un état que nous ne reconnaissons pas n’est pas une permission de supposer le succès.
L’endroit où vit la tâche fait partie de l’API, ce n’est pas un détail
Ce sont les sauvegardes qui nous l’ont appris. Il est naturel d’attendre qu’une opération sur un invité soit accrochée à cet invité : quelque chose sous le chemin propre à la machine virtuelle. Un tel point d’entrée n’existe pas. Une sauvegarde se lance sur le nœud, avec l’identifiant de l’invité en paramètre.
Ce n’est pas une bizarrerie à contourner. Cela change ce que vous devez savoir avant de pouvoir démarrer : quel nœud, quel stockage sur ce nœud, et si ce stockage a seulement le droit d’héberger des sauvegardes. Un stockage qui n’accepte que des images de disque acceptera volontiers votre requête et échouera à l’intérieur de la tâche, ce qui remonte au client sous la forme d’un message d’erreur venu d’un programme dont il n’a jamais entendu parler. Nous filtrons donc la liste des cibles selon ce que le stockage déclare pouvoir contenir, et nous écartons tout stockage qui est listé mais ne répond pas, avant de proposer un choix à qui que ce soit.
La même règle s’applique en sens inverse au placement d’un disque. Même idée, type de contenu opposé, et cela vaut la peine de garder deux filtres explicites plutôt qu’un seul filtre malin partagé.
L’interrogation est un rythme, pas un intervalle
Pendant que la tâche tourne, vous devez demander de façon répétée. Les choix naïfs sont mauvais tous les deux : interrogez vite et sans fin et vous martelez l’API de gestion de quelqu’un pendant une heure, interrogez lentement et chaque opération rapide paraît poussive.
Notre compromis est de trois tentatives rapides espacées d’une seconde, puis une toutes les trois secondes. Le démarrage d’une machine se termine à l’intérieur de la fenêtre rapide et paraît instantané. Une sauvegarde d’une heure coûte une requête toutes les trois secondes, ce qui n’est rien.
La boucle se termine d’exactement trois façons, et les trois sont explicites : la tâche signale qu’elle s’est arrêtée, l’échéance de la commande passe, ou la commande entière est annulée. Il n’y a pas de quatrième branche ni de chemin sur lequel la boucle continue simplement, une propriété qui mérite d’être organisée exprès plutôt qu’espérée. Ce qu’une échéance arrête et n’arrête pas est un sujet à part entière ; il suffit ici de dire que la boucle d’interrogation doit avoir un moyen de perdre.
La plupart des tâches ne vous diront pas où elles en sont
C’est la partie qui déçoit tous ceux qui construisent une barre de progression. L’état de la tâche vous donne en cours ou arrêtée. Il ne vous donne pas de pourcentage.
Certaines tâches écrivent leur progression dans leur journal, sous forme de texte. Un clonage émet des lignes avec un décompte d’octets transférés et un pourcentage entre parenthèses. Pour celles-là, nous lisons donc la fin du journal et nous prenons le dernier pourcentage que nous y trouvons. Extraire un pourcentage d’une ligne de journal n’a rien d’élégant, et c’est le seul endroit où le nombre existe.
Pour tout le reste, nous affichons une estimation, et ici le choix de conception compte plus que la formule. La nôtre approche quatre-vingt-dix pour cent de façon asymptotique à partir d’une durée attendue : la barre bouge donc toujours et n’atteint jamais la fin. Une barre qui reste à 100 pour cent pendant que rien ne se passe est pire que pas de barre du tout, car elle dit à la personne qui regarde que vous avez fini, puis se contredit.
La durée attendue qui alimente cette courbe est un paramètre de forme, pas une échéance. La nôtre dit qu’une sauvegarde prend en général une dizaine de minutes et une restauration une vingtaine, au motif qu’une restauration lit l’archive et écrit les disques et déplace donc environ deux fois plus d’octets. Si une sauvegarde dure plus longtemps que l’estimation, rien ne casse : la courbe reste poliment sous la ligne.
L’erreur est dans le journal, pas dans l’état
Quand une tâche échoue, l’état de sortie est souvent proche de l’inutile. « impossible de créer la VM » est une catégorie, pas une cause. La cause est dans les dernières lignes du journal de la tâche.
Un rapport d’échec est donc assemblé : l’état de sortie, plus la fin du journal avec les lignes de progression filtrées, plafonné à une poignée de lignes et à quelques centaines de caractères. Filtrer les lignes de progression compte davantage qu’il n’y paraît, car un clonage qui échoue peut se terminer par vingt lignes de pourcentages de transfert avec la vraie raison au-dessus.
Une règle à propos de ce texte, à laquelle nous nous tenons strictement : c’est une citation. C’est ce qu’a dit l’hyperviseur, et cela entre dans nos registres et s’affiche à l’écran sans modification. Nous ne le traduisons pas, nous ne le rangeons pas, et nous ne le paraphrasons pas en quelque chose de plus aimable. La phrase autour est la nôtre et se traduit pour le lecteur ; la partie citée reste exactement comme l’autre côté l’a dite, car c’est la seule chaîne de tout l’échange qu’un ingénieur du support peut rechercher.
Ne pliez pas vos propres mots dans la phrase de la machine
La dernière leçon est celle que nous ne nous attendions pas à apprendre d’une boucle d’interrogation de tâches.
Notre boucle d’attente prenait autrefois un libellé en texte libre pour l’opération et le pliait dans trois phrases : quelque chose en cours, quelque chose de terminé, quelque chose d’échoué. Cela fonctionnait, jusqu’à ce que quelqu’un remarque que plusieurs de ces libellés avaient été écrits dans la langue de notre bureau plutôt qu’en anglais. Des clients lisaient des phrases à moitié traduites dans leurs barres de progression, et quand une opération échouait, cette phrase à moitié traduite était stockée définitivement dans le champ d’erreur.
La correction n’a pas été de corriger les libellés. Elle a été de supprimer la possibilité : l’opération est désormais une énumération, et chaque cas porte des identifiants pour « en cours », « terminé » et « échoué » plutôt que des mots. Il ne reste plus de texte à se tromper, et la phrase se construit dans la langue du lecteur tout à la fin. Garder ces catalogues honnêtes se révèle être une discipline à part entière.
La liste de contrôle
Pour toute API qui répond à une mutation par un identifiant plutôt que par un résultat :
- Analysez l’identifiant, ne vous contentez pas de le stocker. Il vous dit où demander, et cet où n’est pas toujours celui où vous avez demandé.
- Séparez fini de réussi, dans le système de types si vous le pouvez.
- Traitez un état terminal non reconnu comme un échec, jamais comme un succès.
- Donnez à la boucle d’interrogation un rythme et une façon garantie de se terminer.
- Montrez la progression que vous avez réellement, et une estimation honnête que l’on ne puisse jamais prendre pour une progression que vous avez.
- Citez l’autre côté mot pour mot dans les erreurs, et construisez votre propre phrase autour de la citation plutôt qu’à l’intérieur.
Aucune de ces règles n’est difficile. Toutes les six sont des choses que nous avons d’abord faites de travers.