Книга: Руководство по DevOps
Назад: Глава 10. Быстрое и надежное автоматизированное тестирование
Дальше: Глава 12. Автоматизация и запуск релизов с низким уровнем риска

Глава 11. Запустить и практиковать непрерывную интеграцию

В предыдущей главе мы создали методы автоматизированного тестирования, чтобы обеспечить быстрое получение разработчиками обратной связи о качестве их труда. Это становится еще более важным по мере увеличения числа разработчиков и количества ветвей в системе контроля версий.
Возможность ветвления в системах контроля версий была создана в основном для обеспечения параллельной работы разработчиков над разными частями проекта без риска, что кто-то из них сможет внести изменения, дестабилизирующие основную ветку (иногда ее называют мастером или главной линией), или добавит в нее ошибки.
Однако чем дольше разработчикам разрешают изолированно работать над их ветками, тем труднее затем становится интегрировать и объединить в основную ветку проекта изменения, сделанные каждым. Фактически сложность интеграции растет экспоненциально по мере увеличения количества веток и количества изменений в каждой ветке.
Проблемы интеграции приводят к необходимости значительного количества переделок, чтобы привести продукт обратно в состояние пригодности к развертыванию, в том числе из-за противоречивых изменений (их приходится обновлять вручную), или изменений, слияние которых нарушает работу автоматизированных тестов или выполняемых вручную, и обычно переделки требуют усилий нескольких разработчиков. И поскольку интеграция традиционно выполняется в конце проекта, если она занимает намного больше времени, чем запланировано, то часто приходится чем-то жертвовать, чтобы выпустить продукт в срок.
Это приводит к другой нисходящей спирали: если слияние кода оказывается затруднено, мы стараемся выполнять его реже, что делает следующие слияния еще более трудными. Для решения этой проблемы была разработана непрерывная интеграция: включение включение разработческих веток в основную — часть повседневной работы.
Неожиданная широта проблем, решаемых с помощью непрерывной интеграции, а также сами решения подтверждены опытом Гэри Грювера, директора по проектированию компании HP LaserJet Firmware, создающей программное обеспечение для всех выпускаемых сканеров, принтеров и многофункциональных устройств.
Отдел состоял из четырех сотен разработчиков, находящихся в США, Бразилии и Индии. Несмотря на размер отдела, работа шла очень медленно. В течение ряда лет они были не в состоянии создать новые функциональные возможности так быстро, как это было необходимо для бизнеса.
Грювер так описывал проблему: «Маркетинг мог прийти к нам с миллионом идей о том, как поразить наших заказчиков, а мы отвечали: “Выберите из вашего списка две вещи, которые вы хотите получить в течение следующих шести — двенадцати месяцев”».
В год выпускалось всего две версии встроенного ПО, при этом основная часть времени затрачивалась на портирование кода для поддержки новых продуктов. По оценке Грювера, лишь 5 % общего времени тратилось на создание новых функциональных возможностей, а остальное время уходило на непродуктивные работы, связанные с погашением технического долга, такие как управление многочисленными ветками кода и тестирование вручную:

 

• 20 % на подробное планирование (низкая производительность и длительное время релиза приводили к ошибочным оценкам, и в надежде получить более устраивающий ответ сотрудникам отдела предлагали оценить работу более подробно);
• 25 % занимало портирование кода, поддерживаемое в отдельных ветках;
• 10 % уходило на интеграцию кода, написанного разработчиками в разных филиалах;
• 15 % занимало завершение тестирования вручную.

 

Грювер и его команда поставили цель увеличить долю времени, затрачиваемого на инновации и новые функциональности, в десять раз. Группа надеялась, что цель может быть достигнута посредством:

 

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

 

