Делай как в Google. Разработка программного обеспечения - Титус Винтерс - E-Book

Делай как в Google. Разработка программного обеспечения E-Book

Титус Винтерс

0,0
10,99 €

-100%
Sammeln Sie Punkte in unserem Gutscheinprogramm und kaufen Sie E-Books und Hörbücher mit bis zu 100% Rabatt.
Mehr erfahren.
Beschreibung

Современные программисты должны не только эффективно программировать, но и знать надлежащие инженерные практики, позволяющие сделать кодовую базу стабильной и качественной. В чем же разница между программированием и программной инженерией? Как разработчик может управлять живой кодовой базой, которая развивается и реагирует на меняющиеся требования на всем протяжении своего существования? Основываясь на опыте Google, инженеры-программисты Титус Винтерс и Хайрам Райт вместе с Томом Маншреком делают откровенный и проницательный анализ того, как ведущие мировые практики создают и поддерживают ПО. Речь идет об уникальной инженерной культуре, процессах и инструментах Google, а также о том, как эти аспекты влияют на эффективность разработки. Вы изучите фундаментальные принципы, которые компании разработчиков ПО должны учитывать при проектировании, разработке архитектуры, написании и сопровождении кода.

Das E-Book können Sie in Legimi-Apps oder einer beliebigen App lesen, die das folgende Format unterstützen:

EPUB
MOBI

Seitenzahl: 872

Veröffentlichungsjahr: 2023

Bewertungen
0,0
0
0
0
0
0
Mehr Informationen
Mehr Informationen
Legimi prüft nicht, ob Rezensionen von Nutzern stammen, die den betreffenden Titel tatsächlich gekauft oder gelesen/gehört haben. Wir entfernen aber gefälschte Rezensionen.



Титус Винтерс, Том Маншрек, Хайрам Райт
Делай как в Google. Разработка программного обеспечения
2021

Научный редактор М. Коробко

Переводчик А.Киселев

Литературные редакторы А. Попова, А. Руденко

Художник В. Мостипан

Корректоры М. Одинокова, Н. Сидорова, Г. Шкатова

Титус Винтерс, Том Маншрек, Хайрам Райт

Делай как в Google. Разработка программного обеспечения. — СПб.: Питер, 2021.

ISBN 978-5-4461-1774-1

© ООО Издательство "Питер", 2021

Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.

Оглавление

Предисловие
Вступление
Программирование в долгосрочной перспективе
Точка зрения Google
Что не отражено в книге
Заключительные примечания
Условные обозначения
Благодарности
От издательства
Часть I. Тезисы
Глава 1. Что такое программная инженерия?
Время и изменения
Масштабирование и эффективность
Компромиссы и затраты
Программная инженерия и программирование
Заключение
Итоги
Часть II. Культура
Глава 2. Успешная работа в команде
Помоги мне скрыть мой код
Миф о гениальности
Сокрытие вредно
Весь секрет в командной работе
Заключение
Итоги
Глава 3. Обмен знаниями
Сложности в обучении
Философия
Создание условий: психологическая безопасность
Расширение знаний
Масштабирование вопросов: вопросы к сообществу
Распространяйте знания: вам всегда есть чему научить других
Распространение знаний с ростом организации
Удобочитаемость: наставничество через обзоры кода
Заключение
Итоги
Глава 4. Инженерия равенства
Предвзятость — это проблема
Понимание необходимости разнообразия
Выстраивание мультикультурного потенциала
Сделать разнообразие действенным
Отказ от единых подходов
Бросьте вызов устоявшимся процессам
Ценности и результаты
Оставайтесь любознательными, двигайтесь вперед
Заключение
Итоги
Глава 5. Как стать лидером в команде
Руководители и технические лидеры (и те и другие)
Переход от роли разработчика к роли лидера
Руководитель
Антипаттерны
Положительные паттерны
Неожиданный вопрос
Другие советы и рекомендации
Люди похожи на растения
Заключение
Итоги
Глава 6. Масштабируемое лидерство
Всегда принимайте решение
Всегда уходи
Всегда масштабируйте себя
Заключение
Итоги
Глава 7. Оценка продуктивности инженеров
Зачем оценивать продуктивность инженеров?
Расстановка приоритетов: что измерять?
Выбор значимых метрик с использованием целей и сигналов
Цели
Сигналы
Метрики
Использование данных для проверки метрик
Принятие мер и оценка результатов
Заключение
Итоги
Часть III. Процессы
Глава 8. Правила и руководства по стилю
Зачем нужны правила?
Создание правил
Изменение правил
Руководства
Применение правил
Заключение
Итоги
Глава 9. Код-ревью
Поток обзора кода
Как проводятся обзоры кода в Google
Преимущества обзоров кода
Передовые практики обзора кода
Виды обзоров кода
Заключение
Итоги
Глава 10. Документация
Что считается документацией?
Зачем нужна документация?
Документация как код
Знание своей аудитории
Виды документации
Обзоры документации
Философия документирования
Когда привлекать технических писателей?
Заключение
Итоги
Глава 11. Основы тестирования
Почему мы пишем тесты?
Проектирование набора тестов
Тестирование в масштабе Google
История тестирования в Google
Ограничения автоматизированного тестирования
Заключение
Итоги
Глава 12. Юнит-тестирование
Важность удобства сопровождения
Как предотвратить хрупкие тесты
Создание ясных тестов
Повторное использование тестов и кода: DAMP, не DRY
Заключение
Итоги
Глава 13. Тестирование с дублерами
Влияние тестовых дублеров на разработку ПО
Тестовые дублеры в Google
Базовые понятия
Приемы использования тестовых дублеров
Реальные реализации
Имитации
Заглушки
Тестирование взаимодействий
Заключение
Итоги
Глава 14. Крупномасштабное тестирование
Что такое большие тесты?
Большие тесты Google
Структура большого теста
Типы больших тестов
Большие тесты и рабочий процесс разработчика
Заключение
Итоги
Глава 15. Устаревание
Почему необходимо заботиться об устаревании
Почему устаревание вызывает такие сложности?
Подходы к прекращению поддержки
Управление процессом прекращения поддержки
Заключение
Итоги
Часть IV. Инструменты
Глава 16. Управление версиями и ветвями
Что такое управление версиями?
Управление ветвями
Управление версиями в Google
Монолитные репозитории
Будущее управления версиями
Заключение
Итоги
Глава 17. Code Search
Пользовательский интерфейс Code Search
Как гуглеры используют Code Search?
Зачем понадобился отдельный веб-инструмент?
Влияние масштаба на дизайн
Реализация в Google
Некоторые компромиссы
Заключение
Итоги
Глава 18. Системы и философия сборки
Назначение системы сборки
Так ли необходимы системы сборки?
Современные системы сборки
Модули и зависимости
Заключение
Итоги
Глава 19. Critique: инструмент обзора кода в Google
Принципы оснащения обзора кода инструментами
Процесс обзора кода
Этап 1: добавление изменений
Этап 2: запрос на рецензирование
Этапы 3 и 4: исследование и комментирование изменения
Этап 5: одобрение изменений (оценка изменений)
Этап 6: фиксация изменения
Заключение
Итоги
Глава 20. Статический анализ
Характеристики эффективного статического анализа
Ключевые уроки внедрения статического анализа
Tricorder: платформа статического анализа в Google
Заключение
Итоги
Глава 21. Управление зависимостями
Почему управлять зависимостями так сложно?
Импортирование зависимостей
Теория управления зависимостями
Ограничения SemVer
Управление зависимостями с бесконечными ресурсами
Заключение
Итоги
Глава 22. Крупномасштабные изменения
Что такое крупномасштабное изменение?
Кто занимается крупномасштабными изменениями?
Препятствия к атомарным изменениям
Инфраструктура для крупномасштабных изменений
Процесс крупномасштабных изменений
Заключение
Итоги
Глава 23. Непрерывная интеграция
Идеи непрерывной интеграции
Непрерывная интеграция в Google
Заключение
Итоги
Глава 24. Непрерывная поставка
Идиомы непрерывной поставки в Google
Скорость — это командная победа: как разделить процесс развертывания на управляемые этапы
Оценка изменений в изоляции: флаги управления функциями
Стремление к гибкости: создание серии выпусков
Качество и ориентация на пользователя: поставляйте только то, что используется
Сдвиг влево: раннее принятие решений на основе данных
Изменение культуры команды: дисциплина развертывания
Заключение
Итоги
Глава 25. Вычисления как услуга
Приручение вычислительной среды
Написание ПО для управляемых вычислений
CaaS во времени и масштабе
Выбор вычислительной услуги
Заключение
Итоги
Часть V. Заключение
Послесловие
Об авторах
Об обложке

Предисловие

Я всегда восхищался тем, как в Google все устроено, и мучил своих друзей-гуглеров (сотрудников Google) расспросами. Как им удается поддерживать такое огромное монолитное хранилище кода? Как десятки тысяч инженеров согласованно работают в тысячах проектов? Как они поддерживают качество систем?

Сотрудничество с «экс-гуглерами» только усилило мое любопытство. Если в одной команде с вами работает бывший инженер Google, вы будете часто слышать: «А вот мы в Google...» Переход из Google в другие компании кажется шокирующим опытом, по крайней мере для инженера. Человек со стороны считает системы и процессы разработки кода в Google одними из лучших в мире, учитывая масштаб компании и то, как часто люди хвалят ее.

