Сборка падает из-за недоступного зеркала: делаем CI устойчивым
Пайплайн падает без единой строчки изменённого кода — просто потому что apt update, pip install или npm ci не смогли достучаться до внешнего репозитория. Через десять минут ретрай проходит, и все делают вид, что ничего не было, пока сборка не упадёт снова в самый неподходящий момент. Разберём, как отличить реальную сетевую проблему от случайной, диагностировать её быстро и построить CI так, чтобы одно недоступное зеркало не решало, поедет релиз сегодня или нет.
Содержание
Симптомы: как выглядит падение из-за зеркала
Проблема с внешним репозиторием редко называет себя прямо. Обычно в логах CI видно что-то из этого набора:
Could not connect to deb.debian.org:80
Temporary failure in name resolution
Failed to fetch http://archive.ubuntu.com/ubuntu/... 403 Forbidden
npm ERR! network request to https://registry.npmjs.org/... failed
E: Unable to fetch some archives, maybe run apt-get update
Ключевой признак именно инфраструктурной проблемы, а не бага в коде или манифесте зависимостей — сборка падает не детерминированно. Один и тот же коммит то проходит, то валится с ошибкой сети; падения кучкуются по времени суток (пиковая нагрузка на зеркало) или по географии раннера (один регион работает, другой — нет). Если пересборка того же коммита без единого изменения то проходит, то падает — с вероятностью 90% дело не в коде, а в доступности внешнего ресурса.
Второй признак — падение именно на этапе установки зависимостей, а не на этапе тестов или сборки артефакта. Это легко перепутать, если лог CI склеен в одну простыню: ищите шаг install, restore, fetch или pull — обычно именно там появляется таймаут, а не в шаге build или test, который просто не успевает начаться.
DNS, TCP или HTTP: где именно рвётся
«Зеркало недоступно» — слишком общая формулировка, чтобы что-то чинить. На практике есть минимум четыре разных проблемы, которые выглядят похоже в логе, но лечатся по-разному:
- DNS не резолвится. Ошибка вида
Temporary failure in name resolution,Could not resolve host. Раннер не может превратитьdeb.debian.orgв IP — либо упал резолвер CI-инфраструктуры, либо сработала блокировка на уровне провайдера/страны именно для DNS-запросов к конкретному домену. - TCP-соединение не устанавливается или рвётся. DNS отработал, IP есть, но
connect()висит до таймаута или получаетConnection refused/Connection reset by peer. Это провал на транспортном уровне: пакеты режутся файрволом где-то по пути, порт закрыт, или сам сервис зеркала лёг. - TLS-хендшейк не проходит. Соединение установилось, но упало на уровне сертификата —
SSL: CERTIFICATE_VERIFY_FAILED,handshake failure. Часто это признак MITM-перехвата на пути (в том числе легитимного корпоративного прокси с подменой сертификата) или банально просроченного/неправильного сертификата на самом зеркале. - HTTP-уровень отвечает, но с ошибкой. Соединение и TLS прошли, а сервер вернул
403,429(rate limit) или5xx. Здесь сеть ни при чём — либо вас забанили за частоту запросов, либо зеркало реально легло на своей стороне.
Разница критична, потому что решения не пересекаются: DNS-проблему не починит retry на уровне HTTP-клиента, а rate limit не вылечит смена DNS-резолвера.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПрактический чеклист диагностики
Когда сборка упала с сетевой ошибкой, прогоняйте эти шаги по порядку — каждый следующий имеет смысл, только если предыдущий не дал ответа:
- Смотрите точное сообщение об ошибке, а не общее «build failed». В логах CI полный текст ошибки обычно доступен по кнопке "raw log" — веб-интерфейс часто обрезает вывод.
- Проверьте, единичный ли это сбой. Перезапустите тот же джоб без изменений. Прошло — сетевая нестабильность. Падает снова — либо зеркало легло надолго, либо это не сеть вовсе.
- Проверьте DNS отдельно от HTTP:
dig +short deb.debian.org
dig +short deb.debian.org @8.8.8.8
Если системный резолвер не отвечает, а публичный (8.8.8.8) отвечает — проблема в DNS-инфраструктуре раннера, а не у зеркала.
- Проверьте TCP-доступность до хоста и порта:
nc -zv -w5 deb.debian.org 443
Таймаут здесь — это уже не DNS, а транспорт: файрвол, маршрутизация или сервис недоступен.
- Проверьте HTTP с полной трассировкой времени:
curl -v -o /dev/null -w "dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n" https://registry.npmjs.org/
Это разложит один вызов на составляющие: если time_namelookup большой — тормозит DNS, если разрыв между connect и tls — проблема на этапе хендшейка, если ttfb большой при быстром коннекте — сервер долго отвечает по существу.
- Проверьте, не локальная ли это проблема раннера. Запустите тот же
curlвручную на другом раннере или с локальной машины. Если там всё быстро — дело либо в конкретном раннере (испорченный/etc/resolv.conf, битый прокси в переменных окружения), либо в сетевом маршруте именно из инфраструктуры CI. - Проверьте, не единственный ли вы, кто жалуется. Для крупных публичных реестров (npm, PyPI, Docker Hub, GitHub) обычно есть статус-страницы. Если статус зелёный, а у вас не работает — вероятнее локальная/сетевая проблема или лимиты по вашему IP, а не глобальный инцидент.
- Зафиксируйте частоту и время падений (день недели, час, регион раннера) — если проблема периодическая, это почти всегда rate limit или пиковая нагрузка на зеркало в конкретные часы, а не случайность.
Этот чеклист стоит держать не в голове, а прямо в раннбуке рядом с CI — в момент, когда релиз горит, разбираться по памяти неудобно.
Свой прокси-кэш вместо надежды на внешнее зеркало
Диагностика объясняет, что сломалось, но не чинит зависимость от чужой инфраструктуры навсегда. Рабочее решение — прослойка между CI и внешним миром: прокси-кэш, который держит уже скачанные пакеты локально и обращается наружу только за тем, чего ещё нет.
Для системных пакетов и Java/Maven/npm/PyPI-артефактов эту роль хорошо закрывает Nexus Repository — он умеет проксировать сразу несколько типов репозиториев (npm, PyPI, Maven, apt, Docker) через один сервис и кэшировать всё, что через него прошло. Разворачивается он на обычном VPS; мы подробно разбирали процесс в статье про установку Nexus Repository на сервер.
Для Docker-образов та же логика реализуется через pull-through registry mirror — свой Docker registry, настроенный как зеркало Docker Hub, а не как хранилище собственных образов. CI обращается к своему registry, тот при промахе кэша сам идёт на Docker Hub и сохраняет результат для следующего раза. Частые грабли этой схемы (протухшие токены, неверные insecure-registries, путаница между режимом mirror и обычным registry) разобраны в статье про частые ошибки приватного Docker registry.
Плюс такой прослойки не только в устойчивости к падению внешнего зеркала — второй пакет с тем же именем и версией скачается за миллисекунды из локального кэша вместо повторного похода наружу, что заодно снижает время сборки. Минус — это ещё один сервис, который сам может стать точкой отказа, если его не бэкапить и не мониторить: кэш имеет свойство незаметно портиться, и такой инцидент мы разбирали отдельно — как повреждённый кеш зависимостей три недели портил сборки, пока никто не заметил. Прокси-кэш — это компонент инфраструктуры, а не «поставил и забыл».
Отдельный сценарий — когда пакета в официальном репозитории вашего дистрибутива попросту нет или он неприемлемо старый (типично для RHEL-совместимых дистрибутивов российской сборки). Тогда прокси-кэш не поможет, потому что кэшировать нечего — нужную сборку разбирали в статье про сборку своего RPM, когда пакета нет в репозитории.
Retry-логика: правильно и неправильно
Простейшая защита от единичного сетевого сбоя — повторить запрос. Но retry без ограничений и без задержки не столько лечит проблему, сколько усугубляет её: если зеркало легло от перегрузки, тысяча CI-джобов, синхронно ретраящих запрос раз в секунду, только продлевают ему агонию — и в ответ вы быстрее словите rate limit.
Разумная схема — экспоненциальная задержка с ограниченным числом попыток и джиттером (случайным разбросом), чтобы параллельные джобы не били по одному хосту синхронно:
# .gitlab-ci.yml — пример для apt внутри джоба
before_script:
- |
for i in 1 2 3 4; do
apt-get update && break
echo "apt update failed, attempt $i, retrying..."
sleep $((RANDOM % 5 + 2 ** i))
done
Для npm и pip retry-логика часто уже встроена, но не всегда включена по умолчанию с разумными параметрами:
# .npmrc
fetch-retries=4
fetch-retry-mintimeout=2000
fetch-retry-maxtimeout=30000
# pip.conf
[global]
retries = 4
timeout = 15
Важный нюанс: retry имеет смысл только для временных сбоев (таймаут, 5xx, 429 с Retry-After). Ретраить 403 или 404 бессмысленно — если пакета нет или доступ запрещён, повторные попытки просто тратят время сборки впустую, доводя её до общего таймаута джоба. Отдельно стоит явно выставлять таймаут на сам запрос — без него зависшее соединение может держать джоб часами, а не падать с понятной ошибкой; поведение таймаутов на прокси и почему их стоит настраивать осознанно, а не по умолчанию, разбирали в статье про обрывы соединений через прокси по таймауту.
Пиннинг версий и локальное кэширование в самом CI
Прокси-кэш и retry снижают частоту сбоев, но не устраняют зависимость от внешнего мира полностью, если каждая сборка всё равно скачивает всё с нуля. Два дополнительных приёма снижают саму потребность обращаться наружу.
Пиннинг версий — фиксация точных версий зависимостей в lock-файле (package-lock.json, poetry.lock, Pipfile.lock, Cargo.lock) вместо диапазонов версий в манифесте. Это не про доступность зеркала напрямую, а про предсказуемость: без lock-файла npm install может в любой момент подтянуть новую минорную версию, которая либо не собирается, либо требует пакет, которого ещё нет в вашем локальном кэше — и тогда сборка всё равно уходит во внешнюю сеть незапланированно. С зафиксированными версиями набор зависимостей стабилен, и прокси-кэш успевает прогреться под конкретный набор пакетов.
Кэширование зависимостей средствами самого CI — отдельный слой поверх прокси-кэша. Большинство CI-систем умеют сохранять директорию между запусками джоба:
# GitLab CI — кэш npm между джобами
cache:
key:
files:
- package-lock.json
paths:
- .npm/
- node_modules/
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
# GitLab CI — кэш pip
cache:
key:
files:
- requirements.txt
paths:
- .cache/pip
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
Ключ кэша, завязанный на хэш lock-файла, — важная деталь: кэш инвалидируется автоматически при изменении зависимостей и не тянет устаревшие версии в новую сборку. Для Docker-сборок аналогичную роль играет --cache-from и multi-stage кэширование слоёв, чтобы неизменившийся слой с зависимостями не пересобирался и не перекачивался заново при каждом запуске.
Разница между этим кэшем и прокси-кэшем Nexus/registry принципиальна: кэш CI живёт в рамках раннера или проекта и может быть холодным после смены раннера или очистки кэша, а прокси-кэш — общий постоянный сервис для всех проектов и раннеров сразу. Их стоит использовать вместе, а не как замену друг другу: CI-кэш убирает повторные закачки в рамках одного раннера, прокси-кэш — зависимость от внешнего зеркала в принципе.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Ретраи в CI сами по себе решают проблему нестабильного зеркала?
Частично и временно. Retry маскирует единичные сбои, но не спасает от продолжительной недоступности зеркала и не снижает нагрузку на внешний сервис — при системной проблеме количество ретраев только приближает вас к rate limit. Долгосрочное решение — свой прокси-кэш.
Обязательно ли поднимать Nexus, если у нас всего один язык зависимостей, например только npm?
Нет, для одного экосистемы можно обойтись более простым и лёгким прокси — например Verdaccio для npm или локальным pip index с зеркалированием. Nexus оправдан, когда нужно закрыть несколько типов репозиториев (apt, Docker, npm, Maven) одним сервисом.
Как понять, что виноват именно провайдер/страна, а не сам внешний сервис?
Сравните доступность с раннера в другом регионе или с VPS в другой стране. Если оттуда тот же ресурс отвечает быстро и стабильно, а с текущего раннера — нет, это указывает на маршрут или блокировку конкретно с вашей точки выхода, а не на глобальную проблему у сервиса.
Свой прокси-кэш гарантированно уберёт падения сборки?
Нет — он снижает частоту обращений наружу и добавляет буфер на время недоступности исходного зеркала, но если кэш ещё не прогрет под нужный пакет, первый запрос всё равно уйдёт во внешнюю сеть. Плюс сам кэш нужно бэкапить и мониторить, иначе он становится новой точкой отказа.
Что делать, если зеркало недоступно прямо сейчас, а релиз горит?
Быстрый обходной путь — временно переключить CI на альтернативное публичное зеркало (у большинства пакетных экосистем их несколько) через переменную окружения или конфиг, без изменения кода. Это тушит пожар здесь и сейчас; постоянное решение — прокси-кэш, чтобы больше не зависеть от одной точки.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →