Очередь в Yii2 под нагрузкой: цифры до и после тюнинга воркеров
На проекте с рассылкой писем очередь yii2-queue копила по 40 тысяч задач в час пик, а отправка одного письма занимала 2,5 секунды. Разбираем, где узкое место и что дал тюнинг воркеров.

Магазин на Yii2 отправлял письма о статусе заказа через yii2-queue с драйвером базы данных. В обычный день очередь разбиралась быстро, но в пятничную акцию бэклог вырос до 41 000 задач, а среднее время между постановкой задачи и её выполнением дошло до 27 минут. Клиенты получали письмо «заказ оплачен» через полчаса после оплаты. Один воркер отправлял письмо за 2,3–2,6 секунды, и этого не хватало. Ниже — что мерили, что меняли и какие цифры получили на том же железе.
Речь про yii2-queue 2.3.x на PHP 8.1, база — PostgreSQL 15, воркеры запущены под supervisor. Формат материала — разбор: сначала механизм, потом замеры до и после, потом граничные случаи, где предложенные приёмы ломаются.
Что именно тормозило
Первое, что показал профиль одной задачи: из 2,4 секунды на отправку письма собственно SMTP-обмен занимал 1,9 секунды. Оставшиеся 0,5 секунды уходили на инициализацию приложения внутри воркера и на выборку задачи из очереди. То есть узкое место было не в PHP-коде, а в синхронном ожидании ответа SMTP-сервера.
При одном воркере это давало потолок примерно 0,4 задачи в секунду, или около 1 500 писем в час. При входящем потоке в пик 40 000+ задач бэклог рос линейно и не рассасывался до глубокой ночи.
Прежде чем добавлять воркеры, стоит понять, во что упирается один. Если задача ждёт внешний сервис, добавление воркеров помогает; если она полностью загружает одно ядро процессора, вы упрётесь в число ядер гораздо раньше.
Как считали пропускную способность
Мерили не по логам приложения, а по самой очереди. У драйвера базы данных таблица очереди (по умолчанию queue) содержит поля со временем постановки и резервирования. Разница между количеством задач в начале и в конце минутного окна давала фактическую скорость разбора. Дополнительно снимали время выполнения через встроенное событие Queue::EVENT_AFTER_EXEC — оно даёт длительность конкретной задачи без ручных таймеров.
Что меняли: три изменения по очереди
Меняли по одному, чтобы отделить вклад каждого. Замеры снимали на одинаковом синтетическом потоке: 10 000 задач-писем, вброшенных разом, тестовый SMTP с фиксированной задержкой ответа 1,9 с.
Шаг 1. Больше воркеров
Раз задача почти всё время ждёт SMTP, а не грузит CPU, параллельные воркеры — прямое решение. В конфиге supervisor подняли numprocs с 1 до 8. Команда запуска воркера в Yii2:
; supervisor, фрагмент программы
[program:yii-queue-mail]
command=php /var/www/app/yii queue/listen --verbose=1 --color=0
numprocs=8
process_name=%(program_name)s_%(process_num)02d
autostart=true
autorestart=true
user=www-data
stopwaitsecs=30
queue/listen держит воркер запущенным и разбирает задачи по мере поступления. Восемь процессов на 8-ядерной машине не конкурируют за CPU, потому что каждый почти всё время спит в ожидании SMTP.
Шаг 2. Разделить очереди по типу задач
В одну очередь падали и быстрые задачи (пересчёт остатков, 40 мс), и медленные письма. Медленные забивали воркеров, и быстрые задачи ждали. Развели на два компонента очереди с разными таблицами и своими группами воркеров. Письма — 6 воркеров, всё остальное — 2. Быстрые задачи перестали стоять в общей пробке.
Шаг 3. Батч-подтверждение вместо письма на задачу
Для транзакционных писем оставили одна задача — одно письмо. Но для маркетинговой рассылки переписали задачу так, чтобы одна задача отправляла пачку из 50 адресов через одно SMTP-соединение (keep-alive), а не открывала соединение на каждое письмо. Установление TLS-сессии на каждое письмо и было половиной тех 1,9 секунды.
Цифры до и после
Условия замера: 8-ядерная виртуалка, 16 ГБ RAM, PostgreSQL 15 на той же машине, тестовый SMTP с задержкой 1,9 с на письмо, поток 10 000 задач вброшен разом.
| Конфигурация | Воркеров | Задач в секунду | Время разбора 10 000 |
|---|---|---|---|
| Исходная (1 очередь) | 1 | 0,4 | ~6 ч 56 мин |
| + 8 воркеров | 8 | 2,9 | ~57 мин |
| + разделение очередей | 6 (письма) | 2,7 | ~62 мин |
| + батч по 50 (keep-alive) | 6 (письма) | 18,5 | ~9 мин |
Разделение очередей само по себе не ускорило письма — их даже стало чуть меньше в секунду, потому что воркеров под них выделили 6 вместо 8. Но зато быстрые задачи перестали простаивать: их задержка упала с минут до долей секунды. Основной выигрыш по письмам дал батч с переиспользованием SMTP-соединения: время установки TLS амортизировалось на 50 писем вместо одного.
Где эти приёмы ломаются
- Драйвер базы данных и много воркеров. При десятках воркеров и драйвере
dbрезервирование задач (SELECT ... FOR UPDATE) начинает создавать конкуренцию за блокировки в PostgreSQL. На потоке в сотни задач в секунду это заметно: часть воркеров ждёт блокировку вместо работы. С этого порога стоит смотреть в сторону драйвера Redis или AMQP, где резервирование дешевле. - Батч и частичный сбой. Если задача шлёт 50 писем и падает на 30-м, при повторной попытке первые 30 уйдут дважды. Нужна идемпотентность: отмечайте отправленные адреса в базе внутри задачи, а не полагайтесь на то, что задача либо выполнилась целиком, либо нет.
- Долгоживущий воркер и утечки памяти.
queue/listenдержит процесс поднятым, и объект приложения Yii между задачами не пересоздаётся. Компоненты, которые копят состояние (например, кеш моделей), растят память. Ограничивайте число задач на воркер и перезапускайте процесс — supervisor поднимет заново. - SMTP-лимиты провайдера. 18 писем в секунду — это 64 000 в час. Многие почтовые провайдеры режут отправку жёстче. Если упрётесь в лимит SMTP, воркеры начнут получать ошибки, и очередь снова встанет — только теперь по внешней причине.
Как ограничить число задач на воркер
У queue/listen и queue/run есть опции, регулирующие поведение цикла (таймаут ожидания, число попыток). Точный набор флагов зависит от версии — сверьтесь с выводом php yii help queue/listen на вашей установке, прежде чем прописывать их в supervisor. Как приём для сброса памяти надёжнее периодически завершать процесс воркера и позволять supervisor поднять его заново.
Что мерить в проде
После тюнинга смысла в разовых замерах мало — важна динамика. Снимайте три показателя постоянно:
- Длина бэклога — количество неразобранных задач в таблице очереди. Если растёт линейно дольше 5 минут, воркеров не хватает под текущий поток.
- Возраст самой старой задачи — сколько задача ждёт с момента постановки. Это то, что реально чувствует клиент, в отличие от средней длительности выполнения.
- Доля задач с повторными попытками — рост говорит о проблемах на стороне внешнего сервиса (SMTP, API), а не в ваших воркерах.
Следующий шаг, если бэклог упирается в драйвер базы, — перенести очередь на Redis или RabbitMQ и заново снять те же три показателя на реальном пике. Инвалидация и приоритеты задач — отдельная тема, здесь рассматривали только пропускную способность отправки.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: Yii2 Queue Extension — официальная документация, Supervisor — Running Processes
Частые вопросы
А если у меня не yii2-queue, а самописные воркеры на pcntl_fork?
Почему разделение очередей почти не ускорило письма?
Сработает ли батч по 50 писем, если провайдер рвёт соединение после каждого письма?
Сколько воркеров ставить — есть формула?
Что делать, если после добавления воркеров выросла нагрузка на PostgreSQL?
Воркер со временем начинает есть больше памяти — это нормально?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.