Ускорение компиляции в 2,5 раза с помощью разделения зависимостей и контейнеризации тестирования
Время компиляции может усугубляться сложными внутренними и внешними зависимостями, которые меняются в процессе разработки, а также изменениями в средах компиляции, таких как операционная система или аппаратная архитектура. Ниже перечислены общие проблемы, с которыми можно столкнуться при работе над крупномасштабными проектами в области ИИ или MLOps:
Непомерно долгая компиляция - Интеграция кода выполняется сотни раз в день. При наличии сотен тысяч строк кода даже небольшое изменение может привести к полной компиляции, которая обычно занимает один или несколько часов.
Сложная среда компиляции - код проекта должен быть скомпилирован в различных средах, которые включают в себя различные операционные системы, такие как CentOS и Ubuntu, базовые зависимости, такие как GCC, LLVM и CUDA, и аппаратные архитектуры. И компиляция в определенном окружении обычно может не работать в другом окружении.
Сложные зависимости - компиляция проекта включает более 30 межкомпонентных и сторонних зависимостей. Развитие проекта часто приводит к изменениям в зависимостях, что неизбежно вызывает конфликты зависимостей. Контроль версий между зависимостями настолько сложен, что обновление версии зависимости может легко повлиять на другие компоненты.
Загрузка сторонних зависимостей происходит медленно или со сбоями - сетевые задержки или нестабильность библиотек сторонних зависимостей приводят к медленной загрузке ресурсов или сбоям в доступе, что серьезно влияет на интеграцию кода.
Развязав зависимости и внедрив контейнеризацию тестирования, нам удалось сократить среднее время компиляции на 60 % во время работы над проектом Milvus по поиску сходства вкраплений с открытым исходным кодом.
Разделение зависимостей проекта
Компиляция проекта обычно включает в себя большое количество внутренних и внешних зависимостей компонентов. Чем больше зависимостей в проекте, тем сложнее ими управлять. По мере роста программного обеспечения становится все сложнее и дороже изменять или удалять зависимости, а также выявлять их последствия. Регулярное обслуживание необходимо на протяжении всего процесса разработки для обеспечения правильного функционирования зависимостей. Плохое обслуживание, сложные или ошибочные зависимости могут привести к конфликтам, которые замедлят или остановят разработку. На практике это может означать задержку загрузки ресурсов, сбои в доступе, которые негативно влияют на интеграцию кода, и многое другое. Развязывание зависимостей проекта может уменьшить количество дефектов и сократить время компиляции, ускорить тестирование системы и избежать ненужного затягивания разработки программного обеспечения.
Поэтому мы рекомендуем разделить зависимости в вашем проекте:
- Разделить компоненты со сложными зависимостями
- Использовать различные репозитории для управления версиями.
- Используйте конфигурационные файлы для управления информацией о версиях, параметрах компиляции, зависимостях и т. д.
- Добавьте конфигурационные файлы в библиотеки компонентов, чтобы они обновлялись по мере итераций проекта.
Оптимизация компиляции между компонентами - извлечение и компиляция соответствующего компонента в соответствии с зависимостями и параметрами компиляции, записанными в конфигурационных файлах. Пометьте и упакуйте результаты бинарной компиляции и соответствующие файлы манифеста, а затем загрузите их в свой личный репозиторий. Если компонент или компоненты, от которых он зависит, не изменились, воспроизведите результаты его компиляции в соответствии с файлами манифеста. Для решения таких проблем, как сетевые задержки или нестабильность библиотек зависимостей сторонних разработчиков, попробуйте создать внутренний репозиторий или использовать зеркальные репозитории.
Для оптимизации компиляции между компонентами:
1.Создайте граф зависимостей - используйте файлы конфигурации в библиотеках компонентов для создания графа зависимостей. Используйте граф зависимостей, чтобы получить информацию о версии (Git Branch, Tag и Git commit ID), а также параметры компиляции и многое другое для зависимых компонентов, как выше, так и ниже по течению.
1.png
2.Проверка наличия зависимостей - генерируйте предупреждения о круговых зависимостях, конфликтах версий и других проблемах, возникающих между компонентами.
3.Сглаживание зависимостей - сортировка зависимостей по принципу Depth First Search (DFS) и фронтальное слияние компонентов с дублирующимися зависимостями для формирования графа зависимостей.
2.png
4.Использование алгоритма MerkleTree для генерации хэша (Root Hash), содержащего зависимости каждого компонента на основе информации о версии, параметрах компиляции и т. д. В сочетании с такой информацией, как имя компонента, алгоритм формирует уникальный тег для каждого компонента.
3.png
5.На основе информации об уникальном теге компонента проверяется, существует ли соответствующий архив компиляции в частном репозитории. Если архив компиляции найден, распакуйте его, чтобы получить файл манифеста для воспроизведения; если нет, скомпилируйте компонент, разметьте сгенерированные файлы объектов компиляции и файл манифеста и загрузите их в приватное хранилище.
Реализация оптимизаций компиляции в компонентах - выберите инструмент кэширования компиляции для конкретного языка, чтобы кэшировать скомпилированные объектные файлы, загрузить и хранить их в личном репозитории. Для компиляции C/C++ выберите инструмент кэширования компиляции, например CCache, для кэширования промежуточных файлов компиляции C/C++, а затем архивируйте локальный кэш CCache после компиляции. Такие инструменты кэширования компиляции просто кэшируют измененные файлы кода один за другим после компиляции и копируют скомпилированные компоненты неизмененного файла кода, чтобы они могли непосредственно участвовать в окончательной компиляции. Оптимизация компиляции внутри компонентов включает следующие шаги:
- Добавление необходимых зависимостей компиляции в Dockerfile. Используйте Hadolint для проверки соответствия Dockerfile, чтобы убедиться, что образ соответствует лучшим практикам Docker.
- Отзеркальте среду компиляции в соответствии с версией спринта проекта (версия + сборка), операционной системой и другой информацией.
- Запустите контейнер с зеркалированной средой компиляции и передайте идентификатор образа контейнеру в качестве переменной окружения. Вот пример команды для получения идентификатора образа: "docker inspect ' - type=image' - format '{{.ID}}' repository/build-env:v0.1-centos7".
- Выберите подходящий инструмент кэширования компиляции: Укажите containter для интеграции и компиляции кода и проверьте в личном репозитории, существует ли соответствующий кэш компиляции. Если да, загрузите и распакуйте его в указанный каталог. После компиляции всех компонентов кэш, созданный инструментом компиляции кэша, упаковывается и загружается в ваш личный репозиторий на основе версии проекта и идентификатора образа.
Дальнейшая оптимизация компиляции
Поскольку наша первоначальная сборка занимает слишком много дискового пространства и пропускной способности сети, а ее развертывание занимает много времени, мы приняли следующие меры:
- Выбрать наиболее компактный базовый образ для уменьшения размера изображения, например alpine, busybox и т. д.
- Сократить количество слоев изображения. Использовать зависимости как можно чаще. Объединяйте несколько команд с помощью "&&".
- Очищайте промежуточные продукты при построении изображения.
- Как можно чаще используйте кэш изображений для построения образа.
По мере развития нашего проекта использование диска и сетевых ресурсов стало расти по мере увеличения кэша компиляции, а некоторые кэши компиляции использовались недостаточно. Тогда мы внесли следующие изменения:
Регулярная очистка файлов кэша - Регулярно проверяйте частный репозиторий (например, с помощью скриптов) и очищайте файлы кэша, которые давно не менялись или редко загружались.
Выборочное кэширование компиляций - кэшируйте только требовательные к ресурсам компиляции и пропускайте кэширование компиляций, которые не требуют много ресурсов.
Использование контейнерного тестирования для уменьшения количества ошибок, повышения стабильности и надежности
Коды приходится компилировать в разных средах, которые включают в себя различные операционные системы (например, CentOS и Ubuntu), базовые зависимости (например, GCC, LLVM и CUDA) и специфические аппаратные архитектуры. Код, который успешно компилируется в определенном окружении, не работает в другом окружении. Запуск тестов внутри контейнеров позволяет ускорить процесс тестирования и повысить его точность.
Контейнеризация гарантирует, что тестовое окружение единообразно и что приложение работает так, как ожидается. При контейнерном тестировании тесты упаковываются в контейнеры-образы и создается по-настоящему изолированная тестовая среда. Наши тестировщики обнаружили, что такой подход весьма полезен, и в итоге время компиляции сократилось на 60 %.
Обеспечение согласованной среды компиляции - поскольку скомпилированные продукты чувствительны к изменениям в системном окружении, в разных операционных системах могут возникать неизвестные ошибки. Мы должны помечать и архивировать кэш скомпилированных продуктов в соответствии с изменениями в среде компиляции, но их сложно классифицировать. Поэтому мы внедрили технологию контейнеризации для унификации среды компиляции, чтобы решить эти проблемы.
Заключение
Проанализировав зависимости проекта, в этой статье были представлены различные методы оптимизации компиляции между компонентами и внутри них, а также идеи и лучшие практики для построения стабильной и эффективной непрерывной интеграции кода. Эти методы помогли решить проблему медленной интеграции кода, вызванной сложными зависимостями, унифицировать операции внутри контейнера для обеспечения согласованности среды, а также повысить эффективность компиляции за счет воспроизведения результатов компиляции и использования инструментов кэширования компиляции для кэширования промежуточных результатов компиляции.
Вышеупомянутые практики позволили сократить время компиляции проекта в среднем на 60 %, значительно повысив общую эффективность интеграции кода. В дальнейшем мы будем продолжать распараллеливать компиляцию между компонентами и внутри них, чтобы еще больше сократить время компиляции.
Для написания этой статьи были использованы следующие источники:
- "Разделение деревьев исходных текстов на компоненты уровня сборки"
- "Факторы, которые следует учитывать при добавлении сторонних зависимостей в проект"
- "Выживание зависимостей в программном обеспечении"
- "Понимание зависимостей: Исследование проблем координации при разработке программного обеспечения"
Об авторе
Чжифэн Чжан - старший DevOps-инженер в Zilliz.com, работающий над Milvus, векторной базой данных с открытым исходным кодом, и авторизованный инструктор университета открытого программного обеспечения LF в Китае. Он получил степень бакалавра в области Интернета вещей (IOT) в Институте программной инженерии в Гуанчжоу. В своей карьере участвует и руководит проектами в области CI/CD, DevOps, управления ИТ-инфраструктурой, Cloud-Native toolkit, контейнеризации и оптимизации процессов компиляции.
Try Managed Milvus for Free
Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.
Get StartedLike the article? Spread the word