使用 Symfony Flex 构建图库博客:数据测试
本文是关于构建示例应用程序(多图像库博客)以进行性能基准测试和优化的系列文章的一部分。 (在此处查看回购协议。)
在上一篇文章中,我们演示了如何使用 Flex 从头开始设置 Symfony 项目,以及如何创建一组简单的固定装置并启动和运行项目。
我们旅程的下一步是用实际数量的数据填充数据库以测试应用程序性能。
注意:如果您在上一篇文章中执行了“应用程序入门”步骤,那么您已经按照本文中概述的步骤进行操作。 如果是这种情况,请使用这篇文章作为解释器来说明它是如何完成的。
作为奖励,我们将演示如何使用基本冒烟测试设置简单的 PHPUnit 测试套件。
更多虚假数据
一旦你的实体被打磨好了,你就有了你的“就是这样! 我受够了!” 现在是创建更重要的数据集的最佳时机,该数据集可用于进一步测试和准备应用程序以进行生产。
像我们在上一篇文章中创建的那样的简单固定装置非常适合开发阶段,在该阶段可以快速完成约 30 个实体的加载,并且在更改 DB 模式时通常可以重复进行。
测试应用程序性能、模拟真实世界的流量和检测瓶颈需要更大的数据集(即该项目的大量数据库条目和图像文件)。 生成数千个条目需要一些时间(和计算机资源),所以我们只想做一次。
我们可以尝试增加 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 仅在 fixture 类的最后才被刷新。 此外,Faker 正在为每个画廊条目下载随机图像。 对于 1,000 个画廊,每个画廊有 5 到 10 张图片,这将是 5,000 – 10,000 次下载,这真的很慢。
有很多关于优化 Doctrine 和 Symfony 以进行批处理的优秀资源,我们将使用其中的一些技巧来优化 fixtures 加载。
首先,我们将定义 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 MB 开始,每批(100 个画廊)增加 2 MB。 如果我们尝试加载 100,000 个图库,则需要 24 MB + 999(999 个批次,每批 100 个图库,99,900 个图库)* 2 MB = ~2 GB 内存。
添加后 $manager->flush()
和 gc_collect_cycles()
对于每个批次,删除 SQL 日志记录 $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();
// e.g. if you've already loaded category or tag entities
// $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 张随机图像并更新 fixture 脚本以随机选择其中一张而不是使用 Faker 的,而不是每次都下载随机图像 $faker->image()
方法。
让我们从 Unsplash 中获取 15 张图像并将它们保存在 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 MB 的内存(峰值使用)。
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 和性能测试的信息。 我们可以利用 Docker——一个强大的容器平台,而不是在我的机器上安装 Siege。
简单来说,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 列表。
要使用 Siege(在 Docker 中)执行单个 HTTP 请求,请运行:
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
要对 50 个并发用户针对 URL 运行 1 分钟的测试 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
从目录中执行此操作…