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

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

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

Урок 1 из 25 ~16 мин +50 XP

Знакомство: почему не HTTP-хендлер

Знакомство с asynq golang начинается с честного вопроса: что делать с письмом, отчётом или вебхуком, которые нельзя ждать внутри HTTP-запроса? Ответ — фоновые задачи go: очередь на Redis, из которой отдельный воркер достаёт работу и выполняет без спешки. Разберём, что уходит в фон, почему хендлер для этого не место и как устроена asynq.

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

Среда, день зарплатных ведомостей. Бухгалтер жмёт «отправить отчёт», и HTTP-хендлер делает всё сам: собирает Excel, лезет в SMTP, зовёт вебхук в бухгалтерскую систему. У вас есть ровно столько времени, сколько балансировщик считает нормальным таймаутом — чаще всего тридцать секунд. SMTP сегодня отвечает по четыре секунды на письмо, тяжёлая ведомость собирается минуту — и запрос умирает с 504 на экране. Обиднее всего, что работа не потеряна по существу: файл собран, письмо могло бы уйти. Просто хендлер взял на себя обязанности, которые не обязаны жить в пути ответа. Дальше начинаются ручные ритуалы: аналитик перезапускает ведомость руками, саппорт колотит письма по спискам, и всё это повторяется каждый день отчётности. Фоновые задачи в go существуют ровно для этого — разложить работу на «ответить пользователю быстро» и «сделать медленную часть спокойно». Asynq — один из самых удобных инструментов для такого разложения, и весь курс про то, как им пользоваться без граблей. Оговорка про формат: asynq — сетевая библиотека, поэтому весь код курса вы запускаете локально, с Go и Redis под рукой; песочница сайта покажет только тематическую программу на чистом Go.

Что уходит в фон: письма, отчёты и вебхуки#

Кандидатов на фон проще всего опознать по одному признаку: пользователю не нужен результат прямо сейчас. Письма — welcome, сброс пароля, уведомление о заказе: SMTP может отвечать секунды, а иногда минуты, и ждать его в хендлере никто не готов. Отчёты и файлы — PDF, Excel, архивы: тяжёлая работа с диском и памятью, у которой нет места в пути запроса. Вебхуки — исходящие вызовы сторонних API: если их сервис лёг, ваш хендлер висит вместе с ним, хотя пользователь виноват не был. Дальше идёт перекладывание данных: пережать картинки, пересчитать рейтинг, выгрузить пачку в CRM, синхронизировать каталог. Общее правило, которым я пользуюсь на ревью: если операция дольше сотни миллисекунд и её результат не участвует в ответе — она кандидат в фон. Порог условный, но он приучает сначала измерить длительность, а уже потом решать, куда операцию класть. Заметьте: речь не про «сложные» задачи — даже простой вызов стороннего сервиса делает хендлер заложником чужого таймаута. И ещё одно следствие, о котором забывают: фоновая задача — это обещание пользователю. Если вы не довели обещание до конца, у хранителя обещаний должна быть статистика, ретраи и алерт — в HTTP-хендлере всего этого нет. И заметьте: кандидаты на фон почти всегда видны в коде по одному запаху — функция делает запрос наружу и молчит о результате.

Признаки задачи для фона#

  • Результат не нужен в HTTP-ответе: письмо придёт позже, пользователь уже увидел страницу.
  • Операция занимает секунды и дольше: отчёт, пережим картинок, выгрузка в CRM.
  • Внешняя зависимость ненадёжна: SMTP, вебхук, чужой API с собственными таймаутами.
  • При сбое операцию нужно повторить — и не силами пользователя, который нажал кнопку.
  • Работу выгодно собирать в пачки: рассылка на тысячу адресов одним батчем.
  • За задачей хочется наблюдать: кто ждёт, кто упал, сколько раз повторялся.

Почему HTTP-хендлер не место для фоновых задач#

