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

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

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

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

Inspector: мониторинг из кода

Тикет «письма не уходят», а терминал пуст — нужен доступ к очереди из кода, и asynq Inspector даёт мониторинг очереди программно: статистика, списки задач, ручной запуск и отмена. Соберём дежурный скрипт и админ-ручку, разберём ошибки и границы операций.

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

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

Asynq Inspector: коннект и статистика#

Asynq Inspector создаётся тем же коннект-оптом, что клиент и сервер: inspector := asynq.NewInspector(asynq.RedisClientOpt{Addr: "localhost:6379"}) — и работает независимо от воркеров: его можно встроить в админ-сервис, CLI-утилиту или тест. Базовая операция — GetQueueInfo(queue): метод возвращает QueueInfo со срезом очереди целиком — счётчики Pending, Active, Scheduled, Retry, Archived, Completed и Aggregating, суммарный Size, память, которую очередь занимает в Redis, и счётчики обработок: Processed и Failed за текущие сутки плюс накопительные ProcessedTotal и FailedTotal. Отдельного слова заслуживает поле Latency — возраст самой старой задачи в pending: это самый честный сигнал затыка, потому что растёт он именно тогда, когда очередь не успевает, а не когда задач много. Поле Paused показывает, стоит ли очередь на паузе — да, очередью тоже можно управлять, и об этом ниже. Поле MemoryUsage честно помечено в документации как приближение: значение получается сэмплированием, так что сравнивайте очереди между собой, а не байты с бухгалтерией. Рядом лежит Timestamp — момент снятия среза: в логах метрик он спасает, когда скрипт отстал от реальности. Свежая операция — UpdateTaskPayload из версии 0.26: метод меняет payload задачи, пока она сидит в scheduled; активную или ожидающую в pending он не тронет и вернёт ошибку. Список имён всех очередей даёт метод Queues() — с него удобно начинать обход в скрипте мониторинга.

Срез очереди из кода

GO
inspector := asynq.NewInspector(asynq.RedisClientOpt{Addr: "localhost:6379"})
defer inspector.Close()

info, err := inspector.GetQueueInfo("default")
if err != nil {
    log.Fatal(err)
}
log.Printf("pending=%d active=%d retry=%d archived=%d latency=%v",
    info.Pending, info.Active, info.Retry, info.Archived, info.Latency)

Inspector строится на RedisClientOpt, как клиент: отдельного порта и регистрации не нужно, а Close освобождает соединение. GetQueueInfo возвращает срез очереди одним вызовом; Latency — возраст самой старой pending-задачи, и именно по нему строят алерт на затык очереди. Логируйте срез целиком, а не только Latency: пара «растущий Archived при плоском Processed» расскажет о фатальных ошибках раньше тикетов.

Мониторинг очереди: Latency и история#

Мониторинг очереди живёт на трёх числах. Первое — Latency из QueueInfo: возраст самой старой задачи в ожидании; порог задаётся бизнесом — для писем минуты, для платежей секунды. Второе — история: метод History(queue, n) возвращает DailyStats за последние n дней — Processed и Failed по суткам; на этих числах строятся графики пропускной способности и сравнение «вчерашний пик против сегодняшнего». DailyStats несёт ровно четыре поля: имя очереди, Processed, Failed и дата — достаточно для графика, ничего лишнего для путаницы. Обратите внимание: Processed включает и успешные, и упавшие задачи, — долю Failed считайте в своём скрипте, готовой её никто не отдаёт. Третье — соотношения внутри среза: растущий Retry при плоском Processed означает, что зависимый сервис деградирует; растущий Archived — что фатальные ошибки из четырнадцатого урока пошли потоком; большое Aggregating — что группы из десятого урока перестали собираться. Пороговые значения вычисляются у вас, а не в библиотеке: Inspector честно отдаёт сырые числа, а «много» для писем и «много» для платежей — разные вещи. Практичная связка: cron-задача из девятого урока каждые пять минут вызывает GetQueueInfo и шлёт метрики в вашу систему наблюдаемости — тем самым очередь становится частью обычного дашборда, а не отдельным миром, в который заглядывают только при инциденте.

Проверка: что такое Latency

Поле Latency в QueueInfo показывает...

Списки задач: List-методы и TaskInfo#

