---
title: "Несколько очередей и приоритеты"
description: "Asynq очереди и приоритеты задач go: critical, default и low с весами против строгого порядка, Config.Queues, StrictPriority и разметка по классам."
course: "Asynq в Go: очередь фоновых задач на Redis"
url: "https://golangmaster.ru/courses/asynq/lessons/07-ocheredi-prioritety/"
published: "2026-09-19"
updated: "2026-09-23"
minutes: 20
---

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

*Курс «Asynq в Go: очередь фоновых задач на Redis», урок 7 из 25. ~20 минут. Обновлено 23.09.2026. Источник: https://golangmaster.ru/courses/asynq/lessons/07-ocheredi-prioritety/*

Asynq очереди появляются там, где один общий пул слотов воркера превращается в затор: ночной пересчёт легко задержит письмо о сбросе пароля. Деление на critical, default и low — с весами или строгим порядком — расставляет задачи по классам обслуживания. Разберём Config.Queues, StrictPriority и разметку задач по классам.

Два часа ночи. Запущенный по расписанию пересчёт рейтингов занимает все десять слотов Concurrency на сорок минут. В 02:05 пользователь жмёт «забыл пароль», система ставит письмо в ту же единственную очередь — и письмо ждёт за пересчётом, хотя цена его задержки измеряется потерянной регистрацией. Пока все задачи равны, самая медленная и самая срочная работа конкурирует за один пул горутин. Очереди asynq — способ объявить, что задачи не равны: критические берутся первыми, тяжёлая фоновая работа живёт в очереди с наименьшим весом и не мешает никому. В Redis каждая очередь — свой набор ключей, а воркер опрашивает настроенные очереди в порядке, который вы задаёте конфигом. В этом уроке — устройство весов, строгий режим, разметка задач по классам обслуживания и цена, которую приходится платить за каждый из вариантов. Спойлер наперёд: очереди — самый дешёвый инструмент из всех, что решают эту проблему; дешевле только отдельные деплои воркеров, которые требуют и дороже в сопровождении.

### Asynq очереди: Config.Queues и веса

Asynq очереди настраиваются полем Queues в asynq.Config — это карта «имя очереди: вес». Вес — сколько голосов очередь имеет при каждом выборе следующей задачи: конфиг critical 6, default 3, low 1 означает, что при непустых очередях примерно шестьдесят процентов опросов уйдёт критической очереди, тридцать — обычной и десять — низкой. Если Queues не задан вовсе, сервер обрабатывает только очередь default — по этой причине первые дни курса всё и работало. Очередь с нулевым или отрицательным весом игнорируется: неактуальные очереди из конфига проще убрать, чем оставлять с нулём. Валидация имён при этом нарочито скромная: NewServer молча пропускает невалидные имена — пустые или состоящие из пробелов, — так что конфиг с опечаткой не уронит старт, а просто оставит очередь без обработчика, и задачи в ней будут ждать так же тихо, как при расхождении клиента и сервера. Вторая сторона — клиент: очередь назначается на постановке опцией asynq.Queue("critical"), и задача помнит своё место в поле info.Queue. Без этой опции задача едет в default независимо от того, какие очереди настроены у сервера. Заметьте асимметрию: клиент может поставить задачу в любую очередь, но исполнять её будет только воркер, у которого эта очередь прописана в конфиге — расхождение ловится не ошибкой, а тишиной ожидания. Имя очереди — непустая строка: библиотека валидирует только это, поэтому дисциплина имён целиком на команде; lowercase и дефисы — та конвенция, которая не выглядит умной до первого тикета «в какой из четырёх регистров очередь Critical».

### Сервер с тремя очередями и клиент с разметкой

```go
srv := asynq.NewServer(redisConnOpt, asynq.Config{
    Concurrency: 10,
    Queues: map[string]int{
        "critical": 6,
        "default":  3,
        "low":      1,
    },
})

// клиент: очередь назначается на постановке
client.Enqueue(resetTask, asynq.Queue("critical"))
client.Enqueue(recalcTask, asynq.Queue("low"))
```