В книге «Разработка ПО. Делай как в Google» группа гуглеров (и экс-гуглеров) раскрывает обширный набор практик, инструментов и даже культурных аспектов, лежащих в основе программной инженерии в Google. Но эта книга не ограничивается простым описанием инструментария (о котором можно говорить бесконечно) и дополнительно описывает философию команды Google, которая помогает сотрудникам адаптироваться к разным обстоятельствам. К моему восхищению, несколько глав книги посвящены автоматизированному тестированию, которое продолжает встречать активное сопротивление в отрасли.

Самое замечательное в программной инженерии — это возможность добиться желаемого результата несколькими способами. В каждом проекте инженер должен учитывать множество компромиссов. Что можно заимствовать из открытого исходного кода? Что может спроектировать команда? Что имеет смысл поддерживать для масштаба? У своих друзей-гуглеров я хотел узнать, как устроен гигантский мир Google, богатый талантами и средствами и отвечающий высочайшим требованиям к программному обеспечению (ПО). Их разнообразные ответы познакомили меня с неожиданными аспектами.

В этой книге изложены эти аспекты. Конечно, Google — уникальная компания, и неверно считать ее способ управления разработкой ПО единственно верным. Цель этой книги — помочь вам организовать работу и аргументировать принятие решений, связанных с тестированием, обменом знаниями и совместной деятельностью в команде.

Возможно, вам никогда не придется создавать свой Google и вы не захотите использовать методы этой компании. Но, отказавшись от знакомства с практиками, разработанными в Google, вы лишите себя богатого опыта, накопленного десятками тысяч инженеров, совместно работающих над разработкой софта более двух десятилетий. Это знание слишком ценно, чтобы закрывать на него глаза.

Камиль Фурнье, автор книги «От разработчика до руководителя» (М.: МИФ, 2018)

Вступление

Что именно мы подразумеваем под программной инженерией? Что отличает «программную инженерию» от «программирования» или «computer science»? И как подход Google связан с другими подходами в этой области, описанными во множестве книг в последние пятьдесят лет?

Термины «программирование» и «программная инженерия» используются в отрасли взаимозаменяемо, хотя каждый из них имеет собственное значение. Студенты университетов, как правило, изучают computer science и работают как «программисты».

Но «инженерия» подразумевает применение теоретических знаний для создания чего-то реального и точного. Инженеры-механики, инженеры-строители, авиационные инженеры и специалисты в других инженерных дисциплинах — все они занимаются инженерным делом: используют теоретические знания для создания чего-то реального. Инженеры-программисты также создают «нечто реальное», но это «нечто» неосязаемо.

В отличие от других инженерных сфер, программная инженерия более открыта для вариантов. Авиационные инженеры должны следовать стандартам, поскольку ошибки в их расчетах могут нанести реальный ущерб. Программирование традиционно не имеет жестких ограничений, но, поскольку ПО интегрируется в нашу жизнь, мы должны определить и использовать более надежные методы его разработки. Надеемся, что эта книга поможет вам увидеть путь к созданию современного ПО.

Программирование в долгосрочной перспективе

На наш взгляд, программная инженерия охватывает не только написание кода, но и все инструменты и процессы создания и дальнейшей поддержки этого кода. Какие методики может внедрить организация, занимающаяся разработкой ПО, чтобы сохранять ценность кода в долгосрочной перспективе? Как инженеры могут сделать кодовую базу более устойчивой, а саму дисциплину программной инженерии более строгой? У нас нет конкретных ответов на эти вопросы, но мы надеемся, что командный опыт Google, накопленный за последние два десятилетия, поможет нам их найти.

Одна из ключевых идей, которой мы делимся в этой книге, заключается в том, что программную инженерию можно рассматривать как «программирование, интегрированное во времени». Какие практики сделают код устойчивым — способным реагировать на изменения — в течение его жизненного цикла (от проекта его создания и внедрения до его устаревания)?

В этой книге подчеркиваются три фундаментальных принципа, которые, по нашему мнению, следует учитывать при проектировании, разработке и написании кода.

Время и изменения

Как код должен адаптироваться на протяжении срока действия.

Масштаб и рост

Как организация должна адаптироваться по мере своего развития.

Компромиссы и издержки

Как организация должна принимать решения, основываясь на показателях времени, изменений, масштаба и роста.

В книге мы часто будем возвращаться к этим принципам и показывать, как они влияют на инженерные практики и обеспечивают их устойчивость. (Полное обсуждение этих принципов смотрите в главе 1.)

Точка зрения Google

В компании Google сложился свой уникальный взгляд на рост и развитие устойчивого ПО, основанный на его масштабе и долговечности. Надеемся, что уроки, которые мы извлекли, найдут отражение и в вашей компании, которая тоже развивается и использует все более устойчивые практики.

Темы, обсуждаемые в этой книге, мы разделили на три основных аспекта ландшафта программной инженерии, сложившихся в Google:

• культура;

• процессы;

• инструменты.

Культура Google уникальна, но опыт, который мы получили, изучая ее, широко применим. В главах, посвященных культуре (часть II), мы подчеркнули коллективный характер разработки ПО и необходимость культуры сотрудничества для развития организации и сохранения ее эффективности.

Методики, описанные в части III «Процессы», знакомы большинству инженеров-программистов, но мы отметили, что большой размер кодовой базы Google и долгий срок ее действия обеспечивают надежную основу для выработки оптимальных практик. Мы научились учитывать время и масштаб, а также определили области, где у нас пока нет удовлетворительных ответов.

Наконец, в части IV «Инструменты» мы использовали инвестиции в инфраструктуру инструментов, чтобы получить дополнительные преимущества по мере роста и устаревания кодовой базы. Некоторые из этих инструментов специфичны для Google, но по возможности мы даем ссылки на их альтернативы. Мы полагаем, что основные идеи с успехом можно применять в большинстве инженерных организаций.

Культура, процессы и инструменты, представленные в этой книге, описывают уроки, которые типичный инженер-программист извлекает в процессе работы. Конечно, Google не обладает монополией на полезные советы, и мы делимся своим опытом не для того, чтобы диктовать вам правила. Эта книга — наша точка зрения, и мы надеемся, что вы найдете ее полезной и сможете применять наш опыт непосредственно или использовать его в качестве отправной точки при выработке своих практик, специфичных для вашей предметной области.

Также эта книга не является учебником. В самой компании Google по-прежнему недостаточно эффективно применяются многие идеи, представленные здесь. Наш опыт является результатом наших неудач: мы все еще допускаем ошибки, внедряем несовершенные решения и должны последовательно улучшать код. Тем не менее огромный размер Google гарантирует существование множества решений для каждой проблемы. Мы надеемся, что в этой книге нам удалось собрать самые лучшие из них.

Что не отражено в книге

Эта книга не охватывает вопросы проектирования ПО (собранная по этой теме информация заслуживает отдельной книги). Мы привели некоторый код, но только для примера, чтобы показать основные принципы, которые не зависят от языка, и в этих главах практически нет рекомендаций по «программированию». Также эта книга не охватывает многих важных вопросов, касающихся разработки ПО, таких как управление проектами, разработка API, усиление безопасности, интернационализация, применение фреймворков пользовательского интерфейса и других проблем, связанных с конкретным языком программирования. Мы решили оставить эти темы в стороне, понимая, что не сможем раскрыть их в достаточной степени, и уделили больше внимания инженерии, а не программированию.

Заключительные примечания

Эта книга является результатом совместных усилий и должна послужить окном в мир программной инженерии крупной организации, создающей свои продукты. Надеемся, что вместе с другими источниками она поможет нашей отрасли внедрять наиболее передовые и устойчивые практики. Но самое главное, мы хотим, чтобы книга вам понравилась и вы взяли на вооружение некоторые из представленных здесь уроков.

Том Маншрек

Условные обозначения

В этой книге приняты следующие обозначения:

Курсив

Используется для обозначения новых терминов.

Моноширинныйшрифт

Применяется для оформления листингов программ и программных элементов внутри обычного текста, таких как имена переменных и функций, баз данных, типов данных, переменных окружения, инструкций и ключевых слов.

Так обозначаются советы, предложения и примечания общего характера.

Благодарности

Эта книга является результатом труда множества людей. Все представленное здесь является квинтэссенцией знаний, накопленных нами и другими сотрудниками Google за долгие годы. Мы лишь передаем вам опыт, которым поделились с нами старшие коллеги из Google и других компаний. Перечислить их всех трудно, но мы выражаем им свою благодарность.

Также мы благодарим Мелоди Мекфессель (Melody Meckfessel) за поддержку этого проекта на начальном этапе, а также Дэниела Джаспера (Daniel Jasper) и Дэнни Берлина (Danny Berlin) за помощь в его завершении.

Эта книга была бы невозможна без колоссальных совместных усилий наших кураторов, авторов и редакторов. Авторы и редакторы указаны в каждой главе или врезке, а здесь мы отметим всех, кто внес вклад в каждую главу, участвуя в обсуждении и подготовке к печати.

• Что такое программная инженерия? Санджай Гемават (Sanjay Ghemawat), Эндрю Хаятт (Andrew Hyatt).

• Успешная работа в команде: Сибли Бэкон (Sibley Bacon), Джошуа Мортон (Joshua Morton).

• Обмен знаниями: Дмитрий Глазков, Кайл Лимонс (Kyle Lemons), Джон Риз (John Reese), Дэвид Симондс (David Symonds), Эндрю Тренк (Andrew Trenk), Джеймс Такер (James Tucker), Дэвид Колбреннер (David Kohlbrenner), Родриго Дамацио Бовендорп (Rodrigo Damazio Bovendorp).

