Docker « Trop de requêtes » lors du pull d’images : corriger le throttling du registre sérieusement

Cet article vous a aidé ?

L’erreur est toujours la même. Votre déploiement est « vert » jusqu’à ce qu’il ne le soit plus, puis chaque nœud commence à répéter :
toomanyrequests, 429, pull rate limit exceeded. Soudainement votre « infrastructure immuable »
paraît très mutable : elle se transforme en tas de pods en Pending et de jobs CI échoués.

Le throttling du registre n’est plus un cas rare en marge. C’est le résultat prévisible du comportement moderne : runners éphémères, clusters autoscalés,
builds parallèles, images multi-arch, et une incapacité collective à laisser les choses tranquilles. Corrigeons-le correctement — diagnostiquez ce qui est throttlé,
stoppez les pull storms, mettez en cache ce que vous pouvez, et rendez votre pipeline à nouveau ennuyeux.

Ce que « trop de requêtes » signifie réellement

« Trop de requêtes » n’est pas une seule chose. C’est une famille de throttles qui intervient à différents niveaux, et la correction dépend de la couche qui vous bloque.
La plupart des équipes le traitent comme un problème Docker. C’est généralement un problème d’architecture système avec un symptôme en forme de Docker.

Manifestations courantes

  • HTTP 429 depuis un endpoint du registre : limitation de débit classique. Vous dépassez un quota par IP, par utilisateur, par token ou par organisation.
  • HTTP 403 avec « denied: requested access » qui n’apparaît qu’en charge : parfois un registre renvoie des erreurs d’auth trompeuses lorsqu’il applique des limites.
  • Kubernetes ImagePullBackOff / ErrImagePull avec « toomanyrequests » : le kubelet tire sur de nombreux nœuds en même temps. Le registre dit « non ».
  • Échecs CI où des jobs parallèles tirent la même image de base à répétition. L’image est « mise en cache » seulement en théorie.
  • Ce n’est pas du throttling, mais ça y ressemble : des défaillances DNS, des problèmes de MTU, des proxies d’entreprise ou une interception TLS peuvent produire des storms de retries ressemblant à du rate limiting.

Où le throttling peut être appliqué

Le throttling peut se produire au niveau du registre, d’un CDN en frontal du registre, de votre proxy de sortie d’entreprise, ou de votre propre passerelle NAT.
Vous pouvez même être throttlé par vous-même : tables conntrack, épuisement de ports éphémères, ou un mirror local sous-dimensionné.

Un modèle mental utile : un « docker pull » n’est pas une seule requête. C’est une séquence de fetch de token, de requêtes de manifest et de téléchargements de couches — souvent de nombreuses couches, parfois pour plusieurs architectures.
Multipliez cela par 200 jobs CI ou 500 nœuds, et vous avez créé un générateur de déni de service via YAML.

Une idée paraphrasée de John Allspaw (opérations/fiabilité) : La fiabilité vient du fait de concevoir pour l’échec, pas d’espérer que les échecs n’arriveront pas.
Traitez le throttling de registre de la même façon : comme un mode d’échec connu autour duquel vous concevez.

Blague n°1 : Un pull storm est simplement la façon dont votre infrastructure exprime qu’elle regrette l’époque où les pannes avaient une seule cause racine.

Playbook de diagnostic rapide (premier/deuxième/troisième)

Quand la production brûle, vous n’avez pas le temps pour une danse interprétative avec les logs. Voici le chemin le plus rapide vers le goulot d’étranglement.

Premier : confirmez qu’il s’agit bien d’une limitation de débit (et non du réseau)

  1. Sur un nœud/runner affecté, reproduisez avec un seul pull (pas tout votre déploiement).
  2. Cherchez HTTP 429 et tout en-tête RateLimit.
  3. Vérifiez si les échecs corrèlent avec des IPs NAT (tous les nœuds sortent via une même IP ? Vous partagez un quota).

Deuxième : identifiez le périmètre et le pattern de trafic

  1. S’agit-il d’une image/tag unique ou de tout ?
  2. S’agit-il de nombreux nœuds simultanément (scale-up cluster, rolling restart, remplacement de nœuds) ?
  3. S’agit-il de parallélisme CI (50 jobs démarrent en même temps) ou de postes développeurs (lundi matin) ?