Раньше релиз новой линейки продуктов требовал создания новой ветки кода, а каждая модель имела уникальное встроенное ПО. Его возможности определялись во время компиляции. Новая архитектура подразумевала, что все разработчики используют общую базу кода с единой версией встроенного программного обеспечения, поддерживающей все модели LaserJet, построенной из кода основной ветки, а особенности принтеров определяются во время выполнения из XML-файла конфигурации.
Четыре года спустя у них имелась одна база кода, поддерживающая все двадцать четыре продукта линеек HP LaserJet, разрабатываемая как в основной ветке. Грювер признавал, что разработка на основе основной ветки требует значительного изменения образа мыслей. Инженеры считали, что такой метод никогда не сможет работать, но после того, как начали его использовать, даже не помышляли вернуться к прежнему методу. За эти годы из HP уволились несколько инженеров, и они говорили, насколько устарела организация разработки в новых компаниях, как трудно быть эффективным и выпускать хороший код при отсутствии обратной связи, предоставляемой непрерывной интеграцией.
Однако разработка на базе основной ветки потребовала создания более эффективного автоматизированного тестирования. Грювер отмечал: «Без автоматического тестирования и непрерывной интеграции это оказывается самым быстрым способом получить большую кучу барахла. Оно никогда не скомпилируется или не будет работать правильно». Вначале цикл полного тестирования вручную требовал шести недель.
Чтобы иметь возможность автоматически протестировать все сборки микропрограмм, значительные средства вложили в эмуляторы принтеров и за шесть недель создали тестовые фермы — в течение нескольких лет две тысячи эмуляторов принтеров запускались на шести стойках с серверами и обрабатывали сборки микропрограмм, взятых из конвейера разработки. Их система непрерывной интеграции (CI) выполняла весь комплекс автоматизированного модульного, приемочного и интеграционного тестирования сборок, сделанных на базе основной ветки, как описано в предыдущей главе. Кроме того, создали культуру производства, останавливающую всю работу, как только кто-то из разработчиков повреждал конвейер развертывания. Так обеспечивался быстрый возврат системы в рабочее состояние.
Автоматизированное тестирование создает быструю обратную связь, позволяющую разработчикам быстро убедиться, что зафиксированный код действительно работает. Модульное тестирование может выполняться на рабочих станциях за минуты, три уровня такого тестирования можно запускать после каждой фиксации кода или каждые два-четыре часа. Завершающий полный регрессионный тест можно запускать каждые двадцать четыре часа. В ходе этого процесса разработчики:

 

• сокращают количество сборок до одной в день, что в конечном счете означает от десяти до пятнадцати сборок в день;
• переходят от примерно двадцати фиксаций кода в день, совершаемых «главным по сборкам», до свыше ста в день, совершаемых самими разработчиками;
• получают возможность изменять или добавлять до 75–100 тысяч строк кода каждый день;
• снижают длительность регрессионного тестирования с шести недель до одного дня.

 

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

 

• доля времени разработчиков, затраченного на инновации и написание новых функциональностей, возросла с 5 до 40 %;
• общая стоимость разработки была сокращена примерно на 40 %;
• количество программ, находящихся в разработке, увеличилось примерно на 140 %;
• расходы на разработку одной программы сократились на 78 %.

 

Опыт, полученный Грювером, показывает: после полномасштабного использования системы контроля версий непрерывная интеграция — один из наиболее важных методов, обеспечивающих быстрый поток работы в нашем потоке создания ценности, позволяя многим командам разработчиков самостоятельно разрабатывать, тестировать и доставлять продукт. Тем не менее непрерывная интеграция остается противоречивым методом. В оставшейся части главы описываются приемы, необходимые для внедрения непрерывной интеграции, а также то, как преодолеть обычно возникающие возражения.
Мелкосерийная разработка, и что происходит, когда мы редко фиксируем код в основной ветке
Как описано в предыдущих главах, когда в систему контроля версий вносятся изменения, нарушающие работу конвейера развертывания, мы быстро набрасываемся на проблему, чтобы ее исправить, в результате чего конвейер развертывания возвращается в «зеленое» состояние. Вместе с тем значительные проблемы появляются, если разработчики трудятся над долго существующими частными ветками (их еще называют «ветки функциональности»). Их обратное слияние с основной веткой выполняется нерегулярно, в результате чего список изменений оказывается весьма велик. Как показано на примере HP LaserJet, это приводит к хаосу и значительным переделкам, требующимся для приведения кода в состояние готовности к релизу.
Джефф Этвуд, основатель сайта Stack Overflow («Переполнение стека») и автор блога Coding Horror («Ужас кодирования»), отмечает, что, в то время как существует множество стратегий ветвления, все они могут быть поделены на две группы.

 

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

 