• Инженерия равенства: Камау Бобб (Kamau Bobb), Брюс Ли (Bruce Lee).

• Как стать лидером в команде: Джон Уили (Jon Wiley), Лоран Ле Брун (Laurent Le Brun).

• Масштабируемое лидерство: Брайан О’Салливан (Bryan O’Sullivan), Бхарат Медиратта (Bharat Mediratta), Дэниел Джаспер (Daniel Jasper), Шайндел Шварц (Shaindel Schwartz).

• Оценка продуктивности инженеров: Андреа Найт (Andrea Knight), Коллин Грин (Collin Green), Кетлин Садовски (Caitlin Sadowski), Макс-Канат Александер (Max-Kanat Alexander), Илай Янг (Yilei Yang).

• Правила и руководства по стилю: Макс-Канат Александер (Max-Kanat Alexander), Титус Винтерс (Titus Winters), Мэтт Аустерн (Matt Austern), Джеймс Деннетт (James Dennett).

• Код-ревью: Макс-Канат Александер (Max-Kanat Alexander), Брайан Ледгер (Brian Ledger), Марк Баролак (Mark Barolak).

• Документация: Йонас Вагнер (Jonas Wagner), Смит Хинсу (Smit Hinsu), Джеффри Ромер (Geoffrey Romer).

• Основы тестирования: Эрик Куфлер (Erik Kufler), Эндрю Тренк (Andrew Trenk), Диллон Блай (Dillon Bly), Джозеф Грейвс (Joseph Graves), Нил Норвитц (Neal Norwitz), Джей Корбетт (Jay Corbett), Марк Стрибек (Mark Striebeck), Брэд Грин (Brad Green), Мишко Хевери (Miško Hevery), Антуан Пикар (Antoine Picard), Сара Сторк (Sarah Storck).

• Юнит-тестирование: Эндрю Тренк (Andrew Trenk), Адам Бендер (Adam Bender), Диллон Блай (Dillon Bly), Джозеф Грейвс (Joseph Graves), Титус Винтерс (Titus Winters), Хайрам Райт (Hyrum Wright), Оги Факлер (Augie Fackler).

• Тестирование с дублерами: Джозеф Грейвс (Joseph Graves), Геннадий Цивил (Gennadiy Civil).

• Крупномасштабное тестирование: Адам Бендер (Adam Bender), Эндрю Тренк (Andrew Trenk), Эрик Кюфлер (Erik Kuefler), Мэтью Бомонт-Гай (Matthew Beaumont-Gay).

• Устаревание: Грег Миллер (Greg Miller), Энди Шульман (Andy Shulman).

• Управление версиями и ветвями: Рейчел Потвин (Rachel Potvin), Виктория Кларк (Victoria Clarke).

• Code Search: Дженни Ван (Jenny Wang).

• Системы и философия сборки: Хайрам Райт (Hyrum Wright), Титус Винтерс (Titus Winters), Адам Бендер (Adam Bender), Джефф Кокс (Jeff Cox), Жак Пиенаар (Jacques Pienaar).

• Critique: инструменты обзора кода в Google: Миколай Додела (Mikołaj Dądela), Герман Луз (Hermann Loose), Ева Мэй (Eva May), Элис Кобер-Соцек (Alice Kober-Sotzek), Эдвин Кемпин (Edwin Kempin), Патрик Хизель (Patrick Hiesel), Оле Ремсен (Ole Rehmsen), Ян Мацек (Jan Macek).

• Статический анализ: Джеффри ван Гог (Jeffrey van Gogh), Сиера Джаспан (Ciera Jaspan), Эмма Седерберг (Emma Söderberg), Эдвард Афтандилиан (Edward Aftandilian), Коллин Винтер (Collin Winter), Эрик Хо (Eric Haugh).

• Управление зависимостями: Расс Кокс (Russ Cox), Николас Данн (Nicholas Dunn).

• Крупномасштабные изменения: Мэтью Фаулз Кулукундис (Matthew Fowles Kulukundis), Адам Зарек (Adam Zarek).

• Непрерывная интеграция: Джефф Листфилд (Jeff Listfield), Джон Пеникс (John Penix), Каушик Шридхаран (Kaushik Sridharan), Санджив Дханда (Sanjeev Dhanda).

• Непрерывная поставка: Дэйв Оуэнс (Dave Owens), Шери Шипе (Sheri Shipe), Бобби Джонс (Bobbi Jones), Мэтт Дафтлер (Matt Duftler), Брайан Шутер (Brian Szuter).

• Вычисления как услуга: Тим Хокин (Tim Hockin), Коллин Винтер (Collin Winter), Ярек Кузьмиерек (Jarek Kuśmierek).

Также мы хотим поблагодарить Бетси Бейер за то, что поделилась опытом работы над книгой «Site Reliability Engineering. Надежность и безотказность как в Google»1. Спасибо Кристоферу Гузиковски (Christopher Guzikowski) и Алисии Янг (Alicia Young) из O'Reilly, которые запустили наш проект и довели его до публикации.

Кураторы также хотели сказать отдельное спасибо:

Том Маншрек (Tom Manshreck): «Маме и папе за то, что помогли мне поверить в себя и решали со мной домашние задания за обеденным столом».

Титус Винтерс (Titus Winters): «Папе за мой путь. Маме — за мой голос. Виктории — за мое сердце. Рафу — за мою спину. А также мистеру Снайдеру, Ранве, Зеду (Z), Майку, Заку, Тому и всем Пейнам, Меку (mec), Тоби, Кгд (cgd) и Мелоди за уроки, наставничество и доверие».

Хайрам Райт (Hyrum Wright): «Маме и папе за поддержку. Брайану и завсегдатаям Бейкерленда за мое первое погружение в мир ПО. Дюэйну (Dewayne) за продолжение этого путешествия. Ханне, Джонатану, Шарлотте, Спенсеру и Бену за их дружбу и интерес. Хизер за то, что все это время была рядом».

От издательства

Ваши замечания, предложения, вопросы отправляйте по адресу [email protected] (издательство «Питер», компьютерная редакция).

Мы будем рады узнать ваше мнение!

На веб-сайте издательства www.piter.com вы найдете подробную информа­цию о наших книгах.

1Бейер Б., Джоунс К., Петофф Д., Мёрфи Н.Р. Site Reliability Engineering. Надежность и безотказность как в Google. СПб.: Питер, 2021. 592 с.: ил.

Часть I. Тезисы

Глава 1. Что такое программная инженерия?

Автор: Титус Винтерс

Редактор: Том Маншрек

Ничто не строится на камнях; все построено на песке, но мы должны строить так, как если бы песок был камнем.

Хорхе Луис Борхес

Программирование и программная инженерия имеют три важных отличия: время, масштаб и компромиссы. В отличие от обычных программистов, инженеры-программисты должны больше внимания уделять течению времени и необходимости внесения изменений, больше заботиться о масштабе и эффективности как самого ПО, так и организации, которая его производит. Инженеры-программисты принимают более сложные решения, дороже платят за ошибки и часто опираются на неточные оценки времени и роста.

Мы в Google иногда говорим: «Программная инженерия — это программирование, интегрированное во времени». Программирование, безусловно, является важной частью программной инженерии: в конце концов, именно в процессе программирования создается новый софт. Но если мы разделяем понятия, то нужно разграничить задачи программирования (разработку) и программной инженерии (разработку, изменение, сопровождение). Время — это новое важное измерение в программировании. Куб — это не квадрат, расстояние — это не скорость. Программная инженерия — это не программирование.

Чтобы понять, как время влияет на программу, задумайтесь: «Как долго будет жить2 код?» Крайние оценки в ответах на этот вопрос могут отличаться в 100 000 раз. Легко представить код, который просуществует несколько минут, или другой код, служащий десятилетиями. Как правило, короткоживущий код не зависит от времени: едва ли нужно адаптировать утилиту, которая проживет час, к новым версиям базовых библиотек, операционной системы (ОС), аппаратного обеспечения или языков программирования. Недолговечные системы фактически являются «мимолетными» задачами программирования и напоминают куб, который сжат вдоль одного измерения до вида квадрата. Но с увеличением продолжительности жизни кода изменения становятся более важными для него. За десяток лет большинство программных зависимостей, явных или неявных, почти наверняка изменятся. Понимание этого явления помогает отличать программную инженерию от программирования.

Это отличие лежит в основе устойчивости в мире ПО. Проект будет более устойчив, если в течение ожидаемого срока его службы инженер-программист сможет сохранить способность реагировать на любые важные технические или коммерческие изменения. Именно «сможет» реагировать в том случае, если обновление будет иметь ценность3. Когда вы теряете способность реагировать на изменения в базовых технологиях или развитии продукта, не надейтесь, что такие изменения никогда не превысят критического уровня. В условиях проекта, развивающегося несколько десятилетий, такие надежды обречены на провал4.

Взглянуть на программную инженерию можно со стороны оценки масштаба проекта. Сколько людей вовлечено в разработку? Как меняются их роли при разработке и сопровождении проекта с течением времени? Программирование часто является актом индивидуального творчества, но программная инженерия — это командная работа. Одно из первых и самых точных определений программной инженерии звучит так: «Разработка многоверсионных программ для большого числа людей»5. Оно означает, что различие между программной инженерией и программированием определяется количеством пользователей и сроком действия продукта. Командная работа создает новые проблемы, но также открывает такие возможности для создания систем, какие не может предложить один программист.

