00:00
← Все статьи

uv в проде: скорость реальна, интеграция не бесплатна

uv 0.9+ · ~90 дней в проде

uv, пакетный менеджер Astral, в бенчмарках быстрее pip в 10-100 раз. У нас на CI вышло скромнее, около 10x. Мне кажется, тут никто не удивлён, скорость заявлялась как основная фича.

Дисклеймер. С Astral я никак не связан. Всё, что ниже, это внутренний опыт VK и собственные замеры на нашей же инфре (кто я, всё в резюме). Чужие маркетинговые цифры не переписываю.

Вердикт, чтобы он был сразу, а не где-то внизу под спойлером: uv оставляем, откатываться не собираемся, скорость того стоит. Но обещание про скорость и есть самая лёгкая часть сделки. Дорогими оказались несколько неочевидных изменений в поведении. За почти каждым стоит одна и та же причина: uv намеренно строже pip. Не баг. Чаще всего верное и осознанное решение, но это не мешает спотыкаться при уходе.

И да, версия проставлена специально. uv движется быстро, конкретные флаги, env'ы и дефолты к моменту прочтения вполне могли измениться.

Скорость, если коротко

Цифру, которую любят все, приведу один раз и пойду дальше. На нашем CI шаг установки ужался с ~60 до ~3-5 секунд. Честные ~10x на этом шаге, заметные на первой же сборке.

На этом про скорость всё. Она реальная, она приятная, она досталась почти даром. Дальше идёт то, что даром не досталось.

Пять особенностей, по заметности

Отсортировал не по тому, когда прилетело, а по тому, насколько громко. Сверху то, что ломает заметно и сразу. Снизу то, что может не работать молча, пока кто-нибудь случайно не заметит.

1. uv меняет дефолт pip по месту установки пакетов

pip, если не активен ни один venv, спокойно ставит пакеты в глобальное или пользовательское окружение. uv так не делает. Нет venv? Тогда либо активируй его, либо скажи явно: --python /path/to/python или --system. Флаг --user uv не понимает в принципе, отвергает сразу.

Забыл активировать venv? uv не угадывает за тебя и не лезет в систему молчком, а просто останавливается:

error: No virtual environment found; run `uv venv` to create an environment, or pass `--system` to install into a non-virtual environment

Было: «клиент просто запускается как раньше». Стало: «клиенту нужен активированный venv или явные флаги». Звучит мелко, пока это не двадцать разных сервисов/образов/репозиториев и несколько скриптов, которые предполагали установку через --user или неявно глобальную.

Идеология понятна - глобальные установки реально портят систему, и pip тут добрее. Но цена ухода настоящая.

2. Авто-установка Python спотыкается о файрвол (но исправимо)

uv умеет сам скачать нужный интерпретатор, тянет он его по запросу из astral'овского python-build-standalone. Казалось бы - теперь есть универсальный способ получить python нужной версии в образе любого дистрибутива. Удобно ровно до первого корпоративного периметра. Нет доступа наружу? Нет и Python. На ноутбуке с интернетом ты даже не заметишь, а в закрытом CI оно встанет.