Queues — карта «имя очереди: вес». Пропорция 6 к 3 к 1 значит: при непустых очередях около шестидесяти процентов опросов уйдёт critical, тридцать — default и десять — low. Клиент помечает каждую задачу опцией Queue, а без неё задача уезжает в default. Воркер опрашивает только те очереди, что перечислены в его конфиге.

### Веса против строгого порядка

У весов есть два режима работы, и разница между ними — главное решение урока. По умолчанию StrictPriority выключен, и порядок опроса очередей перемешивается: сервер собирает список имён, повторяя каждое столько раз, каков его вес, тасует его и опрашивает в получившемся порядке. Отсюда и проценты из документации: 6/3/1 даёт примерно 60/30/10 процентов обработки — low гарантированно выполняется в зазорах, хотя и редко. Включённый StrictPriority превращает порядок в дедикт: очереди выстроены по убыванию веса, и пока в critical есть задачи, до default и low дело не доходит вообще. Строгий режим предсказуем для критической очереди — её задержка почти не зависит от длины остальных, — но делает низкоприоритетную работу заложником потока в critical. Стоит понимать и устройство взвешенного режима до конца: сервер собирает список из шести критических, трёх обычных и одной низкой, тасует его и убирает повторы, сохраняя первый порядок появления — поэтому проценты приблизительные и от опроса к опросу прыгают, но в среднем сходятся к пропорциям весов. На короткой дистанции это значит, что из десяти опросов подряд low может достаться два, а может ни одного: вес обещает долю, а не ритм. Выбор между ними — выбор между справедливым дележом и жёсткой гарантией: первого достаточно большинству систем, второй нужны тогда, когда у critical есть настоящее SLO, а низкоприоритетные задачи умеют ждать сколько угодно.

### Взвешенный режим против строгого

| Вопрос | Веса (по умолчанию) | StrictPriority: true |
| --- | --- | --- |
| Порядок опроса | перемешан по весам | по убыванию веса, всегда |
| Доля при 6/3/1 | примерно 60/30/10% | critical целиком, пока не пуст |
| Судьба low под нагрузкой | выполняется в зазорах | может не выполняться неделями |
| Кому подходит | спокойные системы без жёстких SLO | critical с гарантией и пульсирующим потоком |

### Проверка: доля low

**Вопрос:** Конфиг critical 6, default 3, low 1, StrictPriority не включён. Как часто при непустых очередях опрашивается low?

- ✅ Примерно в десяти процентах случаев
- Никогда — low обслуживают отдельные воркеры
- Примерно в тридцати процентах случаев
- Поровну с critical и default

Веса 6/3/1 дают примерно 60/30/10 процентов опросов: имя каждой очереди повторяется в списке вес раз, список тасуется. Low обрабатывается медленнее всех, но не голодает — для строгого голодания нужен StrictPriority.

### Очередь — свойство задачи, а не хендлера

Хендлеру всё равно, из какой очереди пришла задача: маршрутизацию по типам делает ServeMux, а очередь — это канал обслуживания, и живут они в разных измерениях. Один и тот же тип задачи может ходить в разных очередях: письма для обычных пользователей — в default, письма для VIP-клиентов — в critical, при этом хендлер один и код постановки различается одной опцией. Поле info.Queue после постановки говорит, куда задача легла, — логируйте его вместе с ID, чтобы при разборе жалобы сразу видеть класс обслуживания. Имя очереди входит и в ключ уникальности: постановка с Unique считает задачи различными, если у них разные очереди, поэтому одна и та же рассылка может одновременно существовать уникальной копией в default и в critical без взаимного конфликта. В asynqmon очереди отображаются отдельно: по каждой видно длину, скорость обработки и количество повторов — это удобная точка, где классы обслуживания становятся числами, а не ощущениями. Единственное жёсткое правило связки клиент-сервер: очередь из постановки должна быть в конфиге хотя бы одного живого воркера, иначе задача будет ждать молча. Проверка простая: конфиг воркера — это белое имя очередей, а постановщик не имеет права использовать ничего вне этого списка.

