Aller au contenu principal
pipelinebrut.fr — CI/CD · automatisation · terrain
CI/CD

Files d’attente CI : accélérer sans multiplier les runners

Comment mesurer, limiter et absorber les files d’attente CI avec des règles de concurrence, des priorités et une capacité runners ajustée au flux.

Par Julien Mercier 8 min de lecture
Files d’attente CI : accélérer sans multiplier les runners

Une CI lente n’est pas forcément une CI dont les jobs sont lents. Dans beaucoup d’équipes, le temps perdu se situe avant même la première commande exécutée : un pipeline attend un runner disponible, une ressource d’environnement reste verrouillée, ou plusieurs validations identiques consomment la capacité disponible.

Le réflexe le plus courant consiste à ajouter des runners. Cela peut être justifié, mais ce n’est pas une stratégie de pilotage. Des runners supplémentaires peuvent absorber un pic ponctuel ; ils ne corrigent ni les pipelines déclenchés inutilement, ni les jobs concurrents sur une même ressource, ni les builds devenus obsolètes après un nouveau commit. Ils augmentent aussi directement la facture des plateformes de CI hébergées, ou le coût d’exploitation si l’infrastructure est auto-hébergée.

Traiter les files d’attente CI comme un problème de flux change la discussion. Il faut d’abord voir où se forme l’attente, comprendre quels jobs occupent réellement la capacité, éliminer le travail sans valeur, puis ajuster le nombre et le type de runners. L’objectif n’est pas d’atteindre zéro seconde d’attente en permanence : c’est de rendre les délais prévisibles pour les changements qui comptent.

Pourquoi la file d’attente CI est devenue un problème de fiabilité

Une file d’attente CI rallonge le délai entre un changement de code et son retour de validation. Pour un développeur, un pipeline qui attend avant de démarrer ressemble à une indisponibilité partielle du système de livraison : le feedback est retardé, les corrections arrivent plus tard et les branches divergent davantage.

Le problème se renforce avec plusieurs tendances désormais courantes :

  • les dépôts exécutent davantage de contrôles : tests unitaires, intégration, linting, analyse statique, scans de dépendances, génération de SBOM ou signature d’artefacts ;
  • les pipelines sont déclenchés sur les branches, les merge requests, les tags et parfois les forks ;
  • des services partagés limitent le parallélisme : base de données de test, cluster Kubernetes, environnement de recette ou licence d’un outil propriétaire ;
  • les runners hébergés facturent généralement du temps d’exécution, tandis que les runners auto-hébergés nécessitent des machines, une image maintenue, du réseau et de la supervision ;
  • l’usage de machines plus puissantes réduit parfois la durée d’un job, mais augmente aussi le coût unitaire de la minute exécutée.

La file n’est donc pas qu’un indicateur de confort. Elle peut dégrader la qualité des livraisons. Quand une validation prend trop longtemps à démarrer, les équipes contournent parfois les protections : elles fusionnent avec moins de vérifications, relancent des jobs sans diagnostic ou déploient un correctif manuel. Le résultat est un pipeline moins fiable précisément au moment où il devrait protéger le flux.

Un autre effet est moins visible : les retours de CI arrivent dans le désordre. Un commit récent peut être validé avant un commit plus ancien qui attendait déjà. Ce n’est pas nécessairement une erreur technique, mais c’est une source de confusion si les équipes ne savent pas quel pipeline représente réellement l’état à déployer.

Avant de modifier la capacité, il est utile de relier cette question à l’architecture globale du pipeline. Un pipeline découpé en étapes claires, avec des dépendances explicites, est plus simple à observer et à optimiser. Sur ce sujet, notre article sur la structuration d’un pipeline CI/CD fiable sans usine à gaz fournit une base utile.

Faire la différence entre attente, exécution et blocage

Le mot « attente » recouvre plusieurs réalités. Les confondre conduit souvent à acheter de la capacité là où elle n’a aucun effet.

Le temps d’attente avant attribution d’un runner

C’est le cas le plus évident : le job est prêt, mais aucun runner correspondant à ses contraintes n’est disponible. Ces contraintes peuvent être un tag GitLab CI, un label GitHub Actions, une architecture matérielle, un système d’exploitation, un accès réseau particulier ou une image spécialisée.

