Перевод статьи подготовлен специально для студентов курса «Framework Laravel».




Привет, я Валерио, инженер-программист из Италии.

Это руководство предназначено для всех PHP разработчиков, уже имеющих онлайн-приложения с реальными пользователями, но которым недостает более глубокого понимания того, как внедрить (или значительно улучшить) масштабируемость в своей системе, используя очереди Laravel. Впервые я узнал о Laravel в конце 2013 года на старте 5-й версии фреймворка. Тогда я еще не был разработчиком, вовлеченным в серьезные проекты, и одним из аспектов современных фреймворков, особенно в Laravel, который казался мне самым непостижимым, были очереди (Queues).

Читая документацию, я догадывался о потенциале, но без реального опыта разработки это все оставалось лишь на уровне теорий в моей голове.
Сегодня же я являюсь создателем Inspector.devриалтайм дашборда, который выполняет тысячи задач каждый час, и разбираюсь в этой архитектуре намного лучше, чем раньше.



В этой статье я собираюсь рассказать вам, как я открыл для себя очереди (queues) и задачи (jobs), и какие конфигурации помогли мне обрабатывать большие объемы данных в режиме реального времени, сохраняя при этом экономию ресурсов сервера.

Введение


Когда PHP приложение получает входящий http-запрос, наш код выполняется последовательно шаг за шагом, пока выполнение запроса не закончится и клиенту (например, браузеру пользователя) не будет возвращен ответ. Это синхронное поведение действительно интуитивно, предсказуемо и просто для понимания. Я отправляю http-запрос на эндпоинт, приложение извлекает данные из базы данных, преобразует их в соответствующий формат, выполняет некоторые дополнительные задачи и отправляет их обратно. Все происходит линейно.

Задачи и очереди привносят асинхронное поведение, которое нарушает этот линейный поток. Вот почему эти функции сначала показались мне немного странными.
Но иногда входящий http-запрос может запускать цикл трудоемких задач, например, рассылку уведомлений по электронной почте всем членам команды. Это может означать отправку шести или десяти электронных писем, что может занять четыре или пять секунд. Поэтому каждый раз, когда пользователь нажимает на соответствующую кнопку, ему нужно подождать пять секунд, прежде чем он сможет продолжить использование приложения.

Чем больше приложение разрастается, тем хуже становится эта проблема.

Что такое задача?


Задача (Job) — это класс, который реализует метод «handle», содержащий логику, которую мы хотим выполнять асинхронно.

<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Bus\Queueable;


class CallExternalAPI implements ShouldQueue
{
    use Dispatchable,
        InteractsWithQueue,
        Queueable;
        
    /**
     * @var string
     */
    protected $url;

    /**
     * Создаем новый экземпляр задачи.
     *
     * @param array $Data
     */
    public function __construct($url)
    {
        $this->url = $url;
    }
    

    /**
     * Выполняем то, что нам нужно.
     *
     * @return void
     * @throws \Throwable
     */
    public function handle()
    {
        file_get_contents($this->url);
    }
}

Как упомянуто выше, основная причина заключения части кода в Job — выполнить трудоемкую задачу, не заставляя пользователя дожидаться ее выполнения.

Что мы имеем в виду под «трудоемкими задачами»?


Это закономерный вопрос. Отправка электронных писем является наиболее распространенным примером, используемым в статьях, в которых говорится об очередях, но я хочу рассказать вам о реальном опыте того, что мне нужно было сделать.

Как для владельца продукта, для меня очень важно синхронизировать информацию о поездках пользователей с нашими инструментами маркетинга и поддержки клиентов. Таким образом, основываясь на действиях пользователей, мы обновляем информацию о пользователях в различном внешнем ПО через API (или внешние http вызовы) в целях сервиса и маркетинга.
Один из наиболее часто используемых эндпоинтов в моем приложении может отправить 10 электронных писем и выполнить 3 http-вызова ко внешним службам до завершения. Ни один пользователь не будет ждать столько времени — скорее всего, они все попросту перестанут использовать мое приложение.

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

<?php

class ProjectController 
{
    public function store(Request $request)
    {
        $project = Project::create($request->all());
        
        // Откладываем NotifyMembers, TagUserActive, NotifyToProveSource 
        // и передаем информацию, необходимую для выполнения их работы
        Notification::queue(new NotifyMembers($project->owners));
        $this->dispatch(new TagUserAsActive($project->owners));
        $this->dispatch(new NotifyToProveSource($project->owners));
        
        return $project;
    }
}

Мне не нужно ждать завершения всех этих процессов, прежде чем возвращать ответ; напротив, ожидание будет равно времени, необходимому для публикации их в очереди. А это означает разницу между 10 секундами и 10 миллисекундами!!!

Кто выполняет эти задачи после отправки их в очередь?


Это классическая архитектура «publisher/consumer». Мы уже опубликовали наши задачи в очереди из контроллера, теперь же мы собираемся понять, как используется очередь, и, наконец, выполняются задачи.



Чтобы использовать очередь, нам нужно запустить одну из самых популярных artisan команд:

php artisan queue:work

Как сообщается в документации:
Laravel включает в себя воркера очереди, который обрабатывает новые задачи, когда они помещаются в очередь.

Замечательно! Laravel предоставляет готовый интерфейс для помещения задач в очередь и готовую к использованию команду для извлечения задач из очереди и выполнения их кода в фоновом режиме.

Роль Supervisor


Это была еще одна «странная вещь» в начале. Я думаю, это нормально — открывать для себя новые вещи. Пройдя этот этап обучения, я пишу эти статьи, чтобы помочь самому себе организовать свои навыки, и в то же время помочь другим разработчикам расширить свои знания.