Организация команды, состав проекта, а также стратегия и тактика его развития — важные компоненты программной инженерии, которые зависят от масштаба организации. Растет ли эффективность производства софта по мере увеличения организации и расширения ее проектов? Растет ли эффективность рабочего процесса по мере развития организации и насколько пропорционально этому растет стоимость стратегий тестирования и управления версиями? Проблемы масштаба, связанные с увеличением числа сотрудников и налаживанием коммуникации между ними, обсуждались с первых дней программной инженерии, начиная с появления мифического человеко-месяца6. Часто они имеют политический характер, и от их решения во многом зависят устойчивость ПО и ответ на вопрос: «Как дорого обойдется то, что придется делать снова и снова?»

Еще одно отличие программной инженерии от программирования заключается в сложности принятия решений и цене ошибок. Постоянная оценка компромиссов между несколькими путями движения вперед часто основана на несовершенных данных, и иногда цена ошибки очень высока. Работа инженера-программиста или лидера команды инженеров состоит в стремлении к устойчивости организации, продукта и процесса разработки, а также в управлении ростом затрат. Иногда можно откладывать технические изменения или даже принимать плохо масштабируемые политики, которые потом придется пересмотреть. Но, делая такой выбор, инженер-программист должен понимать, к каким затратам эти решения приведут в будущем.

Универсальные решения в программной инженерии встречаются редко, как вы увидите и в этой книге. Учитывая разброс в 100 000 раз между ответами на вопрос «Как долго будет жить ПО?» и в 10 000 раз — между ответами на вопросы «Сколько инженеров работает в компании?» и «Сколько вычислительных ресурсов доступно для проекта?», опыт Google почти наверняка не будет соответствовать вашей ситуации. Поэтому в этой книге мы постарались показать, как в Google искали правильные пути в разработке и сопровождении ПО, рассчитанного на десятилетия, имея десятки тысяч инженеров и вычислительные ресурсы мирового масштаба. Большинство методов, применение которых мы считаем обязательным в таком масштабе, также хорошо подойдут для меньших организаций: считайте эту книгу отчетом одной инженерной экосистемы, который может вам пригодиться. Иногда сверхбольшие масштабы связаны с повышенными расходами, и, возможно, благодаря нашим предупреждениям, когда ваша организация вырастет до таких масштабов, вы сможете найти более удачное решение по издержкам.

Прежде чем перейти к обсуждению особенностей командной работы, культуры, политики и инструментов, давайте подробнее рассмотрим время, масштаб и компромиссы.

Время и изменения

Когда человек учится программировать, срок действия его кодов обычно измеряется часами или днями. Задачи и упражнения по программированию, как правило, пишутся с нуля, почти без рефакторинга и, конечно, без долгосрочного сопровождения. Часто после первого использования эти программы не пересобираются и не применяются на практике. Получая среднее или высшее образование, вы могли участвовать в групповой реализации проекта, который живет месяц или чуть дольше. Такой проект может включать рефакторинг кода, например в ответ на изменяющиеся требования, но маловероятно, что он будет зависеть от более широких изменений в его окружении.

Разработчиков короткоживущего кода можно также найти в некоторых отраслях. Мобильные приложения, например, обычно имеют довольно короткий срок действия7 и часто переписываются заново. На ранних стадиях развития стартапов инженеры могут вполне обоснованно сосредоточиться на ближних целях в ущерб долгосрочным инвестициям, поскольку сама компания, в которой они работают, может просуществовать недолго. Разработчик серийного стартапа вполне может иметь десятилетний опыт разработки и почти или совсем не иметь опыта поддержки ПО, которое должно работать дольше одного-двух лет.

С другой стороны, есть успешные проекты с практически неограниченным сроком службы: трудно предсказать, когда прекратит свое существование Google Search, ядро Linux или Apache HTTP Server. Большинство проектов Google должны существовать неопределенно долго и периодически претерпевать обновления зависимостей, языковых версий и т.д. С течением времени такие долгоживущие проекты рано или поздно начинают восприниматься иначе, чем задачи по программированию или развитию стартапа.

На рис. 1.1 показаны два программных проекта на противоположных концах спектра «ожидаемого срока службы». Как обслуживать проект с ожидаемым сроком службы, измеряемым часами? Должен ли программист бросить все и заняться обновлением, если во время работы над сценарием на Python, который будет выполнен всего один раз, вышла новая версия ОС? Конечно, нет: такое обновление некритично. Но если проект Google Search, находящийся на противоположном конце спектра, застрянет на версии ОС 1990-х годов, обслуживание станет проблемой.

Рис. 1.1. Срок жизни и важность обновлений

Наличие точек на спектре сроков службы, соответствующих низкой и высокой важности обновлений, предполагает, что где-то есть переход. Где-то на линии, соединяющей одноразовую программу и проект, развивающийся десятилетиями, есть этап появления реакции проекта на изменение внешних факторов8. Любой проект, в котором изначально не планировались обновления, переживает переход болезненно по трем причинам, каждая из которых усугубляет две другие:

• обновления еще не выполнялись в этом проекте: по ним есть только предположения;

• инженеры едва ли имеют опыт проведения обновлений;

• большой объем обновлений: одномоментно приходится применять обновления, накопившиеся за несколько лет, вместо постепенного применения небольших обновлений.

Далее, выполнив такое обновление один раз (полностью или частично), вы рискуете переоценить стоимость следующего обновления и решить: «Никогда больше». Компании, которые приходят к такому выводу, заканчивают тем, что просто выкидывают старый код и пишут его заново или решают никогда не обновлять его снова. Вместо того чтобы поддаться естественному желанию избежать болезненной процедуры, часто полезнее инвестировать в этот процесс, чтобы сделать его менее болезненным. Впрочем, выбор зависит от стоимости обновления, его ценности и ожидаемого срока службы проекта.

Суть устойчивости проекта заключается не только в преодолении первого крупного обновления, но и в достижении уверенности, что проект идет в ногу со временем. Устойчивость требует оценки влияния необходимых изменений и управления ими. Мы считаем, что достигли такой устойчивости во многих проектах в Google, в основ­ном путем проб и ошибок.

Итак, чем отличается программирование короткоживущего кода от производства кода с более долгим ожидаемым сроком службы? С течением времени мы стали намного четче осознавать разницу между «работающим по счастливой случайности» и «удобным в сопровождении». Не существует идеального решения вышеназванных проблем. Это прискорбно, потому что длительное сопровождение ПО — это постоянная борьба.

Закон Хайрама

Если вы поддерживаете проект, который используется другими инженерами, то между «работающим по счастливой случайности» и «удобным в сопровождении» есть одно важное отличие, которое мы назвали законом Хайрама:

Если число пользователей API достаточно велико, неважно, что вы обещаете в контракте: любое наблюдаемое поведение системы будет зависеть от чьих-то действий.

По нашему опыту, эта аксиома является доминирующим фактором в любом обсуждении изменения ПО. На концептуальном уровне ее можно сравнить с энтропией: закон Хайрама обязательно должен учитываться при обсуждении изменений и сопровождения9, так же как при обсуждении вопросов эффективности или термодинамики должна учитываться энтропия. Тот факт, что энтропия никогда не уменьшается, не означает, что мы не должны стремиться к эффективности. То, что закон Хайрама будет действовать применительно к сопровождению ПО, не означает, что мы не должны планировать или пытаться лучше понять это сопровождение. Можно смягчить последствия проблем, даже если мы знаем, что они никогда не исчезнут.

Закон Хайрама основан на практическом понимании, что даже при наличии самых лучших намерений, лучших инженеров и широкого круга методик проверки кода нельзя надеяться на полное соблюдение опубликованных контрактов или передовых практик. Как владелец API вы имеете некоторую свободу, четко понимая возможности интерфейса, но на практике сложность изменения также зависит от того, насколько полезным для пользователя является наблюдаемое поведение API. Если пользователи не зависят от него, изменить API будет легко. Но с течением времени и при достаточном количестве пользователей даже самые безобидные изменения обязательно что-то нарушат10. Анализируя ценность изменений, учитывайте трудности, связанные с поиском, выявлением и устранением нарушений, которые они вызовут.

Пример: упорядоченный хеш

Рассмотрим пример упорядочения итераций по хешу. Если вставить в хеш пять элементов, в каком порядке мы их получим?

>>> for i in {"apple", "banana", "carrot", "durian", "eggplant"}: print(i)

...

durian

carrot

apple

eggplant

banana

Большинство программистов знают, что хеш-таблицы не упорядочивают данные явно. Но почти никто не знает, что, возможно, хеш-таблица, которую они используют, возвращает содержимое в определенном порядке. Этот факт кажется непримечательным, но за последнюю пару десятилетий опыт использования хэша эволюционировал.

• Атаки переполнения хеша (hash flooding)11 стимулируют недетерминированный характер хранения данных в хеше.

• Потенциальный выигрыш от поиска усовершенствованных алгоритмов хеширования или хеш-контейнеров требует изменения порядка итераций в хеше.

• Согласно закону Хайрама, программисты по возможности пишут программы, зависящие от порядка обхода хеш-таблицы.

Если спросить эксперта: «Можно ли положиться на конкретный порядок обхода элементов в хеш-контейнере?» — он наверняка ответит: «Нет». Это правильный ответ, но слишком упрощенный. Более точный ответ мог бы звучать так: «Если код недолговечный и не предполагает будущих изменений в аппаратном или программном окружении или структуре данных, то это вполне допустимо. Но если известно, что код будет жить долго или нельзя гарантировать, что зависимости никогда не изменятся, то предположение неверно». Более того, даже если ваша собственная реализация не зависит от порядка хранения данных в хеш-контейнере, этот порядок может использоваться другим кодом, неявно создающим такую зависимость. Например, если ваша библиотека сериализует значения перед вызовом удаленной процедуры (RPC, remote procedure call), вызывающая сторона может оказаться в зависимости от порядка следования этих значений.

Это очень простой пример различия между «это работает» и «это правильно». Зависимость недолговечной программы от порядка хранения данных в контейнере не вызывает технических проблем. С другой стороны, для проекта, срок жизни которого преодолевает некоторый порог, такая зависимость представляет значительный риск: по прошествии времени кто-то или что-то может сделать этот порядок ценным. Ценность проявляется по-разному: как эффективность, безопасность или просто пригодность структуры данных для изменения в будущем. Когда ценность очевидна, взвесьте все за и против, выбирая между этой ценностью и проблемами, с которыми могут столкнуться разработчики или клиенты.

Некоторые языки намеренно изменяют порядок хеширования в каждой следующей версии библиотеки или при каждом запуске программы, чтобы предотвратить появление зависимости от этого порядка. Но даже в этом случае закон Хайрама преподносит сюрпризы, поскольку такое упорядочивание происходит с использованием генератора случайных чисел. Устранение случайности может нарушить работу кода пользователей, полагающихся на него. В любой термодинамической системе энтропия увеличивается, и точно так же к любому наблюдаемому поведению применим закон Хайрама.

Между кодами, написанными для «работы сейчас» и для «работы всегда», можно выделить четкие взаимосвязи. Рассматривая код как артефакт с переменным (в широких пределах) временем жизни, можно определить стили программирования: код, зависящий от хрупких и недокументированных особенностей, скорее всего, будет классифицирован как «хакерский» или «хитрый», тогда как код, следующий передовым практикам и учитывающий возможность развития в будущем, вероятно, будет считаться «чистым» и «удобным для сопровождения». Оба стиля имеют свои цели, но выбор одного из них во многом зависит от ожидаемого срока службы кода. Мы привыкли использовать термин программирование, если характеристика «хитрый» — это комплимент, и программная инженерия, если слово «хитрый» имеет отрицательный оттенок.

Почему бы просто не «отказаться от изменений»?

Все наше обсуждение времени и реакции на изменения связано с неизбежностью изменений. Верно?

Как и все остальное, о чем говорится в книге, принятие решения об обновлении зависит от обстоятельств. Мы готовы подтвердить, что «в большинстве проектов, существующих достаточно долго, рано или поздно возникает необходимость обновления». Если у вас есть проект, написанный на чистом C и не имеющий внешних зависимостей (или имеющий только зависимости, гарантирующие стабильность в течение долгого времени, такие как стандарт POSIX), вы вполне можете избежать рефакторинга или сложного обновления. Разработчики языка С прикладывают значительные усилия, чтобы обеспечить его стабильность.

