Aller au contenu

Blog ·

Deux commandes, une machine

Un serveur dédié ne se vend qu’une seule fois. Verrouiller tout le vivier a coûté une réservation en silence, et le remède fut de verrouiller une à une.

Par Provibr

Un forfait VPS survend volontairement et personne n’en perd le sommeil. Une machine dédiée ne le peut pas : il n’y en a qu’une, et si deux commandes croient toutes deux l’avoir obtenue, un client apprendra un lundi matin que ce qu’il a payé le vendredi appartient à quelqu’un d’autre.

L’exigence tient en une phrase. Réserver avant les effets de bord, et rendre la capacité de façon transactionnelle après un plantage ou un échec. La mise en œuvre a produit deux mesures qui ont changé la conception, et l’une d’elles nous a coûté une machine qui était simplement libre.

L’arbitre est un index

La règle voulant que deux commandes ne puissent jamais retenir la même machine vit dans un index unique partiel sur l’identifiant de l’équipement, restreint aux réservations à l’état retenue. Pas dans l’application, pas dans un verrou que prend le code, pas dans un commentaire.

C’est volontairement la même forme que l’index d’occupation des unités de baie. La couche applicative produit un refus lisible avant la collision, et la base décide qui gagne réellement. Une seule des deux peut faire autorité, et ce doit être celle qu’aucun chemin de code ajouté l’an prochain ne peut contourner.

Retenue est le seul état qui retient quelque chose, et cela s’exprime dans une table à côté de l’index et non dans une colonne booléenne, car un booléen à côté de l’état est une seconde vérité qui n’est juste que tant que chaque écrivain y pense. Un test compare la table à l’index.

Il y a quatre états, et deux d’entre eux forment volontairement une paire : retenue, convertie, expirée, annulée. La simplification tentante est un unique libérée pour les deux dernières, et elle rendrait sans réponse la question de savoir si quelqu’un a annulé ou si c’est simplement resté en plan, qui est justement celle qu’on vous pose quand un client appelle au sujet d’une commande qui n’a pas abouti. Un balayage qui écrit en silence le même état qu’un humain efface la différence entre une décision et un délai dépassé.

Verrouiller tout le vivier a coûté une réservation

La première version sélectionnait les machines candidates et les verrouillait en une seule requête avec FOR UPDATE SKIP LOCKED, en sautant ce qui était déjà pris. Cela se lit bien et c’est faux, et seul un test de concurrence l’a montré.

Huit commandes simultanées sur trois machines éligibles devraient produire trois réservations. Elles en ont produit deux. La première transaction a saisi les trois lignes, en a utilisé une et a retenu les deux autres jusqu’à sa validation ; tous les autres sont passés à côté des trois et n’ont rien trouvé. Le vivier n’était pas épuisé, il était pris en otage.

Le remède est de verrouiller une candidate à la fois : prenez la première, et si quelqu’un d’autre la détient, passez à la suivante. Le même test à nouveau : trois réservations sur trois machines différentes et cinq refus honnêtes. Deux commandes sur une seule machine donnent une réservation et un refus.

Ce remède apporte son propre risque, à savoir la longueur de la boucle. Avec cent commandes simultanées, la dernière transaction parcourrait cent candidates avant de gagner, et une longue transaction retarde toutes les autres. Le parcours s’arrête donc à vingt-cinq candidates, et au-delà, « réessayez dans un instant » est la réponse honnête.

Notez que sauter et échouer se comptent séparément. Une candidate retenue par quelqu’un d’autre est sautée, et ce n’est pas une tentative, car rien n’a mal tourné. Une candidate qui a perdu une course à l’insertion est une tentative, et il y en a cinq au plus.

Une violation d’unicité empoisonne la transaction

C’est le piège autour duquel la boucle de reprise est bâtie, et il est facile d’écrire du code qui semble correct et s’effondre à la première vraie collision.

Dans une transaction PostgreSQL, une violation de clé unique interrompt la transaction. Toute instruction suivante est alors refusée avec une autre erreur, « la transaction en cours est interrompue », jusqu’à ce que quelqu’un revienne en arrière. Une boucle qui attrape la violation et tente la candidate suivante ne fonctionne donc pas du tout : l’insertion de la deuxième candidate échoue pour une raison qui n’a rien à voir avec elle.

Chaque tentative tourne donc à l’intérieur d’un point de reprise, ce qui, dans notre constructeur de requêtes, est simplement une transaction imbriquée. Revenir au point de reprise, la transaction extérieure est de nouveau saine, candidate suivante.

Un détail s’y rattache, sur la manière dont la violation est attrapée. Notre constructeur de requêtes enveloppe les erreurs du pilote dans un type à lui et accroche la vraie à la cause : tester un code au niveau supérieur ne voit donc rien. Il faut parcourir la chaîne des causes, et il faut aussi regarder le nom de la contrainte, car cette table a deux contraintes d’unicité et elles disent le contraire l’une de l’autre : l’une dit que la machine est prise, l’autre dit que cette commande a déjà une réservation et la voici.

Le refus doit dire de quelle sorte il est

L’allocateur construit la liste des candidates deux fois, et l’écart entre les deux résultats est un message adressé à un client.

Le premier passage est la liste complète, avec une raison pour chaque machine écartée : mauvais site, mauvais constructeur, pas prête, et ainsi de suite. Le second passage est ce qu’il était réellement possible de verrouiller à cet instant. Si la liste complète a donné une gagnante et la liste verrouillable non, alors il y avait quelque chose à vendre et quelqu’un a été plus rapide d’une fraction de seconde. Ce n’est pas la même phrase que « il n’y a rien » : l’une signifie réessayez, l’autre signifie achetez plus de fer. Elles méritent des boutons différents.

