Официальная документация Laravel достаточно подробно описывает установку веб-приложения и сопутствующих процессов-работников, но что если я хочу развернуть продукт в среде AWS Elastic Beanstalk?


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


Эта статья не только покажет как можно легко и просто запустить планировщик и обработчик очередей в AWS, но также в очередной раз докажет, что Laravel очень легко расширяется.


Что такое Elastic Beanstalk?


Для тех, кто не знаком с сервисом EB от AWS, попробую объяснить в двух предложениях. Elastic Beanstalk – это готовая связка сервисов (виртуальные сервера, балансировщики нагрузки, мониторинг) для автоматического масштабирования приложений. Благодаря EB, в команде не обязательно иметь DevOps, и приложение будет само адаптироваться под любую нагрузку.


Elastic Beanstalk: особенности


Amazon предлагает отдельный, специальный вид окружения для приложений-работников – окружение 'worker'. И несмотря на то, что AWS позволяет запускать и запланированные задачи, и задачи из очередей, процесс отличается от стандартного:


Обработка очередей в Laravel

В стандартном процессе, Laravel вставляет задачи в очередь, а другая копия этого же приложения опрашивает очередь периодически, надеясь получить задачу. Запланированные задачи обрабатываются внутренним планировщиком Laravel, который в свою очередь запускается каждую минуту через стандартный UNIX cron tab.


А вот в среде AWS EB, мы уже не сможем устанавливать свои cron файлы или работать с очередью напрямую:


Обработка очередей в AWS EB

Вместо этого, внутренний процесс AWS будет слать нам POST запросы, оповещая наши копии приложений о запланированных задачах, готовых к выполнению, или о новых задачах в очереди. Звучит достаточно просто, но Laravel (текущая версия – 5.2) не поддерживает ни то, ни другое – планировщик запускается только из консоли, а обработчик очередей хочет доступа в очередь напрямую.


Реализация


Планировщик


Начнем с планировщика. Мы хотим, чтобы происходило тоже самое, что происходит при запуска в консоли php artisan schedule:run, но из web-запроса (web-хука). Создавать отдельные хуки (некоторые разработчики выбирают этот путь) не хочется, так как:


  • Хочется полагаться на встроенный планировщик Laravel – синтаксис удобнее для чтения, разрабтчикам не требуются знания UNIX, бизнес-логика остается в приложении, а не за его пределами;
  • Другие среды, в которых приложение работает (локальная, development) могут быть не в AWS, и мы не хотим иметь два разных способа работы для AWS и не-AWS вариантов;
  • Не хочется создавать кучу методов-хуков, которые будут использоваться лишь AWS.

Так выглядит финальная версия метода контроллера, который запускает планировщик. Метод очень похож на встроенный в Laravel ScheduleRunCommand::class:


/**
 * @param Container $laravel
 * @param Kernel $kernel
 * @param Schedule $schedule
 * @return array
 */
public function schedule(Container $laravel, Kernel $kernel, Schedule $schedule)
{
    $events = $schedule->dueEvents($laravel);
    $eventsRan = 0;
    $messages = [];
    foreach ($events as $event) {
        if (! $event->filtersPass($laravel)) {
            continue;
        }
        $messages[] = 'Running: '.$event->getSummaryForDisplay();
        $event->run($laravel);
        ++$eventsRan;
    }
    if (count($events) === 0 || $eventsRan === 0) {
        $messages[] = 'No scheduled commands are ready to run.';
    }
    return $this->response($messages);
}

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


public function schedule(Container $laravel, Kernel $kernel, Schedule $schedule)

Web-приложение Laravel использует свой класс ядра, который не загружает список запланированных задач, но мы попросили предоставить нам консольное ядро (Illuminate\Contracts\Console\Kernel) – Laravel'у придется его загрузить для нас. В процессе загрузки произойдет 'побочный' эффект – будут загружены запланированные задачи из App/Console, наконец-то приложение о них узнает. Когда Laravel будет предоставлять следующую зависимость, класс Schedule – у приложения уже будут задачи.


Важная деталь: поменяйте местами Kernel и Schedule в списке параметров и метод перестанет работать. Ядро надо загрузить перед Schedule, потому что нам нужен побочный эффект от его загрузки.


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


Очереди


Одной из целей было свести количество новых классов к минимуму, так что я не стал вводить свои очереди или соединения – удалось обойтись лишь одним job-классом, который будет передан стандартному обработчику очередей.


Метод получился таким:


/**
 * @param Request $request
 * @param Worker $worker
 * @param Container $laravel
 * @return array
 */
