Blog ·
Planifier les pools d’adresses IPv4 d’un produit VPS
Les adresses sont le seul intrant dont on ne peut pas acheter plus un mardi. Comment former des pools, pourquoi la base arbitre, et ce qui arrive à sec.
Par Provibr
Tout le reste dans un produit VPS est élastique. Besoin de plus de CPU : achetez un nœud. Besoin de plus de disque : achetez du disque. Besoin de plus d’adresses, et vous voilà sur une liste d’attente, à payer un courtier, ou à remplir un formulaire de justification pour un registre régional.
L’IPv4 est le seul intrant qui se comporte comme un bien physique, et c’est en général celui qui reçoit le moins d’attention de conception, car au début vous avez un /24 et 254 semble largement suffisant. Cela cesse de sembler suffisant exactement au moment où le repenser coûte cher.
Commencez par ce qu’un bloc donne réellement
Le nombre utilisable n’est jamais la taille du bloc. Deux adresses partent pour le réseau et la diffusion, et une de plus pour la passerelle. Ce n’est pas une erreur d’arrondi aux petites tailles, c’est l’essentiel du bloc :
| Bloc | Adresses | Réseau + diffusion | Passerelle | Vendables | Perte |
|---|---|---|---|---|---|
| /24 | 256 | 2 | 1 | 253 | 1 % |
| /27 | 32 | 2 | 1 | 29 | 9 % |
| /28 | 16 | 2 | 1 | 13 | 19 % |
| /29 | 8 | 2 | 1 | 5 | 38 % |
| /30 | 4 | 2 | 1 | 1 | 75 % |
Un /29 a l’air de faire huit adresses, jusqu’au moment précis où vous en vendez cinq. Si vous achetez de l’espace d’adressage en petits blocs, budgétez la perte comme une ligne de coût : à /29, vous jetez trois huitièmes de ce que vous avez payé.
Deux bornes méritent d’être inscrites dans ce que vous construisez. Nous acceptons un /8 au maximum, car pour un bloc plus grand une barre d’occupation ne veut plus rien dire, et un /30 au minimum, car /31 et /32 n’ont aucune adresse utilisable sous les règles habituelles des hôtes, et les accepter ne produit qu’un pool incapable d’allouer quoi que ce soit. Les refuser explicitement vaut mieux que les accepter et rester vide.
Une subtilité de plus : l’adresse de la passerelle a le droit de se trouver à l’intérieur de votre plage d’allocation, et elle est simplement sautée. C’est délibéré, car c’est l’arrangement le plus simple à se représenter pour un client, mais cela veut dire que la capacité d’un pool est un calcul et jamais un nombre stocké. Tout ce qui tient un compteur d’adresses libres sera en désaccord avec la réalité le jour où quelqu’un déplace une passerelle.
Formez les pools autour de ce que vous vendez, pas de ce que vous avez acheté
L’unité que voit un client est un plan. L’unité que vous avez achetée à un courtier est un bloc. Ces deux-là n’ont presque jamais la même forme, et les lier tourne mal dès l’achat d’un deuxième bloc.
L’arrangement qui a tenu : un plan pointe vers un pool, et un pool est soit un préfixe unique, soit un groupe nommé de préfixes. Ajouter de la capacité à un produit revient alors à ajouter un préfixe à un groupe, c’est-à-dire une ligne. L’allocateur parcourt le groupe dans l’ordre et prend la première adresse qu’il peut obtenir : un groupe est donc aussi un ordre de vidage, placez en tête le bloc que vous voulez consommer en premier.
Les sous-blocs, et l’erreur qui alloue deux fois en silence
Les clients n’utilisent pas un /24 comme un seul réseau plat. Ils le découpent : un morceau derrière un routeur, un morceau sur un autre VLAN, un morceau avec sa propre passerelle. Un plan d’adressage a donc besoin de blocs enfants.
C’est là que vit la défaillance intéressante, et elle mérite d’être énoncée précisément, car elle est silencieuse.
Le mécanisme qui empêche deux commandes simultanées d’obtenir la même adresse est un index unique partiel sur (préfixe, adresse), limité aux lignes qui n’ont pas été libérées. Cet index est par préfixe. Si deux lignes de préfixe peuvent chacune distribuer la même adresse, alors pour l’index il s’agit de deux adresses différentes, et la plateforme donnera la même IP à deux machines sans que rien ne se passe mal dans le logiciel.
L’index ne peut pas corriger cela. La disposition, si. L’espace d’adressage est donc une partition stricte, et les règles qui en font une sont :
- Un enfant se trouve entièrement à l’intérieur de son parent et est strictement plus petit.
- Les blocs frères ne peuvent pas se chevaucher.
- Le parent ne distribue rien de ce qui tombe dans un bloc enfant. C’est le bloc entier qui est découpé dans la fenêtre du parent, et non la seule plage d’allocation propre à l’enfant, car cela ferait dépendre la réponse d’une plage qu’un administrateur déplacera la semaine prochaine.
- L’imbrication n’a qu’un seul niveau de profondeur.
Avec ces quatre règles, « à quel préfixe appartient 192.0.2.70 » a toujours exactement une réponse, et l’index existant suffit.
Ce qu’un enfant hérite forme un ensemble de décisions à part, et les réponses ne sont pas uniformes. Passerelle : jamais (un enfant sans passerelle propre n’a aucune raison d’exister). DNS : oui, si son propre champ est vide, car les résolveurs appartiennent au réseau plutôt qu’au bloc. Plage d’allocation : seulement là où la plage du parent touche effectivement l’enfant, puisque la plage du parent est exprimée dans les adresses du parent et ne recouvre en général pas du tout l’enfant ; en hériter aveuglément produit une fenêtre vide.
Laissez la base de données arbitrer
La tentation, quand deux commandes arrivent en même temps, est de verrouiller quelque chose ou de tenir un compteur. Les deux valent moins que la réponse ennuyeuse.
Les deux commandes calculent la même « plus petite adresse libre » et tentent toutes deux l’insertion. Une insertion l’emporte, l’autre heurte la violation d’unicité, et le perdant passe au candidat suivant. La base de données est la seule partie qui voit tous ceux qui écrivent, y compris celui qui se trouve dans un autre processus, un autre worker ou un autre onglet de navigateur.
Le corollaire compte au moment d’écrire le code : ne vérifiez pas d’abord. Lire « cette adresse est-elle libre » puis insérer ouvre précisément la fenêtre que la contrainte existe pour fermer. L’insertion ratée est la réponse à « était-elle libre », et c’est la seule réponse fiable.
Un détail d’implémentation qui piège : beaucoup d’ORM enveloppent les erreurs du pilote dans leur propre type d’erreur, si bien que le SQLSTATE que vous cherchez n’est pas sur l’exception que vous attrapez mais sur sa cause. Parcourez toute la chaîne, et servez-vous du nom de la contrainte pour décider quel message afficher. Une même table a souvent quatre règles d’unicité différentes, et leur faire dire la même chose envoie le client vers le mauvais champ.
Ce qui arrive quand un pool est à sec
Trois défaillances différentes se cachent derrière « aucune adresse disponible », et les confondre produit un ticket de support à chaque fois :
| Situation | Ce que cela veut dire | Ce que le client doit lire |
|---|---|---|
| Le plan ne pointe vers aucun pool | Configuration, pas capacité | Ce plan n’a pas de pool d’adresses |
| Un préfixe a des données inutilisables | Une ligne cassée, les autres vont bien | Ces préfixes n’ont pas pu être lus |
| Tous les candidats sont pris | Épuisement réel | Ce pool est plein, et voici lequel |
Celle du milieu vaut le code supplémentaire. Un seul préfixe aux données illisibles ne doit pas bloquer les autres préfixes de son groupe : notez-le, essayez le reste, et ne le signalez que si rien du tout n’a fonctionné. Sinon, une seule ligne défectueuse fait tomber la commande pour toute une gamme de produits.
Et le message d’épuisement doit nommer le préfixe et son CIDR. « Aucune IP disponible » envoie quelqu’un lire des journaux. « Pool plein : customer-block-2 (198.51.100.0/28) » l’envoie acheter des adresses.
Rendez les adresses, et faites attention au moment
La libération est la moitié qui fait discrètement fuir la capacité.
Deux règles se sont révélées plus importantes que prévu. D’abord, libérer n’est pas supprimer la ligne : cela pose un horodatage de libération. L’index unique partiel ne regarde que les lignes vivantes, donc l’adresse est immédiatement réutilisable, tandis que « qui avait 192.0.2.10 l’an dernier » reste une question à laquelle on peut répondre. C’est l’essentiel de la raison de faire de la gestion d’adresses, et c’est la question que pose un signalement d’abus.
Ensuite, la libération a lieu quand la machine est confirmée disparue, pas quand la suppression a été demandée. Seul ce qui se tient à côté de l’hyperviseur sait que la VM a réellement disparu ; libérer au moment de la commande sortante donne l’adresse au client suivant alors que l’ancienne machine tourne encore et répond encore dessus. Le cas miroir est une commande qui échoue à mi-parcours : là, l’allocation est jetée plutôt que libérée, car elle n’a jamais été configurée sur quoi que ce soit.
Il y a ici une asymétrie délibérée qui mérite d’être signalée, car nous faisons le choix inverse une couche plus haut. Les adresses sont réutilisées, la plus petite libre d’abord. Les identifiants de machine ne sont jamais réutilisés. Un message d’état tardif au sujet d’une VM supprimée qui atterrit sur une nouvelle ligne portant le même identifiant corrompt l’histoire de deux machines ; une adresse ne porte pas d’histoire de cette façon, et ne jamais en réutiliser une épuiserait définitivement un /24 après 254 machines. Même produit, règles opposées, raisons différentes.
Les réservations vont dans la même table que les allocations
Vous devrez mettre des adresses de côté : un répartiteur de charge, une passerelle sur un bloc que vous n’avez pas encore déployé, les cinq adresses que quelqu’un a promises à un client pour le trimestre prochain.
Mettez-les dans la même table que les allocations réelles. Chaque lecteur qui demande « qu’est-ce qui est pris », l’allocateur, la barre d’occupation, la liste de suggestions d’un champ d’adresse, les compte alors automatiquement, et il n’existe pas de seconde administration qui puisse se désynchroniser. Une table de réservations séparée est une seconde source de vérité qui sera en désaccord avec la première en moins d’un mois.
Avec une exception qu’il est facile de rater : une adresse réservée ne doit jamais finir dans la liste d’autorisation du pare-feu d’une machine. Cette liste est construite à partir des allocations qui appartiennent à un serveur, et une réservation n’appartient à aucun serveur. Ratez cela, et la VM d’un client se met à accepter du trafic adressé au répartiteur de charge de quelqu’un d’autre.
Et l’IPv6
Nous reconnaissons une saisie IPv6 et la refusons poliment plutôt que de l’accepter et de calculer quelque chose de plausible : un chemin IPv6 à moitié fonctionnel est plus dangereux qu’aucun chemin, car les nombres qu’il produit ont l’air corrects. La décision peu coûteuse qui se rembourse toute seule consiste à stocker les adresses, les passerelles et les serveurs DNS sous forme de texte plutôt que d’entier 32 bits, pour qu’ajouter l’IPv6 plus tard soit du code et non la migration de chaque ligne que vous possédez.
La version courte
- Budgétez la perte. Un /29, c’est cinq adresses vendables, pas huit.
- Faites pointer les plans vers des pools, et les pools vers des préfixes. Acheter plus d’espace devrait tenir en une ligne.
- Si vous autorisez les sous-blocs, faites de l’espace d’adressage une partition stricte, sinon la règle d’unicité qui vous protège est discrètement par préfixe et ne protège rien.
- Laissez la contrainte d’unicité trancher la course. Ne vérifiez jamais d’abord.
- Libérez à la confirmation, pas à la demande, et gardez l’historique.
- Nommez le pool dans le message d’épuisement. C’est la différence entre un ticket et un bon de commande.
Ce texte vient de la construction de la gestion d’adresses dans Provibr, où les adresses sont attribuées au moment de la commande et rendues quand l’agent confirme que la machine a réellement disparu.