За срезом очереди стоят списки задач: ListPendingTasks, ListActiveTasks, ListScheduledTasks, ListRetryTasks, ListArchivedTasks, ListCompletedTasks и ListAggregatingTasks — по одному на состояние. Все методы принимают имя очереди и опции пагинации: asynq.PageSize(n) и asynq.Page(n), где страницы считаются с единицы; без опций вернутся первые тридцать задач — этого хватает для взгляда, но не для обхода, поэтому в скриптах пагинацию листают до конца. Для aggregating-состояния метод отдельный и принимает ещё имя группы: ListAggregatingTasks(queue, group) показывает задачи конкретной пачки из десятого урока. Рядом со списками задач есть и SchedulerEntries — зарегистрированные периодические задачи из девятого урока с их cron-выражениями и временем следующего запуска; для диагностики «а запланировано ли вообще что-то» это первый метод, куда смотреть. Каждый элемент — TaskInfo с полным досье: ID, Type, Payload, состояние, MaxRetry и Retried, LastErr с текстом последней ошибки, LastFailedAt, NextProcessAt для отложенных и ретраев, Timeout и Deadline, поле IsOrphaned для задач, чей воркер умер посреди исполнения. Одиночная задача по ID — GetTaskInfo(queue, id): дежурный сценарий «пользователь дал ID из письма — скажите, что с задачей» реализуется в одну строку. Errors тоже продуманы: несуществующая очередь вернёт ошибку с ErrQueueNotFound, отсутствующая задача — с ErrTaskNotFound, и обе проверяются errors.Is — это не «случайные» ошибки, а часть контракта API.

Обход retry-задач с ошибками

GO
tasks, err := inspector.ListRetryTasks("default", asynq.PageSize(50))
if err != nil {
    log.Fatal(err)
}
for _, t := range tasks {
    log.Printf("task=%s retried=%d/%d next=%v err=%q",
        t.ID, t.Retried, t.MaxRetry, t.NextProcessAt, t.LastErr)
}

ListRetryTasks с PageSize(50) отдаёт до пятидесяти задач retry-состояния; для полного обхода листайте страницы через asynq.Page. Страницы считаются с единицы: Page(1) — первая порция, а нулевая просто трактуется как первая, и вы получите начало списка. Каждый TaskInfo несёт Retried против MaxRetry, время следующей попытки и текст последней ошибки — тройка, по которой ставится диагноз «что и почему ретраится».

Ручные операции: RunTask, ArchiveTask, DeleteTask#

Вмешательство — вторая половина API, и у каждой операции есть чёткие границы по состояниям. RunTask(queue, id) переводит задачу в pending немедленно: работает для scheduled, retry и archived — так запускают вручную отложенное, ретраи и задачи из архива после починки данных; для pending и active метод вернёт ошибку — задача и так в работе. ArchiveTask(queue, id) отправляет в архив задачи из pending, scheduled и retry — мягкая остановка для постановок, которые стали не нужны; активную задачу так остановить нельзя. DeleteTask(queue, id) удаляет задачу совсем: pending, scheduled, retry и archived — но не active. Общая логика границ проста: активную задачу нельзя ни удалить, ни архивировать, потому что воркер уже держит её в руках — для неё есть только отмена исполнения, о которой ниже. Ошибки те же: ErrTaskNotFound для уже исчезнувших задач — в ручных операциях это нормальный исход при гонке с выполнением. Типичный цикл обхода в скрипте: Page(i) в цикле, пока возвращаемая страница полна, — как только она короче PageSize, вы дошли до конца состояния. Для массовых операций есть All-варианты: RunAllArchivedTasks, ArchiveAllScheduledTasks, DeleteAllRetryTasks и другие, знакомые по десятому уроку групповым RunAllAggregatingTasks — они возвращают число обработанных задач и так же уважают активные; логируйте это число, «архивировано 1240 задач» честнее молчаливой массовой операции. Отдельно стоит DeleteQueue(queue, force): с force=false удаляет пустую очередь, с force=true — даже непустую, лишь бы в ней не было активных задач; пригодится при выводе очереди из эксплуатации.

Операции Inspector и допустимые состояния#

ОперацияРаботает дляОткажет для
RunTask(queue, id)scheduled, retry, archivedpending, active
ArchiveTask(queue, id)pending, scheduled, retryactive, уже архив
DeleteTask(queue, id)pending, scheduled, retry, archivedactive
CancelProcessing(id)исполняемая задачавсех остальных: сигнал просто никого не найдёт

Правила ручных операций#

  • Активную задачу нельзя удалить или заархивировать — только отменить исполнение.
  • ErrTaskNotFound при ручной операции — часто гонка с выполнением: трактуйте спокойно.
  • Проверяйте State из TaskInfo перед операцией: API не прощает догадок.
  • Логируйте, кто и что запустил вручную: ручной RunTask — изменение данных, а не чтение.
  • Массовые All-операции стартуют сотни задач разом: считайте нагрузку на зависимые сервисы.
  • Операции не идемпотентны: повторный ArchiveTask вернёт ошибку, а не тишину.

Проверка: запуск активной задачи

Что вернёт inspector.RunTask("default", id) для задачи в состоянии active?

Что сделает отмена с задачей?

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

// хендлер спит на таймере с select по ctx.Done()
// задача исполняется воркером

err := inspector.CancelProcessing(taskID)

Соберите срез очереди

Допишите конструктор Inspector и метод статистики очереди.

