Accélérer la compilation 2,5 fois avec le découplage des dépendances et la conteneurisation des tests
Le temps de compilation peut être compliqué par des dépendances internes et externes complexes qui évoluent tout au long du processus de développement, ainsi que par des changements dans les environnements de compilation tels que le système d'exploitation ou les architectures matérielles. Voici quelques problèmes courants que l'on peut rencontrer lorsqu'on travaille sur des projets d'IA ou de MLOps à grande échelle :
Compilation excessivement longue - L'intégration du code se fait des centaines de fois par jour. Avec des centaines de milliers de lignes de code en place, même une petite modification peut entraîner une compilation complète qui prend généralement une ou plusieurs heures.
Environnement de compilation complexe - Le code du projet doit être compilé dans différents environnements, qui impliquent différents systèmes d'exploitation, tels que CentOS et Ubuntu, des dépendances sous-jacentes, telles que GCC, LLVM et CUDA, et des architectures matérielles. La compilation dans un environnement spécifique peut normalement ne pas fonctionner dans un environnement différent.
Dépendances complexes - La compilation d'un projet implique plus de 30 dépendances entre composants et avec des tiers. Le développement d'un projet entraîne souvent des changements dans les dépendances, ce qui provoque inévitablement des conflits de dépendances. Le contrôle de version entre les dépendances est si complexe que la mise à jour de la version des dépendances affectera facilement les autres composants.
Letéléchargement de dépendances tierces est lent ou échoue - Les retards de réseau ou les bibliothèques de dépendances tierces instables entraînent des téléchargements de ressources lents ou des échecs d'accès, ce qui affecte sérieusement l'intégration du code.
En découplant les dépendances et en mettant en œuvre la conteneurisation des tests, nous avons réussi à réduire le temps de compilation moyen de 60 % lorsque nous travaillions sur le projet open-source de recherche de similarité d'embeddings Milvus.
Découpler les dépendances du projet
La compilation d'un projet implique généralement un grand nombre de dépendances internes et externes. Plus un projet a de dépendances, plus il devient complexe de les gérer. Au fur et à mesure que le logiciel se développe, il devient plus difficile et plus coûteux de modifier ou de supprimer les dépendances, ainsi que d'identifier les effets de ces modifications. Une maintenance régulière est nécessaire tout au long du processus de développement pour garantir le bon fonctionnement des dépendances. Une mauvaise maintenance, des dépendances complexes ou défectueuses peuvent provoquer des conflits qui ralentissent ou bloquent le développement. En pratique, cela peut se traduire par des téléchargements de ressources tardifs, des échecs d'accès qui ont un impact négatif sur l'intégration du code, et bien d'autres choses encore. Le découplage des dépendances d'un projet peut atténuer les défauts et réduire le temps de compilation, en accélérant les tests du système et en évitant de ralentir inutilement le développement du logiciel.
Nous vous recommandons donc de découpler les dépendances de votre projet :
- Séparer les composants ayant des dépendances complexes
- Utiliser différents référentiels pour la gestion des versions.
- Utiliser des fichiers de configuration pour gérer les informations sur les versions, les options de compilation, les dépendances, etc.
- Ajoutez les fichiers de configuration aux bibliothèques de composants afin qu'ils soient mis à jour au fur et à mesure de l'itération du projet.
Optimisation de la compilation entre les composants - Tirer et compiler le composant approprié en fonction des dépendances et des options de compilation enregistrées dans les fichiers de configuration. Marquez et emballez les résultats de la compilation binaire et les fichiers manifestes correspondants, puis téléchargez-les dans votre dépôt privé. Si aucune modification n'est apportée à un composant ou aux composants dont il dépend, lire ses résultats de compilation conformément aux fichiers manifestes. En cas de problèmes tels que des retards de réseau ou des bibliothèques de dépendances tierces instables, essayez de mettre en place un référentiel interne ou d'utiliser des référentiels miroirs.
Pour optimiser la compilation entre les composants :
1. créer un graphe de relations de dépendance - Utilisez les fichiers de configuration des bibliothèques de composants pour créer un graphe de relations de dépendance. Utilisez la relation de dépendance pour récupérer les informations de version (Git Branch, Tag, et Git commit ID) et les options de compilation et plus encore des composants dépendants en amont et en aval.
1.png
2. vérifier les dépendances - générer des alertes pour les dépendances circulaires, les conflits de version et d'autres problèmes qui surviennent entre les composants.
3. aplanir les dépendances - Trier les dépendances par recherche en profondeur (DFS) et fusionner les composants ayant des dépendances en double pour former un graphe de dépendances.
2.png
4. utiliser l'algorithme MerkleTree pour générer un hachage (Root Hash) contenant les dépendances de chaque composant sur la base des informations de version, des options de compilation, etc. Combiné à des informations telles que le nom du composant, l'algorithme forme une étiquette unique pour chaque composant.
3.png
5. en se basant sur les informations de l'étiquette unique du composant, vérifier si une archive de compilation correspondante existe dans le repo privé. Si une archive de compilation est récupérée, décompressez-la pour obtenir le fichier manifeste pour la lecture ; sinon, compilez le composant, marquez les fichiers objets de compilation et le fichier manifeste générés, et téléchargez-les dans le répertoire privé.
Mettre en œuvre des optimisations de compilation dans les composants - Choisissez un outil de cache de compilation spécifique au langage pour mettre en cache les fichiers objets compilés, puis téléchargez-les et stockez-les dans votre dépôt privé. Pour la compilation C/C++, choisissez un outil de cache de compilation comme CCache pour mettre en cache les fichiers intermédiaires de compilation C/C++, puis archivez le cache CCache local après la compilation. Ces outils de cache de compilation mettent simplement en cache les fichiers de code modifiés un par un après la compilation, et copient les composants compilés du fichier de code inchangé afin qu'ils puissent être directement impliqués dans la compilation finale. L'optimisation de la compilation au sein des composants comprend les étapes suivantes :
- Ajouter les dépendances de compilation nécessaires au fichier Dockerfile. Utiliser Hadolint pour effectuer des contrôles de conformité sur Dockerfile afin de s'assurer que l'image est conforme aux meilleures pratiques de Docker.
- Mettre en miroir l'environnement de compilation en fonction de la version sprint du projet (version + build), du système d'exploitation et d'autres informations.
- Exécutez le conteneur de l'environnement de compilation miroir et transférez l'identifiant de l'image au conteneur en tant que variable d'environnement. Voici un exemple de commande pour obtenir l'ID de l'image : "docker inspect ' - type=image' - format '{{.ID}}' repository/build-env:v0.1-centos7".
- Choisissez l'outil de cache de compilation approprié : Entrez votre conteneur pour intégrer et compiler vos codes et vérifiez dans votre dépôt privé si un cache de compilation approprié existe. Si c'est le cas, téléchargez-le et extrayez-le dans le répertoire spécifié. Une fois que tous les composants ont été compilés, le cache généré par l'outil de cache de compilation est empaqueté et téléchargé dans votre dépôt privé en fonction de la version du projet et de l'ID de l'image.
Optimisation de la compilation
Notre version initiale occupe trop d'espace disque et de bande passante, et prend beaucoup de temps à déployer, nous avons pris les mesures suivantes :
- Choisir l'image de base la plus légère pour réduire la taille de l'image, par exemple alpine, busybox, etc.
- Réduire le nombre de couches d'images. Réutiliser les dépendances autant que possible. Fusionner plusieurs commandes avec "&&".
- Nettoyer les produits intermédiaires pendant la construction de l'image.
- Utiliser le cache d'image pour construire l'image autant que possible.
Au fur et à mesure de l'avancement de notre projet, l'utilisation du disque et des ressources réseau a commencé à monter en flèche à mesure que le cache de compilation augmentait, alors que certains des caches de compilation étaient sous-utilisés. Nous avons donc procédé aux ajustements suivants :
Nettoyerrégulièrement les fichiers de cache - Vérifier régulièrement le dépôt privé (à l'aide de scripts par exemple), et nettoyer les fichiers de cache qui n'ont pas été modifiés depuis un certain temps ou qui n'ont pas été beaucoup téléchargés.
Mise en cache sélective des compilations - Ne mettre en cache que les compilations exigeant des ressources, et ne pas mettre en cache les compilations ne nécessitant pas beaucoup de ressources.
Exploiter les tests conteneurisés pour réduire les erreurs, améliorer la stabilité et la fiabilité
Les codes doivent être compilés dans différents environnements, ce qui implique une variété de systèmes d'exploitation (par exemple CentOS et Ubuntu), de dépendances sous-jacentes (par exemple GCC, LLVM et CUDA) et d'architectures matérielles spécifiques. Le code qui se compile avec succès dans un environnement spécifique échoue dans un environnement différent. En exécutant les tests dans des conteneurs, le processus de test devient plus rapide et plus précis.
La conteneurisation garantit que l'environnement de test est cohérent et qu'une application fonctionne comme prévu. L'approche des tests conteneurisés consiste à emballer les tests sous forme de conteneurs d'images et à créer un environnement de test véritablement isolé. Nos testeurs ont trouvé cette approche très utile, ce qui a permis de réduire les temps de compilation de 60 %.
Assurer un environnement de compilation cohérent - Comme les produits compilés sont sensibles aux changements dans l'environnement du système, des erreurs inconnues peuvent se produire dans différents systèmes d'exploitation. Nous devons étiqueter et archiver le cache des produits compilés en fonction des changements survenus dans l'environnement de compilation, mais il est difficile de les classer. Nous avons donc introduit la technologie de conteneurisation pour unifier l'environnement de compilation afin de résoudre ces problèmes.
Conclusion
En analysant les dépendances du projet, cet article présente différentes méthodes d'optimisation de la compilation entre et au sein des composants, en fournissant des idées et des bonnes pratiques pour construire une intégration de code continue stable et efficace. Ces méthodes ont permis de résoudre les problèmes de lenteur de l'intégration du code causés par des dépendances complexes, d'unifier les opérations à l'intérieur du conteneur pour assurer la cohérence de l'environnement, et d'améliorer l'efficacité de la compilation grâce à la lecture des résultats de la compilation et à l'utilisation d'outils de cache de compilation pour mettre en cache les résultats intermédiaires de la compilation.
Les pratiques susmentionnées ont permis de réduire le temps de compilation du projet de 60 % en moyenne, améliorant ainsi considérablement l'efficacité globale de l'intégration du code. À l'avenir, nous continuerons à paralléliser la compilation entre les composants et à l'intérieur de ceux-ci afin de réduire davantage les temps de compilation.
Les sources suivantes ont été utilisées pour cet article :
- "Découplage des arbres de sources dans les composants de niveau bâtiment"
- "Facteurs à prendre en compte lors de l'ajout de dépendances tierces à un projet
- "Survivre aux dépendances logicielles"
- "Comprendre les dépendances : Une étude des défis de coordination dans le développement de logiciels"
A propos de l'auteur
Zhifeng Zhang est ingénieur DevOps senior chez Zilliz.com et travaille sur Milvus, une base de données vectorielle open-source, et instructeur autorisé de l'université de logiciels open-source LF en Chine. Il a obtenu sa licence en Internet des objets (IOT) à l'Institut de génie logiciel de Guangzhou. Il passe sa carrière à participer et à diriger des projets dans le domaine du CI/CD, du DevOps, de la gestion de l'infrastructure informatique, de la boîte à outils Cloud-Native, de la conteneurisation et de l'optimisation du processus de compilation.
Try Managed Milvus for Free
Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.
Get StartedLike the article? Spread the word