error: Failed to install cpython-3.12.12-linux-x86_64-gnu
  Caused by: Request failed after 3 retries
  Caused by: Failed to download https://github.com/astral-sh/python-build-standalone/releases/download/20260203/cpython-3.12.12%2B20260203-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz
  Caused by: error sending request for url (https://github.com/astral-sh/python-build-standalone/releases/download/20260203/cpython-3.12.12%2B20260203-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz)
  Caused by: operation timed out

Фикс есть, и он штатный. Переменная UV_PYTHON_INSTALL_MIRROR переписывает адрес, откуда uv берёт релизы, на ваше внутреннее зеркало (для PyPy исторически отдельная переменная, CPython'овская его не покрывала). То же самое кладётся в [tool.uv], чтобы не зависеть от того, что выставлено в окружении.

Зеркало PyPI это не покроет: python-build-standalone живёт вне индекса пакетов, так что ему нужно отдельное зеркало. У нас это решилось без экзотики: подняли в Nexus проксирующий репозиторий для этих релизов и направили UV_PYTHON_INSTALL_MIRROR на него. Рядом всё равно настраивается HTTP(S)_PROXY и внутренний индекс пакетов, без этого история неполная.

3. Lock: параллельные установки теперь сериализуются

Кеш у uv concurrency-safe: append-only, безопасен для параллельных читателей и писателей. Блокируется venv на время установки и сборку отдельных артефактов, чтобы их не перезаписали на полпути. pip так не делал, и параллельные установки в одно окружение у него теоретически могли сломать друг друга.

× Failed to download and build `pyyaml==6.0.3`
├─▶ Failed to acquire lock on the distribution cache
├─▶ Could not acquire lock
╰─▶ Timeout (300s) when waiting for lock on `/var/lib/jenkins/.cache/uv/sdists-v9/pypi/pyyaml/6.0.3` at `/var/lib/jenkins/.cache/uv/sdists-v9/pypi/pyyaml/6.0.3/.lock`, is another uv process running? You can set `UV_LOCK_TIMEOUT` to increase the timeout.

У нас это вылезло там, где не ждали. На одном Jenkins-раннере может крутиться несколько джоб за раз. Раньше pip на это было плевать. Теперь они упираются в один lock, встают в очередь и начинают ловить таймауты.

Можно сказать, что так честнее: pip ведь мог сломаться тихо, а тут хотя бы видимый таймаут. Только на практике эти таймауты у нас стреляли заметно чаще(пару раз в день), чем когда-либо выстрелил бы исходный гипотетический race. Вывод простой: либо стараешься сократить параллельный доступ, либо поднимаешь UV_LOCK_TIMEOUT. Мы сделали и то, и другое.

4. Порядок индексов: first-index по умолчанию

uv по умолчанию ходит в режиме first-index: берёт версии только из первого индекса, где пакет вообще нашёлся, и дальше не ищет. pip так не умеет, pip собирает кандидатов отовсюду и выбирает «лучшего». Звучит как регресс по удобству. На деле это защита от dependency confusion, той самой атаки, на которой в декабре 2022 погорел torchtriton: подсовываешь в публичный индекс пакет с именем внутреннего, и сборка тянет чужое вместо твоего.

Вернуть pip-подобное поведение можно через --index-strategy или UV_INDEX_STRATEGY. Вариантов «небезопасно» тут, кстати, два, а не один: unsafe-first-match и unsafe-best-match. Второй ближе всего к pip, и он снова даёт возможность dependency confusion.

У нас это осталось теорией: репозитории корпоративные, проблемы dependency confusion решаются на уровне выше, так что на проде мы просто вернули привычное поведение. Но мотив дефолта понятен и, в целом, верный. Ровно это пытаются стандартизировать в PEP 766.

5. Компиляция в байткод выключена по умолчанию, холодный старт медленнее pip

pip при установке компилировал .py в .pyc сразу. uv по умолчанию этого не делает: компиляция ленивая, при первом импорте. На практике это значит, что первый запрос к свежезадеплоенному сервису чуть медленнее, пока байткод не прогрелся. В проде с этим столкнуться не успели - этот момент поймал на чтении доки. Ну, если быть честным, подобных latency-critical Python-сервисов в моей сфере ответственности нет.

Фикс на один флаг: --compile-bytecode или UV_COMPILE_BYTECODE=1. Astral прямым текстом советуют включать его в Docker-сборках: сборка станет чуть дольше, зато прод не платит этот штраф на каждом холодном старте. А раз включать его всё равно советуют для всего, что идёт в прод, трудно не заметить, что «выключено» по умолчанию заодно не портит ту самую цифру в бенчмарке установки, которую все и меряют...

Что не дотянуло до отдельного раздела (не споткнулись об это), но знать стоит: build isolation по PEP 517 включена по умолчанию.

Что в плюсе, помимо скорости

Помимо скорости uv привёз вполне осязаемые вещи.

  • Скорость. Да. Ха-ха, опять она, ~10x. Я упоминал, что утилита написана на Rust? Самое время упомянуть.
  • Управляемый Python. Штука честно хорошая, но со звёздочкой: хорошая после того, как поднято зеркало. До зеркала это боль из пункта 2, после зеркала «один раз настроили и забыли».
  • Сборка и публикация через uv. Часть пакетов перевели на сборку и публикацию командами uv: легко заменили Twine, один инструмент на месте нескольких.
pip, pip-tools, virtualenv, pyenv, pipx и twine - каждый заменяется одной командой uv.
Шесть привычных утилит, и все теперь одна команда uv. Не главная причина переезжать, но приятная.

Итог

Брать, если у вас сколько-нибудь живое Python-окружение (относительно часто обновляетесь и не застряли в прошлом) и болит время установки: выигрыш реальный, окупается быстро, не пожалели. Подождать (точнее, заложить время на интеграцию), если сидите в закрытом контуре с приватными индексами.

Правило. Почти каждый дефолт uv, который удивил, оказался намеренной строгостью. Так что миграция это «найти все места, где старый workflow опирался на доброту pip». uv добрым не будет.

За три месяца я не нашёл ни одного сюрприза, который оказался бы просто кривым решением. Немного неприятно, но полезно.

И последняя особенность, уже не про код: Astral, венчурный стартап без публичной модели монетизации, недавно куплен OpenAI. Проект открытый, но держать в уме стоит.

Оценка миграции: 8.5/10. Полтора балла снял не за uv, а за специфику собственного контура, который пришлось подготавливать.