Эта статья является частью серии по созданию примера приложения — блога галереи с несколькими изображениями — для оценки производительности и оптимизации. (Посмотреть репо здесь.)
В предыдущей статье мы продемонстрировали, как настроить проект 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 Fatal error: Allowed memory size of N bytes exhausted
.
Помимо медленного выполнения, каждая ошибка приведет к пустой базе данных, потому что EntityManager сбрасывается только в самом конце класса фикстуры. Кроме того, Faker загружает случайное изображение для каждой записи галереи. Для 1000 галерей с 5-10 изображениями в каждой галерее будет 5 000 — 10 000 загрузок, что очень медленно.
Существуют отличные ресурсы по оптимизации Doctrine и Symfony для пакетной обработки, и мы собираемся использовать некоторые из этих советов для оптимизации загрузки приборов.
Сначала мы определим размер партии в 100 галерей. После каждого пакета мы сбрасываем и EntityManager
(т. Е. Отсоединяем постоянные сущности) и сообщаем сборщику мусора, что он выполняет свою работу.
Для отслеживания прогресса давайте распечатаем некоторую метаинформацию (идентификатор партии и использование памяти).
Примечание. После вызова $manager->clear()
все сохраненные сущности теперь неуправляемые. Менеджер сущностей больше не знает о них, и вы, вероятно, получите ошибку «сущность не сохранена».
Ключ заключается в том, чтобы объединить сущность обратно с менеджером $entity = $manager->merge($entity);
Без оптимизации использование памяти увеличивается при запуске класса LoadGalleriesData
:
> loading [200] App\DataFixtures\ORM\LoadGalleriesData 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 галерей, нам потребуется 24 МБ + 999 (999 пакетов из 100 галерей, 99 900 галерей) * 2 МБ = ~ 2 ГБ памяти .
После добавления $manager->flush()
и gc_collect_cycles()
для каждого пакета удалите ведение журнала SQL с помощью $manager->getConnection()->getConfiguration()->setSQLLogger(null)
и удалите ссылки на сущности, $manager->getConnection()->getConfiguration()->setSQLLogger(null)
$this->addReference('gallery' . $i, $gallery);
использование памяти становится постоянным для каждой партии.
// Define batch size outside of the for loop $batchSize = 100; ... for ($i = 1; $i <= self::COUNT; $i++) { ... // Save the batch at the end of the for loop 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(); // here you should merge entities you're re-using with the $manager // because they aren't managed anymore after calling $manager->clear(); // eg if you've already loaded category or tag entities // $category = $manager->merge($category); gc_collect_cycles(); } }
Как и ожидалось, использование памяти теперь стабильно:
> loading [200] App\DataFixtures\ORM\LoadGalleriesData 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->image()
чтобы случайным образом выбрать один из них вместо использования метода 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
сразу после 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] App\DataFixtures\ORM\LoadUsersData 300 Memory usage (currently) 10MB / (max) 10MB 500 Memory usage (currently) 12MB / (max) 12MB > loading [200] App\DataFixtures\ORM\LoadGalleriesData 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 на моей машине, мы можем использовать Docker — мощную контейнерную платформу.
Проще говоря, контейнеры Docker похожи на виртуальные машины ( но это не одно и то же ). За исключением создания и развертывания приложений, Docker можно использовать для экспериментов с приложениями, фактически не устанавливая их на локальном компьютере. Вы можете создавать свои изображения или использовать изображения, доступные в Docker Hub , публичном реестре образов Docker.
Это особенно полезно, когда вы хотите поэкспериментировать с разными версиями одного и того же программного обеспечения (например, с разными версиями PHP).
Мы будем использовать изображение yokogawa / siege для тестирования приложения.
Тестирование домашней страницы
Тестирование домашней страницы не является тривиальным, поскольку Ajax-запросы выполняются только тогда, когда пользователь прокручивает страницу до конца.
Мы можем ожидать, что все пользователи окажутся на домашней странице (т.е. 100%). Мы также можем оценить, что 50% из них прокрутят до конца и, следовательно, запросят вторую страницу галерей. Можно также предположить, что 30% из них загрузят третью страницу, 15% запросят четвертую страницу и 5% — пятую страницу.
Эти цифры основаны на прогнозах, и было бы намного лучше, если бы мы могли использовать аналитический инструмент, чтобы получить реальное представление о поведении пользователей. Но это невозможно для совершенно нового приложения. Тем не менее, неплохо бы время от времени взглянуть на аналитические данные и скорректировать свой набор тестов после первоначального развертывания.
Мы протестируем домашнюю страницу (и ленивые 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
Скрипт для тестирования производительности домашней страницы будет запускать 2 процесса Siege параллельно, один для домашней страницы, а другой для сгенерированного списка URL.
Чтобы выполнить один HTTP-запрос с помощью Siege (в Docker), выполните:
docker run --rm -t yokogawa/siege -c1 -r1 blog.app
Примечание: если вы не используете Docker, вы можете опустить docker run --rm -t yokogawa/siege
part и запустить Siege с теми же аргументами.
Чтобы запустить 1-минутный тест с 50 одновременными пользователями на домашней странице с 1-секундной задержкой, выполните:
docker run --rm -t yokogawa/siege -d1 -c50 -t1M http://blog.app
Чтобы запустить 1-минутный тест с 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 запустит 2 процесса осады (способом, предложенным в ответе на переполнение стека ) и выдаст результаты.
Предположим, что мы развернули приложение на сервере с Nginx и PHP-FPM 7.1 и загрузили 25 000 пользователей и 30 000 галерей. Результаты нагрузочного тестирования домашней страницы приложения:
./test-homepage.sh Transactions: 499 hits Availability: 100.00 % Elapsed time: 59.10 secs Data transferred: 1.49 MB Response time: 4.75 secs Transaction rate: 8.44 trans/sec Throughput: 0.03 MB/sec Concurrency: 40.09 Successful transactions: 499 Failed transactions: 0 Longest transaction: 16.47 Shortest transaction: 0.17 Transactions: 482 hits Availability: 100.00 % Elapsed time: 59.08 secs Data transferred: 6.01 MB Response time: 4.72 secs Transaction rate: 8.16 trans/sec Throughput: 0.10 MB/sec Concurrency: 38.49 Successful transactions: 482 Failed transactions: 0 Longest transaction: 15.36 Shortest transaction: 0.15
Несмотря на то, что доступность приложения составляет 100% как для домашней страницы, так и для тестов с отложенной загрузкой, время отклика составляет ~ 5 секунд, чего не следует ожидать от высокопроизводительного приложения.
Тестирование одной страницы галереи
Тестирование одной страницы галереи немного проще: мы запустим Siege для файла galleries.txt
, где у нас есть список URL-адресов одной страницы галереи для тестирования.
Из каталога, в котором находится файл galleries.txt
(этот каталог будет монтироваться как том только для чтения в Docker), выполните следующую команду:
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 ** Preparing 50 concurrent users for battle. The server is now under siege... Lifting the server siege... done. Transactions: 3589 hits Availability: 100.00 % Elapsed time: 59.64 secs Data transferred: 11.15 MB Response time: 0.33 secs Transaction rate: 60.18 trans/sec Throughput: 0.19 MB/sec Concurrency: 19.62 Successful transactions: 3589 Failed transactions: 0 Longest transaction: 1.25 Shortest transaction: 0.10
Тесты, Тесты, Тесты
Чтобы убедиться, что мы ничего не нарушаем с улучшениями, которые мы реализуем в будущем, нам нужно хотя бы несколько тестов.
Во-первых, мы требуем PHPUnit как зависимость dev:
composer req --dev phpunit
Затем мы создадим простую конфигурацию PHPUnit, скопировав phpunit.xml.dist
созданный Flex, в phpunit.xml
и обновим переменные среды (например, переменную DATABASE_URL
для тестовой среды). Также я добавляю phpunit.xml
в .gitignore
.
Далее мы создаем основные функциональные тесты / тесты на дым для домашней страницы блога и отдельных страниц галереи. Дымовое тестирование — это «предварительное тестирование для выявления простых сбоев, достаточно серьезных, чтобы отклонить предполагаемый выпуск программного обеспечения» . Поскольку проводить тесты на дым довольно просто, нет веской причины, по которой вам следует избегать их!
Эти тесты только подтвердят, что URL-адреса, предоставленные вами в urlProvider()
, приводят к успешному коду ответа HTTP (т. urlProvider()
Код состояния HTTP 2xx или 3xx).
Простое тестирование дыма на домашней странице и на пяти отдельных страницах галереи может выглядеть так :
namespace App\Tests; use App\Entity\Gallery; use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\Routing\RouterInterface; 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)
Обратите внимание, что лучше жестко закодировать важные URL-адреса (например, для статических страниц или некоторых известных URL-адресов), чем генерировать их в тесте. Узнайте больше о PHPUnit и TDD здесь .
Будьте на связи
В следующих статьях этой серии будут рассмотрены сведения об оптимизации производительности PHP и MySQL, улучшении общего восприятия производительности и другие советы и рекомендации для повышения производительности приложений.