Делай как вGoogle
Скачать 5.77 Mb.
|
Почему управлять зависимостями так сложно? Даже определение задачи управления зависимостями представляет сложности. Мно- гие полусырые решения в этой области вытекают из слишком узкой формулировки 426 Глава 21. Управление зависимостями проблемы: «Как импортировать пакет, от которого зависит наш локальный код?» Это реальный вопрос, но не полный. Сложность не в том, чтобы выяснить, как управлять одной зависимостью, а в том, чтобы понять, как управлять сетью зависимостей и их будущими изменениями. Часть зависимостей из этой сети прямо необходимы ваше- му коду, другая часть — подтягивается промежуточными зависимостями. В течение достаточно длительного периода все узлы этой сети зависимостей обновятся, и неко- торые из этих обновлений будут играть важную роль 1 . Как управлять получившимся каскадом обновлений? Как упростить поиск совместимых версий зависимостей, которые мы не контролируем? Как проанализировать сеть зависимостей? Как управлять этой сетью, особенно если граф зависимостей постоянно расширяется? Конфликтующие требования и ромбовидные зависимости Центральный вопрос в управлении зависимостями: «Что случится, если два узла в сети зависимостей предъявят конфликтующие требования и ваша организация зависит от них обоих?» Эта ситуация может возникнуть по многим причинам, на- чиная от ограничений платформы (ОС, версии языка, версии компилятора и т. д.) и заканчивая утилитарной проблемой несовместимости версий. Каноническим при- мером несовместимости версий из-за невыполнимого требования может служить проблема ромбовидной зависимости. Мы обычно не включаем в граф зависимостей такие требования, как «выбор версии компилятора», но большинство проблем с кон- фликтующими требованиями сродни «добавлению в граф зависимостей (скрытого) узла, представляющего это требование». Поэтому в первую очередь мы обсудим конфликтующие требования с точки зрения ромбовидных зависимостей, но имейте в виду, что libbase на самом деле может быть абсолютно любым ПО, участвующим в создании двух или более узлов в сети зависимостей. Проблема ромбовидной зависимости и другие формы конфликтующих требований имеют как минимум три уровня (рис. 21.1). libuser libbase libb liba Рис. 21.1. Проблема ромбовидной зависимости В этой упрощенной модели libbase используется двумя компонентами, liba и libb , которые, в свою очередь, используются компонентом более высокого уровня libuser Если когда-нибудь в libbase будет внесено несовместимое изменение, есть шанс, что 1 Например, ошибки безопасности, устаревание, присутствие в наборе зависимостей от за- висимостей более высокого уровня, в которых есть ошибки безопасности, и т. д. Почему управлять зависимостями так сложно? 427 liba и libb , как продукты разных организаций, обновятся не одновременно. Если liba зависит от новой версии libbase , а libb — от старой, у libuser (то есть вашего кода) не будет никакой возможности собрать все вместе. Этот ромб может сформироваться в любом масштабе: если в сети зависимостей существует низкоуровневый узел, кото- рый может присутствовать в виде двух несовместимых версий (в силу наличия двух путей к этим версиям от одного узла более высокого уровня), то возникнет проблема. Проблема ромбовидной зависимости с разной степенью проявляется в разных языках программирования. В некоторых языках допускается встраивать в сборку несколько (изолированных) версий зависимости: обращения к libbase из liba и libb могут вызывать разные версии одного API. Например, в Java есть хорошо отлажен- ные механизмы переименования символов в подобных зависимостях 1 , тогда как C++ практически не допускает ромбовидных зависимостей в обычной сборке и они с большой вероятностью будут вызывать произвольные ошибки и неопределенное поведение вследствие явного нарушения правила определения в C++ ( https://oreil. ly/VTZe5 ). В лучшем случае можно использовать идею затенения, заимствованную из Java, чтобы скрыть некоторые символы в динамически подключаемой библио- теке (DLL, dynamic-link library), или применять раздельную сборку и компоновку. Однако во всех языках программирования, известных нам, эти обходные решения являются неполными: можно встроить несколько версий, настроив имена функций, но если между зависимостями передаются типы, возникнут новые проблемы. Напри- мер, невозможно семантически согласованным способом передать экземпляр типа map , определенного в libbase v1 , через какие-либо библиотеки в API , предоставляемый libbase v2 . Характерные для языка приемы сокрытия или переименования сущностей в раздельно скомпилированных библиотеках могут смягчить проблему ромбовидной зависимости, но в общем случае не являются решением конфликта. Если вы столкнулись с проблемой конфликтующих требований, лучшее решение — взять следующие или предыдущие версии зависимостей, чтобы найти среди них совместимые между собой. Если это невозможно, то необходимо прибегнуть к локаль- ному исправлению конфликтующих зависимостей, что является особенно сложной задачей, потому что причина несовместимости в них, скорее всего, неизвестна инже- неру, который первым обнаружил несовместимость. Это неизбежно: разработчики liba по-прежнему работают в режиме совместимости с libbase v1 , а разработчики libb уже обновились до v2 . И только разработчик, участвующий в обоих этих проектах, имеет шанс обнаружить проблему, и, конечно же, нет никаких гарантий, что он окажется достаточно близко знакомым с libbase и liba , чтобы пройти через обновление. Проще всего понизить версию libbase и libb , хотя это не лучший вариант, если обновление вызвано проблемами с безопасностью. Политики и технологии управления зависимостями в значительной степени сво- дятся к вопросу: «Как избежать конфликтов в требованиях, но при этом разрешить изменения в несогласованных проектах?» Если вам удастся найти общее решение 1 Их называют затенением, или версионированием. 428 Глава 21. Управление зависимостями проблемы ромбовидной зависимости, которое учитывает реалии постоянно меняю- щихся требований (как самих зависимостей, так и требований к платформе) на всех уровнях сети, то вы опишете самое интересное решение в управлении зависимостями. Импортирование зависимостей С точки зрения программирования лучше повторно использовать существующую инфраструктуру, чем строить новую. Если каждый новичок будет заново писать синтаксический анализатор JSON и механизм регулярных выражений, техноло- гический прогресс остановится. Повторное использование выгодно, особенно по сравнению с затратами на переделку качественного ПО с нуля. Если имеющаяся внешняя зависимость удовлетворяет требованиям вашей задачи и у вас нет ничего лучше, то используйте ее. Обещание совместимости Когда в задаче появляется фактор времени, приходится принимать непростые реше- ния. Возможность избежать лишних затрат на разработку не означает, что импорт зависимости является правильным выбором. В софтверной организации ведется учет текущих расходов на обслуживание. Даже если зависимость импортируется без намерения обновлять ее в будущем, обнаруженные уязвимости в системе без- опасности, смена платформы и развивающиеся сети зависимостей могут вынудить провести обновление зависимости. Насколько дорого оно обойдется? Некоторые зависимости позволяют довольно точно определить ожидаемые затраты на их обслуживание, но как оценить совместимость версий? Насколько кардинальными могут быть изменения, обусловленные развитием технологий? Как учитывать такие изменения? Как долго поддерживаются версии? Мы хотели бы, чтобы разработчики зависимостей давали четкие ответы на эти во- просы. Возьмем для примера несколько крупных инфраструктурных проектов, на- считывающих миллионы пользователей, и рассмотрим их обещания в отношении совместимости версий. C++ Для стандартной библиотеки C++ используется модель с почти неограниченной обратной совместимостью. Ожидается, что двоичные файлы, скомпилированные с использованием более старой версии стандартной библиотеки, будут компоновать- ся с новым стандартом: стандарт обеспечивает не только совместимость с API, но и постоянную обратную совместимость двоичных артефактов, известную как ABI- совместимость (application binary interface — двоичный интерфейс приложения). Степень поддержки такой совместимости для разных платформ разная. Код, скомпи- лированный с помощью gcc в Linux, должен нормально работать в течение примерно десяти лет. Стандарт не заявляет о своей приверженности к ABI-совместимости — по этому поводу не было опубликовано официальных документов. Однако был опу- Импортирование зависимостей 429 бликован документ «Standing Document 8» (SD-8, https://oreil.ly/LoJq8 ), в котором перечисляется набор типов изменений, которые могут вноситься в стандартную библиотеку между версиями. Этот документ неявно определяет, к каким видам из- менений нужно готовиться. Аналогичный подход используется для Java: исходный код совместим между версиями языка, а файлы JAR из более старых версий должны работать с более новыми версиями. Go Для разных языков совместимость имеет разный приоритет. Язык Go явно обещает совместимость на уровне исходного кода, но не обещает двоичной совместимости. У вас не получится собрать библиотеку на Go с одной версией языка и связать ее с программой на Go, собранной с другой версией языка. Abseil Проект Google библиотека Abseil очень похож на Go, но с одной важной оговоркой относительно времени. Мы не стремимся обеспечивать совместимость бесконечно долго: Abseil лежит в основе многих наших внутренних вычислительных сервисов, которые, как предполагается, будут долго использоваться. Это означает, что мы оставляем за собой полное право вносить изменения в Abseil, особенно в детали ее реализации и ABI, чтобы обеспечить максимальную производительность. API не раз превращался в источник проблем и ошибок постфактум, и мы не хотим вынуж- дать десятки тысяч разработчиков мириться с подобными проблемами в течение неопределенного времени. У нас уже есть около 250 миллионов строк кода на C++, которые зависят от Abseil, и мы не будем вносить критические изменения в API, не предоставив инструмента для автоматического рефакторинга, который преоб- разует код со старым API в новый. Мы считаем, что это значительно уменьшит риск непредвиденных расходов для пользователей: для какой бы версии Abseil не была написана зависимость, пользователи этой зависимости и Abseil должны иметь возможность применить самую последнюю версию Abseil. Вся сложность должна быть заключена в том, чтобы запустить этот инструмент и, возможно, отправить полученное исправление на проверку в зависимости среднего уровня ( liba или libb из предыдущего примера). Однако пока проект достаточно новый, поэтому мы еще не вносили существенных изменений в API и не можем сказать, насколько хорошо будет работать этот подход для экосистемы в целом, но теоретически баланс между стабильностью и простотой обновления выглядит неплохо. Boost Библиотека Boost на C++ не обещает совместимости между версиями ( https://www. boost.org/users/faq.html ). Большая часть кода, конечно, не меняется, но многие библио- теки Boost активно развиваются и улучшаются, поэтому обратная совместимость с предыдущей версией не всегда возможна. Пользователям рекомендуется выполнять обновление только в те периоды жизненного цикла проекта, когда изменения не вы- зовут проблем. Цель Boost принципиально отличается от стандартной библиотеки 430 Глава 21. Управление зависимостями или Abseil: Boost — это экспериментальный полигон. Отдельные версии Boost ста- бильны и подходят для использования во многих проектах, но совместимость между версиями не является главной целью проекта Boost, поэтому другим долгосрочным проектам, зависящим от Boost, бывает сложно поддерживать актуальность. Разра- ботчики Boost столь же опытны, как и разработчики стандартной библиотеки 1 , но речь идет не о технических знаниях: это исключительно вопрос, что проект делает, а не что обещает и как расставляет приоритеты. Рассматривая библиотеки в этом обсуждении, важно понимать, что перечисленные проблемы совместимости связаны с программной инженерией, а не с программирова- нием. Вы можете загрузить библиотеку, такую как Boost, которая не обещает обратной совместимости, глубоко встроить ее в наиболее важные и долгоживущие системы организации, и они будут работать прекрасно. Основные проблемы связаны не с созданием реализации, а с изменением зависимостей, необходимостью менять свой код в соответствии с обновлениями и принуждением разработчиков беспокоиться об обслуживании. В Google существуют рекомендации, помогающие инженерам понять разницу между утверждениями: «Я заставил работать эту функцию» и «Эта функция работает и поддерживается» (вспомните закона Хайрама). В целом важно понимать, что управление зависимостями имеет совершенно разную природу в программировании и в программной инженерии. В предметной области, где сопровождение играет важную роль, управление зависимостями затруднено. В сиюминутных решениях, где не потребуется ничего обновлять, напротив, вполне разумно использовать столько зависимостей, сколько понадобится, не задумываясь об их ответственном использовании или планировании обновлений. Заставить про- грамму работать сегодня, нарушив все положения в SD-8, и положиться на двоичную совместимость с Boost и Abseil можно, если в будущем вы не собираетесь работать с обновленными версиями стандартной библиотеки, Boost, Abseil и всего остального, что не зависит от вас. Рекомендации при импорте Импорт зависимости в проекте программирования почти не влечет затрат, кроме затрат времени на то, чтобы убедиться, что она делает то, что нужно, не подрывает безопасность и обходится дешевле, чем ее альтернатива. Даже если зависимость дает некоторые обещания в отношении совместимости, то, пока не предполагается обновлять ее в будущем, можно полагаться на выбранную версию, сколько бы правил ни было нарушено при использовании API. Но в проектах программной инженерии зависимости становятся немного дороже и возникает множество скрытых затрат и вопросов, на которые необходимо ответить. Надеюсь, вы учитываете эти расходы перед импортированием и умеете отличать проекты программирования от проектов программной инженерии. 1 Многие разработчики работают сразу в обоих этих проектах. Импортирование зависимостей 431 Мы в Google рекомендуем сначала ответить на следующий (неполный) список во- просов, прежде чем импортировать зависимости: y Есть ли в проекте тесты, которые можно запустить? y Эти тесты выполняются успешно? y Кто разрабатывает зависимость? Даже проекты с открытым исходным кодом, не дающие никаких гарантий, создаются опытными разработчиками. Но зависимость от совместимости со стандартной библиотекой C++ или Java-библиотекой Guava нельзя сравнить с выбором случайного проекта в GitHub или npm. Репутация зависимости — это не главный критерий ее выбора, но ее стоит учитывать. y К какой совместимости стремится проект? y Фокусируется ли проект на определенном сценарии использования? y Насколько популярен проект? y Как долго организация будет зависеть от этого проекта? y Как часто в проект вносятся изменения, ломающие совместимость? Добавьте к ним еще несколько наших внутренних вопросов: y Насколько сложно реализовать зависимость в Google? y Какие стимулы будут побуждать вас поддерживать зависимость в актуальном состоянии? y Кто будет выполнять обновление? y Насколько сложно будет выполнить обновление? Расс Кокс писал об этом более подробно ( https://research.swtch.com/deps ). Мы не можем дать идеальные критерии, которые помогут понять, что лучше — импорти- ровать зависимость или реализовать ее аналог у себя, — потому что мы сами часто ошибаемся в этом вопросе. Как в Google импортируются зависимости Если говорить кратко, то далеко не идеально. В Google подавляющее большинство зависимостей разрабатывается внутри компа- нии. То есть основная часть нашей внутренней истории управления зависимостями на самом деле не имеет отношения к управлению зависимостями — она связана с управлением версиями исходного кода. Как уже упоминалось, управлять слож- ностями и рисками, связанными с добавлением зависимостей, гораздо проще, когда эти зависимости производятся и потребляются внутри одной организации, имеют надлежащую видимость и участвуют в процессе непрерывной интеграции (глава 23). Большинство проблем в управлении зависимостями исчезает, когда есть возможность видеть, как используется код, и знать точно, как повлияет то или иное изменение. Управлять исходным кодом (когда есть возможность контролировать проекты) на- много проще, чем управлять зависимостями (когда такая возможность отсутствует). 432 Глава 21. Управление зависимостями Эта простота управления теряется, когда мы начинаем использовать внешние про- екты. Проекты, импортируемые из экосистемы ПО с открытым исходным кодом или от коммерческих партнеров, мы добавляем в отдельный каталог нашего монолитного репозитория с отметкой third_party . Давайте посмотрим, как это происходит. Алиса работает инженером-программистом в Google и занимается некоторым проек- том. В какой-то момент она замечает, что для ее проекта существует решение с откры- тым исходным кодом. Ей очень хотелось бы завершить и продемонстрировать свой проект в ближайшее время, чтобы потом спокойно поехать в отпуск. Она оказывается перед выбором: реализовать все необходимые функции самой или загрузить пакет с открытым исходным кодом и добавить его в каталог third_party . Скорее всего, Алиса посчитает, что для ускорения разработки есть смысл использовать открытое реше- ние. Она загрузит открытый пакет и выполнит несколько шагов, которые требуют правила включения стороннего ПО: убедится, что пакет собирается в нашей системе сборки и его текущая версия ранее не загружена в third_party , и зарегистрирует не меньше двух инженеров в роли ВЛАДЕЛЬЦЕВ пакета для его обслуживания. Алиса уговаривает Боба — своего товарища по команде — ей помочь. Никому из них не требуется опыт поддержки сторонних пакетов с отметкой third_party , и оба благопо- лучно избегают необходимости что-либо понимать в реализации этого пакета. Они приобретают небольшой опыт работы с интерфейсом пакета, использовав его для демонстрации проекта перед отпуском. После этого пакет становится доступным для других команд в Google. Алиса и Боб могут не подозревать, что пакет, который они загрузили и обещали поддерживать, стал популярным. И даже следя за новым прямыми попытками использовать пакет, они могут не заметить расширение его транзитивного использования. Если, в от- личие от Алисы и Боба, которые использовали внешнюю зависимость только для демонстрации, Чарли добавит эту же зависимость в нашу инфраструктуру поиска, то пакет внезапно превратится из безобидной реализации в очень важную инфра- структуру. Но у нас нет рычагов, которые заставят Чарли насторожиться перед до- бавлением этой зависимости. Возможно, это идеальный сценарий: зависимость хорошо написана, не содержит оши- бок безопасности и не зависит от других проектов с открытым исходным кодом. Воз- можно, пакет просуществует несколько лет, не обновляясь, хотя это не будет разумно: внешние изменения могли бы оптимизировать его, добавить новые особенности или устранить дыры в безопасности до того, как они будут обнаружены CVE 1 . Чем дольше существует пакет, тем выше вероятность появления зависимостей (прямых и косвен- ных). Чем дольше пакет остается стабильным, тем выше вероятность, что мы, согласно закону Хайрама, будем полагаться на его версию, помещенную в каталог third_party Однажды Алиса и Боб узнают, что пакет нужно обновить. Причиной может быть обнаружение уязвимости в самом пакете или в проекте, который зависит от него. Но Боб перешел на руководящую должность и давно не касался кодовой базы, а Алиса 1 Common vulnerabilities and exposures — общий перечень уязвимостей и рисков. |