Troisième : choisissez la classe de mitigation correcte

  • Atténuation court terme : ralentir les pulls (stagger deploy), réutiliser les nœuds, pré-puller, augmenter le backoff, réduire la concurrence.
  • Moyen terme : authentifier les pulls, pinner par digest, réduire la taille/les couches des images, cesser de rebuild des tags identiques.
  • Long terme : ajouter un proxy/mirror cache, exécuter un registre privé, répliquer les images critiques, et concevoir votre CI pour réutiliser les caches.

Faits intéressants et contexte historique (ce qui explique la douleur d’aujourd’hui)

  • Le rate limiting de Docker Hub s’est durci en 2020, et de nombreux workflows « gratuits » s’appuyant sur des pulls anonymes sont devenus fragiles du jour au lendemain.
  • La distribution d’images conteneurisées emprunte à Git et aux dépôts de paquets, mais contrairement à apt/yum, les images sont lourdes et tirées en parallèle—excellent pour la vitesse, catastrophique pour les quotas.
  • Les specs OCI ont standardisé le format, ce qui a amélioré la portabilité mais aussi facilité le martèlement des mêmes registries par tous les outils.
  • Les couches adressées par contenu signifient que des couches identiques sont réutilisées entre tags et images—si vous laissez réellement persister les caches. Les runners éphémères jettent cet avantage.
  • Les CDN frontent la plupart des registries publiques ; vous pouvez être limité par une politique edge même si le registre d’origine va bien.
  • Kubernetes a rendu les pull storms normales : un seul déploiement peut déclencher des centaines de pulls quasi-simultanés lors du churn des nœuds ou de l’autoscaling.
  • Les images multi-arch ont augmenté le nombre de requêtes : votre client peut récupérer un index manifest, puis des manifests spécifiques à l’architecture, puis des couches.
  • Les passerelles NAT concentrent l’identité : mille nœuds derrière une IP d’egress peuvent ressembler à un seul client extrêmement impatient.
  • L’authentification du registre utilise des tokens bearer : chaque pull peut inclure des requêtes au service de tokens ; ces endpoints peuvent être throttlés indépendamment.

Tâches pratiques : commandes, sorties et la décision que vous prenez

Voici les outils de terrain. Chaque tâche inclut une commande, ce que vous pouvez voir, et ce que vous décidez ensuite. Exécutez-les sur un nœud/runner qui échoue.
Ne « corrigez » rien avant d’avoir fait au moins les quatre premiers.

Tâche 1 : reproduire un seul pull avec sortie un peu verbeuse

cr0x@server:~$ docker pull nginx:1.25
1.25: Pulling from library/nginx
no matching manifest for linux/amd64 in the manifest list entries

Sens : Ce n’est pas du throttling. C’est un décalage d’architecture (commun sur des runners ARM ou des images de base étranges).
Décision : Corriger la sélection image/tag/platform avant de poursuivre la chasse aux limites de débit.

cr0x@server:~$ docker pull redis:7
7: Pulling from library/redis
toomanyrequests: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading

Sens : C’est une limitation réelle du registre (formulation classique de Docker Hub).
Décision : Passez immédiatement à l’authentification + mise en cache/miroir ; ralentir peut acheter du temps mais ne résout pas la catégorie de problème.

Tâche 2 : identifier votre IP d’egress (partagez-vous un quota derrière un NAT ?)

cr0x@server:~$ curl -s https://ifconfig.me
203.0.113.42

Sens : C’est l’IP publique que voit le registre.
Décision : Si de nombreux nœuds/runners affichent la même IP, considérez le quota comme partagé. Planifiez un mirror ou séparez l’egress.

Tâche 3 : vérifier les logs du daemon et du runtime pour 429 et churn d’auth

cr0x@server:~$ sudo journalctl -u docker --since "30 min ago" | tail -n 30
Jan 03 10:41:22 node-7 dockerd[1432]: Error response from daemon: toomanyrequests: Rate exceeded
Jan 03 10:41:22 node-7 dockerd[1432]: Attempting next endpoint for pull after error: Get "https://registry-1.docker.io/v2/": too many requests

Sens : Le daemon est throttlé au niveau du endpoint du registre.
Décision : Continuez à mesurer la concurrence de pulls et introduisez mise en cache/miroir.

Tâche 4 : confirmer l’identité du registre et les headers (429 vs proxy)

cr0x@server:~$ curl -I -s https://registry-1.docker.io/v2/ | head
HTTP/2 401
content-type: application/json
docker-distribution-api-version: registry/2.0
www-authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io"