Un parc de runners peut sembler largement disponible dans son ensemble tout en étant saturé sur une seule classe de jobs. Par exemple, dix runners Linux généralistes ne résolvent pas une attente sur un runner macOS, Windows ou un runner isolé dans un réseau privé.

L’attente entre deux étapes du pipeline

Un job peut aussi attendre parce qu’il dépend d’un job amont. Dans GitLab CI, les relations peuvent être exprimées avec needs. Dans GitHub Actions, les jobs peuvent dépendre d’autres jobs avec needs. Ce temps est normal s’il reflète un ordre nécessaire, mais il devient coûteux si la dépendance est artificielle.

Un exemple courant : faire dépendre tous les tests d’une étape de construction monolithique, alors que certains contrôles de qualité pourraient démarrer immédiatement après le checkout. Réduire ce type de dépendance ne demande aucun runner supplémentaire : cela augmente simplement le parallélisme utile.

L’attente causée par un verrou de ressource

Certains jobs ne doivent pas s’exécuter simultanément, même si des runners sont disponibles. Une migration de base de données, un déploiement vers un environnement partagé ou une publication de paquet sont de bons exemples. Ici, la file est volontaire : elle protège une ressource critique.

GitLab CI propose notamment resource_group pour sérialiser des jobs qui ciblent une même ressource. GitHub Actions propose le mécanisme concurrency, qui permet de regrouper des workflows ou des jobs dans un groupe de concurrence. Dans ces cas, ajouter des runners ne raccourcit pas l’attente. Il faut plutôt décider si le verrou est justifié, s’il est trop large, ou si les environnements peuvent être mieux isolés.

Mesurer les bons signaux avant d’agir

La plupart des plateformes affichent l’état des jobs et leur durée. C’est un point de départ, pas une analyse de capacité. Pour piloter une file d’attente, il faut séparer les mesures de demande, de capacité et de résultat.

Les indicateurs à suivre

  • Temps d’attente d’un job : durée entre le moment où le job est prêt et celui où son exécution démarre. Il faut examiner la médiane mais aussi les valeurs élevées, car ce sont elles qui rendent le flux imprévisible.
  • Durée d’exécution : durée réellement consommée par le job. Une hausse peut signaler un cache inefficace, une dépendance externe lente ou une régression dans les tests.
  • Profondeur de la file : nombre de jobs en attente par type de runner, par dépôt ou par équipe. Une vue globale masque souvent le goulot réel.
  • Taux d’occupation des runners : proportion du temps durant lequel les runners exécutent effectivement des jobs. Une saturation persistante est un indice ; une pointe courte ne justifie pas forcément une extension durable.
  • Jobs annulés ou rendus obsolètes : ils indiquent la part de travail qui n’aurait probablement pas dû consommer de capacité.
  • Échecs suivis de relances : ils permettent de distinguer les échecs liés au code des erreurs intermittentes, qui encombrent la CI et faussent la demande réelle.

GitHub Actions expose dans ses interfaces l’historique des exécutions et les informations de durée des jobs. GitLab fournit également des vues sur les pipelines et les jobs, ainsi que des métriques pour les runners selon le mode de déploiement. Pour une vision transverse, les événements de la plateforme peuvent être envoyés vers des outils comme Prometheus, Grafana, Datadog ou Elastic, selon l’environnement déjà en place.

Le découpage est essentiel. Mesurer un « temps d’attente moyen de la CI » n’est presque jamais suffisant. Il faut au minimum pouvoir répondre à ces questions :

  • Quels labels, tags ou pools de runners portent l’attente ?
  • Quels dépôts génèrent le plus de jobs ?
  • Quels workflows sont concernés : validation de merge request, build de branche, release, déploiement ?
  • À quels moments la saturation se produit-elle ?
  • Combien de jobs en attente correspondent déjà à un commit dépassé ?

Un tableau de bord opérationnel peut rester très simple : temps d’attente par pool, durée d’exécution par type de job, nombre de jobs en cours et nombre de jobs annulés. L’important est de regarder l’évolution sur plusieurs semaines, notamment lors des heures de forte activité, plutôt qu’un instantané pris après un incident.

