Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 7 из 25 ~20 мин +50 XP

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

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

Содержание урока · 10 разделов

Два часа ночи. Запущенный по расписанию пересчёт рейтингов занимает все десять слотов 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 под нагрузкойвыполняется в зазорахможет не выполняться неделями
Кому подходитспокойные системы без жёстких SLOcritical с гарантией и пульсирующим потоком

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

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

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

Хендлеру всё равно, из какой очереди пришла задача: маршрутизацию по типам делает 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 ведут себя одинаково.
  • Новая очередь появляется по симптому — реальной задержке важного класса, а не впрок.
  • Очередь из постановки обязана быть в конфиге живого воркера, иначе задача ждёт молча.

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

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 тоже. Какую задачу возьмёт воркер на следующем опросе?

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

Прочитайте код и выберите, что он выведет:

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

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

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

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

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

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

srv := asynq.NewServer(redisConnOpt, asynq.Config{ Queues: map[string]{ "critical": 6, "default": 3, "low": 1, }, Priority: true, }) client.Enqueue(task, asynq.("critical"))

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

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

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

Термин

Config.Queues

нажмите, чтобы перевернуть
Определение

Карта «имя очереди: вес» в конфиге сервера. Без неё воркер обрабатывает только default; очередь с нулевым или отрицательным весом игнорируется.

1 / 6

Очереди освоены: веса дают справедливый делёж с процентами от пропорций, строгий режим — гарантию для critical ценой голодания low, а разметка задач идёт по классам обслуживания, а не по темам. Проверьте себя на своей работе: какие из ваших задач выжили бы в строгом режиме, а какие умерли бы тихо? Формат прежний: постановки и конфиги запускаются локально с Go и Redis в Docker, песочница сайта показывает только тематическую программу на чистом Go. Дальше по курсу: восьмой урок про маршрутизацию ServeMux — очередь и тип задачи вместе дают полную адресацию задачи; пятнадцатый покажет QueueStats, где длина и скорость каждой очереди становятся числами. Если хотите сравнить подходы к приоритетам с полноценным брокером, у сайта есть отдельный курс по RabbitMQ — там приоритет живёт на стороне брокера, и сравнение с решением asynq поучительно.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • 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; песочница сайта для сетевого кода не подойдёт.

Личные заметкиПусто
Хранятся только в вашем браузере (Asynq · Несколько очередей и приоритеты)

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

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

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

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

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

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

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

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Kafka25 уроков~12 чпрактика25 уроков уже доступны

Kafka в Go: продюсеры и консьюмеры на kafka-go

Курс Kafka для Go: топики и партиции, producer и consumer, группы, оффсеты, гарантии доставки, rebalance, Admin API и мониторинг. 25 уроков.

  • Понимать топики, партиции, оффсеты и группы консьюмеров
  • Писать продюсеры с ключами, батчами и ретраями
  • Читать сообщения группой с коммитами оффсетов
  • Выбирать гарантии: at-most-once, at-least-once