### Приоритеты задач go: классы обслуживания

Приоритеты задач go на практике сводятся к разметке по одному вопросу: сколько мы готовы ждать выполнение этой задачи в час пик. Critical — то, за чем цена задержки денежная или репутационная: подтверждения платежей, сброс пароля, вебхуки со сроком по договору. Default — обычный фон: письма, уведомления, отчёты по кнопке; минуты ожидания пользователь не замечает. Low — работа, которую можно прервать и возобновить без всякого вреда: пересчёты рейтингов, пережим картинок, выгрузка аналитики, индексация. Правило, которое держит систему понятной: очередь — это класс обслуживания, а не тема проекта. Очередь «письма» ни о чём не говорит — письмо о сбросе пароля и дайджест новостей живут в разных классах. Если вы пришли из RabbitMQ, где приоритет сообщения — свойство брокера и его очереди, здесь механика на стороне приложения: asynq просто честно перемешивает имена очередей при опросе Redis. Имена весов не абсолютны — важны пропорции: 60/30/10 и 600/300/100 дают одно и то же поведение.

### Разметка очередей одним списком

- critical: платежи, сброс пароля, вебхуки со сроком — задержка видна деньгами или тикетом.
- default: письма, уведомления, отчёты по кнопке — минуты ожидания не заметны.
- low: пересчёты, пережим файлов, аналитика — можно ждать часы и возобновлять после аварий.
- Одна очередь — один класс обслуживания по времени ожидания, а не тема или модуль проекта.
- Веса важны пропорциями: 6/3/1 и 60/30/10 ведут себя одинаково.
- Новая очередь появляется по симптому — реальной задержке важного класса, а не впрок.
- Очередь из постановки обязана быть в конфиге живого воркера, иначе задача ждёт молча.

> **Живое место: StrictPriority и голодающий low.** Команда включила строгий режим «чтобы всё было по-честному», и через девять дней кто-то заметил, что рекомендации в приложении не обновлялись всё это время: critical и default никогда не пустели, low не выполнялся ни разу, а алертов на голодание не было. Продукт деградировал тихо, без единой ошибки в логах. Строгий режим оправдан, когда low — сбрасываемая работа, а поток в critical гарантированно пульсирует и оставляет зазоры; иначе берите веса и следите за возрастом самой старой задачи в low.

### Строгий режим одним флагом

```go
srv := asynq.NewServer(redisConnOpt, asynq.Config{
    Queues: map[string]int{
        "critical": 6,
        "default":  3,
        "low":      1,
    },
    StrictPriority: true, // критические — всегда первыми
})
```

Один флаг меняет политику отбора: без него порядок опроса перемешивается по весам, с ним очереди выстроены по убыванию веса и low проверяется, только когда critical и default пусты. Предсказуемость для critical здесь обменивается на риск голодания low — решение принимайте по своему потоку, а не по вкусу.

### Проверка: строгий режим

**Вопрос:** StrictPriority равен true, в очередях critical и default есть задачи, в low тоже. Какую задачу возьмёт воркер на следующем опросе?

- Любую — порядок перемешивается по весам
- Из low: иначе очередь умрёт от голода
- ✅ Из critical: очередь проверяется первым при строгом порядке
- Из default: между critical и low справедливое среднее

При строгом порядке очереди отсортированы по убыванию веса, и сервер не переходит к следующей, пока в текущей есть задачи. До default воркер дойдёт после опустошения critical, до low — после опустошения default.

### Что станет с задачей в reports?

```go
srv := asynq.NewServer(redisConnOpt, asynq.Config{
    Queues: map[string]int{"critical": 3, "default": 1},
})

client.Enqueue(reportTask, asynq.Queue("reports"))
```

**Что выведет программа?**

- Выполнится с самым низким приоритетом
- Enqueue вернёт ошибку: очередь не настроена
- ✅ Задача ляжет в Redis, но этот воркер её никогда не заберёт
- Задача автоматически переедет в default

