使用后台处理加快页面加载时间
本文是关于构建示例应用程序(多图像库博客)以进行性能基准测试和优化的系列文章的一部分。 (在此处查看回购协议。)
在上一篇文章中,我们添加了按需调整图像大小。 图像在第一次请求时调整大小并缓存以备后用。 通过这样做,我们为第一次加载增加了一些开销; 系统必须即时渲染缩略图,并“阻止”第一个用户的页面渲染,直到图像渲染完成。
优化的方法是在创建图库后呈现缩略图。 您可能会想,“好吧,但我们会阻止正在创建画廊的用户吗?” 这不仅会是糟糕的用户体验,而且也不是可扩展的解决方案。 用户会对较长的加载时间感到困惑,或者更糟糕的是,如果图像太重而无法处理,则会遇到超时和/或错误。 最好的解决方案是将这些繁重的任务移到后台。
后台作业
后台作业是进行任何繁重处理的最佳方式。 我们可以立即通知我们的用户我们已经收到他们的请求并安排处理。 与 YouTube 处理上传视频的方式相同:上传后无法访问它们。 用户需要等到视频处理完成后才能预览或分享。
处理或生成文件、发送电子邮件或任何其他非关键任务应在后台完成。
后台处理如何工作?
后台处理方法中有两个关键组件:作业队列和工作人员。 该应用程序创建应在工作人员等待时处理的作业,并一次从队列中取出一项作业。
您可以创建多个工作实例(进程)以加快处理速度,将大作业分成较小的块并同时处理它们。 如何组织和管理后台处理取决于您,但请注意,并行处理不是一项微不足道的任务:您应该注意潜在的竞争条件并优雅地处理失败的任务。
我们的技术堆栈
我们使用 Beanstalkd 作业队列来存储作业,使用 Symfony 控制台组件将工作人员实现为控制台命令,并使用 Supervisor 来管理工作进程。
如果您使用的是 Homestead Improved,则 Beanstalkd 和 Supervisor 已经安装,因此您可以跳过下面的安装说明。
安装 Beanstalkd
Beanstalkd 是
一个具有通用接口的快速工作队列,最初设计用于通过异步运行耗时的任务来减少大容量 Web 应用程序中页面视图的延迟。
您可以使用许多可用的客户端库。 在我们的项目中,我们使用的是 Pheanstalk。
要在您的 Ubuntu 或 Debian 服务器上安装 Beanstalkd,只需运行 sudo apt-get install beanstalkd
. 查看官方下载页面,了解如何在其他操作系统上安装 Beanstalkd。
安装后,Beanstalkd 将作为守护进程启动,等待客户端连接并创建(或处理)作业:
/etc/init.d/beanstalkd
Usage: /etc/init.d/beanstalkd {start|stop|force-stop|restart|force-reload|status}
通过运行将 Pheanstalk 安装为依赖项 composer require pda/pheanstalk
.
队列将用于创建和获取作业,因此我们将在工厂服务中集中创建队列 JobQueueFactory
:
<?php
namespace AppService;
use PheanstalkPheanstalk;
class JobQueueFactory
{
private $host = 'localhost';
private $port = '11300';
const QUEUE_IMAGE_RESIZE = 'resize';
public function createQueue(): Pheanstalk
{
return new Pheanstalk($this->host, $this->port);
}
}
现在我们可以在任何需要与 Beanstalkd 队列交互的地方注入工厂服务。 我们将队列名称定义为常量,并在将作业放入队列或在 worker 中观察队列时引用它。
安装主管
根据官方页面,主管是
客户端/服务器系统,允许其用户监视和控制类 UNIX 操作系统上的多个进程。
我们将使用它来启动、重启、扩展和监控工作进程。
通过运行在您的 Ubuntu/Debian 服务器上安装 Supervisorsudo apt-get install supervisor
. 安装后,Supervisor 将作为守护进程在后台运行。 使用 supervisorctl
控制主管进程:
$ sudo supervisorctl help
default commands (type help <topic>):
=====================================
add exit open reload restart start tail
avail fg pid remove shutdown status update
clear maintail quit reread signal stop version
要使用 Supervisor 控制进程,我们首先必须编写一个配置文件并描述我们希望如何控制我们的进程。 配置存储在 /etc/supervisor/conf.d/
. 调整工作人员大小的简单主管配置如下所示:
[program:resize-worker]
process_name=%(program_name)s_%(process_num)02d
command=php PATH-TO-YOUR-APP/bin/console app:resize-image-worker
autostart=true
autorestart=true
numprocs=5
stderr_logfile = PATH-TO-YOUR-APP/var/log/resize-worker-stderr.log
stdout_logfile = PATH-TO-YOUR-APP/var/log/resize-worker-stdout.log
我们告诉 Supervisor 如何命名生成的进程、应该运行的命令的路径、自动启动和重新启动进程、我们想要有多少个进程以及在哪里记录输出。 在此处了解有关主管配置的更多信息。
在后台调整图像大小
一旦我们设置了基础设施(即安装了 Beanstalkd 和 Supervisor),我们就可以修改我们的应用程序以在创建图库后在后台调整图像大小。 为此,我们需要:
- 更新图像服务逻辑
ImageController
将调整工作人员大小作为控制台命令为我们的工作人员创建主管配置更新灯具并调整灯具类中的图像大小。
更新图像服务逻辑
到目前为止,我们一直在根据第一个请求调整图像大小:如果请求大小的图像文件不存在,则会即时创建。
我们现在将修改 ImageController 以仅当调整大小的图像文件存在时(即,仅当图像已经调整大小时)返回请求大小的图像响应。
如果不是,应用程序将返回一个通用的占位符图像响应,说明图像正在调整大小。 请注意,占位符图像响应具有不同的缓存控制标头,因为我们不想缓存占位符图像; 我们希望在调整大小过程完成后立即渲染图像。
我们将创建一个名为 GalleryCreatedEvent 的简单事件,并将 Gallery ID 作为有效负载。 该事件将在 UploadController
Gallery创建成功后:
...
$this->em->persist($gallery);
$this->em->flush();
$this->eventDispatcher->dispatch(
GalleryCreatedEvent::class,
new GalleryCreatedEvent($gallery->getId())
);
$this->flashBag->add('success', 'Gallery created! Images are now being processed.');
...
此外,我们将使用“正在处理图像”来更新闪现消息。 所以用户知道在他们准备好之前我们还有一些工作要做。
我们将创建 GalleryEventSubscriber 事件订阅者,它将对 GalleryCreatedEvent
并为新创建的图库中的每个图像请求调整大小作业:
public function onGalleryCreated(GalleryCreatedEvent $event)
{
$queue = $this->jobQueueFactory
->createQueue()
->useTube(JobQueueFactory::QUEUE_IMAGE_RESIZE);
$gallery = $this->entityManager
->getRepository(Gallery::class)
->find($event->getGalleryId());
if (empty($gallery)) {
return;
}
/** @var Image $image */
foreach ($gallery->getImages() as $image) {
$queue->put($image->getId());
}
}
现在,当用户成功创建图库时,应用程序将呈现图库页面,但一些图像在尚未准备好时不会显示为缩略图:
一旦工作人员完成调整大小,下一次刷新应该呈现完整的图库页面。
将 resize worker 实现为控制台命令
worker 是一个简单的进程,为他从队列中获得的每项工作做同样的工作。 工人执行被阻止在 $queue->reserve()
调用直到为该工作人员保留工作,或者发生超时。
只有一个工人可以接受和处理一份工作。 该作业通常包含有效载荷——例如,字符串或序列化数组/对象。 在我们的例子中,它将是创建的图库的 UUID。
一个简单的工人看起来像这样:
// Construct a Pheanstalk queue and define which queue to watch.
$queue = $this->getContainer()
->get(JobQueueFactory::class)
->createQueue()
->watch(JobQueueFactory::QUEUE_IMAGE_RESIZE);
// Block execution of this code until job is added to the queue
// Optional argument is timeout in seconds
$job = $queue->reserve(60 * 5);
// On timeout
if (false === $job) {
$this->output->writeln('Timed out');
return;
}
try {
// Do the actual work here, but make sure you're catching exceptions
// and bury job so it doesn't get back to the queue
$this->resizeImage($job->getData());
// Deleting a job from the queue will mark it as processed
$queue->delete($job);
} catch (Exception $e) {
$queue->bury($job);
throw $e;
}
您可能已经注意到,工作人员将在定义的超时后或处理作业时退出。 我们可以将 worker 逻辑包装在一个无限循环中并让它无限期地重复其工作,但这可能会导致一些问题,例如长时间不活动后数据库连接超时,并使部署更加困难。 为了防止这种情况,我们的 worker 生命周期将在完成单个任务后结束。 Supervisor 将重新启动一个 worker 作为一个新进程。
查看 ResizeImageWorkerCommand 以清楚地了解 Worker 命令的结构。 以这种方式实现的 worker 也可以作为 Symfony 控制台命令手动启动: ./bin/console app:resize-image-worker
.
创建主管配置
我们希望我们的工作人员自动启动,所以我们将设置一个 autostart=true
配置中的指令。 由于 worker 在超时或成功处理任务后必须重新启动,我们还将设置一个 autorestart=true
指示。
后台处理最好的部分是易于并行处理。 我们可以设置一个 numprocs=5
directive 和 Supervisor 会生成我们 worker 的五个实例。 他们将等待作业并独立处理它们,使我们能够轻松扩展我们的系统。 随着系统的增长,您可能需要增加进程数。 由于我们将运行多个进程,因此我们需要定义进程名称的结构,因此我们设置了 process_name=%(program_name)s_%(process_num)02d
指示。
最后但并非最不重要的一点是,我们希望存储工作人员的输出,以便在出现问题时分析和调试它们。 我们将定义 stderr_logfile
和 stdout_logfile
路径。
我们的 resize worker 的完整 Supervisor 配置如下所示:
[program:resize-worker]
process_name=%(program_name)s_%(process_num)02d
command=php PATH-TO-YOUR-APP/bin/console app:resize-image-worker
autostart=true
autorestart=true
numprocs=5
stderr_logfile = PATH-TO-YOUR-APP/var/log/resize-worker-stderr.log
stdout_logfile = PATH-TO-YOUR-APP/var/log/resize-worker-stdout.log
创建(或更新)配置文件后位于 /etc/supervisor/conf.d/
目录,您必须通过执行以下命令告诉 Supervisor 重新读取和更新其配置:
supervisorctl reread
supervisorctl update
如果你正在使用 Homestead Improved(你应该是!),你可以使用 scripts/setup-supervisor.sh 为这个项目生成 Supervisor 配置: sudo ./scripts/setup-supervisor.sh
.
更新夹具
图像缩略图将不再在第一次请求时呈现,因此当我们在 LoadGalleriesData 夹具类中加载我们的夹具时,我们需要明确请求为每个图像呈现:
$imageResizer = $this->container->get(ImageResizer::class);
$fileManager = $this->container->get(FileManager::class);
...
$gallery->addImage($image);
$manager->persist($image);
$fullPath = $fileManager->getFilePath($image->getFilename());
if (false === empty($fullPath)) {
foreach ($imageResizer->getSupportedWidths() as $width) {
$imageResizer->getResizedPath($fullPath, $width, true);
}
}
现在你应该感觉到 fixtures 加载是如何变慢的,这就是为什么我们把它移到后台而不是强迫我们的用户等到它完成!
技巧和窍门
工作人员在后台运行,因此即使您部署了新版本的应用程序,您也会让过时的工作人员继续运行,直到他们没有第一次重新启动。
在我们的例子中,我们必须等待我们所有的工人完成他们的任务或超时(5 分钟),直到我们确定我们所有的工人……