Навели порядок: как мы наконец-то правильно используем кэш GitHub 🧹
В прошлом техническом посте мы измерили, чего на самом деле стоит наш конвейер сборки и проверок, и выяснили: не денег, а времени ожидания и места. Та статья закончилась словами «уборка запланирована, но ещё не сделана». Оказалось, что она пошла быстрее, чем ожидалось: с полудня сегодняшнего дня она в главной ветке. Вот цифры.
Сначала результат. Занятое кэшем место в нашем репозитории сократилось с 5,32 ГБ в 63 записях до 2,46 ГБ в 13. Анализ безопасности C#, который раньше запускался при каждом pull request, теперь занимает там 4 секунды вместо 4,5 минут, при этом проверяя всё, что попадает к вам. И всё это без отказа от единой проверки, которая вас защищает.
Коротко: для чего нужен кэш? Каждая сборка стартует на свежесозданной пустой машине. Чтобы не начинать каждый раз с нуля, она может сохранять промежуточные результаты: в следующий раз она скачивает их, а не создаёт заново. На это даётся 10 ГБ на репозиторий. Когда место заканчивается, GitHub удаляет то, чем дольше всего не пользовались. Именно в этом и была наша проблема: место было занято, но не тем, чем нужно.
Ошибка 1: ключ без платформы. При сборке Unity создаёт огромную промежуточную папку. Её используют четыре сборки — Windows, Linux, macOS, WebGL — и каждой нужна своя. Сборка для Windows была единственной, у чьего ключа кэша не было метки платформы. Поэтому он подходил к первой попавшейся записи, а это была запись WebGL: скачивалось 1,7 ГБ, которые Unity правильно определял как «это не отсюда» и импортировал всё заново. В логе: 257 секунд импорта вместо 85. Теперь ключ называется Library-Windows-, и наваждение закончилось.
Ошибка 2: её никто никогда не сохранял. Находка постыднее. Все четыре сборки добросовестно пытались восстановить промежуточную папку, но ни один рабочий процесс её не записывал. Единственная существующая запись пришла от инструмента, который запускается вручную. То есть самый важный кэш проекта поддерживался случайно. 🙈 Теперь он записывается намеренно.
Решение, которое экономит место: считается только критический путь. Очевидным шагом было бы кэшировать все четыре платформы. Взгляд на тайминги настоящего релиза показывает, почему это было бы бессмысленно: все четыре сборки стартуют одновременно, Windows заканчивает через 11 минут, Linux и macOS через 13,7, а WebGL через 31,6. Пока идёт WebGL, остальные всё равно ждут. Так что сократить ожидание может только его кэш; остальные три стоили бы около 4,5 ГБ из нашего бюджета в 10 ГБ и сэкономили бы ровно ноль минут. Поэтому мы кэшируем ровно одну.
Ошибка 3: Docker съел остальное. При сборке образов нашего сервера раньше сохранялся в кэш каждый промежуточный слой: примерно 3,27 ГБ в 24 записях, 64% всего бюджета, обновляемые при каждом push. Этот потоп регулярно вытеснял тот единственный кэш Unity, который важен. Теперь сохраняется только финальная стадия, и у каждого из наших четырёх образов своё пространство имён: раньше они делили одно и постоянно выбивали друг друга. Из 3,27 ГБ получилось около 150 МБ.
Анализ безопасности: проверяем там, где это важно. CodeQL, инструмент GitHub для поиска дыр в безопасности, был вторым по величине пожирателем нашего времени: 230 минут за восемь дней, почти всё приходилось на часть для C#, которая для своего анализа пересобирает весь проект, и так при каждом pull request. Теперь он полностью запускается при слиянии в главную ветку и раз в неделю, а pull request сохраняют быстрые проверки. Замеры после изменения: 4 секунды в pull request, неизменные хорошие 4 минуты в главной ветке. Так что каждая строка, которая реально доходит до вас, по-прежнему проверяется, просто не трижды.
Мелочи, которые складываются. Пакеты .NET теперь кэшируются, но только на пути, на котором ждёт человек. Эмулятор для чужих архитектур процессоров настраивается, только когда мы действительно собираем под такую. Команда очистки, которой нечего было делать на свежезапущенной машине, но которая стоила до 1,4 минуты на сборку, удалена. А промежуточные файлы релизной сборки теперь очищаются через 7 дней вместо 90: накопилось 703 штуки общим объёмом 7,4 ГБ.
Что мы сознательно не делали. Два предложения из нашего собственного анализа мы отбросили после замеров, а не продали как красивые строки в списке изменений. Разделение тестов на два параллельных задания выиграло бы ровно четыре секунды (одна часть идёт 2:54, другая 4 секунды) и переименовало бы обязательную проверку, из-за чего каждый pull request навсегда застрял бы в состоянии «ожидание». А пропуск большого тестового барьера перед релизом тоже ничего бы не сэкономил, потому что он давно завершён, пока WebGL ещё собирается. Оптимизация, которая ничего не даёт, а только убирает страховку, оптимизацией не является.
А потом из этого выпала настоящая ошибка. Посреди этой уборки вдруг каждый pull request стал падать по лимиту времени: один-единственный тест занял 212 секунд при допустимых 120. Второй по медленности в том же прогоне: 10,5 секунды. Причина была в обработке ошибки на нашем сервере, который просто обрывал битый HTTP-запрос, вместо того чтобы аккуратно его закрыть. В Linux система забирает такие соединения только при уборке, которая запускается раз в две минуты, а завершение работы терпеливо ждало её. В Windows это никогда не проявлялось, поэтому кусалось только на сборочном сервере. Теперь сервер отвечает на такой случай честным кодом ошибки и сам закрывает соединение: тест завершается за добрых 20 секунд, а настоящие игроки, столкнувшись с такой неполадкой, получат понятную ошибку вместо оборванного соединения. День уборки CI, который находит ошибку в рабочей системе, — хороший день. 🐛
Всё это ничего не меняет для вас напрямую: ни нового блока, ни нового животного. Оно просто сокращает путь между «написали код» и «можно установить». И подтверждает ту же фразу, что и в прошлый раз: сначала измерь, потом трогай. 🚀
Комментарии