public function queue(Request $request, Worker $worker, Container $laravel)
{
    $this->validateHeaders($request);
    $body = $this->validateBody($request, $laravel);
    $job = new AwsJob($laravel, $request->header('X-Aws-Sqsd-Queue'), [
        'Body' => $body,
        'MessageId' => $request->header('X-Aws-Sqsd-Msgid'),
        'ReceiptHandle' => false,
        'Attributes' => [
            'ApproximateReceiveCount' => $request->header('X-Aws-Sqsd-Receive-Count')
        ]
    ]);
    try {
        $worker->process(
            $request->header('X-Aws-Sqsd-Queue'), $job, 0, 0
        );
    } catch (\Exception $e) {
        return $this->response([
            'Couldn\'t process ' . $job->getJobId()
        ], 500);
    }
    return $this->response([
        'Processed ' . $job->getJobId()
    ]);
}

Все, что было нужно сделать – это вытащить метаданные SQS из HTTP заголовков, и вставить их в job-класс. Получился эдакий адаптер с HTTP на SQS. Нам не надо самим удалять работу из очереди или помечать ее как неудачную, все сделает сам AWS. Если мы не вернем HTTP код 200 (к примеру, мы поймали ошибку), то AWS сам сделает все последующее.


Вот и всё! Осталось добавить пару маршрутов (всего два маршрута на любое количество задач) и приложение готово к бою!


Настройка AWS


Не забудьте подписать worker-окружение AWS на соответствующую очередь SQS (или топик SNS).


Чтобы AWS начал "приставать" ежеминутно, в момент предоставления новой версии приложения в корне должен быть файл cron.yaml. Можно добавить его в репозиторий, а можно добавлять на последнем шаге. Содержимое файла:


version: 1
cron:
 - name: "schedule"
   url: "/worker/schedule"
   schedule: "* * * * *"

Выводы


Laravel в очередной раз доказал свои гибкость и расширяемость.


Полный исходный код, рабочий пакет с интеграцией для Laravel и Lumen уже залил на GitHub (и Packagist): https://github.com/dusterio/laravel-aws-worker

Поделиться с друзьями
-->