Если задача терпит неудачу во время выбрасывания исключения, команда queue:work прекратит свою работу.

Для того чтобы процесс queue:work работал постоянно (потребляя ваши очереди), вы должны использовать монитор процессов, такой как Supervisor, чтобы гарантировать, что команда queue:work не прекращает работу, даже если задача вызывает исключение.Supervisor перезапускает команду после того, как она выходит из строя, начиная снова со следующей задачи, отставив вызвавшую исключение.

Задачи будут выполняться в фоновом режиме на вашем сервере, больше не имея зависимости от HTTP-запроса. Это вводит некоторые изменения, которые я должен был учитывать при реализации кода задачи.

Вот самые важные, на которые я обращу внимание:

Как я могу узнать, что код задачи не работает?


Работая в фоновом режиме, вы не можете сразу заметить, что ваша задача вызывает ошибки.
У вас больше не будет немедленной обратной связи, как, например, при выполнении http-запроса из вашего браузера. Если задача потерпит неудачу, она сделает это молча, и никто не заметит.

Подумайте об интеграции инструмента мониторинга в режиме реального времени, такого как Inspector, чтобы выводить на поверхность каждый недостаток.

У вас нет http-запроса


Http-запроса больше нет. Ваш код будет выполнен из cli.

Если вам нужны параметры запроса для выполнения ваших задач, вам нужно передать их в конструктор задачи для последующего использования во время выполнения:

<?php

// Пример класса задачи

class TagUserJob implements ShouldQueue
{
    public $data;
    
    public function __construct(array $data)
    {
        $this->data = $data;
    }
}

// Помещаем задачу в очередь из вашего контроллера

$this->dispatch(new TagUserJob($request->all()));

Вы не знаете, кто вошел в систему


Сессии больше нет. Таким же образом вы не будете знать личность пользователя, который вошел в систему, поэтому, если вам нужна информация о пользователе для выполнения задачи, вам нужно передать объект пользователя конструктору задачи:

<?php

// Пример класса задачи
class TagUserJob implements ShouldQueue
{
    public $user;
    
    public function __construct(User $user)
    {
        $this->user= $user;
    }
}

// Помещаем задачу в очередь из вашего контроллера
$this->dispatch(new TagUserJob($request->user()));

Понять, как масштабировать


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

Очереди представляют собой буферы FIFO («первым вошел — первым вышел»). Если вы запланировали много задач, возможно даже разных типов, им нужно ждать, пока другие справятся со своими задания, прежде чем завершиться.

Есть два способа масштабирования:

Несколько потребителей на очередь



Таким образом, пять задач будут извлечены из очереди за раз, ускоряя обработку очереди.

Очереди специального назначения

Вы также можете создать определенные очереди для каждого запускаемого типа задач с выделенным потребителем для каждой очереди.



Таким образом, каждая очередь будет обрабатываться независимо, не дожидаясь выполнения задач других типов.

Horizon


Laravel Horizon — это менеджер очередей, который дает вам полный контроль над тем, сколько очередей вы хотите настроить, и возможность организации потребителей, позволяя разработчикам объединить эти две стратегии и реализовать ту, которая соответствует вашим потребностям в масштабируемости.

Запуск происходит посредством php artisan horizon вместо php artisan queue:work. Эта команда сканирует ваш файл конфигурации horizon.php и запускает ряд воркеров очереди в зависимости от конфигурации:

<?php

'production' => [
    'supervisor-1' => [
        'connection' => "redis",
        'queue' => ['adveritisement', 'logs', 'phones'],
        'processes' => 9,
        'tries' => 3,
        'balance' => 'simple', // может быть simple, auto или null

    ]
]

В приведенном выше примере Horizon запустит три очереди с тремя процессами, назначенными для обработки каждой очереди. Как упоминалось в документации Laravel, кодо-ориентированный подход Horizon, позволяет моей конфигурации оставаться в системе контроля версий, где моя команда может сотрудничать. Это идеальное решение, использующее CI инструмент.

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

Моя собственная конфигурация



<?php

'production' => [
    'supervisor-1' => [
        'connection' => 'redis',
        'queue' => ['default', 'ingest', 'notifications'],
        'balance' => 'auto',
        'processes' => 15,
        'tries' => 3,
    ],
]

Inspector использует в основном три очереди:

  • ingest для процессов для анализа данных из внешних приложений;
  • notifications используются для немедленного планирования уведомлений, если во время приема данных обнаружена ошибка;
  • default используется для других задач, которые я не хочу смешивать с ingest и notifications процессами.

Благодаря использованию balance=auto Horizon понимает, что максимальное количество активируемых процессов равно 15, которые будут распределяться динамически в зависимости от загрузки очередей.

Если очереди пусты, Horizon поддерживает по одному активному процессу для каждой очереди, что позволяет потребителю сразу же обрабатывать очередь, если запланирована задача.

Заключительные замечания


Параллельное фоновое выполнение может вызвать много других непредсказуемых ошибок, таких как MySQL «Превышено время ожидания блокировки» и многие другие проблемы дизайна. Подробнее читайте здесь.

Я надеюсь, что эта статья помогла вам использовать очереди и задачи с большей уверенностью. Если вы хотите узнать больше о нас, посетите наш веб-сайт по адресу www.inspector.dev

Ранее опубликовано здесь www.inspector.dev/what-worked-for-me-using-laravel-queues-from-the-basics-to-horizon



Успеть на курс и получить скидку.