Делай как вGoogle
Скачать 5.77 Mb.
|
386 Глава 18. Системы и философия сборки выиграет, если изначально будет включен в систему сборки на основе артефактов, такую как Bazel. В Google практически весь код — от крошечных экспериментальных проектов до Google Search — собирается с помощью Blaze. Модули и зависимости Проекты, использующие системы сборки на основе артефактов, такие как Bazel, делятся на модули, причем зависимости между модулями выражаются через файлы BUILD . Правильная организация модулей и зависимостей может иметь огромное вли- яние не только на производительность системы сборки, но и на затраты, связанные с ее поддержкой. Дробление на модули и правило 1 : 1 : 1 Первый вопрос, который возникает при структурировании сборки на основе артефак- тов: какой объем функциональности должен включать отдельный модуль? В Bazel модуль представлен целью, определяющей единицу сборки, такой как java_library или go_binary . С одной стороны, весь проект может содержаться в одном модуле, если поместить один файл BUILD в корневой каталог и рекурсивно объединить все исходные файлы этого проекта. С другой стороны, почти каждый исходный файл можно преобразовать в отдельный модуль, фактически потребовав, чтобы каждый файл перечислял в своем файле BUILD все остальные файлы, от которых он зависит. Большинство проектов имеют структуру, занимающую промежуточное положение между этими крайностями, поскольку выбор количества модулей определяется компромиссом между производительностью и удобством сопровождения. Включив проект целиком в единственный модуль, вы избавите себя от необходимости при- касаться к файлу BUILD , кроме как для добавления внешних зависимостей, но при этом система сборки всегда будет вынуждена собирать весь проект целиком. То есть она не сможет выполнять сборку параллельно и кешировать уже собранные части. В противном случае, когда каждому файлу соответствует свой модуль, система сборки обладает максимальной гибкостью в кешировании и планировании этапов сборки, но инженерам приходится прилагать больше усилий для поддержания списков за- висимостей при каждом их изменении. Степень дробления на модули во многом зависит от языка, однако мы в Google склонны отдавать предпочтение модулям значительно меньшего размера, чем можно определить в системе сборки на основе задач. Типичный двоичный файл в Google зависит от десятков тысяч целей, и даже небольшая команда может владеть не- сколькими сотнями целей в своей кодовой базе. В таких языках, как Java, имеющих строгое деление на пакеты, каждый каталог обычно содержит один пакет, цель и файл BUILD (Pants, еще одна система сборки, основанная на Blaze, называет это правилом 1 : 1 : 1 ( https://oreil.ly/lSKbW )). Языки с более слабыми соглашениями о пакетах часто определяют несколько целей для каждого файла BUILD Модули и зависимости 387 Преимущества небольших целей сборки начинают по-настоящему проявляться в мас- штабе, потому что позволяют быстрее производить распределенную сборку и реже повторно собирать цели, и при тестировании, поскольку для мелких целей система сборки может запускать более ограниченное подмножество тестов. Мы в Google верим в преимущества систематического использования небольших целей, поэтому вкладываем силы и время в создание инструментов автоматического управления файлами BUILD , помогающих снять лишнее бремя с плеч разработчиков. Многие из этих инструментов ( https://oreil.ly/r0wO7 ) теперь распространяются с открытым исходным кодом. Уменьшение области видимости модуля Bazel и другие системы сборки позволяют определять область видимости для каж- дой цели, то есть указывать, какие другие цели могут зависеть от нее. Цели могут быть общедоступными ( public ) — на них может ссылаться любая другая цель в ра- бочем пространстве; приватными ( private ) — на них могут ссылаться только цели, перечисленные в том же файле BUILD , или доступными только явно определенному списку других целей. Видимость — по сути это зеркальное отражение зависимости: если необходимо, чтобы цель A зависела от цели B, то цель B должна объявить себя видимой для цели A. Так же как в большинстве языков программирования, обычно лучше минимизиро- вать область видимости, насколько это возможно. Как правило, команды в Google объявляют свои цели общедоступными, только если они представляют широко используемые библиотеки, доступные любой команде в Google. Команды, которым требуется, чтобы другие команды координировали свои действия перед использова- нием их кода, ведут «белый список» клиентов своей цели, ограничивая ее видимость. Видимость внутренних целей, используемых только внутри команды, ограничена каталогами, принадлежащими этой команде, и в большинстве случаев ее файлы BUILD имеют только одну приватную цель. Управление зависимостями Модули должны иметь возможность ссылаться друг на друга. Но управление за- висимостями между модулями требует времени (хотя существуют инструменты, которые могут помочь автоматизировать эту задачу). Для выражения этих зависи- мостей обычно требуется писать большие файлы BUILD Внутренние зависимости В большом проекте, разбитом на множество мелких модулей, большинство за- висимостей почти наверняка будут внутренними, то есть большинство модулей будут зависеть от других модулей, находящихся в том же репозитории. Основное отличие внутренних зависимостей от внешних состоит в том, что они собираются из исходного кода, а не загружаются в виде предварительно собранных артефактов. 388 Глава 18. Системы и философия сборки Поэтому внутренние цели не поддерживают понятия «версии» — цель и все ее вну- тренние зависимости всегда собираются из одной и той же фиксации (исправления) в репозитории. Единственная сложность внутренних зависимостей связана с обработкой транзитив- ных (промежуточных) зависимостей (рис. 18.5). Предположим, что цель A зависит от цели B, которая зависит от цели C, представляющей общую библиотеку. Должна ли цель A иметь возможность использовать классы, которые определены в цели C? A B C Рис. 18.5. Транзитивные зависимости Что касается основных инструментов, здесь нет никаких сложностей; обе цели — B и C — будут связаны с целью A во время сборки, поэтому любые символы, объ- явленные в C, доступны A. Система Blaze позволяла это в течение многих лет, но по мере расширения Google мы стали замечать проблемы этого подхода. Предположим, что код цели B был реорганизован так, что ему стала не нужна зависимость от C. Если теперь удалить зависимость B от C, то работоспособность A и любой другой цели, использовавшей C через зависимость от B, нарушится. Фактически зависимости цели стали частью ее публичного контракта и появился риск нарушить работоспо- собность кода при их изменении. По этой причине зависимости стали накапливаться со временем и процессы сборки в Google стали замедляться. Чтобы решить эту проблему, мы реализовали в Blaze «режим строгой транзитив- ной зависимости». В этом режиме Blaze определяет, пытается ли цель сослаться на символ, отсутствующий в ее непосредственных зависимостях, и если да, то за- вершается неудачей с сообщением об ошибке и примером команды, которую можно выполнить, чтобы автоматически добавить необходимые зависимости. Внедрение этого новшества в кодовую базу Google и рефакторинг каждой из миллионов наших целей сборки для явного перечисления их зависимостей потребовало многолетних усилий, но оно того стоило. Теперь сборка проектов у нас выполняется намного бы- стрее, потому что цели имеют меньше ненужных зависимостей 1 , а инженеры могут удалять ненужные зависимости, не беспокоясь о нарушении работоспособности целей, которые от них зависят. Как обычно, введение более строгого правила потребовало пойти на компромисс: файлы сборки стали более подробными, потому что часто используемые библиоте- ки теперь нужно указывать явно во многих местах, а инженеры должны прилагать больше усилий для добавления зависимостей в файлы BUILD . Мы разработали ин- 1 На самом деле удаление этих зависимостей проводилось в рамках отдельного процесса. Но требование явного объявления всех необходимых зависимостей стало важным первым шагом на пути к новому подходу. Подробнее о проведении крупномасштабных изменений в Google в главе 22. Модули и зависимости 389 струменты, облегчающие этот труд. Они автоматически обнаруживают недостающие зависимости и добавляют их в файлы BUILD без участия разработчика. Но даже если бы у нас не было таких инструментов, этот шаг полностью оправдал себя с увели- чением масштаба кодовой базы, поскольку явное добавление зависимости в файл BUILD — это единовременные затраты, а использование неявных транзитивных за- висимостей может вызывать проблемы постоянно, пока существует цель сборки. Строгие правила в отношении транзитивных зависимостей ( https://oreil.ly/Z-CqD ) по умолчанию применяются системой Bazel к коду на Java. Внешние зависимости Внешние зависимости — это артефакты, которые создаются и хранятся за пределами системы сборки. Они импортируются непосредственно из репозитория артефак- тов (обычно доступного через интернет) и используются как есть, а не создаются из исходного кода. Одно из самых больших отличий внешних и внутренних зависимо- стей заключается в наличии версии у внешних зависимостей, которые существуют независимо от исходного кода проекта. Автоматическое и ручное управление зависимостями. Системы сборки позволяют управлять версиями внешних зависимостей вручную или автоматически. При ручном управлении в файле сборки явно указывается номер версии, которая должна загру- жаться из репозитория артефактов, часто в виде семантической строки версии ( https:// semver.org ), такой как «1.1.4». При автоматическом управлении в исходном файле указывается диапазон допустимых версий, а система сборки всегда будет пытаться загрузить самую последнюю версию. Например, Gradle позволяет объявить версию зависимости как «1.+», чтобы указать, что допускаются любые версия или исправ- ление зависимости с основным номером версии 1. Автоматическое управление зависимостями удобно использовать в небольших про- ектах, но обычно такой подход приводит к катастрофе в проектах большого размера, над которыми работают несколько инженеров. Проблема автоматического управле- ния зависимостями заключается в невозможности управлять обновлением версии. Нет гарантий, что третья сторона не выпустит важное обновление (даже если она утверждает, что использует семантическое управление версиями), поэтому сборка, благополучно выполняющаяся сегодня, может начать терпеть неудачу завтра, и будет нелегко определить причину сбоя и устранить ее. Даже если сборка выполняется успешно, новая версия может иметь незначительные изменения в поведении или производительности, которые невозможно отследить. Ручное управление зависимостями, напротив, требует передачи изменений в VCS, чтобы их можно было легко обнаружить и откатить, а также позволяет извлечь из репозитория старую версию и выполнить сборку со старыми зависимостями. Bazel требует, чтобы версии всех зависимостей указывались вручную. Даже в базах кода умеренного масштаба накладные расходы на ручное управление версиями окупаются благодаря той стабильности, которую оно обеспечивает. 390 Глава 18. Системы и философия сборки Правило единственной версии. Разные версии библиотеки обычно представлены разными артефактами, поэтому теоретически ничто не мешает присвоить разные имена разным версиям одной и той же внешней зависимости в системе сборки. При таком подходе каждая цель сможет выбрать желаемую версию зависимости. Но мы в Google обнаружили, что на практике это вызывает множество проблем, поэтому мы строго следуем правилу единственной версии ( https://oreil.ly/OFa9V ) для всех сторонних зависимостей в нашей внутренней кодовой базе. Самая большая проблема поддержки нескольких версий — образование ромбовид- ных (diamond) зависимостей. Представьте, что цель A зависит от цели B и от версии v1 внешней библиотеки. Если позже будет выполнен рефакторинг цели B для до- бавления зависимости от версии v2 той же внешней библиотеки, то цель A может потерять работоспособность, потому что теперь она неявно зависит от двух разных версий одной и той же библиотеки. В целом всегда небезопасно добавлять в цель новую зависимость от любой сторонней библиотеки с несколькими версиями, по- тому что любой из пользователей этой цели уже может зависеть от другой версии. Следование правилу единственной версии запрещает зависимость от двух версий — если цель добавляет зависимость от сторонней библиотеки, то любые существующие зависимости уже будут в этой же версии и смогут благополучно сосуществовать. К этой проблеме в контексте большого монолитного репозитория мы еще вернемся в главе 21. Транзитивные внешние зависимости. Многие репозитории артефактов, такие как Maven Central, позволяют артефактам определять зависимости от конкретных вер- сий других артефактов в репозитории. Инструменты сборки, такие как Maven или Gradle, часто рекурсивно загружают все транзитивные зависимости по умолчанию, а это означает, что добавление одной зависимости в проект может потребовать за- грузки десятков артефактов. Это очень удобно: при добавлении зависимости от новой библиотеки было бы слож- но вручную выявить и добавить все транзитивные зависимости этой библиотеки. Но у этого подхода есть серьезный недостаток: поскольку разные библиотеки могут зависеть от разных версий одной и той же сторонней библиотеки, между ними воз- никнет ромбовидная зависимость. Если цель зависит от двух внешних библиотек, которые используют разные версии одной и той же зависимости, то неизвестно, какую из них вы получите. Это также означает, что обновление внешней зависимости может вызвать кажущиеся несвязанными ошибки во всей кодовой базе, если для сборки но- вой версии будут извлекаться конфликтующие версии некоторых из ее зависимостей. По этой причине Bazel никогда не загружает транзитивные зависимости автоматиче- ски. К сожалению, эта проблема не имеет универсального решения — альтернативный подход, поддерживаемый системой Bazel, заключается в создании глобального файла, в котором перечислены все внешние зависимости репозитория и явно указано, какая версия каждой зависимости должна использоваться во всем репозитории. К счастью, в Bazel имеются инструменты ( https://oreil.ly/kejfX ), которые могут автоматически Модули и зависимости 391 создать такой файл, перечисляющий транзитивные зависимости для набора артефак- тов Maven. Этот инструмент можно запустить один раз, чтобы получить начальный файл WORKSPACE для проекта, а затем обновлять этот файл вручную, корректируя версии каждой зависимости. И снова приходится выбирать между удобством и масштабируемостью. Небольшие проекты могут добавлять внешние транзитивные зависимости автоматически. В более крупных масштабах стоимость ручного управления зависимостями намного ниже сто- имости решения проблем, вызванных автоматическим управлением зависимостями. Кеширование результатов сборки с использованием внешних зависимостей. Внешние зависимости чаще всего предоставляются третьими сторонами, которые выпускают стабильные версии библиотек, возможно, без предоставления исходного кода. Некоторые организации могут также сделать доступной часть своего кода в виде артефактов, позволяя другим частям кода зависеть от них. Теоретически это может ускорить сборку, если артефакты медленно создаются, но быстро за- гружаются. Однако это также приводит к большим накладным расходам и увеличению слож- ности: кто-то должен нести ответственность за создание каждого из артефактов и их загрузку в репозиторий артефактов, а клиенты должны следить за тем, чтобы всегда оставаться в курсе последних версий. Отладка тоже становится намного сложнее, потому что разные части системы будут собираться из разных точек в репозитории и больше не будет существовать согласованного представления дерева исходных текстов. Лучший способ решить проблему артефактов, сборка которых занимает много време- ни, — использовать систему сборки, поддерживающую удаленное кеширование, как было описано выше. Такая система сборки будет сохранять полученные артефакты в одном месте, совместно используемом инженерами, поэтому если разработчик зависит от артефакта, который недавно был создан кем-то другим, то система сбор- ки автоматически загрузит его, минуя этап сборки из исходного кода. Этот подход обеспечивает все преимущества производительности, связанные с прямой зависимо- стью от артефактов, и гарантирует, что результаты сборки будут оставаться такими же согласованными, как если бы они всегда собирались из одного источника. Эта стратегия используется в Google, и система Bazel предоставляет возможность на- строить удаленный кеш. Безопасность и надежность внешних зависимостей. Зависимость от артефактов, полученных из сторонних источников, по своей природе рискованна. Если сторон- ний источник (например, репозиторий артефактов) выйдет из строя, его артефакт окажется недоступным и сборка проекта может остановиться. Также, если сторонняя система будет скомпрометирована злоумышленником, этот злоумышленник сможет подменить артефакт своей версией и внедрить произвольный код в вашу сборку. Чтобы смягчить обе проблемы, создайте свое зеркало артефактов, от которых вы зависите, на контролируемых вами серверах и запретите своей системе сборки об- 392 Глава 18. Системы и философия сборки ращаться к сторонним репозиториям артефактов, таким как Maven Central. Однако для обслуживания этих зеркал требуются силы и ресурсы, поэтому выбор такого подхода часто зависит от масштаба проекта. Проблему безопасности также можно полностью устранить, потребовав указать хеш для каждого стороннего артефакта в исходном репозитории, что вызовет остановку сборки, если кто-то попытается подделать артефакт. Другая альтернатива, полностью устраняющая эти проблемы, — официальная по- ставка зависимостей для вашего проекта. Когда сторонний проект официально поставляет свои зависимости, он извлекает их из VCS вместе с исходным кодом проекта либо в виде исходного кода, либо в виде двоичных файлов. Фактически это означает преобразование всех внешних зависимостей проекта во внутренние. Google использует этот подход для внутренних нужд, сохраняя каждую стороннюю библиотеку, на которую есть ссылки в Google, в каталог third_party в корне дерева ис- ходных кодов. Однако в Google этот подход работает только потому, что VCS Google специально создана для работы с чрезвычайно большим монолитным репозиторием, поэтому официальная поставка может быть недоступна для других организаций. |