Загрузка...

Очереди Laravel на shared-хостинге

Очереди Laravel на shared-хостинге: cron, лимиты времени и памяти

На shared-хостинге обычно нельзя установить Supervisor или systemd для постоянно работающего worker-а. Это не мешает использовать очереди Laravel: jobs хранятся в базе данных, а короткий worker запускается cron-ом каждую минуту.

Подход подходит для Beget и других shared-хостингов, не требует Redis и хорошо работает для писем, уведомлений, Telegram-рассылок, импорта данных и других фоновых задач.

Главный принцип: HTTP-запрос только ставит работу в очередь. Обработку выполняет отдельный PHP-процесс через cron.


Что потребуется

  • Laravel с доступом к php artisan;
  • cron в панели хостинга;
  • MySQL или PostgreSQL;
  • PHP CLI той же версии, что и у сайта;
  • права на выполнение миграций и запись логов.

На Beget путь к PHP CLI обычно отображается при создании cron-задачи. Не подставляйте путь наугад: используйте путь, который показывает панель хостинга.


1. Включите database queue

В production .env задайте:

QUEUE_CONNECTION=database
CACHE_STORE=database
DB_QUEUE_RETRY_AFTER=300

Затем очистите cache конфигурации:

php artisan optimize:clear

Почему не sync

При такой конфигурации:

QUEUE_CONNECTION=sync

вызов dispatch() запускает job сразу внутри текущего HTTP/AJAX-запроса. Это плохо для долгих действий: пользователь ждёт ответ, PHP может получить timeout, а повтор запроса способен повторить работу.

database сохраняет job в таблицу jobs. Она будет ждать worker-а и не зависит от времени жизни web-запроса.


2. Создайте таблицы очередей

Если таблицы ещё не созданы, выполните:

php artisan make:queue-table
php artisan make:queue-failed-table
php artisan migrate --force

Laravel создаст:

  • jobs — ожидающие и зарезервированные jobs;
  • failed_jobs — jobs, исчерпавшие допустимое число попыток.

В уже работающем проекте обычно достаточно применить штатные миграции:

php artisan migrate --force

3. Добавьте cron для Laravel Scheduler

Scheduler выполняет расписание из routes/console.php: например, проверяет наступившие события, создаёт jobs, запускает регулярную очистку и уведомления.

Создайте cron task раз в минуту:

* * * * * flock -n /tmp/laravel-schedule.lock sh -c 'cd /path/to/project && /path/to/php artisan schedule:run' >> /path/to/logs/laravel-schedule.log 2>&1 || true

Замените значения:

  • /path/to/project — абсолютный путь к корню Laravel-проекта, где находится artisan;
  • /path/to/php — абсолютный путь к PHP CLI;
  • /path/to/logs — каталог, доступный для записи. Можно использовать storage/logs проекта.

flock создаёт lock-файл и не допускает одновременного запуска двух schedule:run. Это важно, если одна scheduler-задача работает дольше минуты.

Scheduler не исполняет queue jobs автоматически. Он может только поставить их в jobs. Для обработки нужен отдельный queue worker.


4. Добавьте cron для worker-а очереди

Worker берёт jobs из jobs и вызывает их handle().

Для основной очереди default добавьте второй cron task, также раз в минуту:

* * * * * flock -n /tmp/laravel-default-queue.lock sh -c 'cd /path/to/project && /path/to/php artisan queue:work database --queue=default --stop-when-empty --max-time=50 --max-jobs=100 --memory=128 --sleep=3 --tries=3 --timeout=60' >> /path/to/logs/laravel-default-queue.log 2>&1 || true

Это не постоянный daemon. Cron запускает короткий worker, тот обрабатывает доступные jobs и завершается. При следующем запуске появляется новый PHP-процесс с чистой памятью.

Значение параметров

Параметр Назначение Рекомендация для cron worker-а
database Подключение очереди Должно совпадать с QUEUE_CONNECTION=database
--queue=default Очередь для обработки Укажите одну или несколько нужных очередей
--stop-when-empty Завершение при пустой очереди Обязательно для cron-сценария
--max-time=50 Максимальное время жизни worker-а Меньше минуты, чтобы cron-процессы не пересекались
--max-jobs=100 Максимум jobs за один запуск Перезапускает процесс при постоянно растущей очереди
--memory=128 Лимит памяти процесса в МБ Подберите ниже PHP memory_limit
--sleep=3 Ожидание после пустой очереди 1–5 секунд обычно достаточно
--tries=3 Число попыток job Подходит для большинства обычных jobs
--timeout=60 Максимум времени одной job Должен быть меньше retry_after

DB_QUEUE_RETRY_AFTER=300 больше --timeout=60, поэтому Laravel не сочтёт ещё выполняемую job брошенной раньше времени.


5. Несколько очередей и приоритеты

У Laravel есть именованные очереди. Их полезно использовать, когда тяжёлые jobs не должны задерживать критичные.

Например:

SendReportJob::dispatch($report)->onQueue('reports');
SendNotificationJob::dispatch($notification)->onQueue('notifications');

Для каждой важной очереди можно завести самостоятельный cron worker.

Пример: критичные уведомления

* * * * * flock -n /tmp/laravel-notifications.lock sh -c 'cd /path/to/project && /path/to/php artisan queue:work database --queue=notifications --stop-when-empty --max-time=50 --max-jobs=100 --memory=128 --sleep=3 --tries=3 --timeout=60' >> /path/to/logs/laravel-notifications.log 2>&1 || true

Пример: тяжёлый импорт

