Infra
6 min de lecture

Spegel : le cache d'images P2P simple et élégant

La plupart des solutions de distribution d'images P2P dans Kubernetes sont des monstres à opérer. Spegel est stateless et automatique.

Arthur Zinck
Arthur Zinck
Expert DevOps Kubernetes & Cloud

Un jour Docker Hub te rate-limit en plein rollout, tes pods partent en ImagePullBackOff. La distribution d’images P2P dans Kubernetes existe pour régler ça. Le souci, c’est que la plupart des solutions sont des gros morceaux à opérer. Spegel, lui, t’apporte l’essentiel du bénéfice sans rien à maintenir. Je l’ai posé sur mon cluster Talos perso et je voulais expliquer pourquoi je trouve ce projet bien fichu.

Le problème

Quand tu pull une image dans un cluster, chaque node va la chercher sur l’upstream (Docker Hub, GHCR, ECR, peu importe). Ça donne deux ennuis classiques.

Le premier, c’est le scale. Tu fais un rollout d’un DaemonSet, ou un scale-up qui démarre 20 nodes d’un coup, et tes 20 nodes tapent le même registre pour la même image en même temps. C’est lent et c’est de l’egress payé en double, en triple, en vingtuple.

Le second, c’est la dépendance. Si l’upstream est down ou te rate-limit (coucou Docker Hub et ses pull limits), tu es bloqué. Ton cluster qui tourne déjà a pourtant l’image quelque part, sur un node voisin, mais il ne sait pas s’en servir. Il va la rechercher dehors.

L’idée du P2P, c’est exactement ça : un node qui a déjà l’image la sert à ses voisins, au lieu de tout le monde qui retape l’upstream.

Ce que fait Spegel

Spegel est un miroir de registre OCI stateless qui vit à l’intérieur de ton cluster, déployé en DaemonSet. Un pod par node. Chaque pod expose un miroir en localhost, et sert les layers que son node a déjà dans le content store de containerd.

C’est le point clé : il n’y a pas de stockage en plus. Spegel ne duplique rien, il ne stocke rien à côté. Il s’appuie sur ce que containerd a déjà sur disque. Les layers d’une image que tu as pull restent là (avant le garbage collect), et Spegel les rend disponibles aux autres nodes.

Pour savoir quel node détient quel layer, Spegel monte un réseau P2P avec libp2p et une DHT Kademlia. Chaque node annonce les digests qu’il a localement. Quand un node a besoin d’un layer, il interroge la DHT, trouve un peer qui l’annonce, et le récupère en interne. Si personne ne l’a, Spegel renvoie un 404 et containerd repart vers l’upstream normalement. Le fallback est transparent, tu ne risques pas de casser tes pulls.

Pas de base de données externe. Pas de registre central à opérer. Pas de scheduler, pas de manager, pas de seed peers. Tu déploies le DaemonSet, tu configures containerd, c’est tout.

La config containerd

Il faut deux choses côté containerd. D’abord lui dire d’utiliser un config_path pour les hosts de registre. Ensuite, et c’est le réglage qu’on oublie, garder les layers décompressés :

version = 3

[plugins."io.containerd.cri.v1.images".registry]
  config_path = "/etc/containerd/certs.d"

[plugins."io.containerd.cri.v1.images"]
  discard_unpacked_layers = false

Certaines distros (Talos par exemple) mettent discard_unpacked_layers = true pour économiser du disque. C’est légitime comme choix. Sauf que si containerd jette les layers après le unpack, Spegel n’a plus rien à servir. Il faut donc les garder.

Sur Talos, le path de config registre n’est pas le path standard, et la valeur par défaut discard les layers. Le patch de machine config ressemble à ça :

machine:
  files:
    - path: /var/etc/cri/conf.d/20-spegel.part
      op: create
      permissions: 0o000
      content: |
        [plugins."io.containerd.cri.v1.images"]
          discard_unpacked_layers = false

Et côté values Helm de Spegel, tu pointes le bon path :

spegel:
  containerdRegistryConfigPath: /etc/cri/conf.d/hosts

Rien de tout ça n’est actif par défaut sur Talos : discard_unpacked_layers est à true et le path de config registre n’est pas le standard, donc tu n’as pas Spegel out of the box. Mais le modèle déclaratif de la machine config rend le patch propre et reproductible une fois que tu sais quoi poser.

Pourquoi pas les autres outils

Il y a plusieurs façons de régler le problème, et elles ne se valent pas en coût d’opération.