Enqueue записывает задачу в любую валидную очередь — конфиг сервера при постановке не проверяется. Но воркер опрашивает только настроенные очереди: reports в его конфиге нет, поэтому задача будет ждать вечно — пока очередь не добавят в Config или не поднимут воркер, у которого она настроена.

### Сколько очередей нужно и связь с масштабом

Очереди в asynq дешёвые: заводятся записью в карте, без миграций и объявления в Redis. Но у каждой есть две цены. Первая — операция опроса: воркер проверяет настроенные очереди при каждом выборе задачи, и двадцать очередей означают двадцать проверок в обход почти пустых. Когда очереди пусты совсем, воркер засыпает примерно на секунду — интервал настраивается полем TaskCheckInterval — и просыпается снова; очередь вне конфига не опрашивается никогда, чем бы она ни была полна. Вторая — конфигурационная: очередь живёт в трёх местах сразу — в постановках клиента, в конфиге каждого воркера и в дашборде — и расползание этого списка замечается по симптомам, а не по ошибкам компиляции. С масштабированием очереди связаны напрямую: все реплики воркеров могут слушать одни и те же очереди — тогда общий пул слотов делится пропорционально весам, — а можно разделить деплой: пара реплик только на critical, остальные на всё остальное. Вариант с разделёнными деплоями дороже в сопровождении, зато тяжелая low-работа физически не может занять слоты критического пула. Полный разбор обоих вариантов, включая связь с Concurrency, ждёт в [двадцатом уроке](/courses/asynq/lessons/20-masshtabirovanie/). Стартовая рекомендация простая: одна очередь default, вторая — по первому реальному симптому конкуренции.

> **Начните с одной очереди.** Расколоть одну очередь на три легко — добавьте имена в конфиг и опции в постановки, старые задачи никуда не денутся. Склеить обратно сложнее: аналитика, алерты и документация уже привязаны к именам. Поэтому очереди появляются по симптому — задержке важного класса, зафиксированной в asynqmon, — а не по предчувствию: три очереди, созданные заранее без нагрузки, обесцениваются в шум.

### Соберите конфиг с приоритетами

Допишите тип весов, флаг строгого порядка и опцию постановки в critical.

```go
srv := asynq.NewServer(redisConnOpt, asynq.Config{
    Queues: map[string]___{
        "critical": 6, "default": 3, "low": 1,
    },
    ___Priority: true,
})

client.Enqueue(task, asynq.___("critical"))
```

Подсказка: Веса — целые числа, флаг — StrictPriority, а очередь на постановке назначает опция Queue.

Правильные ответы в пропусках: `int`, `Strict`, `Queue`

### Соберите critical-постановку и воркер

Расставьте строки в порядке работы: клиент ставит задачу в critical, сервер настроен на обе очереди.

Правильный порядок строк:

1. `opt := asynq.RedisClientOpt{Addr: "localhost:6379"}`
2. `client := asynq.NewClient(opt)`
3. `task := asynq.NewTask(tasks.TypeResetPassword, payload)`
4. `client.Enqueue(task, asynq.Queue("critical"))`
5. `cfg := asynq.Config{Queues: map[string]int{"critical": 6, "default": 3}}`
6. `srv := asynq.NewServer(opt, cfg)`
7. `mux := asynq.NewServeMux()`
8. `srv.Run(mux)`

### Карточки: очереди и приоритеты

- **Config.Queues** — Карта «имя очереди: вес» в конфиге сервера. Без неё воркер обрабатывает только default; очередь с нулевым или отрицательным весом игнорируется.
- **Вес очереди** — Число голосов при выборе следующей задачи: 6/3/1 даёт примерно 60/30/10 процентов опросов. Важны пропорции, не абсолютные значения.
- **StrictPriority** — Флаг строгого порядка: очереди опрашиваются по убыванию веса, lower — только после опустошения вышестоящих. Риск — голодание low.
- **asynq.Queue** — Опция постановки: назначает задаче очередь. Без неё задача уезжает в default; очередь должна быть в конфиге живого воркера.
- **info.Queue** — Поле TaskInfo с очередью задачи. Полезно логировать при постановке: при разборе жалобы сразу виден класс обслуживания.
- **Голодание очереди** — Состояние при StrictPriority: постоянный поток в critical не оставляет зазоров, и low не выполняется неделями. Лечится весами или отдельным деплоем.