* * * * * flock -n /tmp/laravel-imports.lock sh -c 'cd /path/to/project && /path/to/php artisan queue:work database --queue=imports --stop-when-empty --max-time=50 --max-jobs=10 --memory=192 --sleep=3 --tries=2 --timeout=120' >> /path/to/logs/laravel-imports.log 2>&1 || true

Отдельный worker для notifications или imports не обрабатывает jobs из default. Если используются разные очереди, worker нужен для каждой из них — либо укажите несколько очередей в порядке приоритета:

php artisan queue:work database --queue=notifications,default

На shared-хостинге отдельные короткие worker-ы обычно понятнее и безопаснее: одна проблемная очередь не блокирует другую.


6. Время и память: что worker защищает, а что нет

--max-time, --max-jobs и --memory нужны не для ограничения одной job, а для регулярного перезапуска всего процесса worker-а.

Это защищает от медленного накопления памяти библиотеками, ORM, обработкой изображений или долгой серией jobs:

cron → новый worker → обработка jobs → достигнут лимит → worker завершён
     → следующий cron запускает чистый PHP-процесс

Но это не спасает, если одна job сразу превышает доступную память или работает дольше допустимого timeout. Такие jobs нужно проектировать порционно.

Плохо:

$users = User::query()->get();

foreach ($users as $user) {
    // Обработка всех пользователей в одной job
}

Лучше:

User::query()
    ->select(['id', 'email'])
    ->lazyById(500)
    ->each(function (User $user): void {
        // Обработка одного пользователя
    });

Ещё надёжнее — создавать отдельную job для небольшой единицы работы: одного письма, одного пользователя, одного файла или одной страницы импорта.


7. Retry и необратимые действия

Для обычной job можно задать попытки и задержку между ними:

final class FetchRemoteDataJob implements ShouldQueue
{
    public int $tries = 3;

    public function backoff(): array
    {
        return [30, 120];
    }
}

После исчерпания попыток Laravel перенесёт job в failed_jobs. Посмотреть список можно командой:

php artisan queue:failed

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

Рекомендация:

  • хранить состояние операции в БД;
  • делать job идемпотентной;
  • использовать отдельную запись доставки на одного получателя;
  • перед внешним вызовом атомарно захватывать запись;
  • выбирать tries=1, если повтор без идемпотентного ключа опаснее, чем ручная повторная отправка.

Сравнение с WordPress и wp-queue

Пакет wp-queue добавляет Laravel-подобную очередь поверх WordPress. В частности, его worker умеет ограничивать число jobs, время работы и потребление памяти, а при повторных попытках применяет exponential backoff.

У wp-queue worker берёт лимит памяти из WP_MAX_MEMORY_LIMIT, затем из WP_MEMORY_LIMIT (по умолчанию — 256 МБ). После завершения каждой job он измеряет потребление памяти и при достижении 85% лимита завершает worker после текущей job. Также он останавливается по maxJobs, maxTime или при полном превышении memory limit.

Laravel уже предоставляет эти базовые возможности встроенно — через queue:work, настройки job и database queue driver. Параметр --memory задаёт лимит для Laravel worker-а; в сочетании с --max-jobs и короткоживущим cron-процессом это выполняет ту же практическую задачу: не давать одному PHP-процессу бесконечно накапливать память.

Важно: ни wp-queue, ни Laravel worker не способны гарантированно предотвратить OOM внутри одной чрезмерно тяжёлой job — их проверки происходят между jobs. Поэтому лимиты worker-а дополняют, но не заменяют порционную обработку.

Возможность wp-queue Встроенный Laravel
Хранение очереди в БД Да Да, driver database и таблица jobs
Запуск worker-а Собственный Worker php artisan queue:work
Лимит времени worker-а setMaxTime() --max-time
Лимит jobs за запуск setMaxJobs() --max-jobs
Контроль памяти Остановка worker-а при высоком потреблении --memory и перезапуск cron-ом
Повторные попытки Exponential backoff $tries, $backoff, backoff()
Failed jobs Собственный лог/обработчики failed_jobs, failed()
Цепочки и batch Да Bus::chain(), Bus::batch()
UI мониторинга Встроен Horizon требует Redis; без Redis — логи, БД и свои admin-страницы

Проверка после настройки

Проверьте расписание:

php artisan schedule:list

Проверьте failed jobs:

php artisan queue:failed

Запустите worker вручную для диагностики:

php artisan queue:work database --queue=default --once -vvv

Проверьте, что:

  1. HTTP-запрос создаёт запись в jobs;
  2. cron worker забирает и удаляет успешную job;
  3. ошибка появляется в failed_jobs после исчерпания попыток;
  4. файлы логов cron доступны для записи;
  5. не возникают параллельные процессы благодаря flock.

Итоговые рекомендации

Для небольшого и среднего Laravel-проекта на shared-хостинге:

  1. Используйте QUEUE_CONNECTION=database, а не sync.
  2. Запускайте schedule:run cron-ом каждую минуту.
  3. Запускайте отдельный короткий queue:work cron-ом каждую минуту.
  4. Всегда используйте flock, --stop-when-empty и --max-time, чтобы процессы не пересекались.
  5. Добавляйте --max-jobs и --memory, чтобы PHP-процесс регулярно перезапускался с чистой памятью.
  6. Разделяйте критичные и тяжёлые jobs по именованным очередям.
  7. Проектируйте тяжёлые jobs маленькими и идемпотентными.
  8. Проверяйте failed_jobs и cron-логи после каждого изменения.

Если проекту нужна обработка jobs почти без задержки, высокая параллельность или визуальный мониторинг очередей, следующим шагом будет VPS/Docker с постоянным worker-ом. Redis и Laravel Horizon стоит добавлять, когда их преимущества действительно нужны, а не только ради работы очередей.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *