С момента перехода с PHP 4.4 на PHP 5.3 в Badoo прошло уже 4 года, пришла пора обновлять PHP, на этот раз сразу на версию PHP 5.5. Помимо новых фич, новая версия PHP в очередной раз принесла нам существенное увеличение производительности, поэтому у нас было много причин для апгрейда. В этой статье мы расскажем о том, как мы переходили на PHP 5.5, какие «грабли» собрали, и зачем в очередной раз переписывали нашу систему для запуска юнит-тестов на основе PHPUnit.
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/acb/906/de3/acb906de3aa914ea7d207b60e62c7d75.png)
Рис 1. Общая архитектура
В прошлый раз мы переходили с четвертой версии PHP на пятую, причём наша версия PHP 5.3 содержала патчи, чтобы работал «старый» синтаксис PHP, например,
Основные проблемы, которые у нас возникли:
После перехода на PHP 5.5 наши юнит-тесты начали проходить значительно дольше (в несколько раз), поэтому мы решили в очередной раз доработать нашу «пускалку», чтобы решить эту проблему.
С помощью расширения xhprof мы быстро выяснили, что наши тесты «тормозят» из-за того, что производительность расширения runkit существенно упала, поэтому мы стали искать причину. В итоге оказалось, что проблема, по всей видимости, в добавлении «мистического» runtime cache для PHP 5.4, который необходимо сбрасывать каждый раз после вызова функции “runkit_*_redefine”.
Расширение runkit при каждом вызове проходится по всем зарегистрированным классам, методам и функциям и сбрасывает этот кэш. Мы наивно попробовали это отключить, но после этого PHP начал падать, поэтому нам пришлось искать другое решение.
До перехода на PHP 5.5 у нас уже была пускалка юнит-тестов в виде надстройки над phpunit, которая разделяла один большой suite юнит-тестов на несколько более мелких: на тот момент мы использовали запуск тестов в 11 потоков (Илья Кудинов уже рассказывал об этом на конференциях, в том числе на конференции Badoo LoveQA: www.youtube.com/watch?v=gAisPsfbLkg).
Мы провели несколько простых бенчмарков и выяснили, что тесты проходят в несколько раз быстрее, если разделить наш suite не на 11 частей, как это было раньше, а на 128 или больше (при фиксированном количестве процессорных ядер). В каждом сьюте получалось всего около 10-15 файлов, поэтому мы назвали эту концепцию «микросьютами». Таких микросьютов у нас получилось около 150 штук, и каждый из них подозрительно хорошо подходил, чтобы быть «заданием» для какогонибудь скрипта (при этом задание состоит из списка файлов для соответствующего сьюта, а он уже, в свою очередь, запускает phpunit с соответствующими параметрами).
Так уж получилось, что автор этой статьи не имеет никакого отношения к QA, но зато был одним из основных разработчиков «нового скриптового фреймворка», который, по сути, является «облаком» для скриптов и поддерживает концепцию заданий (о нашем облаке мы тоже неоднократно рассказывали на конференциях и обязательно расскажем о нём подробнее на Хабре). А раз у нас есть задания в виде списков файлов для каждого сьюта phpunit, значит, их тоже можно «засунуть в облако», что мы и решили сделать. Идея очень простая: раз у нас есть много мелких заданий, то они могут быть запущены на нескольких серверах независимо друг от друга, что должно ещё больше ускорить прохождение тестов.
Мы запускаем тесты из нескольких разных источников:
Все эти виды прогонов тестов объединяет то, что нужно сначала «стянуть» (fetch) ветку из какогото источника, а потом запустить прогон тестов на этой ветке.
Этот факт и определил архитектуру нашей новой «облачной» пускалки тестов (Рис. 1, в начале статьи):
Сначала создается одно задание для master-процесса, который:
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/5e3/863/a66/5e3863a666435db4d619c097e889a27f.png)
Рис 2. Доступные директории в MySQL
После этого masterпроцесс анализирует оригинальный phpunit suite для прогона тестов и делит его на нужное количество частей (не более 10 файлов на один microsuite). Получившиеся задания (thread-процессы) добавляются в виде заданий в облако и начинают исполнение на доступных для выполнения серверах.
Первое задание, которое попадает на новый сервер, подготавливает выбранную директорию для прогона тестов, забирая нужный коммит с сервера, на котором работает masterпроцесс. Для того чтобы все остальные задания, которые пришлись на соответствующий сервер, не занимались тем же самым одновременно с первым заданием, используются файловые блокировки (Рис. 3).
Одновременно могут идти несколько прогонов тестов для более полной утилизации ресурсов нашего кластера: тесты проходят быстро, и значительную часть времени занимает подготовка исходных текстов, нежели непосредственно исполнение кода.
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/618/147/a7f/618147a7fbfca4f7f49704cb456f2b43.png)
Рис. 3. Блокировки при подготовке директории
Некоторые тесты могут идти значительно дольше остальных, и у нас есть статистика времени прохождения по каждому тесту, поэтому мы используем эту информацию, чтобы запускать «долгие» тесты в первую очередь. Такая стратегия позволяет добиться более равномерной загрузки серверов в процессе выполнения тестов, а также сократить общее время их выполнения (Рис. 4).
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/ccd/b98/39e/ccdb9839e041f4a410ad13c80a150204.png)
Рис 4. Учет времени исполнения тестов
«При хорошей погоде» весь наш suite из 28 000 юниттестов проходит за 1 минуту, поэтому тесты, которые длятся дольше, становятся «бутылочным горлышком», и наша система вывешивает авторов соответствующих тестов на «доску позора», которая показывается при каждом прогоне тестов. Помимо этого, если осталось мало тестов, показывается список тех, кто остался (Рис. 5).
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/9be/46e/2b1/9be46e2b18bde9d6f2dafc414cee826c.png)
Рис 5. Доска позора: список тестов, которые исполняются больше минуты
Сама по себе пускалка юнит-тестов была первым скриптом, который был переведен в облако. Она помогла устранить много багов и недоработок в самом облаке, заодно значительно ускорив прохождение юнит-тестов.
После перехода на PHP 5.5 мы смогли использовать новые фичи языка, значительно снизили потребление CPU на наших серверах (в среднем на 25%), а также перевели в облако нашу пускалку юниттестов. Последнее позволило нам снизить общее время прохождения тестов с 5-6 минут (на PHP 5.5 — десятков минут) до одной минуты, заодно переместив нагрузку с общего dev-сервера в облако.
Юрий youROCK Насретдинов, разработчик Badoo
![](https://habrastorage.org/getpro/habr/post_images/acb/906/de3/acb906de3aa914ea7d207b60e62c7d75.png)
Рис 1. Общая архитектура
«Грабли» при переходе с PHP 5.3 на PHP 5.5
В прошлый раз мы переходили с четвертой версии PHP на пятую, причём наша версия PHP 5.3 содержала патчи, чтобы работал «старый» синтаксис PHP, например,
$a = &new ClassName();
, и чтобы наша кодовая база могла работать на PHP4 и PHP5 одновременно. На этот раз у нас таких ограничений не было, поэтому при переходе мы просто нашли и заменили все устаревшие конструкции на более актуальные, и на этом переписывание кода было закончено.Основные проблемы, которые у нас возникли:
- часть deprecated-фич языка была убрана;
- расширение mysql стало deprecated;
- низкая производительность расширения runkit, которое мы используем при написании юнит-тестов.
После перехода на PHP 5.5 наши юнит-тесты начали проходить значительно дольше (в несколько раз), поэтому мы решили в очередной раз доработать нашу «пускалку», чтобы решить эту проблему.
Runkit и PHP 5.4+
С помощью расширения xhprof мы быстро выяснили, что наши тесты «тормозят» из-за того, что производительность расширения runkit существенно упала, поэтому мы стали искать причину. В итоге оказалось, что проблема, по всей видимости, в добавлении «мистического» runtime cache для PHP 5.4, который необходимо сбрасывать каждый раз после вызова функции “runkit_*_redefine”.
Расширение runkit при каждом вызове проходится по всем зарегистрированным классам, методам и функциям и сбрасывает этот кэш. Мы наивно попробовали это отключить, но после этого PHP начал падать, поэтому нам пришлось искать другое решение.
Концепция «микросьютов» (microsuite)
До перехода на PHP 5.5 у нас уже была пускалка юнит-тестов в виде надстройки над phpunit, которая разделяла один большой suite юнит-тестов на несколько более мелких: на тот момент мы использовали запуск тестов в 11 потоков (Илья Кудинов уже рассказывал об этом на конференциях, в том числе на конференции Badoo LoveQA: www.youtube.com/watch?v=gAisPsfbLkg).
Мы провели несколько простых бенчмарков и выяснили, что тесты проходят в несколько раз быстрее, если разделить наш suite не на 11 частей, как это было раньше, а на 128 или больше (при фиксированном количестве процессорных ядер). В каждом сьюте получалось всего около 10-15 файлов, поэтому мы назвали эту концепцию «микросьютами». Таких микросьютов у нас получилось около 150 штук, и каждый из них подозрительно хорошо подходил, чтобы быть «заданием» для какогонибудь скрипта (при этом задание состоит из списка файлов для соответствующего сьюта, а он уже, в свою очередь, запускает phpunit с соответствующими параметрами).
Тесты «в облаке»
Так уж получилось, что автор этой статьи не имеет никакого отношения к QA, но зато был одним из основных разработчиков «нового скриптового фреймворка», который, по сути, является «облаком» для скриптов и поддерживает концепцию заданий (о нашем облаке мы тоже неоднократно рассказывали на конференциях и обязательно расскажем о нём подробнее на Хабре). А раз у нас есть задания в виде списков файлов для каждого сьюта phpunit, значит, их тоже можно «засунуть в облако», что мы и решили сделать. Идея очень простая: раз у нас есть много мелких заданий, то они могут быть запущены на нескольких серверах независимо друг от друга, что должно ещё больше ускорить прохождение тестов.
Общая архитектура
Мы запускаем тесты из нескольких разных источников:
- автоматические прогоны тестов с помощью AIDA:
— по ветке (git branch) задачи;
— по «билду» — коду, который уедет на production;
— по ветке master; - «ручные» прогоны тестов, инициированные разработчиками или тестировщиками с dev-сервера.
Все эти виды прогонов тестов объединяет то, что нужно сначала «стянуть» (fetch) ветку из какогото источника, а потом запустить прогон тестов на этой ветке.
Этот факт и определил архитектуру нашей новой «облачной» пускалки тестов (Рис. 1, в начале статьи):
Сначала создается одно задание для master-процесса, который:
- выбирает доступную директорию в базе данных (Рис. 2);
- скачивает ветку git из нужного места (общий репозиторий или dev-сервер);
- (опционально) выполняет git merge master;
- (опционально) создает новый коммит со всеми локальными изменениями.
![](https://habrastorage.org/getpro/habr/post_images/5e3/863/a66/5e3863a666435db4d619c097e889a27f.png)
Рис 2. Доступные директории в MySQL
После этого masterпроцесс анализирует оригинальный phpunit suite для прогона тестов и делит его на нужное количество частей (не более 10 файлов на один microsuite). Получившиеся задания (thread-процессы) добавляются в виде заданий в облако и начинают исполнение на доступных для выполнения серверах.
Первое задание, которое попадает на новый сервер, подготавливает выбранную директорию для прогона тестов, забирая нужный коммит с сервера, на котором работает masterпроцесс. Для того чтобы все остальные задания, которые пришлись на соответствующий сервер, не занимались тем же самым одновременно с первым заданием, используются файловые блокировки (Рис. 3).
Одновременно могут идти несколько прогонов тестов для более полной утилизации ресурсов нашего кластера: тесты проходят быстро, и значительную часть времени занимает подготовка исходных текстов, нежели непосредственно исполнение кода.
![](https://habrastorage.org/getpro/habr/post_images/618/147/a7f/618147a7fbfca4f7f49704cb456f2b43.png)
Рис. 3. Блокировки при подготовке директории
Некоторые тесты могут идти значительно дольше остальных, и у нас есть статистика времени прохождения по каждому тесту, поэтому мы используем эту информацию, чтобы запускать «долгие» тесты в первую очередь. Такая стратегия позволяет добиться более равномерной загрузки серверов в процессе выполнения тестов, а также сократить общее время их выполнения (Рис. 4).
![](https://habrastorage.org/getpro/habr/post_images/ccd/b98/39e/ccdb9839e041f4a410ad13c80a150204.png)
Рис 4. Учет времени исполнения тестов
«При хорошей погоде» весь наш suite из 28 000 юниттестов проходит за 1 минуту, поэтому тесты, которые длятся дольше, становятся «бутылочным горлышком», и наша система вывешивает авторов соответствующих тестов на «доску позора», которая показывается при каждом прогоне тестов. Помимо этого, если осталось мало тестов, показывается список тех, кто остался (Рис. 5).
![](https://habrastorage.org/getpro/habr/post_images/9be/46e/2b1/9be46e2b18bde9d6f2dafc414cee826c.png)
Рис 5. Доска позора: список тестов, которые исполняются больше минуты
Сама по себе пускалка юнит-тестов была первым скриптом, который был переведен в облако. Она помогла устранить много багов и недоработок в самом облаке, заодно значительно ускорив прохождение юнит-тестов.
Результаты
После перехода на PHP 5.5 мы смогли использовать новые фичи языка, значительно снизили потребление CPU на наших серверах (в среднем на 25%), а также перевели в облако нашу пускалку юниттестов. Последнее позволило нам снизить общее время прохождения тестов с 5-6 минут (на PHP 5.5 — десятков минут) до одной минуты, заодно переместив нагрузку с общего dev-сервера в облако.
Юрий youROCK Насретдинов, разработчик Badoo