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

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

Выполнение фоновых задач

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

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

Например, представим, что в вашем веб-приложении есть страница контактов. На ней есть текстовый ввод для электронной почты, еще один для темы и текстовая область для содержания.

Когда форма отправлена, она должна:

  1. отправить письмо на почтовый ящик вашей компании
  2. отправить подтверждение по электронной почте веб-пользователю.

Давайте также представим, что вы используете GMail SMTP для отправки электронных писем. В день с низким трафиком это может быть достаточно быстро, чтобы пользователь получил подтверждение.

Но если в минуту отправляются тысячи контактов, это может привести к сбою вашего сервера из-за высокой загрузки процессора. GMail SMTP также может ограничивать скорость, что означает, что в какой-то момент ваши письма не будут отправлены.

Это также может ухудшить работу других пользователей, переходящих на другие страницы, поскольку сервер будет занят обслуживанием веб-страниц и отправкой писем.

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

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

Команды

Команды в Laravel действуют как задание cron. Фактически, Laravel требует установки «главной» команды (называемой планировщиком), чтобы регулярно проверять наличие команд для запуска.

Вы можете контролировать, как часто вы хотите, чтобы ваша команда выполнялась:

// app/Console/Kernel.php

namespace AppConsole;

use IlluminateConsoleSchedulingSchedule;
use IlluminateFoundationConsoleKernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
  protected function schedule(Schedule $schedule): void
  {
    $schedule
      ->command('contact-email:send')
      ->everyTenMinutes();
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Эта команда возьмет все письма контактов, которые еще не были отправлены, и отправит их.

// app/Console/Commands/ContactEmailSend.php

namespace AppConsoleCommands;

use AppModelsContactEmail;
use AppMailContactConfirmation;
use AppMailContact;
use IlluminateConsoleCommand;

class ContactEmailSend extends Command
{
  protected $signature = 'contact-email:send';

  protected $description = 'Send contact email and confirmation emails.';

  public function handle()
  {
    $contactEmails = ContactEmail::where("status", "pending")->get();

    foreach ($contactEmails as $contactEmail) {
      $recipient = $contactEmail->email;
      $subject = $contactEmail->subject;
      $content = $contactEmail->content;

      // For the web user
      Mail::to($recipient)->send(new ContactConfirmation($subject, $content));

      // For the company
      Mail::to("contact@your-company.com")->send(new Contact($recipient, $subject, $content));

      $contactEmail->status = "sent";
      $contactEmail->save();
    }
  }
}
Ввести полноэкранный режим Выйти из полноэкранного режима

Иногда вы можете оказаться в ситуации, когда отправка писем занимает много времени (например, когда нужно отправить тонну писем). На данный момент наши команды должны выполняться в течение 10 минут, иначе начнет выполняться другое задание cron.

Это нежелательное поведение, поскольку если первое задание не успело отправить письмо, второе задание может взять тот же контактный email и начать пытаться отправить его, что приведет к дублированию писем с подтверждением, которые получат ваши веб-пользователи и ваша компания.

Чтобы предотвратить такое поведение, либо увеличьте частоту расписания, либо укажите, что вы хотите, чтобы любое ожидающее задание cron завершилось до запуска другого.

// app/Console/Kernel.php

namespace AppConsole;

use IlluminateConsoleSchedulingSchedule;
use IlluminateFoundationConsoleKernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
  protected function schedule(Schedule $schedule): void
  {
    $schedule
      ->command('contact-email:send')
      ->everyTenMinutes()
      ->withoutOverlapping(); // <--
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Настроить планировщик Laravel очень просто, поскольку каждый сервер поставляется с бегунком cronjob (настраивается с помощью команды crontab).

Задания

Когда команды не привязаны к определенному времени отправки (что означает «выполните эти задачи как можно скорее»), мы можем рассматривать задания как стопку задач, которые нужно выполнить. Сервер будет выполнять их одно за другим, когда у него появится время и ресурсы.

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

С другой стороны, для разгруппировки заданий требуется программа выполнения заданий (Supervisor является наиболее популярной при использовании Laravel — здесь можно найти документацию, чтобы узнать, как ее настроить).

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

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

Если мы оставим тот же пример, вот краткое описание того, как запустить задание:

// app/Http/Controllers/ContactController.php

namespace AppHttpControllers;

use AppJobsSendContactEmail;
use AppJobsSendContactConfirmation;
use AppHttpRequestsStoreContactRequest;

class ContactController extends Controller
{
  public function store(StoreContactRequest $request)
  {
    $email = $request->email;
    $subject = $request->subject;
    $content = $request->content;

    // For the company
    dispatch(new SendContactEmail($email, $subject, $content));

    // For the web user
    dispatch(new SendContactConfirmation($email, $subject, $content));

    return redirect()->route("contact.success");
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

А вот как отправить электронное письмо веб-пользователю:

// app/Jobs/SendContactConfirmation.php

namespace AppJobs;

use AppMailContactConfirmation;
use IlluminateBusQueueable;
use IlluminateContractsQueueShouldQueue;
use IlluminateFoundationBusDispatchable;
use IlluminateQueueInteractsWithQueue;
use IlluminateQueueSerializesModels;
use IlluminateSupportFacadesMail;

class SendContactConfirmation implements ShouldQueue
{
  use Dispatchable;
  use InteractsWithQueue;
  use Queueable;
  use SerializesModels;

  private string $email;
  private string $subject;
  private string $content;

  public function __construct(string $email, string $subject, string $content)
  {
    $this->email = $email;
    $this->subject = $subject;
    $this->content = $content;
  }

  public function handle()
  {
    Mail::to($this->email)->send(new ContactConfirmation($email, $subject, $content));
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Хорошо то, что если отправка письма не удалась, вам не нужно обрабатывать это в контроллере, так как это делается на задании. Кроме того, в этом случае задание будет помещено в таблицу «failed_jobs», и вы сможете решить повторить его позже вручную.

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

// app/Jobs/SendContactConfirmation.php

namespace AppJobs;

use IlluminateContractsQueueShouldQueue;

class SendContactConfirmation implements ShouldQueue
{
  // ...

  public $tries = 2; // retry another time before puting the job in "failed_jobs", for a total of 2 tries

  // ...
}
Войти в полноэкранный режим Выход из полноэкранного режима

Вы можете делать расширенные проверки целостности, например, убедиться, что задание уникально в стеке, указав уникальный id, или убедиться, что два задания не будут пересекаться, используя промежуточное программное обеспечение WithoutOverlapping job, и т.д…

Другим преимуществом возможности повторного выполнения задания вручную или автоматически является просмотр состояния заданий.

Используя Laravel Telescope в сочетании с переменной окружения QUEUE_CONNECTION=database (называемой «QUEUE_DRIVER» в старых версиях Laravel), вы сможете точно знать, сколько заданий ожидает своей очереди и сколько из них не удалось выполнить. Это облегчает понимание того, нужно ли провести проверку, чтобы узнать, что не так, или просто следить за процессом.

Takeway

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

Мне по-прежнему нравится использовать команды, но теперь я использую их для реальных задач планирования, таких как обновление списка одноразовых писем (https://github.com/Propaganistas/Laravel-Disposable-Email).

В конце концов, наша цель — снизить нагрузку на HTTP-запросы и дефферировать то, что можно дефферировать в фоновом режиме. Вот почему я думаю, что оптимальной установкой для сервера будет:

  • парк серверов, предназначенных для обслуживания HTTP-ответов
  • парк серверов, предназначенных для распаковки заданий
  • сервер для запуска запланированных задач cron

Поскольку запланированные задания подчиняются времени, вы не можете рисковать, имея 4 сервера, выполняющих одно и то же задание cron (рискованно).

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

Некоторые гиганты отрасли заметили те же проблемы масштаба с запланированными заданиями, например, Fathom Analytics

Раньше у нас было отставание, поскольку мы использовали INSERT ON DUPLICATE KEY UPDATE для наших сводных таблиц. Это означало, что мы не могли просто запускать запросы, как только поступали данные, так как вы попадали в войну DEADLOCK, а это вызывает множество проблем (поверьте мне, у меня они были). А проблема у нас была в том, что мы должны были объединить сайты в группы, чтобы запускать несколько заданий cron бок о бок, агрегируя данные в изолированных (по группам) процессах. Но знаете что? Задания Cron не масштабируются, и с каждым днем мы начинали видеть все большее отставание в просмотре страниц. Теперь, когда мы работаем в SingleStore, данные поступают полностью в режиме реального времени. Так что если вы просматриваете страницу на своем сайте, она появится на вашей приборной панели Fathom без задержек.

Продолжайте читать статью в блоге Fathom Analytics о создании самой быстрой в мире аналитики веб-сайтов.

Заключение

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

Как всегда, спасибо, что дочитали до конца, и удачного планирования задач!

Фото Glenn Carstens-Peters on Unsplash

Оцените статью
Procodings.ru
Добавить комментарий