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

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

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

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

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

Содержание

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

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

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

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

// src/DataFixtures/ORM/LoadUsersData.php
class LoadUsersData extends AbstractFixture implements ContainerAwareInterface, OrderedFixtureInterface
{
    const COUNT = 500;
    ...
}

// src/DataFixtures/ORM/LoadGalleriesData.php
class 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] AppDataFixturesORMLoadGalleriesData
100 Memory usage (currently) 24MB / (max) 24MB
200 Memory usage (currently) 26MB / (max) 26MB
300 Memory usage (currently) 28MB / (max) 28MB
400 Memory usage (currently) 30MB / (max) 30MB
500 Memory usage (currently) 32MB / (max) 32MB
600 Memory usage (currently) 34MB / (max) 34MB
700 Memory usage (currently) 36MB / (max) 36MB
800 Memory usage (currently) 38MB / (max) 38MB
900 Memory usage (currently) 40MB / (max) 40MB
1000 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] AppDataFixturesORMLoadGalleriesData
100 Memory usage (currently) 24MB / (max) 24MB
200 Memory usage (currently) 26MB / (max) 28MB
300 Memory usage (currently) 26MB / (max) 28MB
400 Memory usage (currently) 26MB / (max) 28MB
500 Memory usage (currently) 26MB / (max) 28MB
600 Memory usage (currently) 26MB / (max) 28MB
700 Memory usage (currently) 26MB / (max) 28MB
800 Memory usage (currently) 26MB / (max) 28MB
900 Memory usage (currently) 26MB / (max) 28MB
1000 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] AppDataFixturesORMLoadUsersData
300 Memory usage (currently) 10MB / (max) 10MB
500 Memory usage (currently) 12MB / (max) 12MB
  > loading [200] AppDataFixturesORMLoadGalleriesData
100 Memory usage (currently) 24MB / (max) 26MB
200 Memory usage (currently) 26MB / (max) 28MB
300 Memory usage (currently) 26MB / (max) 28MB
400 Memory usage (currently) 26MB / (max) 28MB
500 Memory usage (currently) 26MB / (max) 28MB
600 Memory usage (currently) 26MB / (max) 28MB
700 Memory usage (currently) 26MB / (max) 28MB
800 Memory usage (currently) 26MB / (max) 28MB
900 Memory usage (currently) 26MB / (max) 28MB
1000 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=2
http://blog.app/galleries-lazy-load?page=2
http://blog.app/galleries-lazy-load?page=2
http://blog.app/galleries-lazy-load?page=4
http://blog.app/galleries-lazy-load?page=2
http://blog.app/galleries-lazy-load?page=2
http://blog.app/galleries-lazy-load?page=3
http://blog.app/galleries-lazy-load?page=2
http://blog.app/galleries-lazy-load?page=2
http://blog.app/galleries-lazy-load?page=4
http://blog.app/galleries-lazy-load?page=2
http://blog.app/galleries-lazy-load?page=4
http://blog.app/galleries-lazy-load?page=2
http://blog.app/galleries-lazy-load?page=3
http://blog.app/galleries-lazy-load?page=3
http://blog.app/galleries-lazy-load?page=3
http://blog.app/galleries-lazy-load?page=5
http://blog.app/galleries-lazy-load?page=3
http://blog.app/galleries-lazy-load?page=2
http://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/phpunit
PHPUnit 6.5-dev by Sebastian Bergmann and contributors.

...

5 / 5 (100%)

Time: 4.06 seconds, Memory: 16.00MB

OK (5 tests, 5 assertions)

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

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

Данная публикация представляет собой перевод статьи «Building an Image Gallery Blog with Symfony Flex: Data Testing» , подготовленной дружной командой проекта Интернет-технологии.ру

Меню