Мнение Этвуда абсолютно правильно, и если сформулировать точнее, то усилия, необходимые для успешного объединения веток, растут экспоненциально по мере роста числа веток. Проблема заключается не только в объеме переделок, необходимых после «ада слияний», но также в том, что обратную связь от конвейера развертывания мы получаем с задержкой. Например, скорее всего, только в конце процесса разработки удастся выполнить тестирование производительности полностью интегрированной системы — непрерывным данный процесс сделать не получится.
Кроме того, если мы повышаем скорость создания кода, увеличивая количество разработчиков, то усиливаем вероятность того, что любое изменение затронет какого-то другого разработчика и увеличит количество сбоев в конвейере развертывания.
Это один из побочных эффектов слияния больших наборов изменений, вызывающих наибольшие сложности: когда слияние трудно, у нас остается меньше возможностей и, кроме того, понижается мотивация улучшать код и выполнять его рефакторинг, поскольку рефакторинг, скорее всего, будет означать, что кому-то придется переделывать работу. Когда такое происходит, желание модифицировать код, имеющий зависимости во всем репозитории кода, заметно ослабевает, хотя (и в этом трагизм ситуации) именно это могло бы дать нам наибольшую отдачу.
Вот как Уорд Каннингем, разработчик первой wiki, впервые описал технический долг: если мы не выполняем агрессивный рефакторинг нашей базы кода, то с течением времени становится все труднее вносить изменения в код и поддерживать его, а это снижает скорость добавления новых функциональных возможностей. Решение этой проблемы — одна из основных причин создания непрерывной интеграции и практики развития кода на базе основной ветки и оптимизации производительности команды вместо индивидуальной производительности.
Внедрить методы разработки на базе основной ветки
Контрмеры по отношению к большим пакетам слияний — это организация непрерывной интеграции и методы разработки на базе основной ветки, когда все разработчики фиксируют свой код на основной ветке по меньшей мере один раз в день. Частая фиксация кода значительно уменьшает размер одного пакета, и команда разработчиков оказывается способна выполнить задание за день. Чем чаще разработчики фиксируют код в основной ветке, тем меньше размер пакета и тем ближе мы к теоретическому идеалу штучного потока.
Частая фиксация кода в основной ветке означает: мы можем запускать все автоматические проверки системы программного обеспечения как целого и получать уведомления, если изменение «ломает» работу других частей приложения или вмешивается в работу другого разработчика. И поскольку можно обнаруживать проблемы слияния, пока они невелики, мы способны исправить их быстрее.
Мы даже можем настроить конвейер развертывания так, чтобы он отвергал любые фиксации (например, кода или изменений среды), нарушающие состояние готовности к развертыванию. Этот способ называется управляемой фиксацией (gated commits): конвейер развертывания сначала проверяет, будут ли все переданные изменения успешно объединены, пройдет ли сборка так, как ожидалось, и выполнится ли автоматическое тестирование, прежде чем изменения будут внесены в основную ветку. Если на одном из этапов проверка не пройдет, то разработчик получит уведомление и сможет внести исправления, не затрагивая никаких других элементов потока создания ценности.
Принятая практика ежедневной фиксации кода также заставляет нас разделять работу на небольшие кусочки, в то же время обеспечивая нахождение основной ветки в рабочем состоянии, годном к релизу.
И система контроля версий становится неотъемлемым механизмом обмена информацией внутри команды — каждый ее член лучше понимает систему в целом, находится в курсе состояния конвейера развертывания, и они могут помочь друг другу, когда он ломается. В результате получаем более высокое качество и небольшое время разработки.
Рассмотрев эти методы, мы можем теперь снова изменить определение состояния «сделано» (выделено жирным шрифтом): «В конце каждого интервала разработки мы должны иметь интегрированный, протестированный, рабочий и потенциально готовый к поставке (что демонстрируется запуском в среде, близкой к производственной) код, созданный на базе основной ветки процессом, “запускаемым одним щелчком мыши”, и проверенный автоматическими тестами».
Пересмотренное определение поможет нам в дальнейшем гарантировать, что написанный нами код всегда пройдет тестирование и будет пригоден к развертыванию. Поддерживая код в готовности к развертыванию, мы способны исключить традиционные методы, когда тестирование кода и его стабилизация осуществляются отдельными фазами в конце проекта.
Практический пример
Непрерывная интеграция в компании Bazaarvoice (2012 г.)
Эрнест Мюллер, помогавший производить DevOps-трансформацию в компании National Instruments, позже, в 2012 г., занимался преобразованием процессов разработки и релиза ПО в компании Bazaarvoice. Она обеспечивала обработку пользовательского контента (например, обзоры, рейтинги) для тысяч предприятий розничной торговли, в том числе Best Buy, Nike и Walmart.
В то время Bazaarvoice имела 120 миллионов долларов дохода и готовилась к IPO. Основным источником прибыли компании было приложение Bazaarvoice Conversations — монолитное приложение на языке Java, состоявшее из почти пяти миллионов строк кода, написанных начиная с 2006 г. и содержавшихся в 15 тысячах файлов. Оно работало на 1200 серверах в четырех центрах обработки данных с использованием нескольких поставщиков облачных услуг.
В компании, частично в результате перехода на Agile-процесс и двухнедельный цикл разработки, появилось сильное стремление увеличить частоту релизов по сравнению с действовавшим планом релиза версий каждые десять недель. Сотрудники также приступили к разделению частей монолитного приложения, разбивая его на микросервисы.
В первый раз они попытались перейти на двухнедельный график в январе 2012 г. Мюллер отмечал: «Дело не заладилось. Попытка вызвала массовый хаос, мы получили от наших клиентов сорок четыре сообщения о неполадках. Лейтмотивом реакции менеджеров было “давайте не будем повторять такие попытки”».
Мюллер стал руководить процессом релиза. Он поставил целью делать релизы каждые две недели, не допуская простоев при обслуживании клиентов. В бизнес-цели увеличения частоты релизов входило использование более быстрого A/B-тестирования (описано в следующих главах) и увеличение скорости выпуска нового функционала. Мюллер определил три основные проблемы:

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

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

