Содержание урока · 10 разделов
- Asynq очереди: Config.Queues и веса
- Веса против строгого порядка
- Взвешенный режим против строгого
- Очередь — свойство задачи, а не хендлера
- Приоритеты задач go: классы обслуживания
- Разметка очередей одним списком
- Сколько очередей нужно и связь с масштабом
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Два часа ночи. Запущенный по расписанию пересчёт рейтингов занимает все десять слотов 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».
Сервер с тремя очередями и клиент с разметкой
GOsrv := 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?
Очередь — свойство задачи, а не хендлера#
Хендлеру всё равно, из какой очереди пришла задача: маршрутизацию по типам делает 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 ведут себя одинаково.
- Новая очередь появляется по симптому — реальной задержке важного класса, а не впрок.
- Очередь из постановки обязана быть в конфиге живого воркера, иначе задача ждёт молча.
Команда включила строгий режим «чтобы всё было по-честному», и через девять дней кто-то заметил, что рекомендации в приложении не обновлялись всё это время: critical и default никогда не пустели, low не выполнялся ни разу, а алертов на голодание не было. Продукт деградировал тихо, без единой ошибки в логах. Строгий режим оправдан, когда low — сбрасываемая работа, а поток в critical гарантированно пульсирует и оставляет зазоры; иначе берите веса и следите за возрастом самой старой задачи в low.
Строгий режим одним флагом
GOsrv := 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, вторая — по первому реальному симптому конкуренции.
Расколоть одну очередь на три легко — добавьте имена в конфиг и опции в постановки, старые задачи никуда не денутся. Склеить обратно сложнее: аналитика, алерты и документация уже привязаны к именам. Поэтому очереди появляются по симптому — задержке важного класса, зафиксированной в asynqmon, — а не по предчувствию: три очереди, созданные заранее без нагрузки, обесцениваются в шум.
Соберите конфиг с приоритетами
Допишите тип весов, флаг строгого порядка и опцию постановки в critical.
Соберите critical-постановку и воркер
Расставьте строки в порядке работы: клиент ставит задачу в critical, сервер настроен на обе очереди.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: очереди и приоритеты
Очереди освоены: веса дают справедливый делёж с процентами от пропорций, строгий режим — гарантию для 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; песочница сайта для сетевого кода не подойдёт.
Личные заметкиПусто
Вопросы и ответы по уроку
Что будет, если клиент поставит задачу в очередь, которой нет у воркера?
Постановка пройдёт успешно — Redis принимает любую валидную очередь. Но воркер опрашивает только очереди из своего Config, поэтому задача будет ждать бесконечно. Лечится добавлением очереди в конфиг воркера или другим воркером с ней; поэтому список очередей стоит держать общим пакетом констант.
Можно ли менять веса на лету?
Веса читаются при старте сервера: новая пропорция требует перезапуска воркеров. На практике это не боль — воркеры и так перезапускаются деплоями. Учтите только, что при катящемся деплое часть реплик ещё живёт со старой пропорцией: пару минут кластер обрабатывает задачи смешанными весами — для взвешенного режима это безвредно, для строгого заметнее. Постоянная подстройка приоритетов под нагрузку обычно означает, что проблема не в весах, а в разметке классов или в мощности пула Concurrency.
Очередь или несколько воркеров с разной Concurrency?
Это два инструмента одной проблемы. Очереди делят общий пул слотов пропорционально весам — дешево и гибко. Разделённые деплои воркеров дают физически отдельные пулы: heavy-задачи не могут занять слоты критических ни при каком потоке. Начинают с очередей; разделяют деплой, когда делёж общего пула перестал держать SLO.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Kafka в Go: продюсеры и консьюмеры на kafka-go
Курс Kafka для Go: топики и партиции, producer и consumer, группы, оффсеты, гарантии доставки, rebalance, Admin API и мониторинг. 25 уроков.
- Понимать топики, партиции, оффсеты и группы консьюмеров
- Писать продюсеры с ключами, батчами и ретраями
- Читать сообщения группой с коммитами оффсетов
- Выбирать гарантии: at-most-once, at-least-once