Identifier les jobs qui bloquent réellement le flux

La durée d’un job n’est pas son seul coût. Un job de vingt minutes lancé sur chaque commit d’une branche active peut monopoliser une capacité importante, même si chaque exécution est techniquement saine. À l’inverse, un job court mais exécuté des milliers de fois peut devenir le principal consommateur de minutes.

Pour trouver les candidats à l’optimisation, croisez trois éléments : fréquence de déclenchement, durée d’exécution et valeur du résultat produit. Cette analyse fait généralement ressortir quelques catégories.

Les validations dupliquées

Une même révision peut déclencher un pipeline sur la branche et un autre sur la merge request. Cela peut être nécessaire selon les règles de protection du dépôt, mais ce doublon doit être assumé. Vérifiez surtout que les deux pipelines ne lancent pas exactement les mêmes suites coûteuses sans apporter de signal distinct.

Les stratégies de déclenchement conditionnel aident à limiter ce bruit. GitHub Actions utilise par exemple des filtres sur les branches et les chemins, tandis que GitLab CI propose des règles avec rules. Une modification de documentation n’a pas toujours besoin de lancer une suite d’intégration complète, à condition que cette exception soit explicite, revue et compatible avec le niveau de risque.

Les tests instables

Un test instable coûte plus qu’une exécution en échec : il entraîne des relances, rallonge les délais et fait perdre confiance dans le signal de la CI. Isoler les tests intermittents, conserver leurs logs, suivre leurs échecs et leur attribuer un responsable sont des actions de capacité autant que de qualité.

Il ne faut pas masquer le problème en relançant automatiquement sans limite. Une relance contrôlée peut être utile pour diagnostiquer une erreur réseau ponctuelle, mais une politique de retry large peut saturer les runners et dissimuler une régression réelle.

Les téléchargements répétitifs

L’installation répétée de dépendances, la reconstruction d’images et les téléchargements d’outils peuvent représenter une part considérable du temps de job. Un cache correctement conçu réduit la durée d’exécution et libère donc la capacité plus vite. Mais un cache non déterministe ou partagé sans clé pertinente crée des erreurs difficiles à expliquer.

Il faut privilégier des clés liées aux fichiers de verrouillage, comme package-lock.json, yarn.lock, poetry.lock ou les fichiers de dépendances propres à l’écosystème. Pour aller plus loin sans introduire de comportement douteux, consultez notre guide sur le cache CI pour accélérer les builds sans code douteux.

Réduire la pression avec les règles de concurrence

La concurrence n’est pas seulement une limite technique. C’est une règle métier appliquée au flux de livraison : quels travaux peuvent avancer ensemble, et lesquels doivent attendre ?

Les jobs de déploiement sur un environnement partagé sont le cas le plus clair. Deux déploiements concurrents peuvent rendre un état intermédiaire difficile à diagnostiquer. Les sérialiser est raisonnable. En revanche, appliquer un verrou global à tous les pipelines d’un dépôt revient souvent à transformer une CI parallélisable en file unique.

La bonne granularité dépend de la ressource protégée. Quelques exemples :

  • un groupe de concurrence par environnement, tel que production ou staging ;
  • un verrou par service lorsqu’une application possède son propre environnement ;
  • un verrou dédié à la publication d’un paquet ou d’une image de conteneur ;
  • un groupe séparé pour les migrations de base de données si elles ne peuvent pas être parallélisées.

Dans GitHub Actions, les groupes de concurrence peuvent être configurés au niveau du workflow ou du job. La plateforme permet aussi d’annuler une exécution en cours dans un même groupe via cancel-in-progress. Cette option est particulièrement adaptée aux validations de branches : si un nouveau commit arrive, continuer à tester l’ancien peut ne plus avoir de valeur.

GitLab CI propose également des mécanismes d’annulation automatique de pipelines redondants dans certains contextes, ainsi que l’attribut interruptible pour les jobs pouvant être interrompus lorsqu’un pipeline plus récent les remplace. Ces options doivent être utilisées avec discernement : il serait dangereux d’interrompre une publication ou une migration sans mécanisme de reprise clair.