Sens : Un 401 ici est normal ; cela prouve que vous atteignez le registre attendu et le flux d’auth.
Décision : Si vous voyez des en-têtes de proxy d’entreprise ou un serveur inattendu, il se peut que vous soyez throttlé ou bloqué en amont par votre proxy/CDN.

Tâche 5 : inspecter le cache d’images par nœud (re-pull parce que les nœuds sont frais ?)

cr0x@server:~$ docker images --digests | head
REPOSITORY   TAG     DIGEST                                                                    IMAGE ID       CREATED        SIZE
nginx        1.25    sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c   8c3a9d2f1b2c   2 weeks ago    192MB

Sens : Le digest indique l’adressabilité par contenu ; si vous pinnez ce digest, vous pouvez être plus déterministe.
Décision : Si les nœuds n’ont pas l’image, vous devez soit pré-puller, conserver des nœuds plus longtemps, ou fournir un mirror qui rend les hits de cache locaux.

Tâche 6 : vérifier les événements Kubernetes pour pull storms et backoff

cr0x@server:~$ kubectl get events -A --sort-by='.lastTimestamp' | tail -n 12
default   8m12s   Warning   Failed     pod/api-7c8d9c6d9c-7lqjv     Failed to pull image "nginx:1.25": toomanyrequests: Rate exceeded
default   8m11s   Normal    BackOff    pod/api-7c8d9c6d9c-7lqjv     Back-off pulling image "nginx:1.25"

Sens : Le kubelet réessaie de façon répétée. Les retries augmentent le volume de requêtes. Le volume augmente le throttling. Vous voyez la boucle.
Décision : Stoppez la boucle : mettez en pause les rollouts, réduisez le churn des replicas, et placez un cache devant le registre.

Tâche 7 : quantifier combien de nœuds tirent simultanément

cr0x@server:~$ kubectl get pods -A -o wide | awk '$4=="ContainerCreating" || $4=="Pending"{print $1,$2,$4,$7}' | head
default api-7c8d9c6d9c-7lqjv ContainerCreating node-12
default api-7c8d9c6d9c-k3q2m ContainerCreating node-14
default api-7c8d9c6d9c-px9z2 ContainerCreating node-15

Sens : C’est une pull storm en direct : de nombreux pods bloqués sur des pulls d’images simultanément à travers les nœuds.
Décision : Étalez le rollout ou scalez vers l’intérieur, puis implémentez du pré-pull ou un DaemonSet pour réchauffer le cache, plus mirror/mise en cache.

Tâche 8 : valider que imagePullPolicy ne vous sabote pas

cr0x@server:~$ kubectl get deploy api -o jsonpath='{.spec.template.spec.containers[0].imagePullPolicy}{"\n"}'
Always

Sens : Always garantit un hit sur le registre même si l’image existe localement. C’est acceptable pour les usages « latest » ; c’est catastrophique sous limitation de débit.
Décision : Si vous utilisez des tags immuables ou des digests, changez pour IfNotPresent et pinnez correctement les images.

Tâche 9 : vérifier si vous utilisez « latest » (façon polie de dire « non déterministe »)

cr0x@server:~$ kubectl get deploy api -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
myorg/api:latest

Sens : On ne peut pas raisonner sur le caching lorsque les tags flottent. Chaque nœud pourrait légitimement avoir besoin d’un pull frais en même temps.
Décision : Arrêtez d’utiliser :latest en production. Utilisez un tag de version et/ou pinnez par digest.

Tâche 10 : confirmer que containerd est le runtime réel (et où configurer les mirrors)

cr0x@server:~$ kubectl get nodes -o jsonpath='{.items[0].status.nodeInfo.containerRuntimeVersion}{"\n"}'
containerd://1.7.13

Sens : Vous devez configurer les mirrors dans containerd, pas dans le daemon Docker (même si vous dites encore « docker pull » par habitude).
Décision : Configurez les mirrors de registre dans containerd et redémarrez soigneusement (drain du nœud, restart, uncordon).

Tâche 11 : inspecter la config registry de containerd pour des mirrors (commun sur les nœuds Kubernetes)

cr0x@server:~$ sudo grep -n "registry" -n /etc/containerd/config.toml | head -n 30
122:[plugins."io.containerd.grpc.v1.cri".registry]
123:  config_path = ""

Sens : Aucune configuration mirror par registre n’est actuellement utilisée (ou elle est externe via config_path).
Décision : Ajoutez un endpoint mirror pour Docker Hub (ou le registre concerné) via la configuration containerd appropriée.