• релиз в январе 2012 г.: 44 обращения о сбоях от клиентов (начаты усилия по внедрению постоянной интеграции);
• релиз 6 марта 2012 г.: спустя пять дней — пять обращений о сбоях от клиентов;
• релиз 22 марта 2012 г.: сделан вовремя, одно обращение от клиента;
• релиз 5 апреля 2012 г.: сделан вовремя, обращений от клиентов нет.

Позже Мюллер описывал, насколько успешными оказались усилия:

«Мы добились такого успеха с релизами каждые две недели, что решили перейти на еженедельные релизы, что не требовало почти никаких изменений в инженерных командах. Поскольку релизы стали рутинным делом, было очень просто удвоить их количество в календарном плане и делать их тогда, когда об этом напоминал календарь. Действительно, это происходило практически без каких-то значимых событий. Большинство необходимых изменений делались в командах клиентского обслуживания и маркетинга. Они должны были вносить изменения в свои процессы, такие как изменение расписания еженедельных сообщений клиентам, какие изменения в функциях будут произведены. После этого мы начали работать над достижением следующих целей, в итоге приведших к уменьшению времени на тестирование с трех с лишним часов до менее часа, сокращению количества сред с четырех до трех (среда для разработки, для тестирования, производственная среда, а среда для завершающего тестирования была исключена) и переходу к полной модели непрерывной доставки, чтобы мы могли выполнить развертывание быстро, одним щелчком мыши».
Заключение
Разработка на базе основной ветки — наиболее спорный метод из описанных. Многие инженеры не считают это возможным, среди них есть даже те, кто предпочитает работать над своей веткой, не отвлекаясь на другие и не имея дел с другими разработчиками. Однако данные из доклада 2015 State of DevOps Report компании Puppet Labs ясно показывают: разработка на базе основной ветки предсказывает более высокую производительность, лучшую стабильность и даже более высокую удовлетворенность разработчиков своей работой и меньшую вероятность появления у них эмоционального истощения.
Хотя вначале может оказаться трудным убедить разработчиков, рано или поздно они увидят исключительные преимущества такого подхода и, скорее всего, станут сторонниками постоянных преобразований, как это показывают примеры HP LaserJet и Bazaarvoice. Методы непрерывной интеграции создают основание для следующего шага — автоматизации процесса развертывания и низкого риска при релизе ПО.
Назад: Глава 10. Быстрое и надежное автоматизированное тестирование
Дальше: Глава 12. Автоматизация и запуск релизов с низким уровнем риска