Асинхронные задачи в Yii2: как устроен yii2-queue внутри
Расширение yii2-queue выглядит как одна команда push, но между постановкой задачи и её выполнением проходит сериализация, брокер, retry и обработка ошибок. Разбираем механизм, чтобы понимать, что ломается под нагрузкой.

Задача, поставленная через Yii::$app->queue->push(new SendEmailJob()), выглядит как один вызов. Внутри же это цепочка из четырёх шагов: сериализация объекта задачи, запись в брокер (Redis, база, AMQP, SQS), извлечение воркером и десериализация обратно в объект. Каждый шаг — точка, где что-то идёт не так: задача теряется, выполняется дважды или падает на десериализации, потому что класс изменился между push и pop. Разбираем, как yii2-queue устроен внутри и где именно рвётся под нагрузкой. Речь о расширении yiisoft/yii2-queue версии 2.3.x на Yii2 2.0.x.
Что происходит между push и выполнением
Компонент очереди в Yii2 — это наследник абстрактного класса yii\queue\Queue. Конкретная реализация (драйвер) определяет, куда пишется задача. При вызове push() происходит следующее:
- Объект задачи (реализующий интерфейс
yii\queue\JobInterface) передаётся сериализатору. По умолчанию этоyii\queue\serializers\PhpSerializer, который вызывает нативныйserialize(). - Полученная строка отдаётся драйверу — методу
pushMessage(). Драйвер кладёт её в хранилище: список в Redis, строку в таблице БД, сообщение в exchange RabbitMQ. - Драйвер возвращает ID сообщения — строку, по которой позже можно спросить статус через
status($id).
Воркер, запущенный командой yii queue/listen или yii queue/run, крутит цикл: берёт следующее сообщение, десериализует его обратно в объект и вызывает execute(). Если метод отработал без исключения — сообщение удаляется из брокера. Если бросил исключение — включается логика повторов.
Очередь не хранит объект. Она хранит его сериализованное состояние. Всё, что не пережило serialize/unserialize — замыкания, ресурсы, ссылки на компоненты приложения — до воркера не доедет.
listen против run: две модели воркера
Это разные режимы, и путаница между ними — источник половины проблем с «зависшими» задачами.
queue/run— обрабатывает то, что уже лежит в очереди на момент запуска, и завершается. Подходит для крона: запустили, разгребли, вышли. Флаг--verboseпокажет ход выполнения.queue/listen— бесконечный цикл, ждёт новые задачи и берёт их сразу. Это долгоживущий процесс, который нужно держать под супервизором.
Долгоживущий воркер накапливает состояние приложения в памяти между задачами. Это и есть главная ловушка, к которой вернёмся.
Почему воркер держит устаревшие данные
PHP в вебе умирает после каждого запроса — и это спасает от целого класса ошибок. Воркер queue/listen живёт часами. Компоненты приложения (Yii::$app->db, кеш, настройки) инициализируются один раз и переиспользуются для всех задач подряд.
Отсюда типичное поведение: задача читает из базы значение, которое было актуально час назад, потому что соединение и уровень изоляции транзакции держат старый снимок. Или воркер съедает память задача за задачей — Active Record кеширует загруженные модели, а identity map их не отпускает.
Замер утечки
Проверить рост памяти можно без сторонних инструментов — прямо в обработчике события. yii2-queue бросает событие перед и после каждой задачи. Подписка в конфигурации компонента:
// config/console.php, компонент queue
// Yii2 2.0.x, yii2-queue 2.3.x
'queue' => [
'class' => \yii\queue\redis\Queue::class,
'redis' => 'redis',
'as log' => \yii\queue\LogBehavior::class,
],
// В bootstrap или в отдельном компоненте:
Yii::$app->queue->on(
\yii\queue\Queue::EVENT_AFTER_EXEC,
function ($event) {
// Пиковое потребление памяти процессом воркера
$mb = round(memory_get_usage(true) / 1048576, 1);
Yii::info("Память после задачи: {$mb} МБ", 'queue.memory');
}
);
Если число монотонно растёт от задачи к задаче и не откатывается — у вас утечка, и воркер рано или поздно упрётся в memory_limit. Диагностика: строка «Память после задачи: …» в логах с ростом на десятки мегабайт за сотню задач.
Лечится не борьбой с утечкой, а ограничением жизни процесса. Флаг --isolate (включён по умолчанию в listen у большинства драйверов) запускает каждую задачу в отдельном дочернем процессе через fork/exec — состояние не накапливается, но растёт накладной расход на старт процесса. Альтернатива — перезапуск воркера супервизором после N задач.
Retry: как задача повторяется и когда теряется
Когда execute() бросает исключение, yii2-queue решает, повторять ли задачу. Механизм зависит от двух вещей.
Первое — параметр ttr (time to reserve): сколько секунд задача считается «взятой в работу». Если воркер не отчитался об успехе за ttr, брокер считает задачу зависшей и отдаёт её другому воркеру. Слишком маленький ttr — задача уходит на повтор, пока первый воркер ещё её выполняет, и вы получаете дублирование.
Второе — attempts: сколько раз повторять до окончательного провала. Управлять этим тонко можно, реализовав интерфейс yii\queue\RetryableJobInterface:
use yii\queue\RetryableJobInterface;
class ChargeCardJob implements RetryableJobInterface
{
public $orderId;
public function execute($queue)
{
// здесь списание средств
}
// Сколько секунд задача держится зарезервированной
public function getTtr()
{
return 5 * 60;
}
// Повторять ли после ошибки. $attempt начинается с 1
public function canRetry($attempt, $error)
{
// Повторяем только сетевые сбои и не больше трёх раз
return $attempt < 3
&& $error instanceof \yii\httpclient\Exception;
}
}
Тонкое место: если canRetry вернул false, задача считается проваленной и удаляется из очереди. Никакого «мёртвого письма» yii2-queue из коробки не хранит для всех драйверов одинаково — Redis-драйвер, например, не держит dead-letter очередь автоматически. Провалившаяся задача исчезает, а вы узнаёте о ней только из лога, если подписались на событие EVENT_AFTER_ERROR.
Где механизм ломается первым
Под нагрузкой и при изменениях кода первыми отказывают предсказуемые узлы.
Изменился класс задачи между push и pop
Задача сериализована со старой структурой свойств, а деплой выкатил новую версию класса. Воркер делает unserialize и получает объект, у которого не хватает свойств, — либо падение, либо тихо неверное поведение. Правило: не удаляйте и не переименовывайте свойства Job-классов, пока в очереди могут лежать старые сообщения. Разгребите очередь перед несовместимым деплоем.
Идемпотентность
Из-за перезапусков воркера, срабатывания ttr и повторов задача может выполниться больше одного раза. yii2-queue гарантирует «хотя бы один раз», а не «ровно один раз». Если задача списывает деньги или шлёт письмо — она обязана проверять, не сделала ли она это уже. Дублирование списаний — самая дорогая ошибка в этом списке.
Транзакции БД внутри задачи
Долгоживущий воркер переиспользует соединение с базой. Если задача открыла транзакцию и упала, не откатив её, следующая задача унаследует «висящую» транзакцию. Оборачивайте работу с базой в явный try/finally с откатом.
Сравнение драйверов по узким местам
| Драйвер | ttr / зависшие задачи | Приоритеты | Отложенный запуск (delay) |
|---|---|---|---|
| Redis | Возврат по истечении ttr есть | Нет | Есть |
| DB (таблица) | Возврат по ttr есть, требует индексов | Есть | Есть |
| AMQP (RabbitMQ) | Ack/nack, delay через плагин | Есть | Требует плагина брокера |
| Beanstalk | Нативный ttr и reserve | Есть | Есть |
Точный набор возможностей и параметров каждого драйвера сверяйте в документации расширения — они отличаются между версиями и зависят от конфигурации самого брокера.
Что мерить и настроить в проде
- Длину очереди — сколько задач ждёт. Растёт постоянно — воркеров не хватает или задачи медленнее, чем поток входящих.
- Долю провалов через
EVENT_AFTER_ERROR. Всплеск — сигнал о деплое несовместимого кода или падении внешнего сервиса. - Память воркера, как в примере выше, — чтобы ловить утечки до OOM.
- Держите
queue/listenпод супервизором (systemd или supervisord) с автоперезапуском и ограничением числа задач на процесс.
Дальше стоит закрыть идемпотентность каждой задачи, которая меняет данные или деньги, и завести отдельную обработку окончательно проваленных задач — пусть падают в лог или таблицу, а не исчезают молча. Инвалидация кеша внутри воркера — отдельная тема, здесь её не разбираем.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: yiisoft/yii2-queue — репозиторий и документация
Частые вопросы
У меня yii2-queue другой версии, механизм тот же?
Задача выполнилась дважды — это баг расширения?
Что будет, если воркер упадёт посреди выполнения задачи?
Почему воркер видит устаревшие данные из базы?
Как понять, что у воркера утечка памяти, а не просто большая задача?
Стоит ли всегда включать --isolate?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.