Асинхронные задачи в Yii под нагрузкой: что ломается первым
Очередь на yii2-queue держалась до 200 задач в минуту, потом воркеры начали падать по памяти, а в БД повисли задачи в статусе reserved. Разбираем, что отваливается раньше всего и почему.

Типичная картина: проект на Yii 2 с очередью на yii2-queue, драйвер — таблица в MySQL или Redis. Всё работает на демо-нагрузке. Потом приходит реальный трафик: 200–300 задач в минуту, воркеры запущены через supervisor, и через час картина такая — RSS одного воркера пухнет до полутора гигабайт, часть задач висит в статусе reserved и не переотправляется, а в логе висит MySQL server has gone away. Ниже — по порядку, что ломается первым и почему именно в такой последовательности.
Где рвётся раньше всего
Под нагрузкой в Yii-воркере проблемы всплывают не случайно, а в предсказуемом порядке. Он определяется тем, что PHP-процесс воркера долгоживущий, а весь фреймворк изначально рассчитан на короткий цикл «запрос — ответ — смерть процесса».
- Память процесса. Воркер не умирает после каждой задачи, поэтому всё, что раньше собиралось сборщиком мусора при завершении запроса, теперь копится.
- Соединение с БД. MySQL закрывает простаивающее соединение по
wait_timeout, а воркер этого не замечает до следующего запроса. - Зависшие reserved-задачи. Задача взята в работу, воркер убит по OOM или таймауту — задача остаётся заблокированной на время
ttr. - Контеншн на таблице очереди. Драйвер БД под сотнями задач в минуту устраивает блокировки строк на
reserve.
1. Память: компонент, который не отпускает объекты
Главный источник утечек в долгоживущем PHP-процессе — синглтоны в контейнере приложения. Логгер Yii по умолчанию накапливает сообщения в памяти и сбрасывает их только при flush — в вебе это происходит в конце запроса, а в воркере запроса нет. За тысячу задач буфер логгера превращается в мегабайты.
Первое, что делают: включают периодический сброс логов и ограничивают число задач на один процесс.
// config/console.php, Yii 2.0.x
'components' => [
'log' => [
// сбрасывать буфер каждые 100 сообщений, а не копить до конца процесса
'flushInterval' => 100,
'targets' => [
[
'class' => 'yii\\log\\FileTarget',
'levels' => ['error', 'warning'],
'exportInterval' => 100,
],
],
],
],
Второй приём — не полагаться на то, что воркер живёт вечно. У консольной команды queue/listen есть параметр, ограничивающий число обработанных задач, после чего процесс завершается, а supervisor поднимает его заново. Проверьте актуальное имя опции в документации вашей версии yii2-queue — оно исторически называлось --isolate для запуска каждой задачи в отдельном дочернем процессе. Изоляция каждой задачи в отдельном процессе почти полностью снимает вопрос утечек ценой затрат на форк.
Долгоживущий воркер на PHP — это не оптимизация, а компромисс: вы экономите на старте фреймворка, но берёте на себя весь менеджмент памяти, который раньше делал за вас конец запроса.
2. «MySQL server has gone away» на простаивающем воркере
Воркер обработал пачку задач, потом очередь опустела, он висит на опросе таблицы или на блокирующем чтении из Redis. Проходит больше wait_timeout (по умолчанию в MySQL это 28800 секунд, но на многих хостингах его сокращают до 60–120). Соединение MySQL закрывает, а Yii об этом не знает — при следующей задаче падает исключение.
В компоненте db есть опция, которая заставляет драйвер восстанавливать разорванное соединение автоматически:
// config/console.php
'components' => [
'db' => [
'class' => 'yii\\db\\Connection',
'dsn' => 'mysql:host=127.0.0.1;dbname=app',
'username' => 'app',
'password' => 'YOUR_DB_PASSWORD',
// повторно открыть соединение, если оно отвалилось
'serverRetryInterval' => 600,
// очищать закешированную схему таблиц не требуется в воркере,
// но следите за enableSchemaCache при частых миграциях
],
],
Точное имя опции восстановления соединения сверяйте в справочнике класса yii\db\Connection для вашей версии — набор свойств между 2.0.x релизами менялся. Если автоматического восстановления недостаточно, в обработчике задачи имеет смысл делать «пинг» перед тяжёлой операцией: выполнить лёгкий SELECT 1 и при исключении закрыть и открыть соединение вручную через Yii::$app->db->close() и ->open().
3. Задачи, застрявшие в reserved
Драйвер БД для yii2-queue хранит задачи в таблице (обычно queue) со столбцами reserved_at и attempt. Когда воркер берёт задачу, он проставляет reserved_at и держит её невидимой для других воркеров на время ttr (time to run). Если воркер убит по OOM в середине задачи, запись остаётся зарезервированной до истечения ttr — и только потом переотправляется.
Отсюда два симптома под нагрузкой:
- Задачи как будто «теряются» — на самом деле они висят reserved и ждут
ttr. - Если
ttrвыставлен большим «на всякий случай» (например, час), а воркеры регулярно падают, очередь копит балласт.
ttr задаётся при постановке задачи и должен быть чуть больше реального максимального времени выполнения, а не «с запасом на порядок». Слишком большой ttr превращает падение воркера в долгую заморозку задачи; слишком маленький — приводит к тому, что медленная задача переотправляется и выполняется дважды.
4. Контеншн на таблице очереди
Драйвер БД при каждом reserve выполняет UPDATE ... WHERE reserved_at IS NULL ... LIMIT 1 внутри транзакции с блокировкой строки. При десятке воркеров и сотнях задач в минуту это превращается в очередь за блокировкой одной и той же «горячей» части таблицы. В InnoDB вы увидите рост Innodb_row_lock_waits и подрастающую задержку постановки/взятия задач.
Это тот порог, на котором драйвер БД перестаёт быть удачным выбором. Redis-драйвер использует атомарные операции над списками и не упирается в блокировки строк — под высокой параллельностью он ведёт себя предсказуемее.
Сравнение драйверов под нагрузкой
| Драйвер | Взятие задачи | Слабое место под нагрузкой | Когда брать |
|---|---|---|---|
| DB (MySQL/PostgreSQL) | UPDATE с блокировкой строки в транзакции | контеншн на таблице, рост row lock waits | до нескольких десятков задач в минуту, когда не хочется отдельного сервиса |
| Redis | атомарные операции над списком | задачи не переживают падение Redis без persistence | сотни задач в минуту, высокая параллельность |
| AMQP (RabbitMQ) | native-очередь брокера с ack | операционная сложность, отдельный брокер в проде | когда нужны маршрутизация, приоритеты, гарантии доставки |
Что мерить, чтобы поймать проблему до инцидента
Замеры зависят от вашего железа и объёма задач, поэтому вместо чужих цифр смотрите на свои и на динамику:
- RSS воркера во времени. Если память монотонно растёт между задачами и не откатывается — утечка. Снимайте через
ps -o rssпо PID воркеров с интервалом в минуту. - Число reserved-задач с истёкшим
ttr. Запрос к таблице очереди: сколько записей сreserved_atстарше, чем сейчас минусttr. Ненулевое стабильно — воркеры регулярно умирают. - Задержка от постановки до старта. Разница между
pushed_atи моментом взятия. Рост означает, что воркеров не хватает или они заняты долгими задачами. - Innodb_row_lock_waits для драйвера БД:
SHOW STATUS LIKE 'Innodb_row_lock%';Растущее значение — сигнал уходить на Redis.
Граничные случаи, о которых забывают
Долгие задачи и ttr — уже разобрали. Ещё три места, где ломается неочевидно:
- Идемпотентность. При переотправке задача выполнится повторно. Если она списывает деньги или шлёт письмо — нужна проверка «уже сделано». Это не опция очереди, а ответственность вашего обработчика.
- Кеш схемы БД. После миграции долгоживущий воркер продолжает работать со старой схемой, закешированной в памяти, пока его не перезапустят. Перезапуск воркеров при деплое — обязательный шаг.
- Graceful shutdown. Если supervisor убивает воркер
SIGKILL, текущая задача обрывается на середине. Настройте сигналSIGTERMи достаточныйstopwaitsecs, чтобы воркер успел завершить текущую задачу —yii2-queueкорректно обрабатывает мягкую остановку.
Что настроить дальше
Порядок действий на растущем проекте: сначала ограничьте жизнь воркера (изоляция задач или лимит на число обработанных), потом закройте вопрос с разрывом соединения БД, затем поставьте мониторинг reserved-задач и задержки постановки. Когда упрётесь в контеншн на таблице — переезжайте на Redis-драйвер, а если нужны приоритеты и маршрутизация — смотрите в сторону AMQP. Конкретные имена опций и их доступность в вашей версии сверяйте с документацией yii2-queue и справочником классов Yii, а не по памяти: между 2.0.x релизами набор параметров менялся.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: yii2-queue — официальная документация расширения, Yii 2 API — класс yii\db\Connection
Частые вопросы
У меня Yii 2.0.13, а не последняя версия — приёмы применимы?
Воркер всё равно течёт по памяти, хотя я включил flushInterval — что делать?
Задачи пропадают из очереди, но в логах ошибок нет — куда они деваются?
Грозит ли двойное выполнение задачи после падения воркера?
Стоит ли сразу брать Redis или AMQP, минуя драйвер БД?
Почему воркер после деплоя работает по старой логике или схеме БД?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.