La raison figée de la machine choisie part dans la ligne de réservation, car un mois plus tard chaque équipement est dans un autre état et l’on ne peut plus reconstituer pourquoi l’allocateur avait pris celle-là. La ligne le dit alors elle-même.

La clé d’idempotence est demandée deux fois, et cette seconde fois est encore une chose que seul un test de concurrence a trouvée. Cinq requêtes simultanées avec la même clé, sur quatre machines, ont produit une réservation et quatre refus, dont un venant d’une transaction qui n’a rien pu verrouiller et n’est donc jamais arrivée à l’insertion ni à la branche de reprise, alors qu’une réservation portant sa clé existait tout simplement. Redemander juste avant de refuser corrige cela, et cela fonctionne parce qu’à notre niveau d’isolation chaque instruction prend un instantané frais : un voisin qui vient de valider est donc visible.

La garantie qui survit à tout cela est étroite et mérite d’être écrite telle quelle : cinq requêtes avec la même clé donnent une réservation et quatre fois « quelqu’un a été plus rapide ». Les perdants ne voient pas encore la gagnante, car elle n’avait pas validé à cet instant. Il n’existe pas de version où il en irait autrement sans que chacun attende les autres, et attendre transforme chaque minute chargée en file d’attente.

La réservation devient le lien

Quand la commande aboutit, la machine doit passer de réservée à vendue. La mise en œuvre évidente libère la réservation puis écrit le lien de service, et elle contient une fenêtre de quelques millisecondes pendant laquelle la machine est libre. Un jour chargé, cette fenêtre est trouvée.

La conversion écrit donc le lien et fait passer la réservation à convertie dans une seule transaction, et la ligne reste retenue jusqu’à ce que le lien existe. À chaque instant de cette transaction, une seconde commande voit soit la réservation, soit le lien, et les deux l’excluent. Il n’y a pas d’instant libre parce qu’aucune instruction n’en crée. La même forme que la promotion d’une réservation de baie en emplacement, un étage plus haut.

Le balayage est un ménage et non un gardien

Une réservation expire au bout de quinze minutes par défaut, et un ouvrier balaie les expirées chaque minute.

Ce nombre n’est pas les quatre heures qu’obtient une réservation de baie, et la différence tient à ce que chacune attend. Une réservation de baie attend un humain avec un chariot : il faut aller dans la salle, poser des rails, rebrancher des câbles. Une réservation de machine attend un achat : page de paiement, contrôle de fraude, confirmation. Pendant ce temps, le stock le plus cher que possède un hébergeur est invendable. Quinze minutes, c’est généreux pour un paiement interrompu que quelqu’un reprend, et assez court pour qu’une vague de commandes abandonnées ne vide pas le rayon une demi-journée.

L’important est l’endroit où l’échéance est appliquée. Elle est vérifiée dans la conversion, pas par le balayage. Si le balayage était le seul endroit, une réservation expirée entre deux passages se convertirait encore, et un gardien qui passe une fois par minute ne garde rien. Le balayage remet l’administration d’aplomb et rend la machine au stock ; c’est du ménage.

Il n’a en outre aucun interrupteur de fonctionnalité, et c’est une exception délibérée. Il existe un interrupteur de déploiement sur tout le module de centre de données, et le répéter ici voudrait dire qu’éteindre le module laisse des machines réservées pour toujours à des commandes que plus personne ne peut voir. Un interrupteur dont l’effet secondaire est de désactiver une échéance n’est pas un interrupteur, c’est une fuite.

Ce que nous noterions

  1. Mettez l’arbitre dans la base. L’application doit produire le refus lisible ; l’index doit désigner le gagnant.
  2. Verrouillez une candidate à la fois. Verrouiller un ensemble retient des lignes que vous n’utiliserez pas, et le symptôme est une vente perdue, pas une erreur.
  3. Bornez le parcours. Le verrou par candidate échange un vivier pris en otage contre une longue boucle, et cette boucle a besoin d’une borne aussi.
  4. Une violation d’unicité interrompt la transaction. Réessayer sans point de reprise produit une erreur qui nomme la mauvaise chose.
  5. Regardez le nom de la contrainte. Une table a souvent plusieurs unicités, et pour l’utilisateur elles veulent dire des choses différentes.
  6. « Rien de disponible » et « quelqu’un a été plus rapide » sont deux messages. Construire la liste des candidates deux fois est la façon de les distinguer.
  7. Promouvez au lieu de libérer puis réécrire. Toute séquence contenant un instant libre finira par être trouvée.
  8. Appliquez l’échéance au point d’usage. Un balayage périodique est de l’intendance, et l’intendance n’est pas une règle.

Pour être clair sur ce dont il s’agit : le travail de logistique de centre de données se trouve derrière un interrupteur de déploiement éteint par défaut, avec un réglage par organisation par-dessus ; rien de tout cela n’est donc achetable aujourd’hui. Deux choses ne sont pas démontrées. Le balayage des expirations a été mesuré comme fonction et non comme ouvrier en marche. Et une requête brute contre les actions serveur avec l’interrupteur fermé n’a pas été tentée.

Issu du travail de plateforme de Provibr. La même promotion plutôt que réécriture, appliquée aux unités de baie, est expliquée dans la réservation devient l’affectation, et pourquoi des unités libres dans une baie ne valent pas de la capacité vendable est expliqué dans l’espace en rack n’est pas de la capacité.

Commencez avec un hôte et un agent

Créez un compte, installez l’agent avec une seule commande et connectez votre premier hôte Proxmox. La période d’essai dure 14 jours et ne demande aucune coordonnée de paiement.