Ускорение компиляции в 2,5 раза благодаря разделению зависимостей и контейнеризации тестирования
Время компиляции может увеличиваться из-за сложных внутренних и внешних зависимостей, которые развиваются на протяжении всего процесса разработки, а также из-за изменений в средах компиляции, таких как операционная система или аппаратные архитектуры. Ниже приведены распространенные проблемы, с которыми можно столкнуться при работе над крупномасштабными проектами AI или MLOps:
Чрезмерно долгая компиляция - Интеграция кода выполняется сотни раз каждый день. При наличии сотен тысяч строк кода даже небольшое изменение может привести к полной компиляции, которая обычно занимает один или несколько часов.
Сложная среда компиляции - Код проекта необходимо компилировать в разных средах, которые включают разные операционные системы, такие как CentOS и Ubuntu, базовые зависимости, такие как GCC, LLVM и CUDA, а также аппаратные архитектуры. И компиляция в определенной среде обычно может не работать в другой среде.
Сложные зависимости - Компиляция проекта включает более 30 межкомпонентных и сторонних зависимостей. Разработка проекта часто приводит к изменениям в зависимостях, что неизбежно вызывает конфликты зависимостей. Контроль версий между зависимостями настолько сложен, что обновление версии зависимостей легко повлияет на другие компоненты.
Загрузка сторонних зависимостей выполняется медленно или завершается сбоем - Сетевые задержки или нестабильные библиотеки сторонних зависимостей вызывают медленную загрузку ресурсов или сбои доступа, серьезно влияя на интеграцию кода.
Благодаря разделению зависимостей и внедрению контейнеризации тестирования нам удалось сократить среднее время компиляции на 60% при работе над open-source проектом поиска сходства эмбеддингов Milvus.
Разделение зависимостей проекта
Компиляция проекта обычно включает большое количество внутренних и внешних зависимостей компонентов. Чем больше зависимостей у проекта, тем сложнее становится управлять ими. По мере роста программного обеспечения становится все труднее и дороже изменять или удалять зависимости, а также выявлять последствия таких действий. На протяжении всего процесса разработки требуется регулярное обслуживание, чтобы обеспечить корректную работу зависимостей. Плохое обслуживание, сложные зависимости или неисправные зависимости могут вызывать конфликты, которые замедляют или останавливают разработку. На практике это может означать задержки при загрузке ресурсов, сбои доступа, которые негативно влияют на интеграцию кода, и многое другое. Разделение зависимостей проекта может смягчить дефекты и сократить время компиляции, ускоряя системное тестирование и избегая ненужного торможения разработки программного обеспечения.
Поэтому мы рекомендуем разделить зависимости вашего проекта:
- Разделите компоненты со сложными зависимостями
- Используйте разные репозитории для управления версиями.
- Используйте конфигурационные файлы для управления информацией о версиях, параметрами компиляции, зависимостями и т. д.
- Добавьте конфигурационные файлы в библиотеки компонентов, чтобы они обновлялись по мере итераций проекта.
Оптимизация компиляции между компонентами — Извлеките и скомпилируйте соответствующий компонент в соответствии с зависимостями и параметрами компиляции, записанными в конфигурационных файлах. Пометьте и упакуйте двоичные результаты компиляции и соответствующие файлы манифеста, а затем загрузите их в свой частный репозиторий. Если в компонент или компоненты, от которых он зависит, не внесены изменения, воспроизведите его результаты компиляции в соответствии с файлами манифеста. Для таких проблем, как сетевые задержки или нестабильные библиотеки сторонних зависимостей, попробуйте настроить внутренний репозиторий или использовать зеркальные репозитории.
Чтобы оптимизировать компиляцию между компонентами:
1.Создайте граф зависимостей — Используйте конфигурационные файлы в библиотеках компонентов, чтобы создать граф зависимостей. Используйте связь зависимостей для получения информации о версиях (Git Branch, Tag и Git commit ID), параметров компиляции и другого как для вышестоящих, так и для нижестоящих зависимых компонентов.
Figure 1.
2.Проверьте зависимости — Генерируйте оповещения о циклических зависимостях, конфликтах версий и других проблемах, возникающих между компонентами.
3.Сгладьте зависимости — Отсортируйте зависимости с помощью поиска в глубину (DFS) и выполните фронтальное объединение компонентов с дублирующимися зависимостями, чтобы сформировать граф зависимостей.
Рисунок 2.
4.Используйте алгоритм MerkleTree для генерации хеша (Root Hash), содержащего зависимости каждого компонента на основе информации о версии, параметров компиляции и других данных. В сочетании с такой информацией, как имя компонента, алгоритм формирует уникальный тег для каждого компонента.
Рисунок 3.
5.На основе информации об уникальном теге компонента проверьте, существует ли соответствующий архив компиляции в приватном репозитории. Если архив компиляции найден, распакуйте его, чтобы получить manifest-файл для воспроизведения; если нет, скомпилируйте компонент, разметьте сгенерированные объектные файлы компиляции и manifest-файл и загрузите их в приватный репозиторий.
Реализуйте оптимизации компиляции внутри компонентов — Выберите инструмент кэширования компиляции, специфичный для языка, чтобы кэшировать скомпилированные объектные файлы, а затем загрузите и сохраните их в своем приватном репозитории. Для компиляции C/C++ выберите инструмент кэширования компиляции, например CCache, чтобы кэшировать промежуточные файлы компиляции C/C++, а затем заархивируйте локальный кэш CCache после компиляции. Такие инструменты кэширования компиляции просто кэшируют измененные файлы кода один за другим после компиляции и копируют скомпилированные компоненты неизмененного файла кода, чтобы они могли напрямую участвовать в финальной компиляции. Оптимизация компиляции внутри компонентов включает следующие шаги:
- Добавьте необходимые зависимости компиляции в Dockerfile. Используйте Hadolint для выполнения проверок соответствия Dockerfile, чтобы убедиться, что образ соответствует лучшим практикам Docker.
- Зеркалируйте среду компиляции в соответствии со sprint-версией проекта (версия + сборка), операционной системой и другой информацией.
- Запустите контейнер зеркалированной среды компиляции и передайте ID образа в контейнер как переменную окружения. Вот пример команды для получения ID образа: “docker inspect ‘ — type=image’ — format ‘{{.ID}}’ repository/build-env:v0.1-centos7”.
- Выберите подходящий инструмент кэширования компиляции: войдите в свой контейнер, чтобы интегрировать и скомпилировать свой код, и проверьте в своем приватном репозитории, существует ли подходящий кэш компиляции. Если да, скачайте и распакуйте его в указанный каталог. После компиляции всех компонентов кэш, сгенерированный инструментом кэширования компиляции, упаковывается и загружается в ваш приватный репозиторий на основе версии проекта и ID образа.
Дальнейшая оптимизация компиляции
Наш первоначально созданный образ занимает слишком много дискового пространства и сетевой пропускной способности, а также требует много времени для развертывания, поэтому мы приняли следующие меры:
- Выбирайте самый компактный базовый образ, чтобы уменьшить размер образа, например alpine, busybox и т. д.
- Уменьшайте количество слоев образа. Повторно используйте зависимости насколько это возможно. Объединяйте несколько команд с помощью “&&”.
- Очищайте промежуточные продукты во время сборки образа.
- По возможности используйте кэш образов для сборки образа.
По мере дальнейшего развития нашего проекта использование диска и сетевых ресурсов начало резко расти из-за увеличения кэша компиляции, при этом часть кэшей компиляции используется недостаточно. Затем мы внесли следующие изменения:
Регулярно очищайте файлы кэша — Регулярно проверяйте приватный репозиторий (например, с помощью скриптов) и очищайте файлы кэша, которые давно не изменялись или редко скачивались.
Выборочное кэширование компиляции — Кэшируйте только ресурсоемкие компиляции и пропускайте кэширование компиляций, которые не требуют много ресурсов.
Использование контейнеризированного тестирования для снижения количества ошибок, повышения стабильности и надежности
Код приходится компилировать в разных средах, включающих различные операционные системы (например, CentOS и Ubuntu), базовые зависимости (например, GCC, LLVM и CUDA) и конкретные аппаратные архитектуры. Код, который успешно компилируется в определенной среде, может не скомпилироваться в другой. При запуске тестов внутри контейнеров процесс тестирования становится быстрее и точнее.
Контейнеризация обеспечивает согласованность тестовой среды и работу приложения в соответствии с ожиданиями. Контейнеризированный подход к тестированию упаковывает тесты в виде контейнеров-образов и создает по-настоящему изолированную тестовую среду. Наши тестировщики обнаружили, что этот подход довольно полезен, что в итоге сократило время компиляции на целых 60%.
Обеспечьте согласованную среду компиляции — Поскольку скомпилированные продукты чувствительны к изменениям в системной среде, в разных операционных системах могут возникать неизвестные ошибки. Нам приходится помечать тегами и архивировать кэш скомпилированного продукта в соответствии с изменениями в среде компиляции, но их трудно классифицировать. Поэтому мы внедрили технологию контейнеризации, чтобы унифицировать среду компиляции и решить такие проблемы.
Заключение
Анализируя зависимости проекта, эта статья представляет различные методы оптимизации компиляции между компонентами и внутри них, предлагая идеи и лучшие практики для построения стабильной и эффективной непрерывной интеграции кода. Эти методы помогли решить проблему медленной интеграции кода, вызванной сложными зависимостями, унифицировать операции внутри контейнера для обеспечения согласованности среды и повысить эффективность компиляции за счет воспроизведения результатов компиляции и использования инструментов кэша компиляции для кэширования промежуточных результатов компиляции.
Вышеупомянутые практики сократили время компиляции проекта в среднем на 60%, значительно повысив общую эффективность интеграции кода. В дальнейшем мы продолжим распараллеливать компиляцию между компонентами и внутри них, чтобы еще больше сократить время компиляции.
Для этой статьи были использованы следующие источники:
- “Decoupling Source Trees into Build-Level Components”
- “Factors to consider when adding third party dependencies to a project”
- “Surviving Software Dependencies”
- “Understanding Dependencies: A Study of the Coordination Challenges in Software Development”
Об авторе
Zhifeng Zhang — старший DevOps-инженер в Zilliz.com, работающий над Milvus, векторной базой данных с открытым исходным кодом, и авторизованный преподаватель университета программного обеспечения с открытым исходным кодом LF в Китае. Он получил степень бакалавра в области Интернета вещей (IOT) в Институте программной инженерии Гуанчжоу. В своей карьере он участвует в проектах и руководит ими в области CI/CD, DevOps, управления ИТ-инфраструктурой, Cloud-Native toolkit, контейнеризации и оптимизации процесса компиляции.
Читать далее

Zilliz Cloud Just Landed in Claude Code
The Zilliz Cloud Plugin brings the full power of Zilliz Cloud directly into your Claude Code terminal as natural-language conversations.

Zilliz Named "Highest Performer" and "Easiest to Use" in G2's Summer 2025 Grid® Report for Vector Databases
Zilliz shines in G2's Summer 2025 Grid® Report as both "Highest Performer" and "Easiest to Use," solving the performance-usability dilemma.

1 Table = 1000 Words? Foundation Models for Tabular Data
TableGPT2 automates tabular data insights, overcoming schema variability, while Milvus accelerates vector search for efficient, scalable decision-making.