Но большинство проектов подвержены изменениям в базовой технологии. В основном языки программирования и среды выполнения меняются активнее, чем язык C. Даже библиотеки, реализованные на чистом C, могут меняться для поддержки новых функций и тем самым влиять на пользователей. Проблемы безопасности есть во всех технологиях, от процессоров до сетевых библиотек и прикладного кода. Каждый элемент технологии может стать причиной критической ошибки и уязвимости безопасности, о которых вы не узнаете заранее. Если вы не применили исправления для Heartbleed (http://heartbleed.com) или не смягчили проблемы с упреждающим выполнением, такие как Meltdown и Spectre (https://meltdownattack.com), потому что полагали (или были уверены), что ничего не изменится, последствия будут серьезными.

Повышение эффективности еще больше осложняет картину. Мы стараемся оснастить вычислительные центры экономичным оборудованием, разумно использующим процессоры. Но старые алгоритмы и структуры данных на новом оборудовании работают хуже: связанный список или двоичное дерево поиска продолжают работать, но растущий разрыв между скоростями работы процессора и памяти влияет на «эффективность» кода. Со временем ценность обновления аппаратного обеспечения может уменьшаться в отсутствие изменений в архитектуре ПО. Обратная совместимость гарантирует работоспособность старых систем, но не гарантирует эффективности старых оптимизаций. Нежелание или неспособность воспользоваться новыми аппаратными возможностями чреваты большими издержками. Подобные проблемы сложны: оригинальный дизайн может быть логичным и разумным для своего времени, но после эволюции обратно совместимых изменений предпочтительным становится новый вариант (ошибок не было, но время сделало изменения ценными и востребованными).

Подобные проблемы объясняют, почему в долгосрочных проектах необходимо уделять внимание устойчивости независимо от того, влияют ли они непосредственно на нас или только на технологии. Изменения — это не всегда хорошо. Мы не должны меняться только ради перемен, но должны готовиться к изменениям и инвестировать в их удешевление. Как известно каждому системному администратору, одно дело — знать, что есть возможность восстановить данные с ленты, и совсем другое — знать, как это сделать и во что это обойдется. Практика и опыт — это двигатели эффективности и надежности.

Масштабирование и эффективность

Как отмечается в книге Бетси Бейер и др. «Site Reliability Engineering. Надежность и безотказность как в Google» (СПб.: Питер, 2019) (далее Site Reliability Engineering — SRE), производственная система в компании Google относится к числу самых сложных систем, созданных человеком. Формирование такой машины и поддержание ее бесперебойной работы потребовали бесчисленных часов размышлений, обсуждений и проектирования с участием экспертов со всего мира.

Большая часть этой книги посвящена сложностям, связанным с масштабированием в организации, которая производит такие машины, и процессам поддержания этой машины в рабочем состоянии в течение долгого времени. Давайте снова вернемся к понятию устойчивости кодовой базы: «База кода организации является устойчивой, если вы можете без опаски изменять то, что нужно, в течение срока ее службы». В данной формулировке под возможностями также понимаются издержки: если обновление чего-либо сопряжено с чрезмерными затратами, оно, скорее всего, будет отложено. Если со временем затраты растут сверхлинейно, это значит, что выполняемая операция не масштабируется12 и наступит момент, когда изменения станут неизбежными. Когда проект вырастет вдвое и понадобится выполнить эту операцию снова, окажется ли она вдвое более трудоемкой? Найдутся ли человеческие ресурсы для ее выполнения в следующий раз?

Люди — не единственный ограниченный ресурс, который необходимо наращивать. Само ПО должно хорошо масштабироваться в отношении традиционных ресурсов, таких как вычислительная мощность, память, объем хранилища и пропускная способность. Разработка ПО также должна масштабироваться с точки зрения количества участвующих в ней людей и объема вычислительных ресурсов. Если стоимость вычислений на тестовом кластере растет сверхлинейно и на каждого человека в квартал приходится все больше вычислительных ресурсов, это значит, что положение неустойчиво и скоро придется что-то изменить.

Наконец, самый ценный актив организации, производящей ПО, — кодовая база — тоже нуждается в масштабировании. При сверхлинейном росте системы сборки или системы управления версиями (VCS, version control system) (возможно, в результате увеличения истории изменений) может наступить момент, когда работать с ней станет невозможно. Многим аспектам, таким как «время для полной сборки», «время для получения новой копии репозитория» или «стоимость обновления до новой языковой версии», не уделяется должного внимания из-за того, что они меняются очень медленно. Но они с легкостью могут превратиться в метафорическую сварившуюся лягушку (https://oreil.ly/clqZN): медленно накапливающиеся проблемы слишком легко усугубляются и почти никогда не проявляются в виде конкретного момента кризиса. Только имея полное представление об организации в целом и стремясь к масштабированию, вы, возможно, сможете оставаться в курсе этих проблем.

Все, что организация использует для производства и поддержки кода, должно быть масштабируемым с точки зрения издержек и потребления ресурсов. В частности, все операции, которые организация должна выполнять снова и снова, должны быть масштабируемыми относительно человеческих усилий. С этой точки зрения многие распространенные политики не могут считаться хорошо масштабируемыми.

Плохо масштабируемые политики

Даже небольшой опыт разработки позволяет легко определять политики, плохо поддающиеся масштабированию, чаще всего по изменению объема работы, приходящемуся на одного инженера, при расширении компании. Если организация увеличится в 10 раз, увеличится ли в 10 раз нагрузка на одного инженера? Увеличится ли объем работы, которую он должен выполнять, при увеличении кодовой базы? Если на какой-то из этих вопросов будет дан положительный ответ, найдутся ли механизмы для автоматизации или оптимизации работы этого инженера? Если нет, значит, в организации есть явные проблемы с масштабированием.

Рассмотрим традиционный подход к устареванию (подробнее об устаревании в главе 15) в контексте масштабирования. Представьте, что принято решение использовать новый виджет вместо старого. Чтобы мотивировать разработчиков, руководители проекта говорят: «Мы удалим старый виджет 15 августа, не забудьте перейти к использованию нового виджета».

Такой подход хорошо работает в небольших проектах, но быстро терпит неудачу с увеличением глубины и широты графа зависимостей. Команды зависят от постоянно растущего числа виджетов, и любое нарушение в сборке может повлиять на рост компании. При этом вместо перекладывания хлопот, связанных с миграцией нового кода, на клиентов команды могут сами внедрить все необходимое, используя преимущества экономии от масштабируемого решения.

В 2012 году мы начали смягчать проблему обновления и поручили командам, отвечающим за инфраструктуру, переводить внутренних пользователи на применение новых версий или выполнять обновление на месте с обеспечением обратной совместимости. Эта стратегия, которую мы назвали «правилом обновления», хорошо масштабируется: зависимые проекты перестали тратить все больше и больше усилий, чтобы просто не отстать. Мы также выяснили, что специальная группа экспертов внедряет обширные изменения намного лучше, чем пользователи: эксперты некоторое время изучают всю глубину изменения, а затем применяют полученные знания в каждой подзадаче. Внедрение изменений пользователем вызывает замедление в работе: он вынужден решать проблему непосредственно у себя, а затем выбрасывать ставшие бесполезными знания. Опыт экспертов масштабируется лучше.

Еще один пример плохо масштабируемой политики — традиционное использование ветвей разработки. Если слияние крупных изменений с главной ветвью дестабилизировало продукт, можно сделать вывод: «Нужно более жесткое управление слиянием. Слияния должны производиться реже!» и для каждой команды (или функциональной возможности) создать отдельные ветви разработки. Когда такая ветвь достигнет конечной точки разработки, она будет протестирована и объединена с главной ветвью, из-за чего другие инженеры, работающие над другими ветвями, будут вынуждены повторно синхронизировать репозитории и проводить тесты. Такой способ управления ветвями можно использовать в небольшой организации, где одновременно разрабатывается 5–10 подобных ветвей. Но по мере роста организации (и количества ветвей) этот подход увеличивает накладные расходы на многократное выполнение одной и той же задачи. Более эффективный подход обсудим в главе 16.

Хорошо масштабируемые политики

Какие политики помогают оптимизировать затраты по мере роста организации? Точнее, какие виды политики можно внедрить, чтобы обеспечить суперлинейный рост ценности при росте организации?

Одна из наших любимых внутренних политик позволяет командам поддержки инфраструктуры безопасно вносить изменения в инфраструктуру. «Если в работе продукта есть сбои или другие проблемы после изменений в инфраструктуре, но их причины не выявлены тестами в системе непрерывной интеграции (CI, continuous integration), значит, эти причины не связаны с изменениями». Другими словами: «Если вам что-то понравилось, добавьте соответствующий тест в систему непрерывной интеграции». Мы называем это «правилом Бейонсе»13. С точки зрения масштабирования правило Бейонсе подразумевает, что сложные одноразовые специализированные тесты, которые не запускаются системой непрерывной интеграции, не нужно учитывать: инженер инфраструктурной команды может найти другую команду, добавившую код, и спросить, как этот код тестировать. Это удобно, когда в команде сто инженеров, но со временем нам пришлось расширить эту стратегию.

Мы обнаружили высокую эффективность форумов для общения в крупных организациях. Когда инженеры обсуждают вопросы на форумах, знания быстро распространяются, и новые специалисты набираются опыта. Если у вас есть сотня инженеров, пишущих на Java, то один опытный эксперт по Java, готовый ответить на их вопросы, вскоре даст вам сотню инженеров, пишущих лучший код на Java. Знания — это вирусы, а эксперты — их носители. Более подробно коснемся этой темы в главе 3.

Пример: обновление компилятора

Рассмотрим сложную задачу обновления компилятора. Теоретически обновление компилятора не должно вызывать сложностей, учитывая, сколько усилий прилагают разработчики языков, чтобы сохранить обратную совместимость. Но насколько простым является это обновление на практике? Если прежде вам никогда не приходилось выполнять такое обновление, как бы вы оценили его совместимость с вашей базой кода?

По нашему опыту, обновление языка и компилятора — тонкая и сложная задача, даже если ожидается, что новая версия сохранит обратную совместимость. Обновление компилятора почти всегда приводит к незначительным изменениям в его поведении и требует исправления ошибок компиляции, настройки флагов оптимизации или других ранее не выявленных изменений. Как оценить исправность всей кодовой базы, учитывая все потенциальные проблемы обновления компилятора?

Самое сложное обновление компилятора за всю историю Google было проведено в 2006 году. На тот момент мы уже несколько лет имели в штате тысячи инженеров и не обновляли компиляторы около пяти лет. Большинство инженеров не имели опыта смены компилятора, а большая часть кода компилировалась только одной версией компилятора. Обновление стало трудной и утомительной задачей для (в основ­ном) добровольцев, которая в конечном итоге свелась к поиску коротких путей обхода изменений с проблемами адаптации14. Внедрение изменений происходило болезненно: многие проблемы, согласно закону Хайрама, проникли в кодовую базу и углубили его зависимости от конкретной версии компилятора, сломать которые было очень трудно. Инженеры пошли на риск и применили изменения, четко не представляя последствия и не зная правила Бейонсе и вездесущей системы непрерывной интеграции.

Этот опыт не был чем-то необычным. Инженеры многих компаний могут рассказать похожие истории. Необычность заключается в нашем осознании сложности задачи. Благодаря тому опыту мы стали уделять больше внимания изменениям в технологиях и организации, чтобы потом менее болезненно преодолевать проблемы масштабирования и находить в нем преимущества. Мы стали развивать автоматизацию (чтобы один человек мог сделать больше), согласованность (чтобы ограничить влияние проблем на низком уровне) и обмен опытом (чтобы несколько человек могли сделать больше).

Чем чаще вы вносите изменения в инфраструктуру, тем меньше проблем они вызывают. Мы обнаружили, что в большинстве случаев код, переживший обновление, становится менее хрупким и его легче обновлять в будущем. В экосистеме, где большая часть кода прошла несколько обновлений, он перестает зависеть от нюансов базовой реализации и начинает зависеть от фактических абстракций, гарантированных языком программирования или ОС. Независимо от того, что именно обновляется, первое обновление всегда будет обходиться базе кода значительно дороже, чем последующие, даже с учетом других факторов.

Мы выявили множество факторов, влияющих на гибкость кодовой базы.

Опыт

Мы знаем, как это сделать. Для некоторых языков мы провели сотни обновлений компиляторов на множестве платформ.

Стабильность

Благодаря регулярному обновлению версий нам приходится вносить меньше изменений. Для некоторых языков мы внедряем обновления компиляторов раз в 1–2 недели.

Согласованность

Объем кода, не прошедшего обновление, постоянно уменьшается, опять же благодаря регулярным обновлениям.

Осведомленность

Поскольку обновления происходят достаточно регулярно, мы можем выявить избыточные операции в процессе обновления и попытаться их автоматизировать. Это в значительной степени совпадает со взглядом SRE на рутину15.

Стратегия

У нас есть процессы и политики, такие как правило Бейонсе, применяя которые мы делаем обновления выполнимыми. Команды поддержки инфраструктуры беспокоятся не о каждом неизвестном случае использования, а только о том, что видно в системе непрерывной интеграции.

Мы осознали не частоту или сложность обновления компилятора, а необходимость обновлений и проводим их с постоянным числом инженеров даже при постоянном росте кодовой базы16. Если бы мы решили, что эта задача слишком дорогостоящая и ее следует избегать в будущем, мы могли бы до сих пор использовать версию компилятора десятилетней давности и из-за упущенных возможностей оптимизации заплатить за вычислительные ресурсы на 25 % больше. А наша центральная инфраструктура могла бы подвергнуться значительным рискам безопасности, поскольку компилятор 2006 года, безусловно, не смягчил бы уязвимости упреждающего выполнения. Стагнация — это вариант, но не самый разумный.

Сдвиг влево

Одна из общих истин, которую мы считаем верной, заключается в том, что выявление проблем на ранних этапах разработки обычно снижает издержки. Рассмотрим временную шкалу разработки некоторой функциональной возможности (рис. 1.2). Процесс разработки движется слева направо — от обсуждения идеи и проектирования к реализации, проверке, тестированию, фиксации и канареечному развертыванию до развертывания в производственной среде. Сдвиг момента выявления проблемы влево на этой временной шкале удешевляет ее устранение.

Рис. 1.2. Временная шкала процесса разработки

Это утверждение основано на аргументе, что решение проблем безопасности не должно откладываться до конца процесса разработки, поскольку если она будет обнаружена после выпуска продукта в производство, ее исправление обойдется очень дорого. В противном случае для ее устранения могут потребоваться значительные усилия, но конечная цена исправлений будет ниже. Если проблему удастся выявить до того, как разработчик отправит уязвимый код в репозиторий, ее исправление обойдется совсем дешево: разработчик понимает, как работает код, и исправит проблему с меньшими затратами, чем кто-то другой.

В этой книге много раз повторяется один и тот же основной паттерн. Исправление ошибок, обнаруженных статическим анализом и проверкой кода перед его отправкой в репозиторий, обходится намного дешевле устранения проблем, возникающих в ходе эксплуатации кода. Использование инструментов и методов, подтверждающих качество, надежность и безопасность на ранних этапах разработки, является общей целью для наших команд поддержки инфраструктуры. Никакой отдельный процесс или инструмент не может быть идеальным, поэтому мы должны использовать подход эшелонированной защиты, который, надеемся, поможет выявить большинство дефектов в левой части шкалы.

Компромиссы и затраты

Если вы умеете программировать, знаете, как долго будет служить ПО и как его поддерживать с увеличением штата инженеров, производящих и сопровождающих новые функциональные возможности, вам остается только научиться принимать правильные решения. Очевидно, что в программной инженерии, как и везде, хороший выбор ведет к хорошим результатам. Однако на практике эту истину легко упустить из виду. В Google неприемлема фраза: «Потому что я так сказал». В любом нашем обсуждении по любой теме участвуют человек, принимающий решения, и люди, не согласные с этими решениями. Наша цель — согласие, а не единогласие. Это нормально, и я привык слышать фразу: «Я не согласен с вашими метриками/оценками, но я понимаю, как вы могли прийти к такому выводу». В основе этого подхода лежит идея, что всему должна быть причина, а аргументы «просто потому», «потому что я так сказал» или «потому что все так делают» — это признаки плохих решений. Мы всегда должны уметь объяснить и обосновать выбор между затратами двух инженерных решений.

Что мы подразумеваем под затратами? Не только деньги, а сумму издержек, включающую любые или все следующие факторы:

• финансовые затраты (например, деньги);

• затраты ресурсов (например, процессорное время);

• затраты на персонал (например, инженерный труд);

• операционные издержки (например, стоимость принятых мер);

• издержки упущенных возможностей (например, стоимость непринятых мер);

• социальные издержки (например, влияние нашего выбора на общество в целом).

Традиционно легче всего игнорировать социальные издержки. Тем не менее Google и другие крупные технологические компании сегодня могут с уверенностью развертывать продукты с миллиардами пользователей. Масштаб этих продуктов не только открывает возможности, но и усиливает последствия даже небольших проблем в удобстве использования, доступности, справедливости, особенно в отношении маргинализованных групп. ПО проникает во многие аспекты общества и культуры, поэтому с нашей стороны полезно осознавать как достоинства, так и недостатки продукта и его технической стороны при его обсуждении (глава 4).

Затраты также зависят от предубеждений: стремления сохранить текущее положение дел, нежелания что-то потерять и др. Оценивая затраты, мы должны иметь в виду все перечисленные факторы. Здоровье организации — это не только наличие денег в банке, но также осознание ее сотрудниками своей ценности и продуктивности. В творческих и прибыльных областях, таких как программная инженерия, в первую очередь нужно оценивать не финансовые затраты, а затраты на персонал. Увеличение эффективности труда от того, что инженеры довольны, сосредоточены и вовлечены, требует контроля, потому что показатели сосредоточенности и продуктивности настолько изменчивы, что легко представить себе разницу от 10 до 20 %.

Пример: маркеры

Во многих организациях простые маркеры для белой доски считаются ценным товаром. Их распределение жестко контролируются, и они всегда в дефиците. Практически всегда половина маркеров рядом с любой доской уже высохла и непригодна. Вы часто присутствовали на встречах, прерванных из-за отсутствия маркера? Часто теряли ход мыслей, когда маркер заканчивался? Случалось, что все маркеры просто пропадали, например когда их забирала другая команда? А ведь этот товар стоит меньше доллара.

В Google, как правило, есть открытые шкафы, полные канцелярских принадлежностей, включая маркеры. Достаточно просто послать уведомление, чтобы получить десятки маркеров различных цветов. В этом смысле мы пошли на очевидный компромисс: гораздо важнее обеспечить беспрепятственный мозговой штурм, чем разбираться, кому маркер сейчас нужнее.

Мы стремимся открыто и честно взвешивать компромиссы между затратами и выгодой, от канцелярских принадлежностей и льгот для сотрудников до обмена опытом между разработчиками и всеобщей поддержки во всем. Мы часто говорим: «Google — это культура, основанная на данных». На самом деле это упрощение: даже в отсутствие данных могут существовать доказательства, прецеденты и аргументы. Принятие правильных инженерных решений — это взвешивание всех доступных входных данных и выбор компромиссов. Мы можем основать решение на интуиции или общепринятой практике, но только после применения подходов, основанных на измерениях или оценках истинных затрат.

В конце концов, выбор решения в группе инженеров должен сводиться к одному из двух вариантов:

• мы будем делать так, потому что обязаны (по требованиям законодательства или клиента);

• мы будем делать так, потому что это лучший вариант (как определено кем-то, наделенным правом решения), в чем можно убедиться, основываясь на текущих данных.

Решения не должны обосновываться фразой: «Мы будем делать так, потому что я так сказал»17.

Основа для принятия решений

Оценивая данные, мы используем два основных сценария:

• Все учитываемые величины измеримы или, по крайней мере, могут быть оценены. Обычно это означает возможность оценки компромиссов между процессорами и сетью, деньгами и оперативной памятью или двумя неделями труда инженеров и экономией N процессоров в вычислительных центрах.

• Некоторые величины незначительны или мы не знаем, как их измерить. Например, иногда «мы не знаем, сколько времени нужно инженерам». Работать с такими величинами иногда сложнее, чем «оценить затраты на разработку плохо спроектированного API» или «оценить влияние выбора продукта на общество».

В первом случае не должно быть недостатка в исходных данных. Любая организация, занимающаяся разработкой ПО, может и должна контролировать текущие затраты на вычислительные ресурсы, трудозатраты инженеров и др. Если вы не хотите публиковать точные суммы финансовых затрат, создайте таблицу для пересчета: какое количество процессоров стоит столько же, сколько стоит данный объем ОЗУ или данная пропускная способность сети.

Имея согласованную таблицу пересчета, каждый инженер сможет провести собственный анализ: «Если я потрачу две недели на преобразование связного списка в структуру с более высокой производительностью, то использую на 5 ГБайт больше оперативной памяти, но сэкономлю две тысячи процессоров. Стоит ли овчинка выделки?» Ответ на этот вопрос зависит не только от относительной стоимости оперативной памяти и процессоров, но и от затрат на персонал (две недели поддержки инженера-программиста) и стоимости упущенных возможностей (что еще этот инженер мог бы произвести за две недели?).

Выбрать решение во втором случае сложнее. Обсуждая трудноизмеримые величины, мы полагаемся на опыт, лидерство и прецедент и вкладываем средства в исследования, которые помогут количественно оценить то, что трудно поддается количественной оценке (глава 7). Главное, осознавать, что не все измеримо или предсказуемо, и подходить к решениям с осторожностью. Часто трудноизмеримые величины не менее важны, чем измеримые, но сложнее в управлении.

Пример: распределенная сборка

Рассмотрим пример сборки. Согласно ненаучным опросам в Twitter, 60–70 % разработчиков выполняют сборку ПО (даже большого и сложного) на локальном компьютере. Это стало предметом множества шуток, как в комиксе «Compiling» (https://xkcd.com/303). Сколько рабочего времени вы теряете в ожидании окончания сборки? Сравните это с затратами на использование чего-то вроде distcc в небольшой группе или с затратами на создание маленькой сборочной фермы для большой группы. Сколько недель/месяцев потребуется, чтобы эти затраты окупились?

Еще в середине 2000-х сборка (проверка и компиляция кода) в Google выполнялась исключительно на локальных компьютерах. Да, были огромные локальные компьютеры (позволяющие собирать Google Maps!), но с ростом кодовой базы время компиляции все росло и росло. Неудивительно, что увеличивались затраты на персонал (из-за потерянного времени), а также на ресурсы (из-за закупки более мощных локальных машин). Затраты на ресурсы были особенно заметны, поскольку большую часть времени высокопроизводительные машины простаивали. Мы посчитали неправильным вкладывать деньги в невостребованные ресурсы.

Так в Google была создана своя система распределенной сборки. Разумеется, разработка этой системы потребовала определенных затрат: инженерам понадобилось время на разработку, изменение привычек и рабочих процессов, освоение новой системы, и, конечно, были задействованы дополнительные вычислительные ресурсы. Но общая экономия того стоила: сборка ускорилась, трудозатраты на разработку окупились, а инвестиции в оборудование были направлены на общую инфраструктуру (подмножество нашего производственного парка), а не на приобретение все более мощных настольных компьютеров. Подробнее о нашем подходе к распределенной сборке в главе 18.

Итак, мы создали новую систему, внедрили ее в производство и ускорили процесс сборки для всех. Можно ли назвать это счастливым концом истории? Не совсем: со временем распределенная сборка стала замедляться, поскольку в граф сборки стали бесконтрольно проникать лишние зависимости. Раньше каждый отдельный инженер страдал от неоптимальной сборки, был заинтересован в ее ускорении и стремился что-то улучшить. Избавив инженеров от проблемы оптимизации процесса сборки, мы создали ситуацию, в которой потребление ресурсов вышло из-под контроля. Это было похоже на парадокс Джевонса18 (https://oreil.ly/HL0sl): потребление ресурса может увеличиться в ответ на повышение эффективности его использования.

В целом затраты, связанные с внедрением распределенной системы сборки, намного перевесили расходы, связанные с ее созданием и обслуживанием. Но мы не преду­смотрели расходов, обусловленных ростом потребления. Забегая вперед, скажу, что мы оказались в ситуации, когда нам пришлось переосмыслить цели и ограничения системы и особенности ее использования, определить оптимальные подходы (небольшое число зависимостей, машинное управление зависимостями) и финансировать создание инструментов для обслуживания новой экосистемы. Даже относительно простой компромисс в виде «мы потратим вот столько на вычислительные ресурсы, чтобы окупить время инженера» имел непредвиденные последствия.

Пример: выбор между временем и масштабированием

Часто темы, связанные со временем и масштабированием, пересекаются и дополняют друг друга. Хорошо масштабируемые стратегии, такие как правило Бейонсе, помогают управлять ситуацией с течением времени. Изменение интерфейса ОС одинаково влияет на все проекты, поэтому мелкие изменения в проектах, вызванные изменениями в ОС, хорошо масштабируются.

Но иногда время и масштабирование вступают в конфликт, и особенно четко это проявляется в базовом вопросе: лучше добавить зависимость или создать (заимствовать) новую ветвь, чтобы удовлетворить локальные потребности?

Этот вопрос может возникнуть на разных уровнях стека, поэтому обычно индивидуальное решение для узкой предметной области бывает эффективнее более общего решения. Ветвление действующего решения и его подгонка под свои нужды упрощает добавление новых возможностей и дает больше уверенности при его оптимизации, будь то микросервис, кеш в памяти, подпрограмма сжатия или что-то еще в программной экосистеме. При этом вы получаете контроль: изменения в базовых зависимостях не будут определены другой командой или сторонним поставщиком, и вы сами решите, как и когда реагировать на возникающую необходимость перемен.

С другой стороны, если вместо повторного использования каждый разработчик будет создавать новые версии всего, что нужно проекту, пострадают масштабируемость и устойчивость. Реагирование на проблему безопасности в базовой библиотеке перестанет быть вопросом обновления отдельной зависимости: оно потребует определить все уязвимые версии этой зависимости и их пользователей.

И снова программная инженерия не дает универсального ответа, что выбрать. Если проект имеет небольшую продолжительность жизни или ветвление имеет ограниченный объем, применение ветвления уместно. Но избегайте ветвления интерфейсов, которые могут жить дольше проекта (структур данных, форматов сериализации, сетевых протоколов). Согласованность имеет большое значение, но она тоже имеет цену, и часто выгоднее создавать свои решения, если делать это осторожно.

Пересмотр решений, совершение ошибок

Одним из недооцененных преимуществ приверженности культуре, основанной на данных, является возможность признавать ошибки. Представьте решение, основанное на имеющихся данных (вероятно, достоверных) и нескольких предположениях, неявно выведенных из этих данных. По мере поступления новых данных, изменения контекста или исключения предположений может выясниться, что решение было ошибочным или потеряло актуальность. Такая ситуация характерна для организаций, работающих много лет: со временем меняются не только технические зависимости и программные системы, но и данные, используемые для принятия решений.

Мы твердо верим в решения, основанные на данных, но понимаем, что данные со временем меняются. Поэтому время от времени в течение срока службы системы решения должны пересматриваться. Для долгоживущих проектов важно иметь возможность менять направление после принятия первоначального решения, и лица, принимающие решения, должны иметь право на ошибки. Вопреки стереотипам лидеры, способные признавать ошибки, пользуются большим уважением.

Опирайтесь на доказательства, но имейте в виду, что даже то, что невозможно измерить, может иметь ценность. Если вы лидер, придерживайтесь суждения, что нет ничего маловажного. Подробнее о лидерстве в главах 5 и 6.

Программная инженерия и программирование

Узнав, по каким признакам мы различаем программную инженерию и программирование, вы можете спросить, есть ли у нас какие-то внутренние суждения об их ценности. Можно ли сказать, что программирование хуже программной инженерии? Или утверждать, что проект, который, как ожидается, будет развиваться сотнями людей в течение десятилетий, ценнее проекта, который просуществует месяц и будет создан двумя людьми?

Конечно нет. Мы не считаем, что программная инженерия лучше программирования. Это две разные предметные области с разными ограничениями, ценностями и методами. Мы осознаем, что некоторые инструменты хороши в одной области и непригодны в другой. Едва ли имеет смысл внедрять интеграционные тесты (глава 14) и непрерывное развертывание (continuous deployment, глава 24) в проект, который просуществует несколько дней. Точно так же наши достижения в области семантического управления версиями (SemVer) и управления зависимостями в программной инженерии (глава 21) не применимы к краткосрочным проектам, в которых можно смело использовать все, что доступно.

Научитесь различать схожие термины «программирование» и «программная инженерия». Большая часть их различий заключается в подходах к управлению кодом, влиянии времени на масштабирование и особенностях принятия решений. Программирование — это непосредственный акт создания кода. Программная инженерия — это набор стратегий, методов и инструментов, помогающих сохранить полезность кода в течение всего времени его использования и обеспечить возможность совместной работы в команде.

Заключение

В этой книге обсуждаются политики как для организаций, так и для отдельных программистов, оценка и совершенствование методов, а также инструменты и технологии, используемые в ПО, удобном для сопровождения. Компания Google немало потрудилась, чтобы получить устойчивые кодовую базу и культуру. Мы не считаем свой подход единственно верным, но он наглядно показывает, чего можно добиться. Надеемся, что он послужит полезной основой для размышлений об общей проблеме: как правильно поддерживать код, чтобы он работал столько времени, сколько понадобится.

Итоги

• «Программная инженерия» более широкое понятие, чем «программирование». Программирование — это создание кода. Программная инженерия добавляет к этому понятию обслуживание кода для увеличения срока его использования.

• Продолжительность жизни короткоживущего и долгоживущего кода может отличаться как минимум в 100 000 раз. Было бы неправильно полагать, что одни и те же практики применимы на обоих концах спектра.

• ПО устойчиво, если в течение ожидаемого срока его службы мы сохраняем способность реагировать на изменения в зависимостях, технологиях или требованиях к продукту. Мы можем ничего не менять, но должны быть готовы к изменениям.

• Закон Хайрама: при достаточно большом количестве пользователей API не имеет значения, что вы обещаете в контракте: любое наблюдаемое поведение системы будет зависеть от чьих-то действий.

• Каждая задача, которую организация должна выполнять снова и снова, должна масштабироваться (линейно или еще лучше) в отношении участия человека. Политики — прекрасный инструмент для масштабирования процесса.

• Проблемы, обусловленные неэффективностью процесса, и некоторые другие имеют свойство накапливаться медленно. Они с легкостью могут превратиться в метафорическую сварившуюся лягушку.

• Опыт окупается особенно быстро в сочетании с экономией за счет масштабирования.

• «Потому что я так сказал» — это плохое обоснование решения.

• Принятие решений на основе данных — хорошее правило, но на практике большинство решений основывается на сочетании данных, предположений, прецедентов и аргументов. Лучше всего, когда объективные данные составляют большую часть основы для принятия решений, но не всегда есть возможность опираться только на них.

• Принятие решений на основе данных подразумевает необходимость изменения направления, если данные изменились или предположения были опровергнуты. Ошибки и пересмотр планов неизбежны.

2 Мы не имеем в виду «продолжительность выполнения», мы имеем в виду «продолжительность поддержки» — как долго код будет продолжать развиваться, использоваться и поддерживаться? Как долго это ПО будет иметь ценность?

3 Это вполне точное определение технического долга: он возникает тогда, когда что-то «должно» быть сделано, но еще не сделано, и является разницей между текущим кодом и будущим желаемым кодом.

4 Тут вполне уместен вопрос: как заранее узнать, что проект будет долгосрочным?

5 Сейчас трудно установить авторство определения: одни считают, что впервые оно было сформулировано Брайаном Рэнделлом (Brian Randell) или Маргарет Гамильтон (Margaret Hamilton), другие — что автором является Дейв Парнас (Dave Parnas). Это определение часто приводят как цитату из отчета «Software Engineering Techniques: Report of a conference sponsored by the NATO Science Committee», Рим, Италия, 27–31 октября 1969 г., Брюссель, отдел по научным вопросам, НАТО.

6Брукс Ф. Мифический человеко-месяц, или Как создаются программные системы. СПб.: Питер, 2021. — Примеч. пер.

7 Как заявляют в компании Appcelerator, «ничто в мире не определено, кроме смерти, налогов и короткого срока службы мобильных приложений» (https://oreil.ly/pnT2_, блог Axway Developer, 6 декабря 2012 года).

8 Точка перехода во многом зависит от ваших приоритетов и предпочтений. Судя по нашему опыту, большинство проектов подходит к черте, когда обновление становится необходимостью, где-то через пять лет работы ПО. По более консервативным оценкам этот переход находится где-то между пятью и десятью годами службы.

9 Надо признать, что сам Хайрам собирался назвать этот закон «законом неявных зависимостей», но в Google предпочли более краткое название «закон Хайрама».

10 См. комикс «Workflow» (http://xkcd.com/1172) на сайте xkcd.

11 Разновидность атак типа «отказ в обслуживании» (DoS, denial-of-service), при которых злоумышленник, зная внутреннюю организацию хеш-таблицы и особенности хеш-функции, может сформировать данные таким образом, чтобы снизить алгоритмическую производительность операций над таблицей.

12 Всякий раз, когда в этой главе мы используем слово «масштабируемость» в неформальном контексте, мы подразумеваем «сублинейное масштабирование в отношении человеческих возможностей».

13 Это отсылка к популярной песне «Single ladies», в которой рефреном звучит фраза: «If you liked it then you shoulda put a ring on it». («Если я тебе нравилась, так надел бы мне на палец колечко».)

14 В частности, на интерфейсы из стандартной библиотеки C++ нужно было ссылаться с использованием пространства имен std, а изменения в оптимизациях для std::string отрицательно повлияли на производительность кода, что потребовало искать дополнительные обходные пути.

15 Site Reliability Engineering. Надежность и безотказность, как в Google, глава 5 «Избавляемся от рутины». — Примеч. пер.

16 По нашему опыту, средний инженер-программист производит относительно постоянное количество строк кода за единицу времени. В фиксированной группе программистов база кода растет линейно — пропорционально количеству человеко-месяцев. Если ваши задачи требуют усилий, объем которых зависит от количества строк кода, переоценить влияние совместной работы будет трудно.

17 Это не означает, что решения должны приниматься единогласно или широким консенсусом. Кто-то должен взять на себя ответственность за принятие решения. Мы описали сам процесс принятия решений.

18https://ru.wikipedia.org/wiki/Парадокс_Джевонса. — Примеч. пер.