Используем Symfony Flex для создания блога с фотогалереей: тестирование

Данная статья будет частью серии, описывающей процесс создания блога с фотогалереей.(Смотрите репозиторий здесь).

В предыдущей статье мы продемонстрировали, как создать проект Symfony с нуля при помощи Flex и запустить его. Следующим шагом будет заполнение БД информацией и тестирование приложения на производительность.

Если вы следовали инструкции «Начало работы с приложением» в предыдущей публикации из данной серии, то вы выполнили действия, описанные в данной статье. В таком случае данный материал станет объяснением того, как это было сделано.

В виде бонуса мы покажем, как изменить простой набор тестов PHPUnit с базовыми « дымовыми тестами ».

Больше данных

Фикстуры, которые мы создали в предыдущей статье, хорошо подходят для фазы разработки. Загрузка примерно 30 объектов выполняется быстро, и ее можно без проблем часто повторять при изменении схемы базы данных.

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

Можно без труда попробовать увеличить константу COUNT в классах фикстуры и посмотреть, что из этого выйдет:

// src/DataFixtures/ORM/LoadUsersData.phpclass LoadUsersData extends AbstractFixture implements ContainerAwareInterface, OrderedFixtureInterface{ const COUNT = 500;...}// src/DataFixtures/ORM/LoadGalleriesData.phpclass LoadGalleriesData extends AbstractFixture implements ContainerAwareInterface, OrderedFixtureInterface{ const COUNT = 1000;...}

Сейчас после запуска bin/refreshDb.sh появится сообщение: «Фатальная ошибка PHP: допустимый объем памяти в N байтов исчерпан».

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

Сначала определим размер генерируемых тестовых данных в 100 галерей. После каждой сотни мы будем сбрасывать и очищать EntityManager.

После вызова $manager->clear() все сохраненные сущности перестанут быть управляемыми. Менеджер сущностей больше о них не знает, и может появиться ошибка «сущность не сохраняется».

Надо снова объединить сущность с менеджером $entity = $manager->merge($entity);

Если не проводить оптимизацию, то при запуске класса фикстуры LoadGalleriesData увеличивается использование памяти:

> loading [200] AppDataFixturesORMLoadGalleriesData100 Memory usage(currently) 24MB /(max) 24MB200 Memory usage(currently) 26MB /(max) 26MB300 Memory usage(currently) 28MB /(max) 28MB400 Memory usage(currently) 30MB /(max) 30MB500 Memory usage(currently) 32MB /(max) 32MB600 Memory usage(currently) 34MB /(max) 34MB700 Memory usage(currently) 36MB /(max) 36MB800 Memory usage(currently) 38MB /(max) 38MB900 Memory usage(currently) 40MB /(max) 40MB1000 Memory usage(currently) 42MB /(max) 42MB

Потребление памяти начинается с 24 МБ и увеличивается на 2 МБ для каждых 100 галерей. Если загрузить 100 000 галерей, то потребуется примерно 2 ГБ памяти.

Можно легко добавить $manager->flush() и gc_collect_cycles() в каждую сотню галерей. Потом очистить журналы в SQL при помощи $manager->getConnection()->getConfiguration()->setSQLLogger. А также удалить ссылки на сущности, закомментировав $manager->getConnection()->getConfiguration()->setSQLLogger. Благодаря данному использование памяти становится постоянным для генерации каждой партии галерей в сто штук.

// Определяем размер партии вне цикла for$batchSize = 100;...for($i = 1; $i <= self::COUNT; $i++) {... // Сохраняем партию в конце цикла for if (($i % $batchSize) == 0 || $i == self::COUNT) { $currentMemoryUsage = round(memory_get_usage(true) / 1024); $maxMemoryUsage = round(memory_get_peak_usage(true) / 1024);       echo sprintf("%s Memory usage (currently) %dKB/ (max) %dKB n", $i, $currentMemoryUsage, $maxMemoryUsage);        $manager->flush();        $manager->clear();        // здесь необходимо объединить все сущности, которые повторно используются с $manager        // так как они становятся неуправляемыми после вызова $manager->clear();        // к примеру, если вы  загрузили категории или теги сущностей        // $category = $manager->merge($category);        gc_collect_cycles();    }}