Комментарии (16)


  1. splatt
    04.06.2016 17:34
    +1

    А вот в среде AWS EB, мы уже не сможем устанавливать свои cron файлы или работать с очередью напрямую:

    Поясните человеку, который поверхностно знаком с AWS EB и Laravel, а что все-таки мешает установить свой cron-файл в среде AWS EB? Ведь насколько я понимаю, в AWS EB с помощью файлов конфигурации (.ebextensions/something.config) вы можете выполнять любые команды внутри инстанса EC2, которые будут выполняться при развертке, в том числе добавить задачу в cron, нет?


    1. mitja
      04.06.2016 19:17

      Добавить задачу в cron можно, но в этом случае эта задача добавится в cron на всех EC2 инстансах. Соответственно, задача запустится в означенное время на всех инстансах, что обычно нежелательно.


      1. dusterio
        05.06.2016 03:49

        Да, mitja ответил за меня :) Такой способ не рекомендуется в документации AWS. Проблемы тоже никак обрабатываться не будут.


      1. random1st
        05.06.2016 08:57

        Это неправда. В EB можно сконфигурировать cron только на leader instance в группе autoscaling. Также environment в EB -это обычный Amazon EC2 instance c Amazon Linux на борту, а в конфигах EB (.ebextensions) можно указать выполнение любых команд и скриптов на этом инстансе. Вот как описано здесь, например Worker-environment представляет собой инстанс с встроенным сервисом sqsd, который забирает таски SQS и отправляет на заданный url. Однако я это вариант обычно не использую, потому что этот environment нельзя одновременно использовать для развертывания веб-сервера.

        В Elastic Beanstalk нельзя (точнее, от Вас этого не ожидается) запускать дополнительные демоны.

        Вот этой фразы вообще не понимаю. Я писал конфиг для монтирования сетевой файловой системы в EB и настраивал autoscaling. Вы полагаете, что я не должен был этого делать, потому что от меня это не ожидается?


        1. dusterio
          05.06.2016 09:13

          А Вы обратите внимание на постскриптум в Вашей же ссылке:

          Update: There is an issue with this solution when Elastic Beanstalk scales up your instances. For example, lets say you have one instance with the cron job running. You get an increase in traffic so Elastic Beanstalk scales you up to two instances. The leader_only will ensure you only have one cron job running between the two instances. Your traffic decreases and Elastic Beanstalk scales you down to one instance. But instead of terminating the second instance, Elastic Beanstalk terminates the first instance that was the leader. You now don't have any cron jobs running since they were only running on the first instance that was terminated. See the comments below.)

          Если этого недостаточно, напомню еще про несимметричную нагрузку в таком сценарии – один instance будет всегда иметь больше нагрузки, чем другие. Кроме того, неработающий скрипт, я предполагаю, не будет показан в health-статусе приложения, а вот если перестанет работать веб-хук, то будет HTTP ошибка и разработчик заметит это по статусу.


          1. random1st
            05.06.2016 12:04

            Да, такая ошибка будет возникать при переменном количестве инстансов в группе. Однако выше утверждали, что крон-задача обязательно добавляется во все инстансы — а это не так. Опять же, любое архитектурное решение зависит от конкретных обстоятельств. Что касается несимметричной нагрузки на инстансы — ничего не мешает использовать тот же SQS для распределения нагрузки между инстансами. Я в принципе предпочитаю использовать использовать AWS Lambda для тяжелых, но непродолжительных задач — с некоторыми ограничениями выходит получше, чем держать отдельный инстанс для воркера, однако, насколько я понимаю, для php подобный вариант не очень подходит из-за отсутствия поддержки со стороны Амазона.


  1. mnv
    05.06.2016 00:59

    Мы хотим, чтобы происходило тоже самое, что происходит при запуска в консоли php artisan schedule:run, но из web-запроса (web-хука).

    Если время обработки одного элемента очереди — несколько минут, как поведет себя такая реализация?


    1. dusterio
      05.06.2016 03:50

      Это нормально решается с помощью настроек – в настройках очереди надо повысить время обработки письма, а в настройках nginx/FPM – время работы


  1. saggid
    05.06.2016 01:57
    +1

    Эээ. Ну вообще-то, судя по документации, Laravel вполне дружит с целой кучей всяких разных видов систем обработки очередей, в том числе и Amazon SQS тоже. И по-нормальному, у Laravel есть свой демон, который запускается и эти очереди обрабатывает.


    В общем, что-то я не понял тему статьи.


    1. dusterio
      05.06.2016 03:52

      Laravel нормально дружит если Вы его запустите, скажем, в EC2. Проблемы начинаются, когда среда приложения – Elastic Beanstalk. В таком случае, Laravel вообще никак не готов, и уже много месяцев на Laracasts пользователи ищут решение :)

      В Elastic Beanstalk нельзя (точнее, от Вас этого не ожидается) запускать дополнительные демоны.


  1. JhaoDa
    06.06.2016 05:46

    В стандартном процессе, Laravel вставляет задачи в очередь, а другая копия этого же приложения опрашивает очередь периодически, надеясь получить задачу. Запланированные задачи обрабатываются внутренним планировщиком Laravel, который в свою очередь запускается каждую минуту через стандартный UNIX cron tab.
    Под «стандартным процессом» вы подразумеваете, что слушатель очередей работает именно так везде, кроме ЕВ? Если да, то это вы что-то себе костыльное придумали, так слушателя на шареде запускать можно, когда ни ssh нету, ни супервизора не настроить, ни процесс отдельный запустить.


    1. dusterio
      06.06.2016 11:04

      Под стандартным я имею ввиду рекомендованный в официальной документации способ – cron-запись для планировщика, supervisor для обработки очередей.

      В среде EB такой стандартный способ не работает


  1. Big_Shark
    06.06.2016 14:09

    Очереди в Laravel работают без cron'а, там бесконечный цикл который опрашивается BD, Redis, Beanstalkd, Amazon SQS или IronMQ на наличие задач. А вот schedule как раз служит для управление крон задачами из приложения, без необходимости лазить в crontab на сервере.
    Вы тут спутали 2 понятия, и запутали себя и читателей.


    1. dusterio
      06.06.2016 14:15

      Нет, ничего не перепутал :) Просто эти две функции, как правило, вынесены на worker instances. Поэтому я показал как обе функции развернуть в Elastic Beanstalk.

      php artisan queue:listen действительно запускает бесконечный цикл, но как Вы запустите его из worker, без доступа к консоли, supervisor или cron tab?

      AWS не хочет, чтобы разработчики на worker работали «напрямую» с очередями.


      1. random1st
        06.06.2016 18:51

        Скорее AWS не дает удобного механизма работы с очередями из Elastic Beanstalk. Ничего абсолютно не мешает прописать эту же команду в .ebextensions. Там же можно любой скрипт запустить. Преимущество ElasticBeanstalk — удобный деплой, конфигурирование и автомасштабирование, никто никого ни к чему не вынуждает. Я, хоть с PHP давно дела не имею вот нашел сходу https://github.com/jasonagnew/laravel-elastic-beanstalk-sqs-supervisord


        1. dusterio
          07.06.2016 00:59

          Все равно будет один минус упомянутый выше – команда будет работать на всех instances.

          Еще, совсем забыл упомянуть в статье, я использую Docker-вариант Elastic Beanstalk, потому что 1) у Amazon до сих пор нет PHP 7 2) Docker – это удобно.

          И тут, насколько я понимаю, .ebextensions сразу отпадает – это команды, которые будут в host OS запускаться, а не в контейнере. Можно, конечно, в Dockerfile установить supervisor, но будет проблема из первого предложения :)

          При работе с планировщиком через очереди, как предложено в статье, неудачные задачи будут корректно в Dead Messages очередь перемещены, и не важно сколько в EB инстансов, 1 или 500, каждая задача будет выполнена один раз.