Три причины, из которых первая злит пользователя, вторая — вас, третья — всю команду. Первая: таймауты. Балансировщик, прокси и браузер рассчитаны на короткие ответы; длинный хендлер заканчивается 504, и проделанная работа выброшена. Вторая: потеря при рестарте. Деплой или падение пода убивает хендлер вместе с тем, что он не успел сделать: письмо не уйдёт, отчёт не соберётся, и узнаете вы об этом по тикету в саппорте через два дня. Третья: пропускная способность. Хендлеры обслуживают живой трафик, и десяток параллельных «собрать отчёт» съедает память и соединения с базой, которые нужны запросам: p99 ползёт вверх, хотя реальных пользователей больше не стало. Асинхронность внутри хендлера не спасает — она лишь прячет те же проблемы в горутину, у которой нет ни ретраев, ни очереди. Лечение всегда одно: медленную часть вынести из пути запроса и поручить компоненту, который умеет ждать, повторять и переживать рестарты. Сюда же относится правило одного экрана: если хендлер длиннее одного экрана, среди его строк почти наверняка спрятана работа для фона. Отдельно скажу: распухший хендлер обычно выглядит как рабочая функция, а не как архитектурная проблема — потому что каждый вызов по отдельности оправдан.

Первый взгляд: клиент и воркер

GO
// клиент: положить задачу в очередь
opt := asynq.RedisClientOpt{Addr: "localhost:6379"}
client := asynq.NewClient(opt)
task := asynq.NewTask("email:welcome", payload)
info, err := client.Enqueue(task)

// воркер: достать задачу и выполнить
srv := asynq.NewServer(opt, asynq.Config{Concurrency: 10})
mux := asynq.NewServeMux()
mux.HandleFunc("email:welcome", handleWelcomeEmail)
srv.Run(mux)

Эскиз без импортов — весь API на десять строк. NewClient открывает доступ к Redis, NewTask создаёт задачу с типом и данными, Enqueue кладёт её в очередь и возвращает info с ошибкой. На другой стороне NewServer запускает воркер, ServeMux маршрутизирует по типу задачи, Run блокируется до сигнала остановки. Подключение разбираем во втором уроке, задачи и хендлеры — в третьем.

Asynq golang: очередь фоновых задач на Redis#

Asynq golang — библиотека очереди задач поверх Redis от инженера Birdie Кена Хибино: открытый код под лицензией MIT, компактное API и то, что обычно дописывают руками вокруг любого брокера. Из коробки есть: отложенный запуск на нужное время, ретраи с экспоненциально растущей задержкой, несколько очередей с приоритетами, периодические задачи по cron-выражениям, группировка мелких задач в пачки и готовый веб-дашборд asynqmon, где видно каждую задачу. Порог входа низкий: Enqueue у клиента, функция-хендлер у воркера, всё остальное — опции вызова. Вокруг ядра живёт экосистема: отдельная утилита asynq для статистики из терминала, веб-дашборд asynqmon, интеграции с Prometheus. За несколько лет библиотека стала типовым ответом на вопрос «чем делать фоновые задачи в go» в проектах, где Redis уже стоит, — а он стоит почти везде. В этом курсе мы проходим библиотеку целиком, от первой задачи до продакшен-граблей; полный план из двадцати пяти уроков лежит на странице курса asynq.

Фоновое в Go: четыре подхода#

ПодходГде живётСильная сторонаСлабая сторона
Горутины и каналыпамять одного процессанулевая инфраструктурарестарт теряет задачи, ретраев нет
cron-скриптырасписание ОС или планировщикапериодика без кодане для событий «здесь и сейчас»
RabbitMQ, Kafkaотдельный брокергарантии доставки и масштабсвой деплой, мониторинг, порог входа
asynqRedis как хранилище задачретраи, отложка, дашборд из коробкинужен живой Redis

Меню опций Enqueue: обещания одной строкой

GO
// одна и та же задача — разные обещания
client.Enqueue(task, asynq.MaxRetry(5))
client.Enqueue(task, asynq.Queue("critical"))
client.Enqueue(task, asynq.ProcessIn(30*time.Minute))
client.Enqueue(task, asynq.TaskID("welcome-100500"))

Это меню, а не программа: каждая строка добавляет задаче обещание. MaxRetry задаёт число попыток после сбоя, Queue отправляет задачу в приоритетную очередь, ProcessIn откладывает запуск на полчаса, TaskID даёт задаче собственное имя. Каждая опция получит свой урок: ретраи — в шестом, очереди — в седьмом, отложка — в пятом, идентификаторы — в четвёртом.

Как устроен asynq: клиент, воркер и Redis#

Внутри библиотеки три действующих лица, и это же три её типа. Клиент — часть вашего API-сервиса: сериализует данные, кладёт задачу в Redis командой Enqueue и сразу отвечает пользователю. Enqueue синхронен: запись в Redis уже случилась к моменту возврата, а вместе с ней вернулся и TaskInfo — идентификатор, имя очереди, время запуска; если Redis недоступен, ошибка видна клиенту немедленно, а не потерей молча. Redis — хранилище и посредник: задачи ждут в списках и сортированных множествах, поэтому переживают рестарты любых участников. Воркер — отдельный процесс, а часто и отдельный деплой: вечно крутится, пока не получит сигнал остановки, достаёт задачи и распределяет их по пулу горутин в количестве Concurrency. Если хендлер вернул ошибку, asynq кладёт задачу обратно с растущей задержкой — повтор виден в дашборде, а не в тишине логов. Схема масштабируется в обе стороны: хотите — клиент и воркер в одном main.go на локальной машине, хотите — API-сервис и десяток реплик воркеров на другом континенте. Логика программы при этом не меняется: клиент ставит, Redis хранит, воркер делает. Если вы прошли курс по go-redis, половина картины у вас уже есть: asynq лишь добавляет протокол «как задача готовится, ждёт, повторяется и выполняется» поверх тех же структур данных.

Asynq против горутин и cron: где граница#

Спорное мнение, которое я защищаю на архитектурных созвонах: не каждой задаче нужна очередь. Пока работа fire-and-forget внутри одного процесса и её потеря незаметна пользователю, горутина дешевле любой инфраструктуры — и заводить Redis ради инкремента счётчика смешно. Граница проходит там, где появляется хотя бы одно из трёх: потеря задачи вызывает тикет, инстансов сервиса больше одного, нужен ретрай с паузой. Тогда очередь окупается немедленно: она дешевле расследования «почему половина писем пропала». У готовой очереди есть и вторая выгода, которую не заметишь в спокойный день: она повторяет задачу сама — asynq по умолчанию делает до 25 попыток с растущей паузой, пока вы спите, а не пишет «перезапусти, пожалуйста» в чат. Cron рядом с asynq живёт спокойно: расписание вроде ночного пересчёта остаётся в cron, а asynq берёт события «здесь и сейчас» и долгие операции — у периодических задач asynq есть и свой аналог cron, ему посвящён отдельный урок. Проверка перед внедрением простая: назовите задачу, которую вы не доверите горутине, и цену её потери. Есть ответ — заводите очередь; ответа нет — экономьте зависимости.

Проверка: хранилище asynq

Где asynq хранит задачи между запусками воркера?

Проверка: горутина в хендлере

Сервис запустил отправку письма горутиной в HTTP-хендлере и перезапустился в этот момент. Что стало с задачей?

Проверка: кандидат в фон

Какая из операций — типичный кандидат в фоновую задачу?

Что вернёт Enqueue при мёртвом Redis?

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

client := asynq.NewClient(asynq.RedisClientOpt{Addr: "localhost:6379"})
task := asynq.NewTask("email:welcome", payload)

info, err := client.Enqueue(task) // Redis остановлен

Соберите постановку задачи

Восстановите три строки клиента: конструктор задачи, конструктор клиента с адресом Redis и постановку в очередь.

