Около года назад перед нашей командой была поставлена задача стартовать разработку серверных частей ряда игровых MMO проектов. Специфика такого рода проектов помимо требований к гибкости, стабильности и масштабируемости также включает в себя:
- необходимость A/B-тестирования разных версий одной и той же игры
- возможность по максимуму переиспользовать функционал от одной игры в другой
- высокую вероятность географической удаленности от разработчиков занимающихся клиентским функционалом
Более того, в дальнейшем нашу команду предполагалось расширить, возможно за счет аутсорс разработчиков, в том числе и для задач поддержки. В этих условиях для успешной реализации было решено наравне с версионированием проектов, пакетированием и стандартизацией ряда шагов разработки внедрить и практику continuous delivery.
Цель данной статьи – рассказать о проделанных шагах, принятых решениях и описать полученный результат.
Инфраструктура
Исторически сложилось, что основным языком разработки серверных веб-приложений в нашей компании является PHP, поэтому это во многом предопределило выбор инструментов.
Итоговый список:
- Git – система контроля версий
- Gitolite – управление репозиториями
- Composer – менеджер зависимостей
- Phing – сборочный и инсталляционный скрипты
- Jenkins – continuous integration сервер
- phpunit, behat – тесты
- phploc, phpmd, pdepend, phpcs, phpcpd, phpcb, phpdox – прочие утилиты
Модель ветвления
При выборе модели ветвления за основу была взята “A successful Git branching model”, описанная здесь с одним небольшим отличием: проводить A/B-тестирование было решено путем подготовки отдельных релизных веток, формирующихся из разного набора feature-веток. В результате роль ветки develop была полностью возложена на релизные ветки, и сама эта ветка исчезла. В противном случае при создании следующего релиза мы были бы вынуждены включать в него все выпущенные до этого feature, что не всегда являлось приемлемым.
Эту ситуацию можно продемонстрировать на следующем примере. Напомним, что согласно оригиналу:
At least all features that are targeted for the release-to-be-built must be merged in to develop at this point in time. All features targeted at future releases may not—they must wait until after the release branch is branched off.
И допустим, что уже выпущены два релиза – релиз 1.0 с фичей A и релиз 2.0 с фичами A и B, и необходимо выпустить релиз 1.1 с фичами A и C. Так как develop ветка на данный момент уже содержит в себе фичи А и B, то наиболее простым решением будет создание feature ветки С от ветки релиза 1, и последующий ее merge обратно:
Пакетирование и версионирование
Все проекты оформлены как composer-пакеты.
Для переиспользования функционала от одного проекта к другому широко применяется выделение какого-то обособленого функционала в отдельный пакет.
Это сопровождается заменой одного пакета другим, разделением одного пакета на два или переносом функционала из одного пакета в другой. В таких условиях для более тонкого контроля было используется семантическое версионирование пакетов.
Этот тип версионирования поддерживается в composer с использованием символа “~”, например:
"require": {
...
"alawar/packet-post-process-server": "~1.3",
...
},
“Сборка” проекта
В случае с PHP, говорить о сборке в классическом смысле, как процессе конвертации исходников проекта в исполняемый код, нельзя. Тем не менее, так как основной задачей по-прежнему является получение готового к использованию ПО, то название “сборка” вполне корректно.
Этапы сборки:
- выкачивание зависимостей через composer
- миграция БД, — обновление только структуры и статических данных базы данных
Для реализации сборки в корне каждого проекта находится сборочный phing-скрипт с target'ами:
- build – для выполнения этапов сборки
- runtests и runtest-with-coverage – для выполнения как сборки так и запуска тестов и сбора метрик
Сборочный скрипт для большинства проектов одинаков и отличается лишь названием проекта: аттрибутом name, тега project.
Тестирование
Реализация автоматического тестирования проектов сделана при помощи двух фреймворков: Behat и PHPUnit.
Использование первого дает существенное преимущество не только для тестирования, но и для создания так называемой living documentation. Тесты на Gherkin являются одной из отправных точек при знакомстве с проектом нового программиста, при проведении code review, а также ряде других работ.
Несмотря на знакомство с материалами тут и тут, единых рекомендаций относительно глубины и детальности этих тестов у нас нет, поэтому их содержимое может варьироваться от например таких:
Сценарий: Получение пользовательского бонус кода и списка возможных наград
# Пусть мы получили бонус код для какого-то игрока
Пусть мы успешно отправили запрос:
| action | uid |
| get-user-bonus-code | player1 |
Тогда мы получим ответ в соответствии с шаблоном "GetUserBonusCodeResponse.txt"
# Тогда запросив список возможных наград мы получим награды за использование выбранного бонус кода
Пусть мы успешно отправили запрос:
| action | code |
| get-rewards-info | Полученный бонус код |
Тогда мы получим ответ в соответствии с шаблоном "template8.txt"
до таких:
Сценарий: Получение и обработка данных
Пусть веб-сервис приложения получает данные по отчетам
И после этого запускается команда на обработку данных
Тогда в БД появятся обработанные согласно схеме данные
PHPUnit используется не только для реализации unit-тестов, наличие и содержимое которых полностью остается за программистом, но и для запуска Behat тестов, с использованием небольшого workaround'а. Это дает возможность запускать все тесты одной командой, а также иметь единые отчеты по результатам работы тестов и покрытию ими кода.
Сборочный сервер
Сборка производится с использованием CI-сервера Jenkins. При этом для каждой релизной ветки releases/X.Y заведено отдельное сборочное задание, которое на staging среде:
- выполняет сборочный phing-скрипт с target'ами “build runtests-with-coverage”
- собирает отчеты тестирования и результатов работы вспомогательных утилит
- в случае безошибочного завершения процесса создает в репозитории новый тег вида $VERSION_NO.$BUILD_NUMBER, где $VERSION_NO – номер версии, получаемый из названия ветки, например 2.1, а $BUILD_NUMBER – порядковый номер сборки для данного сборочного задания
Само сборочное задание, равно как и сборочный скрипт были построены на основе описанных здесь. Именно этим и обусловлен столь богатый список дополнительных утилит.
В дополнение к указанному по ссылке выше списку плагинов были установлены:
- EnvInject – для инициализации переменной $VERSION_NO
- ChuckNorris и CI-Game – just for fun
Деплоймент
Требовалось найти решение позволяющее одновременно управлять развертыванием нескольких приложений, каждое из которых может быть установлено на несколько групп серверов (testing, production).
Первоначально deployment осуществлялся phing-скриптом, который согласно файлу настроек выполнял ряд действий:
- создавал файлы, папки и симлинки
- делал checkout исходников нужной ветки/тега
- выполнял сборку проекта
- и так как каждый раз checkout выполнялся в новую папку вида 2012-01-01T23:59:59, то обновлял симлинк latest, указывающий на последнюю развернутую версию
Это было не совсем удобно в силу полного отсутствия поддержки инсталляции на удаленные сервера.
После нескольких экспериментов с Capistrano, Magallanes и другими инструментами, в дополнение к этому скрипту было реализовано консольное приложение Installer. Оно копирует на нужную удаленную группу серверов инсталляционный скрипт с нужными настройками и выполняет его там.
Также в это приложение были заложены команды по получению возможных версий приложения и запросу установленной на серверах версии (на картинке показа возможность обновления проекта в production environment'е с версии 1.0.19 до 1.0.20):
А формат файлов настроек был заменен на более удобный .yml:
Данное консольное приложение было развернуто на сборочном сервере, к нему был сделан веб-интерфейс в виде параметризуемого сборочного задания Jenkins, выполняющего консольную команду:
/home/projects/installer/installer.phar $command $recipe $environment
где,
- $command — имя выполняемой команды, например install, status, versions
- $recipe — код присваемый версии проекта, предназначенной для инсталляции
- $environment — опциональное имя группы серверов, на которые необходимо установить проект
И это задание в свою очередь было отмечено как downstream project для сборочных заданий релизных веток с использованием плагина Parameterized Trigger Plugin.
В итоге
В итоге нами была успешно решена задача реализации continuous delivery со следующей последовательностью шагов:
- разработчик вносит изменения в релизную ветку
- post-receive hook gitolite инициирует соответствующее этой ветке сборочное задание Jenkins
- сборочное задание проводит тестирование и помечает успешную версию тегом
- Jenkins запускает downstream project Installer с нужными параметрами для проекта и группы серверов, на которых проект надо обновить
- Installer, последовательно пройдя по всем серверам группы, разворачивает на них свежую версию и обновляет симлинк latest
В дальнейшем
Используемая модель ветвления способствует тому, что разные ветки со временем начинают сильно отличаться друг от друга, это приводит к проблемам внедрения новых фич в старые релизы. Пока это не стало критичным, но есть мысль попробовать вернуть интеграционную ветку develop, а для подготовки A/B-версий использовать другую технику, возможно, что-то навроде feature toggles.
Есть интерес попробовать различного вида интеграции с трекером Jira. Как-то например автоматизировать:
- создание веток под новые тикеты определенного типа
- обновление статуса и/или комментариев к тикетам в соответствии с результатами тестирования
- формирование change log'ов
Текущее время работы composer'а составляет порядка нескольких минут, а большое количество собственных пакетов приводит к сильно разросшейся секции repositories файлов composer.json. Хочется поэкспериментировать с Satis, для решения этих проблем.
Заключение
Мы успешно решили поставленные перед нами проблемы:
- для создания A/B-версий используются отдельные ветки в системе контроля версий
- пакетный менеджер позволяет переиспользовать функционал от проекта к проекту
- тесты на Gherkin и их реализация помогают существенно упростить подключение к проекту новых разработчиков
- а описанная выше схема continuous delivery позволяет минимизировать время получения фидбека от разработчиков клиентской части игры
Следует заметить, что данная схема почти не используется для фактического обновления проектов в production, так как не покрывает всех проблем выпуска нового и возможно не совместимого с предыдущей версией релиза. Ее основное применение быстрая и автоматизированная доставка нового функционала на все сервера, где развернуто приложение, и обновление в тех местах, где это возможно, либо не критично – тестовые и предназначенные для закрытого бета-тестирования сервера.
Suerte!
Автор: grelkin