Tâche 12 : valider rapidement DNS et TLS (similaires au rate limit)

cr0x@server:~$ getent hosts registry-1.docker.io
2600:1f18:2148:bc02:6d3a:9d22:6d91:9ef2 registry-1.docker.io
54.85.133.21 registry-1.docker.io

Sens : DNS résout. Si cela échoue de façon intermittente, les retries du kubelet peuvent imiter le throttling avec un impact opérationnel similaire.
Décision : Si le DNS est instable, corrigez le DNS d’abord. Sinon, passez à la gestion des quotas/du cache du registre.

Tâche 13 : vérifier la pression conntrack ou des ports éphémères pendant les pull storms (throttling auto-infligé)

cr0x@server:~$ sudo conntrack -S | head
entries  18756
searched 421903
found    13320
new      9321
invalid  12
ignore   0
delete   534
delete_list 24
insert   9321
insert_failed 0
drop     0
early_drop 0

Sens : Si insert_failed ou les drops montent pendant les storms, vous perdez des connexions localement, provoquant des retries et plus de charge.
Décision : Ajustez conntrack, réduisez la concurrence, ou corrigez le dimensionnement des nœuds. Ne blâmez pas le registre tant que votre propre maison n’est pas en ordre.

Tâche 14 : voir si vos runners CI mettent quoi que ce soit en cache

cr0x@server:~$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          3         1         1.2GB     1.1GB (90%)
Containers      1         0         12MB      12MB (100%)
Local Volumes   0         0         0B        0B
Build Cache     0         0         0B        0B

Sens : Votre runner est essentiellement amnésique. Le cache de build est à zéro ; les images sont majoritairement reclaimable. Recette pour des pulls répétés.
Décision : Utilisez des runners persistants, un cache partagé (BuildKit), ou des images pré-populées. Ou acceptez que vous ayez besoin d’un proxy cache local.

Tâche 15 : pinner par digest et tester un pull (réduit le churn de tags et les surprises)

cr0x@server:~$ docker pull nginx@sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c
sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c: Pulling from library/nginx
Digest: sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c
Status: Image is up to date for nginx@sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c

Sens : Les pins par digest rendent le caching et les rollouts prévisibles. Si l’image existe localement, le runtime peut éviter de télécharger des couches.
Décision : En production, préférez le pinning par digest (ou au moins des tags de versions immuables) et alignez la pull policy en conséquence.

Corrections efficaces (et pourquoi)

1) Authentifier les pulls (oui, même pour les images publiques)

Les pulls anonymes sont traités comme un service public. Les services publics sont mesurés. Si votre production dépend de pulls anonymes, votre production dépend de « la générosité d’autrui ».
Ce n’est pas une stratégie ; c’est un état d’esprit.

L’authentification peut augmenter les limites, améliorer l’attribution et faciliter la compréhension de qui tire quoi.
Sur Kubernetes, cela signifie souvent imagePullSecrets. En CI, cela signifie docker login avec un token et s’assurer que les jobs ne partagent pas une identité unique trop throttlée.

2) Arrêtez de tirer la même chose 500 fois : ajoutez un mirror cache

Un registre miroir/proxy cache est la solution adulte. Votre cluster devrait tirer depuis quelque chose que vous contrôlez (ou au moins plus proche),
qui récupère depuis le registre public une fois puis sert beaucoup d’instances.

Les options incluent :

  • Docker Registry en mode proxy cache : simple, fonctionne, mais vous devez l’exploiter (stockage, HA, sauvegardes).
  • Harbor proxy cache : plus lourd, mais bonnes options d’entreprise (projects, RBAC, réplication).
  • Registres d’artefacts des fournisseurs cloud avec pull-through caching ou modèles de réplication (varie selon le fournisseur ; vérifiez les limites).

Le mirror devrait être proche de vos nœuds (même région/VPC) pour réduire latence et bande passante. Il doit utiliser un stockage rapide et gérer les téléchargements concurrents de couches.
Et il doit avoir suffisamment de disque. Rien ne dit « opérations pro » comme un cache qui évince les couches chaudes toutes les heures.

3) Pinner par digest, et faire correspondre la pull policy à la réalité

Si vous pinnez par digest et conservez imagePullPolicy: IfNotPresent, vous obtenez le meilleur des deux mondes : contenu déterministe et moins de hits sur le registre.
Si vous gardez des tags flottants et Always, vous choisissez de frapper le registre. Cela peut aller pour un petit cluster de dev. C’est imprudent à grande échelle.