Annuler les builds obsolètes sans annuler les mauvais jobs

L’annulation des pipelines dépassés est l’un des moyens les plus efficaces de réduire une file, car elle diminue la demande au lieu d’augmenter l’offre. Mais elle demande une classification simple des jobs.

Les validations de code sur une branche sont souvent remplaçables. Si le commit B contient le commit A plus des modifications supplémentaires, l’exécution de A n’est généralement plus prioritaire pour décider de la fusion. À l’inverse, certains jobs sont non remplaçables :

  • un déploiement déclenché volontairement vers un environnement précis ;
  • une publication de release ou de paquet ;
  • une migration de données ;
  • un job de conformité ou d’archivage imposé par une procédure interne ;
  • un pipeline associé à un tag de version.

Une politique saine consiste à annuler d’abord les validations de commits plus anciens d’une même branche, puis à observer l’effet sur le temps d’attente. Il faut aussi rendre l’annulation visible : un développeur doit comprendre qu’un job a été stoppé parce qu’un pipeline plus récent l’a remplacé, et non parce que la CI a échoué.

Attention aux jobs qui produisent ou modifient un état externe. Pour eux, l’idempotence reste une protection indispensable. Un job interrompu ou relancé ne doit pas laisser une ressource dans un état ambigu. Ce point est développé dans notre article sur les jobs idempotents pour réduire les erreurs humaines.

Prioriser les flux qui ont un impact immédiat

Toutes les files ne doivent pas être traitées de la même manière. Un pipeline de merge request bloquant une livraison mérite habituellement un accès plus rapide à la capacité qu’un scan planifié ou qu’un rapport analytique nocturne.

La priorisation peut prendre plusieurs formes :

  • séparer les pools de runners entre validations interactives et tâches de fond ;
  • réserver des labels à des workflows critiques, sans multiplier les catégories inutilement ;
  • planifier les tâches lourdes, comme certains scans complets, en dehors des heures où les équipes poussent le plus de changements ;
  • réduire la fréquence des contrôles qui n’ont pas besoin d’être exécutés à chaque commit ;
  • définir des règles d’exécution par chemin modifié lorsque l’architecture du dépôt permet réellement de cibler les tests concernés.

La séparation des pools a un coût : plus les labels sont spécifiques, plus il est facile de créer une capacité fragmentée. Un runner spécialisé qui reste inactif alors que des jobs généralistes attendent est une forme de gaspillage. Commencez avec peu de classes compréhensibles, par exemple « standard », « accès réseau privé » et « système spécifique », puis n’ajoutez une segmentation qu’à partir d’une contrainte observée.

Il est également utile de différencier les contrôles rapides des contrôles exhaustifs. Un lint, une compilation ou une suite de tests ciblés peuvent fournir un premier retour rapide, tandis qu’une suite d’intégration longue peut continuer en parallèle ou être exigée avant fusion selon le niveau de risque. Cette organisation ne doit pas supprimer les garanties nécessaires ; elle vise à avancer le premier signal fiable.

Dimensionner les runners à partir du flux réel

Après avoir réduit les déclenchements inutiles et clarifié les verrous, il reste souvent un besoin légitime de capacité. C’est le moment d’ajouter ou de redimensionner des runners, avec des données plutôt qu’une estimation intuitive.

Le raisonnement de base est simple : la capacité nécessaire dépend du volume de jobs à traiter, de leur durée et du délai acceptable. Si l’arrivée de jobs dépasse durablement la capacité de traitement, la file croît. Si les runners sont largement inoccupés hors de quelques pics très courts, une capacité permanente supplémentaire est peut-être une réponse coûteuse à un problème de planification ou de concurrence.

La loi de Little, utilisée en théorie des files d’attente, relie le nombre moyen d’éléments présents dans un système, le débit et le temps passé dans ce système. Sans transformer l’exploitation CI en exercice académique, elle rappelle un point pratique : réduire la durée des jobs et réduire les jobs inutiles diminue aussi la congestion.

Choisir entre capacité fixe et runners éphémères

Un pool fixe est plus simple à prévoir. Il convient à une charge régulière et à des besoins spécifiques, par exemple un accès à un réseau interne. En revanche, il faut le mettre à jour, le surveiller, gérer ses images et éviter qu’il devienne un point de dérive de sécurité.

Les runners éphémères, créés pour un job ou pour une période courte, permettent d’absorber plus facilement les pointes et de repartir d’un environnement propre. GitHub Actions peut être utilisé avec des runners auto-hébergés éphémères ; GitLab Runner peut aussi être exécuté avec différents exécuteurs et intégré à des mécanismes d’autoscaling selon l’infrastructure choisie. Kubernetes est fréquemment utilisé pour ce type de modèle, mais il ne le rend pas automatiquement simple : les images, le stockage de cache, les quotas et le temps de démarrage restent à maîtriser.

Les runners éphémères sont traités plus en détail dans notre analyse des runners éphémères CI/CD hors grands groupes. Leur intérêt ne se limite pas à la montée en charge : ils réduisent aussi la persistance d’état entre jobs.

Éviter la capacité inutile

Avant d’augmenter un pool, vérifiez ces points :

  • la saturation concerne-t-elle tous les runners ou seulement un tag spécifique ?
  • les jobs les plus longs utilisent-ils réellement les ressources de la machine, ou attendent-ils un service externe ?
  • des jobs obsolètes consomment-ils encore des slots ?
  • une optimisation de cache ou de parallélisation peut-elle réduire la durée totale ?
  • la pointe est-elle récurrente et prévisible, ou liée à un incident ponctuel ?

Une augmentation de capacité doit être suivie d’une vérification. Si le temps d’attente baisse mais que le coût grimpe fortement sans amélioration notable du délai de livraison, le pool est probablement surdimensionné ou mal segmenté. Si l’attente ne baisse pas, le problème est sans doute un verrou de ressource, un mauvais label ou une dépendance de pipeline, pas le nombre total de runners.

Mettre en place une boucle d’amélioration opérationnelle

Les files d’attente changent avec les équipes, les dépôts et les pratiques de livraison. Une politique figée de runners ne reste pas adaptée longtemps. Le plus efficace est d’instaurer une revue légère et régulière du flux CI.

À chaque revue, examinez les pools les plus attendus, les workflows les plus coûteux, la part de jobs annulés, les échecs intermittents et l’évolution de la durée des étapes principales. Associez les personnes qui maintiennent la plateforme CI et celles qui possèdent les pipelines applicatifs : les premières voient la capacité, les secondes connaissent la valeur réelle des contrôles.

Documentez aussi les décisions importantes. Si un déploiement est sérialisé, indiquez quelle ressource est protégée. Si un test ne s’exécute que sur certains chemins, expliquez son périmètre. Si un workflow est annulable, précisez pourquoi il ne modifie pas d’état externe. Ces règles évitent que les optimisations de flux deviennent, quelques mois plus tard, des comportements incompréhensibles.

Enfin, reliez les métriques de file d’attente à l’observation du déploiement complet. Un pipeline qui démarre vite mais reste bloqué après la CI ne résout pas le délai de livraison. Une supervision de bout en bout permet de voir les attentes entre validation, déploiement et retour d’état ; le sujet est abordé dans notre guide pour superviser le flux de déploiement de bout en bout.

Conclusion : moins de travail inutile, puis la bonne capacité

Une file d’attente CI ne se résume pas à un compteur de runners. Elle révèle une relation entre la demande de validation, la durée des jobs, les verrous nécessaires et la capacité réellement disponible. Ajouter des machines est parfois la bonne décision, mais seulement après avoir éliminé les pipelines redondants, annulé les validations obsolètes, réduit les dépendances inutiles et identifié les pools réellement saturés.

Commencez par mesurer le temps d’attente par type de runner et par workflow, puis choisissez une amélioration ciblée : une règle de concurrence mieux définie, l’annulation d’un build dépassé, un cache fiable ou un ajustement limité de capacité. Ce sont ces itérations concrètes qui rendent la CI plus rapide, plus prévisible et plus facile à financer.