Сборка вчера проходила, а сегодня внешний репозиторий не отвечает. Бывает и хуже: модуль, который собрал коллега, лежит только в его каталоге ~/.m2/repository, потому что до общего хранилища он так и не дошёл. Обе беды похожи по симптомам («не находит зависимость»), но лечатся разными частями Nexus. В первом случае нужен промежуточный склад для чужих библиотек. Во втором нужен общий дом для своих.
Стоит сразу различить эти две потребности, потому что от них зависят права, политика хранения и размер диска на VPS.
Две потребности, которые легко принять за одну
Первая потребность: не ходить за каждой чужой библиотекой в интернет, а один раз получить её и дальше отдавать из внутреннего сервера. Это скорее кеш на пути к внешнему источнику. Такой склад можно потерять и наполнить заново, если внешний источник снова доступен. Его содержимое определяется тем, что команда реально запрашивала, а не тем, что существует в мире.
Вторая потребность: публиковать то, что команда сделала сама. Внутренняя библиотека, общий parent POM, клиент к собственному сервису, плагин сборки. Эти файлы нигде больше не лежат. Если сервер с ними потерян, внешний источник их не восстановит, и вернуть их можно только пересборкой из исходников, если сборка воспроизводима.
Отсюда первое практическое следствие: у этих двух хранилищ разная цена потери. Потерянный proxy-кеш неприятен, но обратим, пока доступен источник. Потерянный hosted-репозиторий означает пропажу опубликованных артефактов. Поэтому относиться к ним надо по-разному, и в Nexus для этого есть разные типы репозиториев.
Путь одного запроса: где вмешивается proxy, а где hosted
Проследим за простым случаем. Разработчик запускает сборку, Maven видит в POM зависимость и должен найти для неё файл. Если в настройках клиента указан один адрес Nexus, запрос идёт именно туда. Что происходит дальше, зависит от устройства сервера.
Обычно на стороне Nexus для Maven собирают три вида репозиториев:
- proxy — смотрит на внешний репозиторий. Если файла у него ещё нет, он запрашивает его снаружи, сохраняет у себя и отдаёт клиенту. При следующем запросе той же версии отдаёт уже своё;
- hosted — хранит то, что в него опубликовали. Внешнего источника у него нет, поэтому отсутствующий файл он не найдёт нигде;
- group — объединяет несколько репозиториев под одним адресом, чтобы клиенту не приходилось перечислять каждый отдельно.
Group полезен не как украшение, а как способ упростить клиентскую настройку: в settings.xml прописан один адрес, а порядок опроса участников определяется на сервере. Но для публикации group не годится: деплой всегда адресуют в конкретный hosted-репозиторий. Это важная асимметрия. Скачивать можно через единый вход, а записывать нужно в строго определённое место.
Из асимметрии вытекает схема прав. Чтение зависимостей нужно всем сборочным машинам и разработчикам. Публикация нужна узкому кругу: чаще всего CI-задаче, которая собирает проверенный коммит. Если выдать право записи каждому, в репозиторий начнут попадать артефакты с ноутбуков, и вопрос «из какого коммита этот файл» потеряет ответ. Разумная раскладка: отдельная учётная запись или роль с правом чтения для людей и отдельная с правом публикации для CI. Права на изменение и удаление стоит держать ещё уже, чем права на деплой.
Обратите внимание, что proxy-репозиторий тоже не должен принимать публикации от команды. Если кто-то попытается сложить свой артефакт «в общий кеш», он смешает чужое и своё, и очистка потом может удалить именно его. Свои файлы живут только в hosted.
Разберём один запрос по шагам на конкретном примере. Новая сборка просит внешнюю библиотеку, которой в кеше ещё нет. Group опрашивает участников: hosted-репозитории отвечают, что такого файла у них нет, а proxy идёт во внешний источник, сохраняет результат и отдаёт его клиенту. Теперь та же версия лежит на сервере, и следующий разработчик получит её без выхода наружу. Со своим модулем всё наоборот: если он не был опубликован в hosted, ни один из участников group его не найдёт, и ошибка «не удалось разрешить зависимость» окажется честной. Внешний источник про ваш внутренний артефакт ничего не знает, и proxy тут не поможет.
Из этого следует практическая проверка прав. Учётная запись разработчика должна успешно читать через group и получать отказ при попытке деплоя в hosted. Учётная запись CI, напротив, должна уметь публиковать в нужный hosted-репозиторий. Если отказа нет, схема прав сложена неверно, и стоит разбираться сразу, пока в репозитории нет файлов с неизвестным происхождением.
Proxy-кеш не архив интернета
Распространённое ожидание: «поставим Nexus, и все зависимости останутся у нас навсегда». На деле proxy хранит только те файлы, которые через него уже запрашивали. Если библиотеку версии 2.3 никто не просил, её в кеше нет, и при недоступном источнике взять её неоткуда. То же касается побочных файлов: вы получили JAR, но в кеше нет, скажем, POM транзитивной зависимости, потому что ни одна сборка её ещё не разрешала.
Отсюда первая проверка честности вашей схемы: возьмите чистое окружение, например пустой локальный кеш, и соберите проект через Nexus. Всё, что скачалось, теперь в proxy. Но даже успешная сборка доказывает лишь то, что нужные файлы сейчас доступны, а не то, что они будут лежать вечно. Если для proxy настроена очистка, часть файлов может уйти, и тогда они снова будут запрошены снаружи. Одно существование кеша не делает сборку воспроизводимой.
Есть и другая сторона. Источник может со временем изменить или убрать версию. Proxy, уже получивший файл, продолжит его отдавать, пока файл не удалён политикой очистки или вручную. Это удобно, но не стоит принимать за стратегию хранения. Если какая-то внешняя библиотека критична для продукта, надёжнее осознанно решать, как вы её сохраняете, чем надеяться на побочный эффект чьей-то сборки.
Release и snapshot: файл, который не должен меняться, и файл, который меняется всё время
В Maven у артефакта есть версия. Если она заканчивается на -SNAPSHOT, это рабочая сборка, которая обновляется. Версия без этого суффикса считается релизом, и по договорённости опубликованный релиз больше не переписывают.
Разница важна для устройства хранилища. Релиз опубликован один раз и затем лишь читается. Snapshot публикуют снова и снова, и каждая публикация добавляет новый файл с временной отметкой и номером сборки, а клиенту по запросу 1.4.0-SNAPSHOT отдаётся самая свежая из них. В итоге один условный «адрес» версии скрывает цепочку из десятков и сотен физических файлов.
Поэтому и политика у них разная. Практичная схема — два hosted-репозитория, один для релизов, другой для снапшотов. Release-репозиторию запрещают перезапись, так что повторная публикация той же версии отклоняется. Для snapshot-репозитория перезапись и накопление — норма, и именно там нужна очистка. Если смешать оба типа в одном репозитории, придётся выбирать между «запретить повторную запись» и «разрешить snapshot», и оба выбора плохи.
Удобное следствие разделения: в клиентской конфигурации и в POM можно разделить, откуда разрешаются релизы и откуда снапшоты, а в блоке публикации указать два адреса, и Maven сам выберет подходящий по суффиксу версии.
Почему снапшоты разрастаются и что нельзя вычищать
Допустим, условный проект собирается в CI на каждый коммит в основную ветку и публикует snapshot библиотеки. Возьмём такие числа для расчёта: 40 публикаций в рабочий день, на каждую по 6 МБ суммарно (JAR, POM, исходники и контрольные суммы). Это сценарий для арифметики, а не измерение.
За день набегает 40 × 6 = 240 МБ. За 22 рабочих дня получается 5 280 МБ, то есть около 5,2 ГБ в месяц от одной библиотеки. Если таких библиотек пять и у всех похожий ритм, это уже около 26 ГБ в месяц. За полгода без очистки выходит порядка 150 ГБ. Причём настоящая ценность почти всех этих файлов близка к нулю: через неделю никто не вспомнит, что нужен был именно снапшот вторника.
Теперь наоборот: что очистка может уничтожить по ошибке. Предположим, QA протестировала конкретную сборку, и именно её собираются выкатить. Если в зависимостях релизной сборки продукта указан -SNAPSHOT, то при каждом запросе она получает самое свежее. А если сборка закреплена конкретным идентификатором, например версией с отметкой времени вида 1.4.0-20250115.101530-17, повторный запрос той же версии вернёт тот же файл при условии, что файл не удалён. Значит, сохранность воспроизводимой сборки определяется двумя вещами: вы запрашиваете её по точному идентификатору, а очистка не успела её убрать. Поэтому правило хранения должно оставлять те сборки, на которые кто-то ссылается.
Разберём это на примере с двумя вариантами записи зависимости. В первом POM указана версия 1.4.0-SNAPSHOT. Во вторник QA проверила одну сборку, в среду CI опубликовал новую, и тот же запрос 1.4.0-SNAPSHOT теперь возвращает среду. Протестированный вторничный файл перестал быть тем, что вы получаете, хотя на диске он, возможно, ещё лежит. Во втором варианте указан точный идентификатор с отметкой времени и номером сборки. Запрос вернёт тот же файл, пока очистка его не убрала. Поэтому вместе с отчётом QA нужно записывать именно этот идентификатор, а не общее имя snapshot-версии.
Но и точный идентификатор не гарантирует воспроизводимость сам по себе. Если политика удаляет компоненты старше 30 дней, а выкладка планируется через 45, запрос закончится ошибкой. Поэтому срок хранения snapshot должен быть заведомо длиннее самого долгого цикла от тестирования до выкладки, либо такую сборку нужно оформить релизом, как описано выше. Заодно полезно проверить, что вместе с основным JAR сохранены POM и прочие файлы этой версии: сборка разрешает их все, а не только архив.
Практический выход: то, что действительно нужно воспроизводить, переводить из snapshot в релиз. Если сборка достойна выкладки, пусть у неё будет настоящая версия в release-репозитории, где перезапись запрещена и очистка по возрасту не применяется. Snapshot-репозиторий остаётся рабочим «черновиком», который можно подчищать смело.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Как работает очистка в Nexus и где она подводит
Политики очистки в Nexus Repository описываются как набор критериев, которые назначаются репозиториям. Они применимы и к hosted, и к proxy, поэтому ими можно регулировать оба склада. Подробности и названия пунктов интерфейса зависят от версии и редакции, а точный перечень критериев нужно смотреть в своей установке; описание функции есть в документации Sonatype по cleanup policies.
Три особенности стоит держать в голове, потому что именно они объясняют типичное «политику создали, а диск не освободился».
- Политика сама по себе ничего не запускает. Для её выполнения нужно отдельное задание по расписанию. Создали политику, назначили репозиторию, но задание не настроили, и ничего не произойдёт.
- Несколько критериев работают совместно. Если в одной политике указать, например, возраст и шаблон имён, под удаление попадут только компоненты, удовлетворяющие обоим условиям сразу. Это легко принять за «или» и удивиться, почему кандидатов меньше ожидаемого. Если нужна логика «или», это отдельные политики.
- Очистка делает мягкое удаление. Файлы помечаются удалёнными, но место на диске возвращается только после операции compact blob store. Следовательно, на VPS с ограниченным диском нужны оба шага в расписании: сначала очистка, потом уплотнение хранилища блобов.
Ещё одна деталь: предварительный просмотр показывает кандидатов на удаление, но итог реального запуска может отличаться. Между просмотром и запуском в репозиторий могут прийти новые публикации, а состояние метаданных может измениться. Поэтому просмотр хорош как проверка здравого смысла («не попадает ли сюда то, что нельзя трогать»), но не как точный прогноз освобождённого места.
Из этих трёх особенностей складывается порядок настройки, который удобно проверить на небольшом репозитории. Сначала назначьте политику snapshot-репозиторию и посмотрите список кандидатов в preview: в нём не должно быть сборок, закреплённых в чьих-то отчётах. Затем запустите задание очистки и убедитесь, что компоненты исчезли из выдачи. После этого проверьте занятое место: оно, скорее всего, почти не изменится. Только после задания compact blob store диск начнёт освобождаться. Если пропустить последний шаг, легко решить, что очистка не работает, и начать удалять файлы вручную, а это уже грубое вмешательство в хранилище.
Задания стоит ставить в расписании не одновременно, а друг за другом: сначала очистка, затем уплотнение. Если уплотнение стартует раньше, оно просто не увидит файлов, помеченных удалёнными только что, и место вернётся лишь при следующем цикле. Интервал между заданиями подбирайте по длительности очистки на вашем объёме данных; это число нужно замерить на своей установке, из документации его не получить. Подробности о самих политиках смотрите в описании cleanup policies.
Как сложить правила для двух разных складов
Для snapshot-репозитория логика обычно строится на возрасте и количестве копий. Нужно сохранить свежие сборки, потому что разработчики ещё могут их использовать, а всё старое отпустить. Пример условных правил: удалять snapshot-компоненты, которым больше 30 дней. Если команда держит открытыми долгие ветки, срок увеличивают, и тогда в расчёте выше диск растёт пропорционально: при 30 днях в хранилище остаётся около полутора месяцев работы, а при 90 днях втрое больше.
Для proxy-кеша правило иное. Здесь удалять разумно то, что давно не запрашивали: критерий вроде давности последнего скачивания (если он доступен в вашей версии) отражает суть кеша лучше, чем возраст публикации. Но надо помнить, что удалённый из proxy файл при следующем запросе потребует внешнего источника. Если именно сегодня источник недоступен, сборка встанет. То есть агрессивная очистка proxy сокращает ту самую страховку, ради которой он поставлен.
Для release-репозитория, как правило, самое безопасное — не применять автоматическую очистку вовсе или применять её только с явным пониманием, какие версии вам больше не нужны. Удаление опубликованного релиза ломает любые сборки, которые на него ссылаются.
Диск: считаем рост по своим публикациям
Планировать место на VPS удобнее не по «среднему Nexus», а по собственному потоку публикаций и по тому, как часто вы чистите. Для каждого типа хранилища получается своя формула.
| Хранилище | Что растёт | Что сдерживает рост |
|---|---|---|
| Proxy | Набор уникальных чужих версий, которые когда-либо запрашивали | Очистка по давности запроса; сокращение числа разных версий в проектах |
| Hosted snapshots | Число публикаций × размер одной сборки | Очистка по возрасту; перевод нужных сборок в релизы |
| Hosted releases | Число выпущенных версий × размер | Решение команды о том, какие версии больше не поддерживаются |
Возьмём тот же условный пример: 26 ГБ в месяц на снапшоты пяти библиотек. Если очистка оставляет 30 дней, установившийся объём около 26 ГБ плюс то, что ещё не освобождено до compact. Это уже нельзя забывать: между очисткой и уплотнением диск остаётся занятым, и запас по месту должен покрывать этот зазор. Без политики к концу полугода выходит около 150 ГБ, и на тарифе с 160 ГБ NVMe диск оказывается заполнен целиком, ещё до учёта операционной системы, самого Nexus и proxy-кеша.
Для сравнения с ресурсами VPS: у MAATRIX в публичном каталоге есть тарифы от Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) до Djed (16 vCPU, 32 ГБ RAM, 320 ГБ NVMe); промежуточные Uab, Heka и Maat дают 80, 160 и 240 ГБ диска. Это размеры ресурсов, а не обещание ёмкости под конкретное число артефактов. Практически вывод такой: для небольшой команды с умеренным потоком снапшотов и работающей очисткой диск обычно важнее процессора, а при бесконтрольных снапшотах любой диск рано или поздно кончится. Оперативная память нужна самому Nexus как Java-приложению, поэтому маленький тариф стоит выбирать осторожно и проверять поведение под реальной нагрузкой. Развёртывание на сервере с root-доступом, NVMe и выбором расположения — UK, US, RU, FR — подробно разобрано в статье про установку и настройку Nexus Repository на VPS; доступность ОС и образов уточняйте в форме заказа.
Чтобы расчёт опирался на факты, а не на мои условные 6 МБ, достаточно два-три дня посмотреть на реальные публикации: сколько их в день и сколько весит типичный набор файлов одной сборки. Умножьте на срок хранения, добавьте запас под период между очисткой и compact, и получится рабочая оценка для вашей команды.
Сборка на новой машине как проверка всей схемы
Лучший тест всего описанного прост. Берёте новую рабочую машину или чистый контейнер без локального кеша Maven. В settings.xml прописан единственный адрес group-репозитория и учётные данные с правом чтения. Запускаете сборку основного проекта.
Если всё сложено правильно, происходит следующее. Внешние библиотеки приходят через proxy: уже лежащие в кеше отдаются сразу, недостающие запрашиваются снаружи и остаются на сервере. Внутренние модули приходят из hosted: релизы по зафиксированным версиям, снапшоты по последним публикациям CI. Ничего не берётся с чьего-то ноутбука, потому что ничьего ноутбука в цепочке нет. А когда нужная сборка не находится, причина видна сразу: либо её не публиковали, либо её убрала очистка, либо до источника не достучаться. Каждая из этих причин лечится своим понятным действием, а не поиском того, у кого в локальном кеше лежит единственный нужный JAR.