Le pull-through cache centralisé (un registry:2 en mode mirror, le proxy cache de Harbor, Zot) marche très bien. Mais c’est un registre que tu dois opérer toi-même : le scaler, le sécuriser, lui donner du stockage, le sauvegarder. C’est un composant stateful de plus dans ton infra, et un point de contention potentiel si tout le cluster pull à travers lui. Spegel n’a rien de tout ça : décentralisé, stateless, zéro stockage en plus.

Dragonfly (projet CNCF) est une vraie solution de distribution P2P, très puissante à très grande échelle. Manager, scheduler, seed peers, dfdaemon sur chaque node. C’est excellent si tu as les volumes et l’équipe pour, et ça fait des choses que Spegel ne fait pas, comme le preheat. Mais ça fait beaucoup de pièces à opérer.

Kraken (par Uber) est un P2P type BitTorrent, avec origin, tracker et agents. Puissant aussi, mais lourd et moins activement maintenu aujourd’hui.

kube-image-keeper (kuik, par Enix) prend un angle vraiment différent, et c’est important de le comprendre parce qu’il ne répond pas tout à fait au même besoin. Son but premier, c’est la durabilité : garder une copie des images dont tes pods dépendent, même si l’upstream supprime le tag ou disparaît. Pour ça, un webhook mutating réécrit les image: de tes pods pour les faire pointer vers un cache local, et kuik recopie les images utilisées dans le cluster vers un registre cible que tu lui donnes. Donc oui, c’est stateful (il y a du stockage et un registre à opérer) et oui, ça touche aux refs d’images de tes pods. En échange tu as une vraie garantie de rétention : kuik retient une copie pour de bon, il ne dépend pas du garbage collect de containerd. Spegel, lui, reste transparent (un simple mirror containerd, aucune réécriture) et stateless, mais il ne te garantit pas qu’une image survive au GC une fois que plus aucun node ne l’utilise. Deux compromis légitimes pour deux besoins : Spegel pour accélérer et résister aux coups durs au quotidien, kuik si ce que tu veux c’est ne jamais perdre une image dont un pod a besoin.

Tous ces projets sont sérieux et bien faits. La question n’est pas lequel est le meilleur dans l’absolu, c’est lequel correspond à ton échelle, à ton besoin et à ce que tu es prêt à opérer. Pour la grande majorité des clusters qui veulent juste des pulls rapides et résilients sans rien babysitter, Spegel donne autour de 80 % du bénéfice du P2P avec un coût d’opération proche de zéro, parce qu’il est stateless et adossé au content store de containerd qui est déjà là.

Les limites

Spegel n’est pas magique.

→ Le premier pull d’une image neuve tape toujours l’upstream. Pas de pré-seeding par défaut, contrairement à Dragonfly. Le bénéfice arrive quand un node a déjà l’image et que les autres en ont besoin.

→ Il faut containerd, et pouvoir configurer son config_path. Sur EKS et AKS ça marche moyennant un peu de conf (sur EKS, via nodeadm sur les AMI AL2023 : config_path + discard_unpacked_layers = false). GKE et Scaleway Kapsule par contre ne te laissent pas régler le config_path registre sur les nodes (côté Scaleway il y a une feature request ouverte pour ça), donc Spegel n’y tourne pas tel quel.

→ C’est intra-cluster uniquement. Spegel partage les images entre les nodes d’un même cluster, pas entre clusters.

Ce que j’en retiens

Si tu as déjà mangé un ImagePullBackOff parce que Docker Hub t’a rate-limit au pire moment, ou si tu fais des scale-up où des dizaines de nodes pull la même image, Spegel répond pile à ça. Et la vraie raison pour laquelle je le recommande, c’est qu’il ne te rajoute pas un composant stateful à babysitter. Un cache P2P qui ne te coûte rien à opérer, c’est rare. Celui-là, tu peux le laisser tourner et l’oublier.

Points clés à retenir

  • Spegel est un miroir de registre OCI stateless qui vit à l'intérieur de ton cluster, déployé en DaemonSet. Chaque node sert les layers déjà présents dans le content store de containerd, sans stockage en plus.
  • Découverte des layers en P2P via libp2p + DHT Kademlia. Fallback transparent vers l'upstream si aucun peer n'a le digest. Pas de registre central ni de composant stateful à opérer.
  • Idéal contre les rate limits Docker Hub, les ImagePullBackOff quand l'upstream est down, et les gros scale-up où N nodes pull la même image. Limite honnête : le premier pull d'une image neuve tape toujours l'upstream.

Cet article t'a plu ?

Je publie régulièrement mes retours d'expérience infra, Kubernetes et FinOps sur LinkedIn. Abonne-toi pour les suivre. Mes MP sont ouverts, tu peux m'écrire direct.

spegel kubernetes containerd p2p talos registry finops

Partager cet article

Twitter LinkedIn