4) Pré-puller les images délibérément (réchauffer les caches)

Si un cluster va scaler, ou si vous savez qu’un rollout va toucher chaque nœud, pré-pullez l’image une fois par nœud avant de basculer le trafic.
Le pattern est old-school, ennuyeux et extrêmement efficace.

Approche Kubernetes : un DaemonSet qui tire l’image (et peut-être ne fait rien d’autre) pour que les nœuds la mettent en cache. Ensuite, déployez la charge réelle.
Cela répartit les pulls dans le temps et rend les échecs visibles avant le rollout.

5) Réduire la concurrence là où cela fait mal

Vous pouvez absolument moins vous throttler en étant moins enthousiaste. Réduisez les jobs parallèles en CI qui tirent la même image de base.
Étalez les vagues de déploiement. Évitez les redémarrages à l’échelle du cluster pendant les heures de pointe à moins d’aimer attirer l’attention.

6) Rendre les images plus petites et les couches réutilisables

Le rate limiting concerne le nombre de requêtes, mais la bande passante et le temps comptent aussi. Des images plus petites signifient pulls plus rapides, moins de connexions concurrentes, moins de retries et moins de temps passé dans la zone dangereuse.
Aussi : moins de couches peuvent réduire le nombre total de requêtes, mais ne poursuivez pas « une seule couche » au détriment de la cachabilité. Un bon découpage en couches reste une compétence.

7) Contrôler l’identité d’egress (séparer le NAT, ne pas tout concentrer)

Si toute votre flotte sort par une seule IP NAT, vous avez fait d’une IP la responsable du pire moment du monde : votre événement de montée en charge.
Envisagez plusieurs IPs d’egress, un egress public par nœud (avec précaution), ou une connectivité privée vers votre registre/mirror quand c’est possible.

Blague n°2 : Les NAT gateways sont comme les machines à café du bureau — tout le monde les partage jusqu’à ce que le lundi matin prouve que c’était une erreur.

Modes de défaillance spécifiques à Kubernetes (parce que kubelet ne tire jamais « un peu »)

Kubernetes transforme « tirer une image » en une activité distribuée et concurrente. C’est efficace quand le registre tolère et que les caches sont chauds.
C’est un désastre quand vous avez du churn de nœuds, des caches froids et un registre externe avec des quotas stricts.

ImagePullBackOff est un multiplicateur

Le backoff est censé réduire la charge, mais dans de grands clusters il devient un mécanisme de synchronisation : beaucoup de nœuds échouent, puis beaucoup réessaient à peu près au même moment,
surtout après des accrocs réseau ou après que le registre se soit rétabli. Le résultat est une meute tonitruante.

Le churn des nœuds crée des caches froids

Autoscaling, spot instances et recyclage agressif des nœuds passent très bien — jusqu’à ce que vous réalisiez que vous créez constamment des machines nouvelles avec des caches vides.
Si votre registre est throttlé, les caches froids ne sont pas une simple gêne ; ce sont des déclencheurs d’incident.

La politique de pull et la discipline des tags comptent plus que prévu

Kubernetes définit par défaut imagePullPolicy à Always quand vous utilisez :latest.
C’est Kubernetes qui vous dit poliment de ne pas utiliser :latest si vous tenez à la stabilité. Écoutez-le.

containerd vs Docker : configurez la bonne chose

Beaucoup d’équipes « corrigent Docker » sur des nœuds Kubernetes qui tournent avec containerd. La correction n’aboutit jamais parce qu’elle s’applique à un service qui n’est pas dans le chemin.
Identifiez d’abord le runtime, puis configurez correctement son support de mirror de registre.

CI/CD : pourquoi vos runners sont les pires pullers

Les systèmes CI sont optimisés pour le débit et la disposabilité. C’est excellent pour la sécurité et la reproductibilité.
C’est aussi excellent pour tirer la même image de base à répétition jusqu’à ce que le registre vous dise d’aller voir ailleurs.

Les runners éphémères jettent les deux plus grands avantages que vous avez

  • Cache de couches : les couches adressées par contenu n’aident que si vous les conservez.
  • Cache de build : BuildKit peut éviter de retélécharger et de rebuilder, mais pas si chaque job démarre depuis un disque vide.

Le parallélisme n’est pas gratuit

Les fournisseurs CI et les équipes adorent le parallélisme. Les registries, non. Si vous avez besoin de 50 jobs parallèles, donnez-leur un mirror partagé dans votre réseau et authentifiez-les.
Sinon, vous payez juste pour découvrir les limites de débit plus vite.