Как и ожидалось, использование памяти сейчас стабильно:

> loading [200] AppDataFixturesORMLoadGalleriesData100 Memory usage (currently) 24MB / (max) 24MB200 Memory usage (currently) 26MB / (max) 28MB300 Memory usage (currently) 26MB / (max) 28MB400 Memory usage (currently) 26MB / (max) 28MB500 Memory usage (currently) 26MB / (max) 28MB600 Memory usage (currently) 26MB / (max) 28MB700 Memory usage (currently) 26MB / (max) 28MB800 Memory usage (currently) 26MB / (max) 28MB900 Memory usage (currently) 26MB / (max) 28MB1000 Memory usage (currently) 26MB / (max) 28MB

Чтобы оптимизировать загрузку тестовых данных, можно без проблем подготовить 15 случайных картинок. А далее переделать программный код фикстур так, чтобы выбирать одно из картинок случайным образом вместо использования способа Faker $faker->image().

Возьмем 15 картинок из Unsplash и сохраним их в var/demo-data/sample-images. Далее обновим способ LoadGalleriesData::generateRandomImage:

private function generateRandomImage($imageName)    {        $images = [            'image1.jpeg',            'image10.jpeg',            'image11.jpeg',            'image12.jpg',            'image13.jpeg',            'image14.jpeg',            'image15.jpeg',            'image2.jpeg',            'image3.jpeg',            'image4.jpeg',            'image5.jpeg',            'image6.jpeg',            'image7.jpeg',            'image8.jpeg',            'image9.jpeg',        ];        $sourceDirectory = $this->container->getParameter('kernel.project_dir') . '/var/demo-data/sample-images/';        $targetDirectory = $this->container->getParameter('kernel.project_dir') . '/var/uploads/';        $randomImage = $images[rand(0, count($images) - 1)];        $randomImageSourceFilePath = $sourceDirectory . $randomImage;        $randomImageExtension = explode('.', $randomImage)[1];        $targetImageFilename = sha1(microtime() . rand()) . '.' . $randomImageExtension;        copy($randomImageSourceFilePath, $targetDirectory . $targetImageFilename);        $image = new Image(            Uuid::getFactory()->uuid4(),            $randomImage,            $targetImageFilename        );        return $image;    }