inspector := asynq.(asynq.RedisClientOpt{Addr: "localhost:6379"}) info, err := inspector.("default")

Соберите дежурный скрипт

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

Строки

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

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

CancelProcessing: отмена активной задачи#

Единственная операция над активными задачами — CancelProcessing(id): она принимает только идентификатор, без очереди, и работает сигналом, а не командой. Внутри — публикация в Redis pubsub-канал, который слушают все ваши серверы: каждый держит реестр исполняемых задач, находит нужный ID и отменяет контекст хендлера у себя. Документация называет метод best-effort, и это важно: результат вызова говорит лишь о том, что сигнал отправлен, — задача могла уже закончиться, сигнал мог прийти на сервер, где её нет, хендлер мог игнорировать контекст. Что происходит с задачей после отмены, определяется общими правилами: контекст отменён — хендлер, слушающий ctx.Done(), возвращает управление, воркер засчитывает неудачу с ошибкой контекста, и задача ретраится по обычной политике; хендлер, игнорирующий контекст, доделает свою работу, как в одиннадцатом уроке. Отсюда честные ожидания: CancelProcessing — это «попросить остановиться», а не «убить»; для повторов, которые должны прекратиться совсем, есть ArchiveTask после отмены или SkipRetry в самом хендлере. И помните: отмена не снимает задачу с очереди — она лишь завершает текущую попытку.

Карточки: Inspector

Термин

asynq.NewInspector

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

Конструктор управления: тот же RedisConnOpt, что у клиента и сервера. Живёт независимо от воркеров — в админке, CLI, тестах.

1 / 6

Inspector собран в рабочий инструмент: GetQueueInfo для среза и Latency, списки с TaskInfo для диагноза, RunTask и ArchiveTask для вмешательства, CancelProcessing для вежливой остановки активной задачи. Мини-эксперимент: поставьте задачу с ProcessIn(5*time.Minute), найдите её через ListScheduledTasks и запустите вручную через RunTask — она перейдёт в pending и исполнится сразу. Формат прежний: код запускается локально с Go и Redis в Docker, песочница сайта показывает тематическую программу на чистом Go без Redis. Дальше по курсу: шестнадцатый урок покажет те же данные глазами — в веб-дашборде asynqmon, а двадцать третий вплетёт эти числа в логи и метрики; групповые операции для агрегатов уже видели в десятом уроке.

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

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

Вывод

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

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

  • Asynq Inspector — клиент управления очередью: тот же RedisConnOpt, независим от воркеров.
  • GetQueueInfo отдаёт счётчики по состояниям, Processed и Failed, память и Latency — возраст самой старой pending-задачи.
  • List-методы перечисляют задачи по состояниям; по умолчанию 30 задач, пагинация через PageSize и Page.
  • TaskInfo — полное досье: Retried против MaxRetry, LastErr, NextProcessAt, IsOrphaned.
  • RunTask запускает scheduled, retry и archived; активная задача защищена от удаления и архива.
  • CancelProcessing — best-effort сигнал: контекст отменяется, задача ретраится по обычной политике.
  • ErrTaskNotFound и ErrQueueNotFound — часть контракта: проверяйте errors.Is.
  • PauseQueue ставит очередь на паузу, Servers() показывает живые воркеры и их настройки.

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

Напишите CLI-утилиту с двумя командами: stats печатает GetQueueInfo для всех очередей из Queues(), retry <id> запускает задачу через RunTask с проверкой errors.Is(ErrTaskNotFound). Поставьте задачу с ProcessIn(10*time.Minute), найдите её в ListScheduledTasks и запустите вручную — убедитесь, что NextProcessAt сменился исполнением. Добавьте в stats проверку Latency с порогом и алертным логом, а рядом — ArchiveTask с обязательным флагом подтверждения: массовые операции в админке без подтверждения не живут. Это каркас, который переживёт первый ночной инцидент.

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

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

Чем Inspector отличается от asynqmon?

Asynqmon — готовый веб-дашборд: смотрят глазами и руками. Inspector — Go-библиотека: те же данные и операции, но в вашем коде — для скриптов, админ-ручек, алертов и тестов. Дашборд приятно иметь всегда, а Inspector нужен там, где решение должно приниматься программно.

Можно ли давать Inspector наружу — в админку для дежурных?

Можно, но считайте это административным интерфейсом: RunTask и DeleteTask меняют данные, значит, нужны авторизация, логирование действий и подтверждения на массовых операциях. Только для чтения — GetQueueInfo и списки — безопаснее и закрывает большинство вопросов дежурных без риска.

CancelProcessing гарантированно останавливает задачу?

Нет: метод best-effort — он лишь шлёт сигнал через pubsub, и гарантии исполнения нет. Хендлер, который слушает ctx.Done(), остановится; игнорирующий контекст — доделает работу. Гарантию даёт только ваш хендлер: проверка контекста между шагами делает отмену реальной.

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

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

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

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

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

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

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