La discipline des tags réduit les pulls inutiles

Réutiliser le même tag pour différents contenus (« on a écrasé dev encore ») force les clients à revérifier et à retélécharger.
Utilisez des tags uniques par build et conservez un tag « humain » seulement comme pointeur.

Trois mini-récits d’entreprise du front

Incident : la mauvaise hypothèse (« les images publiques sont essentiellement gratuites »)

Une entreprise SaaS de taille moyenne exploitait un cluster Kubernetes autoscalé agressivement. L’équipe utilisait principalement des images de base publiques—des images courantes et bien connues—
et supposait qu’internet gérerait la charge. Ils avaient un registre interne, mais seulement pour leurs propres images d’application. Tout le reste venait directement du registre public.

Un matin de semaine chargé, les nœuds ont commencé à se recycler plus vite qu’à l’habitude suite à un déploiement kernel combiné au churn des spot instances.
De nouveaux nœuds sont arrivés avec des caches vides. Kubelet a fait son boulot : il a tout pullé. En même temps. Sur de nombreux nœuds.

En quelques minutes, les pods se sont empilés en ContainerCreating puis sont passés en ImagePullBackOff.
L’ingénieur on-call a vu « too many requests » et a supposé que c’était un pic transitoire. Ils ont redémarré quelques nœuds—créant encore plus de caches froids et plus de pulls.
Le graphique des pulls échoués ressemblait à un escalier vers le regret.

La cause racine n’était pas « Docker qui bugge ». C’était une mauvaise hypothèse : penser que la capacité et les quotas du registre public étaient alignés avec leur comportement de scale.
La correction fut simple mais pas rapide : authentifier, introduire un proxy cache pour le registre public, et pré-puller les images critiques lors du provisionnement des nœuds.
Après cela, le churn des nœuds restait pénible, mais il a cessé d’être un déclencheur d’incident.

Optimisation qui s’est retournée contre eux : « purgeons les caches pour économiser du disque »

Une autre entreprise s’enorgueillissait de nœuds légers. Ils exécutaient un job sur chaque nœud pour supprimer les images de façon agressive. Les graphiques de disque étaient immaculés.
Les slides de la revue infra mensuelle étaient magnifiques : « Nous avons réduit le stockage gaspillé de 40% ». Tout le monde acquiesçait.

Puis ils ont introduit une stratégie canary qui roulait fréquemment les pods à travers la flotte. Chaque rollout provoquait une vague de pulls.
Mais parce que le job de prune avait supprimé la plupart des couches, chaque nœud se comportait comme neuf.

Le registre les a throttlés de façon intermittente. Quand le throttling est arrivé, les pods ont échoué les readiness, le canary a tenu, l’automatisation a réessayé, et tout le pipeline a prolongé sa propre souffrance.
La partie vraiment amusante : l’incident était intermittent, et par conséquent parfaitement calibré pour gaspiller du temps humain.

L’« optimisation » a économisé du disque et dépensé de la fiabilité. La correction fut d’arrêter de considérer le cache d’images comme des déchets.
Ils ont défini des seuils disques sensés, gardé une base d’images chaudes épinglées sur les nœuds, et déplacé le nettoyage agressif vers des fenêtres hors pics.
Le stockage est moins cher que l’indisponibilité, et aussi moins cher que les personnes.

Pratique ennuyeuse mais correcte qui a sauvé la mise : « pré-pull et pin »

Une équipe de services financiers exécutait des charges régulées avec un contrôle strict des changements. Leur processus de release était lent, ce qui irritait les développeurs,
mais il avait une habitude qui payait : chaque release candidate était pinée par digest et pré-pullée à travers le cluster avant la bascule du trafic.

Un soir, un registre public a commencé à throttler dans leur région à cause d’un événement en amont. Les autres équipes ont paniqué et rollbacké.
Cette équipe a à peine remarqué. Leurs nœuds avaient déjà les couches requises localement, et le déploiement utilisait IfNotPresent.

Ils ont tout de même vu des erreurs sur des pulls en arrière-plan pour des images non liées, mais le rollout de production n’a pas été affecté.
Le rapport on-call était presque embarrassant : « Aucun impact client. Observé throttling externe. Poursuite comme prévu. »
Voilà le rêve : le monde extérieur peut être en feu, et votre système hausse les épaules.