client := asynq.(asynq.Opt{Addr: "localhost:6379"}) task := asynq.("email:welcome", payload) info, err := client.(task)

Соберите связку клиент плюс воркер

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

Строки

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

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

Карточки: словарь asynq

Термин

Asynq

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

Библиотека очереди задач для Go поверх Redis (hibiken/asynq, MIT): Enqueue у клиента, хендлеры у воркера, ретраи и дашборд из коробки.

1 / 6

Карта местности собрана: фоновые задачи go — это работа без результата в ответе, HTTP-хендлер для неё не место, а asynq закрывает вопрос клиентом, воркером и Redis. Дальше по порядку: во втором уроке ставим библиотеку и поднимаем Redis в Docker, в третьем пишем первую задачу с письмом, в четвёртом разбираем payload и собственные ID, в пятом — отложенные задачи. Оговорка о формате честная: asynq — сетевая библиотека, и в песочнице сайта её код не запускается; вместо runner-ов здесь живая тематическая программа на чистом Go (мини-очередь с ретраями, где задачи сортируются по времени запуска), а реальные примеры вы гоняете локально — нужен Go и Redis одной командой Docker. Мой совет: не читайте курс как статью. Поставьте Redis из следующего урока и запускайте каждый пример в терминале — очередь из пяти строк становится понятной на ощупь, а не на глаз. К моменту пятого урока у вас будет работающая связка с отложенными задачами, а не конспект о ней.

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

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

Вывод

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

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

  • Фоновая задача — та, чей результат не нужен в HTTP-ответе: письма, отчёты, вебхуки, пережим файлов.
  • HTTP-хендлер упирается в таймауты, теряет работу при рестарте и тратит пропускную способность на чужую работу.
  • Горутина — не очередь: рестарт в момент отправки теряет задачу без следа, повтора и лога.
  • Asynq — очередь задач поверх Redis: клиент ставит, воркер исполняет, Redis переживает всех участников.
  • Из коробки: ретраи с растущей задержкой, отложка, приоритеты, периодика, группировка и дашборд asynqmon.
  • Enqueue синхронен и возвращает ошибку при недоступном Redis — проверять её обязаны.
  • Граница внедрения: потеря задачи заметна, инстансов больше одного, нужен ретрай — иначе горутины дешевле.
  • Песочница сайта не исполняет asynq: примеры курса запускаются локально — нужен Go и Redis в Docker.

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

Разложите свой текущий сервис на два контура: выпишите из кода пять операций, которые сейчас живут в HTTP-хендлере, и для каждой отметьте — нужен ли результат в ответе и что стоит её потере. Потом поднимите Redis командой docker run --name asynq-redis -p 6379:6379 -d redis:7 и проверьте связь: docker exec -it asynq-redis redis-cli ping должен вернуть PONG. Песочница сайта для этого не подойдёт — нужен локальный запуск.

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

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

Чем asynq отличается от RabbitMQ и Kafka?

Это очереди разных весов. RabbitMQ и Kafka — отдельные брокеры со своей инфраструктурой и гарантиями для межсервисных потоков событий. Asynq — очередь задач для приложения, где хранилищем служит уже стоящий у вас Redis: ставится за минуты, а ретраи, приоритеты и дашборд идут из коробки.

Можно ли обойтись горутинами и каналами?

Да, пока задача fire-and-forget живёт в одном процессе и её потеря незаметна пользователю. Как только потеря вызывает тикет, инстансов становится несколько или нужен повтор с паузой — очередь на Redis дешевле расследования пропавших писем. Каналы при этом не пропадают: внутри воркера asynq они и остались механизмом доставки задач в горутины.

Вытянет ли asynq большую нагрузку?

Потолок обычно задают Redis и число воркеров, а не библиотека: воркеры масштабируются репликами, Redis — тюнингом и кластером. Для типовых почтовых, отчётных и вебхук-нагрузок запас огромный; исключение — задачи, требующие брокерских гарантий квитирования уровня Kafka.

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

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

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

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

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

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

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