7 points par GN⁺ 2024-10-28 | 1 commentaires | Partager sur WhatsApp
  • 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.md et CHANGELOG.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=250 a eu un effet positif, mais en exécutant le packing basé sur les chemins avec git repack -adf --path-walk dans le fork Microsoft Git 2.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 true ainsi que la nouvelle commande git 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-sizer a 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 versioned miroir de main
    • 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
  • La branche versioned ne contenait principalement que des commits ajoutant du contenu dans des fichiers CHANGELOG.md et CHANGELOG.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 checkout et sparse index, il est apparu que récupérer la branche versioned entraî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.md appartenant à 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 avec repo/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=250 a é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
  • 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.2 peuvent 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.md et CHANGELOG.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.md ou 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 survey fournit 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

 
GN⁺ 2024-10-28
Avis de Hacker News
  • Quant à savoir où se trouve la nouvelle commande git-survey, elle n’a pas encore été intégrée à git.git
    L’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-hash et path-walk semblent eux aussi n’exister que dans ce fork, ou seulement dans Git HEAD
      Impossible 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

    • J’ai lu cette phrase comme une anecdote
      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
    • Ici, « l’Europe » est très probablement une façon de désigner une latence élevée et une faible bande passante
      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
    • Je travaille depuis l’Australie pour une autre équipe interne de Microsoft, avec un gros monorepo Azure DevOps, et il m’est presque impossible de faire un clone complet
      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é
    • Le dépôt est sans doute hébergé sur la côte ouest des États-Unis, donc j’imagine que chaque clonage depuis l’Europe doit traverser l’Atlantique
    • On peut aussi être dans un pays où l’infrastructure Internet est moins développée, par exemple l’Allemagne
  • 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.md pour produire un diff

    • Derrick l’explique mieux dans cette lettre de couverture : https://lore.kernel.org/git/pull.1785.git.1725890210.gitgitg...
      La lettre de couverture de l’API path-walk vaut 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.md et bar/CHANGELOG.md ne font que 13 caractères, donc l’exemple tel quel ne convient pas ; il faut imaginer des chemins avec un suffixe commun plus long
      Cette partie a été corrigée avec l’option --full-name-hash, qui compare désormais le chemin complet plutôt que 16 octets
      L’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-hash
    • J’aurais aimé que l’article explique ce qui s’est exactement passé plutôt que sa narration « colorée »
      Je 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
    • Le chemin complet est bien pris en compte
      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 compte
      Si 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ère
      L’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 pour aaa et zzz, on voit à quel point elles influencent peu la valeur finale
    • Le nom de fichier n’inclut pas forcément le chemin complet
      Les 16 derniers caractères de CHANGELOG.md correspondent au nom de fichier complet, et avec cette interprétation, cela explique aussi pourquoi la solution filepathwalk règle le problème
      Mais 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
    • Ce passage m’a aussi chiffonné
      Je pense que la confusion vient sans doute des métavariables foo et bar, et que les vrais noms de packages avaient probablement un suffixe commun, comme firstPkg et secondPkg
      Rien 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ères
  • Je viens de tester directement sur nixpkgs : cloné tel quel depuis GitHub, il fait environ 5 Go
    La première option mentionnée dans l’article, --window 250, réduit la taille à 1,7 Go, tandis que la nouvelle option --path-walk du fork Git de Microsoft est moins efficace et donne 1,9 Go au total
    Dans 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-branch pour 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.

    • Pour ceux que filter-branch effraie, 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.
    • Le problème central n’est pas un fichier binaire ajouté une fois puis jamais modifié, mais des petits fichiers binaires qui changent fréquemment.
    • La méthode recommandée aujourd’hui est filter-repo : https://github.com/newren/git-filter-repo
    • C’est plus facile de rejeter la faute sur Linus.
  • 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.

    • Le nombre de lignes de code a généralement tendance à croître de façon exponentielle.
      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.
    • Alors il suffit d’avoir 2 500 dépôts Git séparés, avec toute la surcharge associée.
    • C’est exactement la première chose à laquelle j’ai pensé moi aussi.
      Ç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.

    • Si c’est si bénéfique pour l’open source, je me demande s’ils comptent ouvrir une demande de merge pour réintégrer leurs commandes Git personnalisées dans Git officiellement.
  • 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.

    • Avoir des liens étroits avec une organisation comme GitHub aide certainement, mais toute organisation comptant autant de développeurs peut justifier le coût de faire venir des consultants hautement spécialisés pour résoudre ce genre de problème très pointu.