Очереди освоены: веса дают справедливый делёж с процентами от пропорций, строгий режим — гарантию для critical ценой голодания low, а разметка задач идёт по классам обслуживания, а не по темам. Проверьте себя на своей работе: какие из ваших задач выжили бы в строгом режиме, а какие умерли бы тихо? Формат прежний: постановки и конфиги запускаются локально с Go и Redis в Docker, песочница сайта показывает только тематическую программу на чистом Go. Дальше по курсу: [восьмой урок](/courses/asynq/lessons/08-mux-marshruty/) про маршрутизацию ServeMux — очередь и тип задачи вместе дают полную адресацию задачи; [пятнадцатый](/courses/asynq/lessons/15-inspektor/) покажет QueueStats, где длина и скорость каждой очереди становятся числами. Если хотите сравнить подходы к приоритетам с полноценным брокером, у сайта есть отдельный курс по RabbitMQ — там приоритет живёт на стороне брокера, и сравнение с решением asynq поучительно.

## Коротко: что запомнить

- Queues — карта «имя: вес» в Config; без неё воркер обрабатывает только default.
- Веса 6/3/1 дают примерно 60/30/10 процентов опросов при непустых очередях.
- StrictPriority выстраивает очереди по убыванию веса: critical целиком, low — в остатке.
- Очередь назначается опцией asynq.Queue на постановке; без неё задача едет в default.
- Очередь — класс обслуживания по времени ожидания, а не тема проекта.
- Очередь из постановки обязана быть в конфиге живого воркера, иначе задача ждёт молча.
- Строгий режим голодает low под постоянным потоком critical — следите за возрастом задач.
- Очереди дешевле в расколе, чем в склейке: начинайте с одной, добавляйте по симптому.

## Домашнее задание

Разметьте свой сервис на три класса: выпишите пять типов задач и назначьте каждой очередь critical, default или low — с обоснованием «сколько готовы ждать». Поднимите воркер с весами 6/3/1, забейте очереди задачами и сравните фактическую долю обработки low с теоретическими десятью процентами. Потом включите StrictPriority и посмотрите, как долго самая старая задача в low ждёт исполнения. Всё локально: Go и Redis из Docker; песочница сайта для сетевого кода не подойдёт.

## Вопросы и ответы по уроку

### Что будет, если клиент поставит задачу в очередь, которой нет у воркера?

Постановка пройдёт успешно — Redis принимает любую валидную очередь. Но воркер опрашивает только очереди из своего Config, поэтому задача будет ждать бесконечно. Лечится добавлением очереди в конфиг воркера или другим воркером с ней; поэтому список очередей стоит держать общим пакетом констант.

### Можно ли менять веса на лету?

Веса читаются при старте сервера: новая пропорция требует перезапуска воркеров. На практике это не боль — воркеры и так перезапускаются деплоями. Учтите только, что при катящемся деплое часть реплик ещё живёт со старой пропорцией: пару минут кластер обрабатывает задачи смешанными весами — для взвешенного режима это безвредно, для строгого заметнее. Постоянная подстройка приоритетов под нагрузку обычно означает, что проблема не в весах, а в разметке классов или в мощности пула Concurrency.

### Очередь или несколько воркеров с разной Concurrency?

Это два инструмента одной проблемы. Очереди делят общий пул слотов пропорционально весам — дешево и гибко. Разделённые деплои воркеров дают физически отдельные пулы: heavy-задачи не могут занять слоты критических ни при каком потоке. Начинают с очередей; разделяют деплой, когда делёж общего пула перестал держать SLO.