La leçon n’est pas « soyez lents comme la finance ». La leçon est : les pratiques ennuyeuses — pinning, pré-pull, rollouts contrôlés — créent du slack.
Le slack est ce qui empêche des dépendances externes de devenir des pannes.

Erreurs courantes : symptôme → cause racine → correctif

1) Symptom : « toomanyrequests » seulement pendant les déploiements

Cause racine : pull storms causés par des rollouts simultanés, scale-ups ou remplacements de nœuds.

Correctif : étaler les rollouts, pré-puller via DaemonSet, ajouter un mirror cache, et arrêter d’utiliser des tags flottants.

2) Symptom : fonctionne sur les laptops, échoue en CI

Cause racine : les machines développeurs ont des caches chauds ; les runners CI sont éphémères et démarrent froids à chaque fois.

Correctif : runners persistants ou cache partagé, authentifier les pulls, introduire un proxy cache dans votre réseau.

3) Symptom : erreurs 403/401 aléatoires sous charge

Cause racine : throttling du service de tokens ou erreurs d’auth mal interprétées causées par le rate limiting.

Correctif : authentifier correctement, éviter le partage de tokens sur une massive concurrence, et inspecter headers/logs pour confirmer 429 vs auth.

4) Symptom : seulement un cluster/région est affecté

Cause racine : une IP d’egress spécifique est chaude, ou un PoP CDN régional applique une politique plus stricte.

Correctif : séparer l’egress/NAT, déployer un cache/mirror régional, ou répliquer les images critiques dans un registre plus proche.

5) Symptom : « nous avons un mirror » mais les pulls touchent encore Docker Hub

Cause racine : mirror configuré pour le daemon Docker mais les nœuds utilisent containerd ; ou le hostname du mirror n’est pas approuvé ; ou seules certaines machines ont été mises à jour.

Correctif : confirmer le runtime, configurer le mirror au bon niveau, le déployer en drainant les nœuds, et tester avec un pull contrôlé.

6) Symptom : le throttling a empiré après des changements de « cleanup »

Cause racine : le pruning agressif a supprimé les couches chaudes ; chaque rollout est devenu un cold-start.

Correctif : conserver des images de base, ajuster les seuils de garbage collection, et aligner le nettoyage avec le risque réel (pression disque), pas l’esthétique.

7) Symptom : échecs de pull ressemblant à du throttling mais pas de 429

Cause racine : flaps DNS, MTU/interception TLS, resets de proxy, épuisement conntrack, ou saturation réseau locale.

Correctif : vérifier DNS/TLS, surveiller conntrack/ports, consulter les logs du proxy, et réduire les pulls concurrents pendant que vous corrigez les fondamentaux réseau.

Listes de contrôle / plan étape par étape

Phase 0 : stabiliser la production (aujourd’hui)

  1. Arrêter la meute tonitruante : stopper/ralentir les rollouts ; réduire temporairement les replicas si c’est sûr.
  2. Authentifier les pulls pour les systèmes affectés immédiatement (CI et nœuds du cluster quand possible).
  3. Pinner l’artifact de release (tag ou digest) pour que les retries ne pullent pas une cible mouvante.
  4. Réduire la concurrence : parallélisme des jobs CI, maxUnavailable/maxSurge des rollouts, agressivité de l’autoscaler.
  5. Choisir un nœud test et vérifier un chemin de pull propre avant de retenter le rollout.

Phase 1 : arrêter de dépendre du comportement des registries publiques (cette semaine)

  1. Déployer un registre miroir cache proche du cluster/runners.
  2. Configurer containerd/Docker pour utiliser le mirror (et confirmer qu’il est réellement utilisé).
  3. Pré-puller les images critiques sur les nœuds (DaemonSet warm-up ou bootstrap nœud).
  4. Corriger tags/pull policy : arrêter d’utiliser :latest, utiliser IfNotPresent avec tags/digests immuables.
  5. Rendre les échecs observables : alerter sur les taux d’ImagePullBackOff et les 429 du registre dans les logs.

Phase 2 : rendre ça ennuyeux et résilient (ce trimestre)

  1. Répliquer les dépendances : mirror/répliquer les images tierces sur votre propre registre.
  2. Adopter une stratégie d’images de base : moins d’images de base, standardisées, patchées régulièrement, stockées en interne.
  3. Planifier la capacité du cache : disque, IOPS, concurrence et exigences HA ; traitez-le comme de la production.
  4. Gouverner la concurrence : politiques de rollout du cluster, contrôles de concurrence CI, et garde-fous d’auto-scale.
  5. Organiser des game days : simuler des pannes/throttles de registre ; vérifier que votre système dégrade gracieusement.

