Эта статья является частью серии по созданию примера приложения — блога галереи с несколькими изображениями — для оценки производительности и оптимизации. (Посмотреть репо здесь.)
В предыдущей статье мы добавили изменение размера изображения по требованию . Размер изображений изменяется по первому запросу и кэшируется для последующего использования. Сделав это, мы добавили некоторые накладные расходы к первой загрузке; система должна визуализировать миниатюры на лету и «блокирует» рендеринг страницы первого пользователя до тех пор, пока не будет завершено рендеринг изображения.
Оптимизированный подход заключается в визуализации миниатюр после создания галереи. Вы можете подумать: «Хорошо, но тогда мы заблокируем пользователя, который создает галерею?». Это не только плохо для пользователя, но и не является масштабируемым решением. Пользователь может запутаться из-за длительного времени загрузки или, что еще хуже, из-за превышения времени ожидания и / или ошибок, если изображения слишком тяжелы для обработки. Лучшее решение — перенести эти тяжелые задачи на задний план.
Фоновые задания
Фоновые задания — лучший способ выполнить тяжелую обработку. Мы можем немедленно уведомить нашего пользователя, что мы получили его запрос и запланировали его обработку. То же самое, что YouTube делает с загруженными видео: они не доступны после загрузки. Пользователь должен дождаться полной обработки видео, чтобы просмотреть или поделиться им.
Обработка или генерация файлов, отправка электронных писем или любые другие некритические задачи должны выполняться в фоновом режиме.
Как работает фоновая обработка?
Есть два ключевых компонента в подходе фоновой обработки: очередь заданий и рабочие. Приложение создает задания, которые должны обрабатываться, пока рабочие ждут и берут из очереди по одному заданию за раз.
Вы можете создать несколько рабочих экземпляров (процессов) для ускорения обработки, разбить большую работу на более мелкие куски и обрабатывать их одновременно. Вам решать, как вы хотите организовать фоновую обработку и управлять ею, но учтите, что параллельная обработка не является тривиальной задачей: вы должны заботиться о возможных условиях гонки и корректно обрабатывать невыполненные задачи.
Наш технический стек
Мы используем очередь заданий Beanstalkd для хранения заданий, компонент Symfony Console для реализации рабочих в качестве команд консоли, а Supervisor — для обслуживания рабочих процессов.
Если вы используете Homestead Improved , Beanstalkd и Supervisor уже установлены, так что вы можете пропустить инструкции по установке ниже.
Установка Beanstalkd
Бобовый стебель
быстрая очередь работ с универсальным интерфейсом, изначально разработанным для уменьшения задержки просмотра страниц в веб-приложениях большого объема за счет асинхронного выполнения трудоемких задач.
Есть много доступных клиентских библиотек, которые вы можете использовать. В нашем проекте мы используем Pheanstalk .
Чтобы установить Beanstalkd на ваш сервер Ubuntu или Debian, просто запустите 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 App\Service;
use Pheanstalk\Pheanstalk;
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. Мы определяем имя очереди как константу и ссылаемся на нее при помещении задания в очередь или при просмотре очереди у рабочих.
Установка Supervisor
Согласно официальной странице , Supervisor является
клиент-серверная система, позволяющая пользователям отслеживать и контролировать ряд процессов в UNIX-подобных операционных системах.
Мы будем использовать его для запуска, перезапуска, масштабирования и мониторинга рабочих процессов.
Установите Supervisor на свой сервер Ubuntu / Debian, запустив
sudo 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/
Простая конфигурация 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
Мы рассказываем Supervisor, как назвать порожденные процессы, путь к команде, которая должна быть запущена, для автоматического запуска и перезапуска процессов, сколько процессов мы хотим иметь и где регистрировать результаты. Узнайте больше о конфигурации супервизора здесь .
Изменение размера изображения в фоновом режиме
После того, как мы настроили нашу инфраструктуру (т.е. установили Beanstalkd и Supervisor), мы можем изменить наше приложение для изменения размера изображений в фоновом режиме после создания галереи. Для этого нам необходимо:
- обновить логику обслуживания изображений в
ImageController
- реализовать изменения размера рабочих как консольные команды
- создать конфигурацию супервизора для наших работников
- обновить приборы и изменить размеры изображений в классе приборов.
Обновление логики обслуживания изображений
До сих пор мы изменяли размеры изображений по первому запросу: если файл изображения для запрошенного размера не существует, он создается на лету.
Теперь мы изменим ImageController, чтобы он возвращал ответы изображения для запрошенного размера, только если файл изображения с измененным размером существует (то есть, только если изображение уже было изменено).
Если нет, приложение вернет общий ответ на изображение-заполнитель, сообщая, что в данный момент размер изображения изменяется. Обратите внимание, что ответ на изображение-заполнитель имеет разные заголовки элемента управления кэшем, поскольку мы не хотим кэшировать изображения-заполнители; мы хотим, чтобы изображение отображалось, как только процесс изменения размера будет завершен.
Мы создадим простое событие GalleryCreatedEvent с идентификатором галереи в качестве полезной нагрузки. Это событие будет отправлено в UploadController
...
$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());
}
}
Теперь, когда пользователь успешно создает галерею, приложение отобразит страницу галереи, но некоторые изображения не будут отображаться в виде миниатюр, пока они еще не готовы:
Как только рабочие закончат с изменением размера, следующее обновление должно отобразить всю страницу галереи.
Реализовать изменения размера рабочих как консольные команды
Рабочий — это простой процесс, выполняющий одну и ту же работу для каждой работы, которую он получает из очереди. Выполнение работника блокируется при вызове $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;
}
Возможно, вы заметили, что работники уйдут после определенного времени ожидания или после обработки задания. Мы могли бы обернуть рабочую логику в бесконечный цикл и заставить ее повторять свою работу бесконечно, но это может вызвать некоторые проблемы, такие как тайм-ауты соединения с базой данных после долгого простоя и усложнить развертывание. Чтобы предотвратить это, наш рабочий жизненный цикл будет завершен после выполнения одной задачи. Затем Supervisor перезапустит работника как новый процесс.
Взгляните на ResizeImageWorkerCommand, чтобы получить четкое представление о том, как структурирована команда Worker. Реализованный таким образом работник также может быть запущен вручную как консольная команда Symfony: ./bin/console app:resize-image-worker
Создать конфигурацию супервизора
Мы хотим, чтобы наши работники запускались автоматически, поэтому в конфигурации мы установим директиву autostart=true
Поскольку работник должен быть перезапущен после истечения времени ожидания или успешного выполнения задачи обработки, мы также установим директиву autorestart=true
Лучшая часть фоновой обработки — это простота параллельной обработки. Мы можем установить директиву numprocs=5
Они будут ждать работы и обрабатывать их самостоятельно, что позволяет нам легко масштабировать нашу систему. По мере роста вашей системы вам, вероятно, придется увеличивать количество процессов. Поскольку у нас будет запущено несколько процессов, нам нужно определить структуру имени процесса, поэтому мы устанавливаем директиву process_name=%(program_name)s_%(process_num)02d
И последнее, но не менее важное: мы хотим сохранить результаты работы рабочих, чтобы мы могли анализировать и отлаживать их, если что-то пойдет не так. Мы определим пути stderr_logfile
stdout_logfile
Полная конфигурация супервизора для наших сотрудников по изменению размера выглядит следующим образом:
[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/
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);
}
}
Теперь вы должны почувствовать, как замедляется загрузка приборов, и поэтому мы переместили его в фоновый режим, вместо того, чтобы заставлять наших пользователей ждать, пока это не будет сделано!
Советы и приемы
Рабочие работают в фоновом режиме, поэтому даже после развертывания новой версии приложения у вас будут работать устаревшие рабочие, пока они не будут перезапущены в первый раз.
В нашем случае нам придется подождать, пока все наши работники закончат свои задачи или тайм-аут (5 минут), пока мы не убедимся, что все наши работники обновлены. Помните об этом при создании процедур развертывания!