Большие старые проекты обычно живут по своим законам.
Ты уже не спрашиваешь, почему именно так, — просто делаешь свою часть работы и стараешься ничего не сломать.
Наш проект был именно таким: монорепозиторий, десятки микросервисов, сотни зависимостей и общие библиотеки для всего подряд. В кодовой базе было около 220 Python-пакетов и примерно 70 Docker-контейнеров, которые собирались из них. Всё хранилось в одном репозитории, а пайплайн для pull request’ов проходил через Azure TFS. Он был разделён на два больших этапа.
На первом прогонялись линтеры, юнит-тесты и выполнялась сборка контейнеров. На втором — запускались интеграционные тесты. Чтобы не собирать все образы каждый раз, использовался скрипт, который с помощью git diff определял, какие пакеты изменились, и по зависимостям вычислял, какие образы нужно пересобрать. Юнит-тесты запускались всегда полностью — это помогало ловить регрессии, даже если код не пересобирался.
Эта схема действительно экономила ресурсы, но была уязвима в одном месте: все образы всё равно собирались последовательно. Если затронуто два-три контейнера — всё быстро. Если десятки — процесс превращается в длинное ожидание. В худшем случае полная сборка с интеграционными тестами занимала до четырёх часов. На каждом этапе стояло ограничение в два часа, и если тесты ещё успевали завершаться, то сборка нет — пайплайн просто прерывался по таймауту, и до тестов иногда дело не доходило. Понятно, что всегда можно было поднять это ограничение, но оно как раз таки нам напоминало, что всему есть предел и это была та красная черта, которую мы не должны были пересекать из соображений здравого смысла
После нескольких таких случаев мне поручили разобраться и ускорить процесс, не ломая существующую архитектуру пайплайна.
До этого мы решали проблему привычным способом — просто просили у DevOps-команды “пожирнее” сервера. Это действительно помогало: добавляли пару ядер, больше памяти — и какое-то время всё укладывалось в лимит. Но эффект быстро заканчивался.
Через пару месяцев мы снова упирались в ту же стену: 2–3 сервера из общего пула стабильно вылетали за лимит в два часа. В какой-то момент стало понятно, что бесконечно наращивать железо нельзя — и в этот раз решили подойти к проблеме по-другому.
Сначала я попробовал классические способы: оптимизация Dockerfile, кеширование слоёв, очистка зависимостей. Помогало — но незначительно.
Основная проблема была в том, что все сборки шли на одном агенте, и в какой-то момент просто упирались в физические ресурсы. Решением стало распределить процесс. Я предложил использовать Docker Swarm не как систему оркестрации для продакшена, а как “кластер сборки”, способный параллельно собирать образы на нескольких нодах.
Чтобы не переписывать весь пайплайн, в TFS добавили отдельный агент-диспетчер, который управлял логикой сборки. Он проверял, нужно ли делать полную сборку или частичную, и решал, как именно её запустить.
Вот логика этого процесса, если описать её схематично:

После внедрения параллельной сборки время этапа docker build сократилось с полутора часов до 40 минут.
CPU на агентах перестал простаивать, кэш стал использоваться эффективнее, а разработчики — меньше ждать. Среднее время PR сократилось примерно на два часа.
Если раньше на проверку и доработку одной задачи могло уходить полдня, теперь цикл стал заметно короче — “зелёные флаги” появлялись гораздо раньше, а ревью стало проходить быстрее.
Через некоторое время к этому решению добавили ещё одно небольшое улучшение: в пайплайн добавили проверку количества изменённых образов.
Если их оказывалось больше N (например, 40), сборка автоматически запускалась в параллельном режиме. Если меньше — по старинке, локально. Это помогло сбалансировать нагрузку и не занимать кластер по пустякам.
До оптимизации:
Линтеры и тесты — 20 мин
Полная сборка — 90-100 мин
Интеграционные тесты — 60–90 мин
После:
Линтеры и юнит-тесты — 20 мин
Полная сборка — 40-50 мин
Интеграционные тесты — 60–90 мин
→ Средний PR ускорился на ~2 часа
Самое главное - код микросервисов при этом не менялся вообще, подход к управлению пакетами и Docker-образами тоже — всё улучшение было только на уровне CI. Но именно это дало наибольший эффект. Пожалуй, это тот случай, когда простое распределение задач действительно ускорило работу всей команды.
? P.S.
На моей практике это было самое большое ускорение не в коде, а в том, как мы его собираем.
?? Сергей Наталенко
Backend Developer / Python & Go
Комментарии (2)

Hackitect7
11.11.2025 07:22Интересно написано, живо и по делу. Проблема реально знакомая - когда вроде всё вылизано, а сборки всё равно полдня тянутся. Хорошо, что автор не пошёл по пути “накидаем железа и успокоимся”, а копнул в архитектуру пайплайна.
Но, если честно, не совсем ясно, насколько стабильно это всё работает в реальной нагрузке. Параллелка - штука капризная, особенно с кешем и зависимостями. Не ловили ли вы ситуации, когда один сервис пересобрался не с тем артефактом или старым образом?
В целом, статья хорошая, но хочется чуть больше реальных цифр и подводных камней, а не только общий итог “стало быстрее”. Вот тогда бы разговор получился ещё интереснее.
baldr
Наверное вы молодец, но, честно говоря, статья выглядит просто как "я сделяль", без каких-либо подробностей: "Я ускорил что-то на 2 часа, вот ссылки на мои Github, Telegram, Linkedin".
Это хабр, сэр. Давайте технические детали. При чём тут Swarm? Как кто-то другой может что-то подобное сделать?