Comment nous avons réduit de 94 % la taille Git d’un monorepo JavaScript
(jonathancreamer.com)- Le grand monorepo JavaScript 1JS de Microsoft a grossi jusqu’à atteindre 1 000 utilisateurs actifs mensuels, environ 2 500 packages et près de 20 millions de lignes de code, et sa taille de clonage a récemment atteint 178 Go, au point que certains développeurs avaient du mal à simplement le cloner
- Le premier goulot d’étranglement identifié venait de la structure des fichiers de changement Beachball, qui s’accumulaient par dizaines de milliers dans un seul dossier ; une fois le nombre de fichiers monté à 40 000, chaque ajout de nouveau fichier créait un gros tree object
- La cause plus importante venait du fait que, dans des cas où des noms de fichiers longs comme
CHANGELOG.mdetCHANGELOG.jsonétaient mis à jour de façon répétée, l’ancien code de packing de Git choisissait ses cibles de delta en ne regardant que les 16 derniers caractères du nom de fichier git repack -adf --window=250a eu un effet positif, mais en exécutant le packing basé sur les chemins avecgit repack -adf --path-walkdans le fork Microsoft Git2.47.0.vfs.0.2, le dépôt local est passé de 178 Go à 5 Go- Dans un grand monorepo, si des fichiers aux noms longs changent souvent, il faut vérifier
pack.usePathWalk trueainsi que la nouvelle commandegit survey, et les changements concernés devraient aussi être intégrés dans Git upstream
Taille du monorepo 1JS et goulots d’étranglement
- Le grand monorepo JavaScript interne 1JS de Microsoft est imposant non seulement par la taille du dépôt, mais aussi par le volume de code et de contributions
- plus de 1 000 utilisateurs actifs mensuels
- environ 2 500 packages
- environ 20 millions de lignes de code
- La taille du dépôt après un clonage récent était de 178 Go, et certains développeurs en Europe ne pouvaient pas cloner le dépôt à cause de cette taille
- Lors du premier clonage, il ne faisait que 1 à 2 Go, puis il est monté à environ 4 Go quelques mois plus tard, avant de continuer à croître
Leçon n°1 : ne pas entasser des milliers de fichiers dans un seul dossier
git-sizera mis en évidence le problème des gros blobs et des fichiers de changement Beachball- Les gros blobs apparaissent lorsqu’un binaire est commité par erreur, et peuvent être évités par des fonctions comme la limite de taille des check-ins d’Azure DevOps
- les fichiers déjà présents dans l’historique ne sont pas faciles à supprimer après coup
- 1JS utilisait les Beachball change files pour automatiser l’augmentation des plages semver des packages
- l’objectif d’usage est similaire à celui de Changesets
- le but est d’automatiser l’augmentation des versions des packages, comme avec semantic-release
- Comme les fichiers de changement n’étaient pas supprimés, ils se sont accumulés jusqu’à 40 000 dans un seul dossier, et dans cet état l’ajout d’un nouveau fichier générait un gros tree object
- Deux mesures ont été prises
- une PR Beachball a modifié le fonctionnement pour regrouper plusieurs changements dans un seul fichier de changement au lieu d’un fichier par package
- un pipeline a été écrit pour nettoyer automatiquement et périodiquement le dossier des fichiers de changement
Leçon n°2 : branche versioned et problème de packing des changelogs
- Le flux de gestion des versions de 1JS maintient une branche
versionedmiroir demain- elle stocke les versions réelles des packages afin d’éviter les conflits Git dans
main - elle permet de voir précisément la correspondance entre les versions semver publiées en packages NPM et les commits Git
- elle stocke les versions réelles des packages afin d’éviter les conflits Git dans
- La branche
versionedne contenait principalement que des commits ajoutant du contenu dans des fichiersCHANGELOG.mdetCHANGELOG.json, mais elle est devenue suffisamment volumineuse pour rendre le clonage de plus en plus difficile - Depuis 2021, le dépôt a naturellement grossi avec l’ajout annuel de centaines de milliers de lignes de code et de centaines de développeurs, mais comme sa croissance dépassait celle du monorepo Office de Microsoft, une cause spécifique a été suspectée
- Après examen par un développeur ayant conçu des fonctions Git pour les grands monorepos, comme
shallow checkoutetsparse index, il est apparu que récupérer la brancheversionedentraînait un fetch de 125 Go de données Git supplémentaires
La manière dont le code de packing de Git créait de mauvais deltas
- La cause était que l’ancien code de packing de Git, au moment de comparer les fichiers avant compression, ne regardait que les 16 derniers caractères du nom de fichier
- En général, Git ne push que le diff des fichiers modifiés, mais à cause de ce problème de packing, il comparait entre eux des fichiers
CHANGELOG.mdappartenant à des packages différents - Par exemple, si
repo/packages/foo/CHANGELOG.jsonétait modifié, Git pouvait générer au moment de préparer le push un diff avecrepo/packages/bar/CHANGELOG.json - Cela conduisait dans de nombreux cas à repousser de manière répétée le fichier complet, et certains de ces fichiers pouvaient faire plusieurs dizaines de Mo
- Les détails du problème sont résumés dans ce message de la mailing list Git de Stolee
Résultats de repack et du path-walk
- D’abord,
git repack -adf --window=250a été utilisé pour améliorer la compression du fichier pack avec une fenêtre plus large, ce qui a déjà réduit de façon notable la taille du dépôt - Ensuite, la PR git-for-windows #5171 a ajouté une méthode de packing parcourant les chemins Git au lieu de la méthode par défaut basée sur le parcours des commits
- Avec le fork Microsoft de Git, Microsoft git
git version 2.47.0.vfs.0.2, un nouveau clonage a été suivi d’un repack avec cette méthode- commande utilisée :
git repack -adf --path-walk - résultat : la taille du dépôt est passée de 178 Go à 5 Go
- commande utilisée :
- La nouvelle option de configuration permet aussi de générer les bons deltas au moment du push
git config --global pack.usePathWalk true
- Les développeurs utilisant
git version 2.47.0.vfs.0.2peuvent exécuter un repack après le clonage local, puis réduire le rythme de croissance grâce au nouvel algorithme de push path-walk
Impact sur GitHub, Azure DevOps et Git upstream
- GitHub effectue périodiquement des repack et du garbage collection Git, mais sa méthode de packing actuelle ne calcule pas correctement les deltas pour des fichiers comme
CHANGELOG.mdetCHANGELOG.json - Le problème peut aussi toucher des cas où des noms de fichiers longs partageant les 16 derniers caractères sont fréquemment modifiés au fil du temps
- un exemple cité est celui de gros fichiers de chaînes pour l’i18n
- Azure DevOps, utilisé par 1JS, n’effectue pas encore ce type de repack, et un travail est en cours pour réduire aussi la taille des dépôts côté serveur
- Les changements concernés devraient également être intégrés dans Git upstream
Points à vérifier dans les grands monorepos
- Dans un monorepo de grande taille, si
CHANGELOG.mdou d’autres fichiers au nom relativement long sont mis à jour de façon répétée, il faut surveiller les changements liés à path-walk - La nouvelle commande
git surveyfournit des heuristiques permettant d’évaluer l’état du dépôt- Top Files By Disk Size
- Top Directories By Inflated Size
- Top Files By Inflated Size
- Ces heuristiques aident à estimer si le travail autour de path-walk pourrait aussi avoir un impact sur la taille de ce dépôt
1 commentaires
Avis de Hacker News
Quant à savoir où se trouve la nouvelle commande
git-survey, elle n’a pas encore été intégrée à git.gitL’auteur utilise le fork de Git de Microsoft, et cette commande y a été ajoutée cet été : https://github.com/microsoft/git/pull/667
full-name-hashetpath-walksemblent eux aussi n’exister que dans ce fork, ou seulement dans Git HEADImpossible de les trouver dans les pages de manuel ni dans le journal des changements de la 2.47
Je me demande ce que signifie « les gens en Europe ne peuvent même pas cloner le dépôt à cause de sa taille »
C’est étrange qu’un gros dépôt ne puisse pas être récupéré en Europe mais le puisse ailleurs, et on dirait qu’ils ne pourront toujours pas le cloner tant que les changements côté serveur n’auront pas été appliqués
La phrase « ils repushaient sans cesse tout le fichier » semble coupée au milieu, et pendant la lecture, le GIF est beaucoup trop distrayant ; il reste visible même en mode lecture
Formulée plus complètement, ce serait quelque chose comme : « quelqu’un voulait cloner tout le dépôt sur son ordinateur portable pour travailler en voyageant en Europe, mais son disque était plein à ce moment-là et il n’a pas pu. Il a ensuite fait de la place et a pu le cloner »
Je ne pense pas que cela désigne un gros problème selon lequel l’Europe ne pourrait pas gérer un fichier de 180 Go
Surtout si la personne passe par un VPN, et plus encore par cet horrible bricolage qu’est un « SSL VPN » ; les logiciels qui gèrent mal la latence ou les serveurs avec un window scaling cassé restent encore étonnamment courants
Si la plupart des développeurs sont proches du serveur et disposent d’une connexion à bonne bande passante, ce genre de problème ne se voit pas beaucoup
Le clonage depuis Azure DevOps est très loin de saturer une connexion gigabit filaire, et comme cela prend beaucoup trop de temps, ça finit par s’arrêter soit de mon côté, soit côté Azure DevOps, et j’abandonne
Heureusement, je fais presque tout mon travail avec des clones superficiels dans Codespaces, donc ce n’est pas un gros problème ; et si notre dépôt connaît une explosion de taille similaire à celle du dépôt 1JS mentionnée dans l’article, j’espère que ce sera corrigé
Mise à jour : c’était une erreur stupide, et le nom de fichier n’inclut pas le chemin complet
L’explication semble s’être retrouvée noyée entre les GIF, mais ici les 16 derniers caractères sont différents : l’exemple est qu’on modifie
repo/packages/foo/CHANGELOG.md, mais que Git, en préparant le push, le compare àrepo/packages/bar/CHANGELOG.mdpour produire un diffLa lettre de couverture de l’API
path-walkvaut aussi le détour : https://lore.kernel.org/all/pull.1786.git.1725935335.gitgitg...L’exemple du blog n’est pas très clair, mais Git prenait en fait toutes les versions de tous les fichiers du dépôt, mettait les 16 derniers octets du chemin dans une table de hachage, puis regroupait pour la compression delta ce qu’il supposait être différentes versions d’un même fichier
Dans l’article,
foo/CHANGELOG.mdetbar/CHANGELOG.mdne font que 13 caractères, donc l’exemple tel quel ne convient pas ; il faut imaginer des chemins avec un suffixe commun plus longCette partie a été corrigée avec l’option
--full-name-hash, qui compare désormais le chemin complet plutôt que 16 octetsL’augmentation de la taille de fenêtre évoquée ensuite ressemble davantage à un hack pour contourner un mauvais regroupement de fichiers qu’à une vraie correction
C’est une manière de contourner le problème en donnant toujours des entrées terribles au compresseur et en consommant énormément de mémoire ; la présenter comme une solution prêtait donc à confusion
La partie vraiment intéressante, c’est l’API path-walk et
--full-name-hashJe me demande ici si « nom de fichier » signifie chemin, ou si Git choisit n’importe quel fichier portant le même nom pour produire un diff
J’aimerais aussi savoir s’il existe un motif dans la manière dont il choisit l’autre fichier
Il suffit de regarder le message du premier commit de la PR
--full-name-hash: https://github.com/git-for-windows/git/pull/5157/commits/d5c...Pour en citer un extrait,
"/CHANGELOG.json"fait 15 caractères et est généré par l’outil beachball ; seule la dernière lettre du répertoire parent permet de distinguer les différentes versions de ce fichier, et encore, seuls ses 2 bits de poids fort sont pris en compteSi cette lettre est une lettre alphabétique, il y aura toujours collision, et le chemin
"/CHANGELOG.md"a un problème similaire, même s’il laisse un peu plus de marge pour que le répertoire parent diffèreL’algorithme de regroupement donne un poids de plus en plus faible à chaque caractère à mesure qu’on s’éloigne de la droite du nom :
hash = (hash >> 2) + (c << 24)Le hash fait 32 bits et ajoute successivement chaque caractère de 8 bits du chemin complet dans les 8 bits de poids fort du hash ; comme les bits existants du hash sont décalés de 2 bits vers la droite, seuls les 16 derniers caractères influencent le hash final
On peut voir ce qui se passe concrètement ici : https://go.dev/play/p/JQpdUGXdQs7
Je l’ai porté en Go pour comparer les valeurs finales de
"aaa/CHANGELOG.md"et"zzz/CHANGELOG.md"; en essayant différentes valeurs pouraaaetzzz, on voit à quel point elles influencent peu la valeur finaleLes 16 derniers caractères de
CHANGELOG.mdcorrespondent au nom de fichier complet, et avec cette interprétation, cela explique aussi pourquoi la solutionfilepathwalkrègle le problèmeMais si c’était vraiment basé uniquement sur les 16 derniers caractères du nom de fichier, et non sur le chemin complet, ce problème aurait dû être beaucoup plus courant, surtout dans les monorepos
Je pense que la confusion vient sans doute des métavariables
fooetbar, et que les vrais noms de packages avaient probablement un suffixe commun, commefirstPkgetsecondPkgRien que
"/CHANGELOG.md"fait déjà 13 caractères, donc un suffixe commun de seulement 3 caractères suffit à provoquer une collision sur 16 caractèresJe viens de tester directement sur
nixpkgs: cloné tel quel depuis GitHub, il fait environ 5 GoLa première option mentionnée dans l’article,
--window 250, réduit la taille à 1,7 Go, tandis que la nouvelle option--path-walkdu fork Git de Microsoft est moins efficace et donne 1,9 Go au totalDans les deux cas, c’est moins de la moitié de la taille initiale ; ce serait bien que GitHub fournisse un moyen d’exécuter ce genre d’opération, et encore mieux si les gens commençaient à héberger cela de manière à pouvoir le contrôler eux-mêmes
L’article mentionne Derrick Stolee, qui a creusé et publié les changements nécessaires
Si les structures internes de Git, la réduction de la taille des clones Git en local et en CI, etc. vous intéressent, Derrick a écrit beaucoup d’excellents billets sur le blog GitHub : https://github.blog/author/dstolee/
Son site personnel vaut aussi le détour : https://stolee.dev/
Si vous avez commit par erreur un gros binaire, on peut considérer que la correction consiste à lancer
filter-branchpour supprimer le binaire en question, à apprendre à tout le monde à utiliser git-lfs pour les binaires et à le configurer, puis à faire un push forcé et à remettre les environnements de travail de chacun dans un état normal.Ce n’est pas idéal, mais c’est mieux que de laisser indéfiniment dans Git un gros fichier dont personne ne se sert.
filter-brancheffraie, il y a aussi BFG : https://rtyley.github.io/bfg-repo-cleaner/Comme quelqu’un d’autre l’a souligné, ce problème porte sur des fichiers petits mais qui changent souvent ; donc, pour économiser de l’espace, il suffit de supprimer les anciennes versions de l’historique et d’utiliser LFS à l’avenir.
Bidouiller Git peut être amusant, mais je me demande s’il n’y a pas moyen, dès le départ, d’éviter de mettre 2 500 packages dans un monorepo.
Plus la base de code grossit, plus il devient difficile d’attendre des gens qu’ils ne réinventent pas la roue, soit parce qu’ils ignorent que le code existe, soit parce qu’ils craignent de casser les usages existants en le modifiant pour gérer leur propre cas d’usage.
Intuitivement, il faut moins de temps pour passer de 100 à 200 modules que de 50 à 100.
Ça ressemble vraiment à un problème entièrement créé par eux-mêmes.
J’ai récemment regardé les présentations GitMerge publiées par GitButler et je suis les avancées autour des monorepos et de la scalabilité ; il y a beaucoup de bonnes choses qui sortent de Microsoft, GitHub et GitLab.
J’aimerais mieux comprendre comment le problème de la vérification des 16 derniers caractères et de la vérification du chemin complet s’articule avec la compression delta, les index de packs et les index multi-packs.
C’est agréable de voir que Microsoft utilise directement Azure DevOps.
J’avais l’impression que de plus en plus de services Azure ne proposaient des connecteurs natifs que pour GitHub, et qu’Azure DevOps partait vers l’abandon.
Avoir quelqu’un qui connaît aussi bien les entrailles de Git à portée de main pour aider doit vraiment être un bel avantage quand on mène ce genre de projet dans une entreprise de cette taille.