Удалим старые файлы в var/uploads при перезагрузке фикстур. Для этого добавим команду rm var/uploads/* в скрипт bin/refreshDb.sh сразу же после сброса схемы базы данных.

Загрузка 500 посетителей и 1000 галерей сейчас занимает около 7 минут и примерно 28 МБ памяти.

Dropping database schema...Database schema dropped successfully!ATTENTION: This operation should not be executed in a production environment.Creating database schema...Database schema created successfully!  > purging database  > loading [100] AppDataFixturesORMLoadUsersData300 Memory usage (currently) 10MB / (max) 10MB500 Memory usage (currently) 12MB / (max) 12MB  > loading [200] AppDataFixturesORMLoadGalleriesData100 Memory usage (currently) 24MB / (max) 26MB200 Memory usage (currently) 26MB / (max) 28MB300 Memory usage (currently) 26MB / (max) 28MB400 Memory usage (currently) 26MB / (max) 28MB500 Memory usage (currently) 26MB / (max) 28MB600 Memory usage (currently) 26MB / (max) 28MB700 Memory usage (currently) 26MB / (max) 28MB800 Memory usage (currently) 26MB / (max) 28MB900 Memory usage (currently) 26MB / (max) 28MB1000 Memory usage (currently) 26MB / (max) 28MB

Взгляните на скрипты классов фикстур: LoadUsersData.php и LoadGalleriesData.php .

Производительность

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

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

Тестирование производительности

Для тестирования загрузки мы будем использовать Siege. Вместо Siege можно использовать Doker . Контейнеры Docker похожи на виртуальные компьютеры ( но это не одно и то же ).

Тестирование домашней страницы

Запустим два параллельно работающих теста и протестируем домашнюю страницу (и URL-адреса ленивой загрузки). Первый тест будет проверять только URL домашней страницы, а второй –  конечные URL-адреса ленивой загрузки.

Файл lazy-load-urls.txt включает рандомизированный список URL-адресов страниц, загруженных при помощи ленивой загрузки в прогнозируемых соотношениях:

  • 10 URL для второй страницы (50%);
  • 6 URL для третьей страницы (30%);
  • 3 URL для четвертой страницы (15%);
  • 1 URL для пятой страницы (5%).
http://blog.app/galleries-lazy-load?page=2http://blog.app/galleries-lazy-load?page=2http://blog.app/galleries-lazy-load?page=2http://blog.app/galleries-lazy-load?page=4http://blog.app/galleries-lazy-load?page=2http://blog.app/galleries-lazy-load?page=2http://blog.app/galleries-lazy-load?page=3http://blog.app/galleries-lazy-load?page=2http://blog.app/galleries-lazy-load?page=2http://blog.app/galleries-lazy-load?page=4http://blog.app/galleries-lazy-load?page=2http://blog.app/galleries-lazy-load?page=4http://blog.app/galleries-lazy-load?page=2http://blog.app/galleries-lazy-load?page=3http://blog.app/galleries-lazy-load?page=3http://blog.app/galleries-lazy-load?page=3http://blog.app/galleries-lazy-load?page=5http://blog.app/galleries-lazy-load?page=3http://blog.app/galleries-lazy-load?page=2http://blog.app/galleries-lazy-load?page=3

Скрипт тестирования производительности домашней страницы будет параллельно запускать два процесса Siege: один для домашней страницы, а другой – для сгенерированного случайным образом списка URL-адресов.

Чтобы выполнить один HTTP-запрос при помощи Siege (в Docker), запустите:

docker run --rm -t yokogawa/siege -c1 -r1 blog.app

Чтобы запустить одноминутный тест с 50 сеансами посетителей на домашней странице с односекундной задержкой, выполните:

docker run --rm -t yokogawa/siege -d1 -c50 -t1M http://blog.app

Чтобы запустить одноминутный тест с 50 посетителями по URL-адресам из файла lazy-load-urls.txt, выполните:

docker run --rm -v `pwd`:/var/siege:ro -t yokogawa/siege -i --file=/var/siege/lazy-load-urls.txt -d1 -c50 -t1M

Запуск необходимо произвести из каталога, в котором размещен файл lazy-load-urls.txt . Данный каталог будет выводиться в Docker как том, доступный только для чтения.

Скрипта test-homepage.sh запустит два процесса Siege и выведет результаты. Предположим, что мы развернули приложение на сервере с Nginx и PHP-FPM 7.1, загрузили 25 000 посетителей и 30 000 галерей. Ниже приведены результаты тестирования загрузки домашней страницы приложения:

./test-homepage.shТранзакций:                 499 hitsДоступность:                100.00 %Времени прошло:             59.10 secsПередано данных:            1.49 MBВремя отклика:              4.75 secsСкорость транзакции:        8.44 trans/secПропускная способность 0.03 MB/secСогласованность:            40.09Успешных транзакций:        499Неудачных транзакций:       0Самая долгая транзакция:    16.47Самая короткая транзакция:  0.17Транзакций:                 482 hitsДоступность:                100.00 %Времени прошло:             59.08 secsПередано данных:            6.01 MBВремя отклика:              4.72 secsСкорость транзакции:        8.16 trans/secПропускная способность 0.10 MB/secСогласованность:            38.49Успешных транзакций:        482Неудачных транзакций:       0Самая долгая транзакция:    15.36Самая короткая транзакция:  0.15

Доступность приложения составляет 100%. Но время отклика составляет около 5 секунд, что недопустимо.

Тестирование отдельной страницы галереи

Тестирование отдельной страницы галереи проще: запустим Siege для файла galleries.txt со списком URL-адресов отдельных страниц.

Из каталога, в котором располагается файл galleries.txt, запустите следующую команду:

docker run --rm -v `pwd`:/var/siege:ro -t yokogawa/siege -i --file=/var/siege/galleries.txt -d1 -c50 -t1M

Результаты тестирования загрузки отдельных страниц галереи лучше:

./test-single-gallery.sh** SIEGE 3.0.5** Подготовка 50 одновременных посетителей к «битве».The server is now under siege...Lifting the server siege...      done.Транзакций:                 3589 hitsДоступность:                100.00 %Времени прошло:             59.64 secsПередано данных:            11.15 MBВремя отклика:              0.33 secsСкорость транзакции:        60.18 trans/secПропускная способность 0.19 MB/secСогласованность:            19.62Успешных транзакций:        3589Неудачных транзакций:       0Самая долгая транзакция:    1.25Самая короткая транзакция:  0.10

Тесты, тесты, тесты

Чтобы убедиться, что обновление приложения в будущем будет проходить без проблем, необходимо провести пару тестов. Сначала надо установить PHPUnit как dev-зависимость:

composer req --dev phpunit

Далее создать простую конфигурацию PHPUnit, скопировав phpunit.xml.dist, созданный Flex, в phpunit.xml. И после это обновить переменные среды (к примеру, переменную DATABASE_URL для среды тестирования). Кроме этого я добавляю phpunit.xml в .gitignore.

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

Дымовое тестирование ‒ это предварительное тестирование для выявления простых сбоев, достаточно серьезных, чтобы отменить выпуск программного обеспечения в будущем.

Данные тесты только подтвердят, что URL-адреса, предоставленные в способе urlProvider(), приводят к успешному ответному коду HTTP.

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

namespace AppTests;use AppEntityGallery;use PsrContainerContainerInterface;use SymfonyBundleFrameworkBundleTestWebTestCase;use SymfonyComponentRoutingRouterInterface;class SmokeTest extends WebTestCase{    /** @var ContainerInterface */    private $container;    /**     * @dataProvider urlProvider     */    public function testPageIsSuccessful($url)    {        $client = self::createClient();        $client->request('GET', $url);        $this->assertTrue($client->getResponse()->isSuccessful());    }    public function urlProvider()    {        $client = self::createClient();        $this->container = $client->getContainer();        $urls = [            ['/'],        ];        $urls += $this->getGalleriesUrls();        return $urls;    }    private function getGalleriesUrls()    {        $router = $this->container->get('router');        $doctrine = $this->container->get('doctrine');        $galleries = $doctrine->getRepository(Gallery::class)->findBy([], null, 5);        $urls = [];        /** @var Gallery $gallery */        foreach ($galleries as $gallery) {            $urls[] = [                '/' . $router->generate('gallery.single-gallery', ['id' => $gallery->getId()],                    RouterInterface::RELATIVE_PATH),            ];        }        return $urls;    }}

Запустите ./vendor/bin/phpunit и посмотрите, выполняются ли тесты:

./vendor/bin/phpunitPHPUnit 6.5-dev by Sebastian Bergmann and contributors....5 / 5 (100%)Time: 4.06 seconds, Memory: 16.00MBOK (5 tests, 5 assertions)

Оставайтесь в теме

В следующих статьях данной серии будут рассмотрена оптимизация производительности PHP и MySQL, улучшение общей производительности приложения.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *