Aller au contenu principal

Garage : cluster multi-nœuds sur Kubernetes

· 9 minutes de lecture

Un stockage objet S3 hébergé sur un seul serveur finit par buter sur la taille de son disque. Dans le cas traité ici, une instance Garage déployée sur un cluster Kubernetes disposait d'environ 500 Go, et un besoin de 2 To est apparu. Aucune machine du cluster ne porte un disque de cette taille, il n'existe pas de baie de stockage, et un volume Longhorn ne peut pas dépasser le disque d'un nœud. La capacité existe pourtant : plusieurs nœuds ont des disques en grande partie libres. Garage sait agréger ces disques, à condition de tourner sur plusieurs machines et de former un cluster. Le faire sous Kubernetes pose une difficulté moins visible : Kubernetes traite les pods comme interchangeables, alors qu'un nœud de stockage distribué ne l'est pas. Cet article décrit ce décalage, les pannes qu'il a provoquées lors du passage d'un à quatre nœuds, et la façon de les éviter.

Agréger les disques de plusieurs nœuds​

OptionLimite
Agrandir le disque du nœudMatériel à changer, borne physique atteinte
Volume Longhorn plus grandUn volume ne dépasse pas le disque d'un nœud, moins la réserve de Longhorn
NFS ou baie partagéeAbsente, et point unique de défaillance
Garage sur plusieurs nœudsChaque nœud apporte son disque ; Garage répartit les objets entre eux

Garage découpe les objets en blocs et les répartit sur ses nœuds selon un layout, une table qui attribue à chaque nœud une zone et une capacité. Ajouter un nœud au layout ajoute sa capacité au cluster ; les clients S3 continuent de s'adresser à un point d'entrée unique et ignorent sur quel disque se trouvent leurs objets.

Ce que Kubernetes ignore d'un nœud Garage​

Un Deployment repose sur une hypothèse simple : ses pods sont identiques, remplaçables et utilisables dès que leur sonde de disponibilité répond. Un nœud Garage contredit cette hypothèse sur trois points.

Propriété d'un nœud GarageHypothèse de Kubernetes par défautConséquence si rien n'est fait
Identité : ses pairs le joignent à une adresse fixeUn pod recréé change d'adresse IPLes autres nœuds perdent sa trace
Données : ses blocs sont sur un disque précisUn volume peut être placé n'importe oùPod et données sur des machines différentes
Appartenance : il ne sert les données qu'une fois intégré au layoutUn pod prêt reçoit du traficRequêtes servies par un nœud qui ne connaît rien

Les deux dernières ont chacune provoqué une panne lors de l'extension.

Identité : StatefulSet et Service headless​

Chaque nœud Garage annonce aux autres l'adresse à laquelle le joindre (rpc_public_addr). Un StatefulSet donne à ses pods des noms stables (garage-0, garage-1…), et un Service headless leur associe un nom DNS qui survit aux recréations : garage-1.garage-headless.storage.svc.cluster.local. Un initContainer insère ce nom dans la configuration, commune aux pods :

rpc_public_addr = "__POD_NAME__.garage-headless.storage.svc.cluster.local:3901"

Deux réglages complètent ce socle. Le Service headless porte publishNotReadyAddresses: true, pour que l'adresse d'un nœud qui démarre résolve avant qu'il ne soit déclaré prêt : sans cela, ses pairs ne peuvent pas le joindre et la formation du cluster dépend de l'ordre de démarrage. Une anti-affinité interdit deux pods Garage sur une même machine, qui partageraient alors un disque en annonçant chacun sa capacité.

Données : un volume local, créé là où est le pod​

Garage répartit lui-même les blocs entre ses nœuds. Confier en plus à Longhorn une réplication de chaque volume sur trois machines multiplierait l'espace consommé, à l'opposé du but recherché. Les données de chaque nœud Garage vont donc sur un volume Longhorn à un seul réplica, contraint au disque de la machine qui l'utilise :

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: longhorn-garage
provisioner: driver.longhorn.io
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
parameters:
numberOfReplicas: "1"
dataLocality: strict-local # le réplica reste sur la machine du pod
diskSelector: garage # uniquement les disques réservés à Garage

Les métadonnées de Garage suivent la logique inverse : quelques Go seulement, mais leur perte rend inexploitables les blocs du nœud. Elles restent sur un volume répliqué trois fois.

Panne n° 1 : le volume créé sur la mauvaise machine​

La première version de la StorageClass n'indiquait pas volumeBindingMode, donc Immediate. Dans ce mode, le volume est créé dès l'apparition de la PVC, avant que le scheduler n'ait choisi une machine pour le pod. Longhorn le place selon ses propres critères, puis fixe le PV sur cette machine. Lors de l'ajout du troisième nœud Garage, le volume a été créé sur une machine dont le disque n'était pas réservé à Garage : réplica impossible à placer, pod bloqué, déploiement en échec.

WaitForFirstConsumer inverse l'ordre : le scheduler place d'abord le pod en tenant compte de toutes ses contraintes, puis le volume est créé sur cette machine. C'est le mode attendu pour tout stockage local à un nœud. Les paramètres d'une StorageClass étant immuables, la correction impose de supprimer l'objet et de le recréer ; les volumes existants ne sont pas touchés.

Appartenance : le rôle dans le layout​

Un pod Garage qui démarre n'est pas encore un membre utile du cluster. Il doit être connecté aux autres nœuds, puis recevoir un rôle dans le layout. Cette dernière étape est une décision d'administration, faite par la CLI de Garage, que Kubernetes ne connaît pas.

La sonde de disponibilité ne peut pas refléter ce rôle. L'endpoint /health de Garage renvoie 503 tant que le layout n'est pas prêt : utilisé comme readiness probe, il bloquerait le tout premier déploiement, où aucun layout n'existe encore. La sonde se limite donc à vérifier que le port S3 répond.

Panne n° 2 : des requêtes servies par un nœud qui ne sait rien​

Lors d'un ajout de nœud, le nouveau pod a été déclaré prêt dès l'ouverture de son port, et le Service garage lui a aussitôt confié sa part des requêtes, réparties entre tous les pods prêts. Sans rôle dans le layout, ce nœud ne connaissait ni les clés ni les buckets : l'API S3 répondait 403 No such key et l'API d'administration une erreur 500. Pendant environ 25 minutes, le temps de remarquer le problème et d'intégrer le nœud, une fraction des requêtes a échoué sans motif visible côté client.

La correction porte sur l'ordre des opérations. Un nœud n'a pas besoin de stocker des données pour servir correctement les requêtes : le rôle de passerelle (-g) lui donne la connaissance des métadonnées du cluster sans lui attribuer de capacité. Ce rôle s'attribue dès que le pod tourne, et le rôle de stockage peut attendre :

g() { kubectl -n storage exec garage-0 -- /garage "$@"; }

# Dès que le nouveau pod est Running : membre du cluster, rôle de passerelle
g node connect <node_id>@garage-3.garage-headless.storage.svc.cluster.local:3901
g layout assign -z dc1 -g <node_id>
g layout apply --version 5 # version courante + 1

# Plus tard, une fois son volume à la taille voulue : rôle de stockage
g layout assign -z dc1 -c 725G <node_id>
g layout apply --version 6

Une boucle lancée avant le déploiement, qui attend le conteneur en Running puis enchaîne ces trois commandes, a ramené la fenêtre d'erreur à moins d'une seconde lors de l'ajout suivant. Après l'attribution du rôle de stockage, Garage déplace des partitions vers le nouveau nœud ; le transfert est terminé lorsque garage layout history ne montre plus de version en cours de drainage.

La capacité déclarée reste un peu inférieure à la taille du volume : elle sert à répartir les partitions, et le système de fichiers a besoin de marge.

Ce que l'agrégation coûte​

Avec replication_factor = 1, chaque bloc n'existe qu'en un exemplaire : quatre nœuds apportent quatre fois la capacité, sans aucune redondance. La documentation de Garage réserve cette valeur aux déploiements de test. Le risque augmente avec le nombre de nœuds : les blocs d'un objet volumineux (1 Mio chacun par défaut) se répartissent sur plusieurs disques, si bien que la perte d'un seul disque peut endommager une part des objets bien supérieure à sa part de la capacité.

replication_factorToléranceCapacité utile sur 4 nœuds
1Aucune panne de disqueSomme des capacités
2Lecture pendant une panne, écritures bloquéesMoitié
3Lecture et écriture pendant une panneTiers

Le choix de 1 n'est défendable que pour des données reproductibles ou sauvegardées ailleurs, et doit être connu des utilisateurs du stockage.

Savoir quand ajouter un nœud​

L'extension suivante se décide sur l'occupation des buckets et de leurs quotas. L'endpoint /metrics de Garage expose l'état des disques mais pas celui des buckets. Ces compteurs existent pourtant, tenus à jour par Garage, et l'API d'administration v2 les fournit (ListBuckets, puis GetBucketInfo par bucket). Un exporter de quelques dizaines de lignes les publie au format Prometheus :

MétriqueUsage
garage_bucket_bytes{bucket}Volume occupé par bucket
garage_bucket_quota_max_bytes{bucket}Quota du bucket
garage_bucket_objects{bucket}Nombre d'objets
garage_bucket_exporter_upDistingue un bucket vide d'une API injoignable

Le rapport garage_bucket_bytes / garage_bucket_quota_max_bytes donne la part de quota consommée, utilisable dans une alerte. L'appel ne parcourt aucune liste d'objets : son coût dépend du nombre de buckets, pas de leur contenu.

Conclusion​

Garage permet de dépasser le disque d'une machine en agrégeant ceux de plusieurs nœuds, sans matériel de stockage dédié. Sous Kubernetes, l'essentiel du travail consiste à rendre au nœud Garage ce que Kubernetes ignore de lui : une identité fixe (StatefulSet et Service headless), des données liées à une machine (volume local en WaitForFirstConsumer) et une appartenance au cluster qui ne se résume pas à une sonde qui répond (rôle de passerelle attribué dès le démarrage). Les deux pannes rencontrées viennent de valeurs par défaut pensées pour des pods interchangeables. Le même raisonnement vaut pour les autres systèmes de stockage distribué exécutés sous Kubernetes, comme les bases de données réparties.

Application / Projet lié​

Mis en pratique dans le projet2024 → aujourd'huiCluster Kubernetes interne DiscoDiodeExtension du stockage objet Garage d'un à quatre nœuds, de 500 Go à 2,4 Tio utiles, sans matériel supplémentaire : chart Helm à un pod par nœud, volumes Longhorn locaux, procédure d'ajout de nœud écrite après une coupure partielle, et métriques d'occupation par bucket.KuberneteskubeadmCalicoTailscaleTraefikHelmGitHub ActionsPrometheusGrafana