FAQ

1) Est-ce seulement un problème Docker Hub ?

Non. Tout registre peut throttler : registries publiques, registries cloud, et votre propre registre derrière un load balancer.
Docker Hub est juste l’endroit le plus célèbre pour recevoir un 429 et une leçon de vie.

2) Pourquoi le rate limiting nous frappe « au hasard » ?

Parce que votre trafic est en rafales. Les déploiements, l’autoscaling et le fan-out CI créent des pics.
Les quotas sont souvent appliqués par fenêtre, par IP ou par token ; une fois la limite franchie, tous ceux partageant cette identité en souffrent.

3) Si nous nous authentifions, est-ce que c’est fini ?

L’authentification aide, mais n’élimine pas le problème architectural. Vous pouvez toujours dépasser des quotas authentifiés, et vous pouvez toujours saturer votre NAT/proxy.
Considérez l’auth comme le ticket d’entrée, pas comme le plan complet.

4) Quelle est la meilleure solution long terme ?

Un mirror/proxy cache proche de vos compute. Il convertit une « dépendance internet » en « dépendance locale », et les dépendances locales sont au moins votre problème à résoudre.

5) Devons-nous pinner par digest partout ?

Pour les déploiements production, oui quand c’est possible. Les digests rendent les rollouts déterministes et s’alignent bien avec IfNotPresent.
Pour les workflows dev, des tags de version immuables peuvent suffire, mais « latest » reste un piège.

6) Nous utilisons déjà IfNotPresent. Pourquoi tirons-nous encore ?

Parce que l’image n’est pas présente sur ce nœud (nœud neuf, cache prune, architecture différente), ou parce que le tag pointe vers un nouveau contenu et que le runtime vérifie quand même.
Vérifiez la présence locale du cache et arrêtez de réutiliser des tags pour différents builds.

7) Peut-on juste augmenter le backoff Kubernetes et être tranquilles ?

Le backoff réduit la pression immédiate mais ne résout pas la demande sous-jacente. De plus, un backoff synchronisé sur de nombreux nœuds peut créer des vagues de retries.
Utilisez le tuning du backoff seulement comme stabilisateur pendant que vous implémentez des caches et des politiques correctes.

8) Qu’en est-il des environnements air-gapped ou régulés ?

Vous finirez par exécuter votre propre registre et curer les images en interne. L’avantage : pas de throttling externe.
L’inconvénient : vous devez gérer le patching, le scanning et la disponibilité. C’est néanmoins la bonne approche pour de nombreux environnements régulés.

9) Comment savoir si notre mirror est réellement utilisé ?

Vérifiez la config du runtime sur le nœud, puis observez les logs/métriques du mirror pendant un pull. Comparez aussi les résolutions DNS et les connexions sortantes :
si les nœuds parlent encore directement au registre public, votre mirror est ornemental.

10) Le goulot peut-il venir de notre stockage ?

Oui — surtout pour les caches auto-hébergés. Si votre proxy cache repose sur un disque lent, il va sérialiser les lectures de couches et rendre les pulls lents,
provoquant davantage de tentatives concurrentes et plus de retries. Un stockage rapide et la capacité de concurrence sont essentiels.

Conclusion : prochaines étapes pratiques

Le throttling des registries n’est pas un accident. C’est le résultat attendu d’une infrastructure élastique moderne frappant un service externe partagé.
Votre travail n’est pas d’espérer que cela n’arrive plus. Votre travail est de le rendre sans importance.

  1. Aujourd’hui : confirmez 429 vs problèmes réseau, arrêtez la pull storm, authentifiez, pinnez l’artifact de release.
  2. Cette semaine : déployez un mirror cache près du compute, configurez le runtime réel (containerd/Docker), et pré-pullez les images critiques.
  3. Ce trimestre : internalisez les dépendances tierces, standardisez les images de base, et gouvernez la concurrence pour que « autoscaling » ne signifie pas « auto-panne ».

Rendez la livraison d’images ennuyeuse. L’ennui, c’est ce que vous voulez à 3h du matin.

← Précédent
ZFS sur root : installer pour que les retours en arrière fonctionnent vraiment
Suivant →
PostgreSQL vs OpenSearch : l’architecture de recherche hybride qui fonctionne vraiment

Laisser un commentaire