Мы списали uv со счетов. А потом он ускорил наш CI на 80%

Всем привет! Меня зовут Артем Целин и я Бэкенд-разработчик в компании “Исходный Код”.
В Python-сообществе вокруг uv в последние месяцы много шума: быстрый резолвинг зависимостей, единый инструмент вместо россыпи утилит и обещания заметно ускорить привычный workflow.
Мы решили проверить это на практике и выбрали для эксперимента один из наших микросервисов: Python 3.14, 37 прямых зависимостей, 153 пакета в lock-файле и типичный для корпоративной разработки стек с приватными пакетами и внутренним Nexus.
Ожидания были простые. Если uv действительно настолько быстрее Poetry, миграция быстро себя окупит.
Результат оказался неожиданным.
На локальной машине uv действительно заметно быстрее резолвил зависимости и генерировал lock-файл. Но как только дело доходило до установки зависимостей по уже готовому lock-файлу, преимущество исчезало, а иногда Poetry оказывался даже быстрее.
После нескольких прогонов мы пришли к выводу, что реального выигрыша для нашей команды нет, а значит и смысла мигрировать тоже нет.
Мы завершили эксперимент. Через несколько недель к этому вопросу пришлось вернуться из-за алерта от сканера уязвимостей Trivy.
Локальный бенчмарк и первое разочарование
Для первого теста был выбран локальный сценарий, максимально приближенный к обычной разработке: несколько прогонов подряд, перед каждым запуском удаляли виртуальное окружение и очищали кеши pip, Poetry и uv, чтобы каждый прогон начинался с нуля.
Сравнивали два отдельных сценария для каждого инструмента:
Генерация lock-файла (poetry lock / uv lock)
Установка по уже готовому lock-файлу (poetry install / uv sync)
Картина получилась неоднозначной.
В части резолвинга uv действительно подтвердил свою репутацию, создавая lock-файл заметно быстрее Poetry.
В сценарии, который для команды важнее всего в ежедневной работе (установка зависимостей по уже закоммиченному lock-файлу), преимущество исчезало.
По результатам тридцати прогонов с использованием Poetry 1.7.1 и uv 0.11.8 средняя картина выглядела так:
Сценарий | Poetry | uv |
Генерация lock-файла | 25 с | 16 с |
Установка по готовому lock-файлу | 7 с | 13 с |
uv побеждал в генерации lock-файла, которая происходит реже. Poetry показывал себя лучше в более частом сценарии: установке по уже готовому lock-файлу.
Почему uv так медленно устанавливал пакеты в этом конкретном окружении, мы не исследовали глубоко.
Результат стабильно воспроизводился на нашей локальной сети и в нашем окружении. Делать выводы о внутренних причинах поведения сетевого стека без отдельного профилирования было бы спекуляцией.
Практический вывод был простым: uv быстрее резолвит зависимости. Поскольку наша команда коммитит lock-файл в репозиторий и большую часть времени работает уже с готовым графом зависимостей, эта разница в скорости почти не ощущалась.
Миграция не выглядела оправданной, поэтому мы завершили эксперимент и остались на Poetry.
Trivy не оставил нам выбора
Через некоторое время вопрос пакетного менеджера вернулся снова, уже не как эксперимент с производительностью, а как требование со стороны безопасности.
В пайплайнах мы используем Trivy для проверки зависимостей на уязвимости. В какой-то момент стало понятно, что оставаться на Poetry 1.7.1 дальше нельзя.
Проблема была не в нашем коде, а в собственных транзитивных зависимостях Poetry и связанных с ним компонентах. Trivy начал стабильно подсвечивать проблемы безопасности, нарушавшие наши внутренние регламенты.
Обновление до Poetry 2.x из задачи с низким приоритетом превратилось в срочную необходимость.
И вот здесь начались действительно неприятные сюрпризы.
Сам по себе Poetry 2 работал нормально, но изменилась логика работы с источниками пакетов.
В CI у нас используется внутренний Nexus и приватные пакеты из корпоративного GitLab, поэтому в pyproject.toml пришлось добавить описание этих репозиториев:
Ini, TOML
[[tool.poetry.source]]
name = "company-nexus"
url = "https://nexus.example.com/repository/pypi/simple"
priority = "primary"
Эта настройка имела смысл для CI, где пайплайнам нужны явные объявления источников для внутренних пакетов.
Добавление [[tool.poetry.source]] привело к тому, что Poetry стал обращаться к этим внутренним индексам при каждой операции lock и install, в том числе при локальной разработке.
Обычные локальные команды вроде poetry lock или poetry install начали делать сетевые запросы во внутренний Nexus прямо во время повседневной работы.
Это создало организационные сложности:
Разработчики не всегда были подключены к корпоративному VPN.
Внутренний DNS по умолчанию был доступен не на всех локальных машинах.
Локальная разработка внезапно стала требовать постоянно включенного корпоративного VPN.
Главный вопрос состоял уже не в том, быстрее ли uv, а в том, что привычный процесс работы с Poetry перестал быть удобным.
Временное решение: свой парсер poetry.lock
Нам не хотелось сразу менять весь стек.
Poetry все еще был удобен для локальной работы при условии, что мы сможем обойти проблему с внутренними репозиториями в CI. Мы попробовали устранить ограничение без полной миграции.
План был простой: оставить Poetry для локальной разработки, убрать его со стейджинга и ставить пакеты через обычный pip.
Для этого требовалось только одно: научиться превращать poetry.lock в обычный requirements.txt.
Готового инструмента под наши требования не нашлось. Команды вроде poetry export все равно требовали Poetry и тащили за собой те же транзитивные зависимости, на которые ругался Trivy. Я написал небольшой скрипт-парсер для этого преобразования.
Схема работы выглядела так:
Разработчики продолжают работать локально через Poetry.
Обновленный poetry.lock коммитится в репозиторий.
На стейджинге скрипт преобразует lock-файл в requirements.txt.
Зависимости устанавливаются через стандартный pip install.
На бумаге схема выглядела надежно.
Она сохраняла привычную локальную разработку, убирала алерты Trivy на транзитивные зависимости Poetry, выводила Poetry из пайплайна развертывания и обходила проблемы с обращением к первичным источникам.
Однако самописный парсер добавлял накладные расходы.
Перед каждым запуском пайплайна системе приходилось:
Распарсить формат lock-файла.
Сгенерировать корректный requirements.txt.
Проверить совместимость формата.
Поддерживать код парсера на случай будущих изменений в структуре lock-файла.
Код парсера был небольшим, но это создавало дополнительный технический слой между проектом и установкой зависимостей.
Выполнение оказалось медленнее, чем хотелось. Время уходило на генерацию requirements.txt, а затем на этап установки через pip.
Решение ощущалось как временный костыль, а не постоянная стратегия. Это заставило нас снова оценить uv как быстрый установщик для CI-пайплайнов.
Второй шанс для uv
Когда мы вернулись к uv, наша цель изменилась.
При первой оценке мы смотрели на процесс глазами разработчика: насколько быстро поднимается локальное окружение, насколько удобны команды и стоит ли замена Poetry потраченных усилий.
Теперь задача стала конкретной и прагматичной: может ли uv просто быстрее ставить пакеты в пайплайне?
Мы сознательно не меняли сам сценарий пайплайна. Набор устанавливаемых зависимостей, порядок выполнения, этапы пайплайна и группы зависимостей остались точно такими же, как в версии с Poetry.
Мы обновили только слой подготовки окружения, заменив вызовы Poetry на uv sync.
Мы встроили uv на стейджинг через легкий bootstrap-скрипт и отдельный шаг синхронизации:
Bash
. ./tools/bootstrap_uv.sh && ./tools/uv_sync.sh
Для отдельных групп инструментов, например линтеров, запускали выборочную установку групп:
Bash
. ./tools/bootstrap_uv.sh && ./tools/uv_sync.sh --only-group linters
Затем выполнялись стандартные шаги проверки:
ruff format и ruff check
mypy
pytest
Проверки Sonar
Сканирование безопасности Trivy
Время выполнения существенно сократилось.
Мы прогнали по 30 запусков пайплайна для обоих вариантов и сравнили полное время работы всех этапов.
Средние результаты по времени:
Инструмент | Среднее время пайплайна |
Poetry 1.7.1 | 5 мин 10 с |
uv 0.11.8 | 2 мин 50 с |
Общее время выполнения пайплайна сократилось примерно на 45% (с 5 минут 10 секунд до 2 минут 50 секунд), что означает прирост эффективности выполнения на 80%.
Это был ощутимый практический выигрыш, а не мелкая разница в бенчмарках: обратная связь приходила быстрее, код быстрее доезжал на стейдж, а цикл правок сократился.
Причина была очевидной.
При локальном тестировании мы запускали uv в условиях нестабильного доступа к внешним индексам и сравнивали его с оптимизированным Poetry.
В CI доступ по сети к внутреннему Nexus был быстрым и стабильным, что позволило реализации uv на Rust проявить себя в полной мере.
Наша первоначальная оценка практической пользы uv была преждевременной.
Полный переход на uv
После результатов в CI стало понятно, что первое впечатление от uv исказили условия локального тестирования.
Сначала uv казался непривычным из-за другого синтаксиса команд, измененной конфигурации pyproject.toml и иной модели работы с зависимостями по сравнению с Poetry.
Например, команда uv sync выглядела непривычно, так как Poetry разделяет генерацию lock-файла и установку зависимостей на разные команды, тогда как uv выполняет их за один шаг.
Когда uv начал стабильно работать в CI и давать постоянный прирост по скорости, держать отдельный инструмент только для локальной разработки стало нерационально.
Использование двух инструментов означало бы поддержку параллельных схем:
Poetry для локальной разработки
uv для CI-пайплайнов
Такой подход требовал:
Поддерживать два набора инструкций к проекту.
Отслеживать два набора команд.
Управлять возможными расхождениями между локальной средой и CI.
Эта сложность была лишней.
Если uv надежно работал в CI, продолжать использовать Poetry локально только из привычки не имело смысла.
Мы перевели локальную разработку на uv. Это решило и проблему с VPN: локально uv забирает пакеты напрямую из публичных реестров без постоянного подключения к корпоративному VPN, избегая обращения к первичным источникам, как в Poetry 2.
Адаптация к uv прошла гладко, как только мы перестали сравнивать каждую команду напрямую с Poetry.
Теперь проект использует единый подход к управлению зависимостями, гарантирующий одинаковое поведение окружения локально и в CI.
Выводы
Наш переход проходил по следующим шагам:
Протестировали uv локально.
Не увидели достаточного выигрыша локально и остановили миграцию.
Уперлись в ограничения безопасности и конфигурации Poetry 2.
Сделали временный обходной скрипт для poetry.lock.
Повторно протестировали uv целевым образом внутри CI-пайплайнов.
Заметно сократили время выполнения пайплайна.
Стандартизировали uv для локальной среды и CI.
Если бы мы остановились на первом бенчмарке, мы бы остались на Poetry и полностью пропустили uv.
Когда инструмент дошёл до реальной эксплуатации, выигрыш проявился совсем не там, где мы его искали на старте.
Если инженерный инструмент не дает ожидаемого эффекта с первой проверки - это еще не значит, что он не работает. Часто сценарий теста просто не попал в настоящее узкое место процесса.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.