Сборка образа занимает десять минут, из которых девять уходят на установку зависимостей, которые не менялись полгода. Разработчик ждёт, конвейер занят, а изменилась одна строка в коде.
Тупик: начать с уменьшения образа
Первое, что советуют и что делают, — взять базовый образ поменьше. Замена обычного базового слоя на урезанный действительно уменьшает результат, иногда заметно.
Это не решает ту задачу, которая болит. Размер образа влияет на выгрузку и на скачивание при запуске; время сборки он почти не определяет. Заменив базовый слой, вы получите образ меньше, а ждать будете столько же — потому что девять минут уходили не на базовый слой, а на установку зависимостей, и она никуда не делась.
Хуже того, урезанный базовый образ часто отбирает время назад. В нём нет компилятора и заголовков, поэтому пакеты, которые раньше ставились готовыми, начинают собираться из исходников. Сборка удлиняется, а разбираться приходится с чужими ошибками сборки.
Второй частый ход — параллелить и покупать машины побыстрее. Это работает, стоит денег и не трогает причину: вы платите за то, чтобы быстрее делать работу, которую можно не делать.
Что на самом деле определяет время
Время сборки определяется не объёмом работы, а тем, какая её часть выполняется заново. Сборщик кеширует шаги, и каждый шаг зависит от всего, что было до него. Изменился шаг — пересчитывается он и всё, что ниже.
Отсюда единственное правило, из которого следует почти всё остальное: редко меняющееся должно идти раньше часто меняющегося.
Типичная ошибка занимает одну строку. Сначала в образ копируется весь проект, потом ставятся зависимости. Формально верно. Практически — любая правка в любом файле меняет шаг копирования, а значит, установка зависимостей выполняется заново. Каждый раз. Даже когда список зависимостей не менялся.
Правильный порядок обратный: сначала копируется только описание зависимостей, потом ставятся зависимости, и лишь потом копируется остальной код. Тогда правка в коде не трогает установку, и она берётся из кеша.
Это одно изменение обычно даёт больше, чем все остальные вместе.
Тот же принцип работает и внутри установки. Если зависимости описаны в двух местах — редко меняющиеся системные пакеты и часто меняющиеся библиотеки проекта, — их стоит разнести по разным шагам. Слитые в один шаг, они пересчитываются вместе: добавили одну библиотеку — переставляются и системные пакеты, которые не трогали месяцами.
Проверить, где именно теряется время, можно без инструментов. Соберите образ дважды подряд, ничего не меняя: вторая сборка покажет, что берётся из кеша. Затем поменяйте одну строку в коде и соберите снова. Разница между вторым и третьим прогоном — это и есть цена вашей типичной правки, и обычно она объясняет всё.
Что делать дальше
Разделить сборку и запуск. Инструменты, нужные для сборки, в готовый образ попадать не должны. Многоступенчатая сборка решает это без ухищрений: в одной ступени собираем, из другой берём только результат. Заодно это отвечает на вопрос размера, с которого все начинают, — но уже как следствие, а не как самоцель.
Исключить лишнее из контекста. Сборщик копирует к себе каталог проекта целиком, включая историю изменений, локальные зависимости и артефакты прошлых сборок. Это и передаётся дольше, и ломает кеш: изменился любой файл — изменился контекст. Список исключений рядом со сборочным файлом — минута работы с ощутимым эффектом.
Не ставить лишнего. Часть зависимостей нужна только для тестов и линтеров. В образ, который поедет в бой, они попадать не обязаны: это и время установки, и лишняя поверхность.
Закрепить версии. Это не про скорость, а про повторяемость, но связано напрямую: сборка, которая при каждом запуске подтягивает свежие версии, не может пользоваться кешем осмысленно и вдобавок непредсказуема.
Где кеш теряется незаметно
Локально всё быстро, в конвейере — те же десять минут. Причина обычно в том, что у конвейера кеша нет: каждая сборка стартует на чистой машине, где предыдущих слоёв не существует.
Лечится это не переписыванием сборочного файла, а настройкой: кеш выносится наружу и подключается к следующей сборке. Без этого правильный порядок шагов помогает только на машине разработчика, а болит как раз в конвейере.
Второй способ потерять кеш — сборка с аргументом, который меняется каждый раз. Метка времени или номер сборки, переданные слишком рано, обесценивают всё, что идёт следом. Такие аргументы передают как можно позже, на последний шаг.
Третий способ — сборка на разных архитектурах. Образ, собранный для одной архитектуры процессора, не переиспользуется для другой, и команда, где часть машин на одной, а конвейер на другой, регулярно недоумевает, почему кеш «не работает». Он работает, просто их два.
Отдельная история — базовый образ без закреплённой версии. Метка вроде «последняя» меняется на стороне поставщика, и в момент обновления у вас пересобирается всё, целиком, без предупреждения и обычно не вовремя. Это ещё и вопрос воспроизводимости: сборка месячной давности не повторяется, потому что фундамент под ней уже другой.
Как понять, что стало лучше
Мерить надо не полную сборку с нуля, а типичную: правка в одной строке кода при неизменных зависимостях. Именно она случается десятки раз в день, и именно её время определяет, ждёт разработчик или работает.
Полная сборка с чистого листа тоже нужна, но как отдельная величина. Смешивать их в одну цифру бессмысленно: они меняются от разных вещей.
Стоит мерить и время до готовности, а не только до конца сборки. Образ, который собрался быстро, но стартует долго, переносит ожидание с разработчика на выкатку — суммарно не выиграли ничего, просто ждёт теперь другой человек в другом месте.
Позиция редакции
Мы считаем, что порядок шагов в сборочном файле важнее выбора базового образа, и начинать надо с него. Одна перестановка строк даёт больше, чем неделя подбора урезанных образов, и не приносит с собой чужих ошибок сборки.
Мы неправы, если у вас нет зависимостей, которые долго ставятся, — для маленького статического бинарника вся эта арифметика не нужна, там сборка и так быстрая. Граница простая: посмотрите, сколько времени занимает установка зависимостей. Если меньше четверти сборки, наводить порядок незачем.