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

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

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

Урок 19 из 25 ~18 мин +50 XP

Плавная остановка воркеров

Деплой — это SIGTERM старому поду, а в воркерах в этот момент живут задачи. Asynq graceful shutdown: перестать брать новое, дожать активное, вернуть недоделанное в очередь. Разберём Run против Start, дожим с ShutdownTimeout и остановку воркеров, при которой деплой не теряет задачи.

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

Новый релиз — это всегда убийство старого процесса: оркестратор шлёт SIGTERM, ждёт grace period и добивает упрямых SIGKILL. Если в этот момент воркер держит задачу, вопрос не в удобстве, а в данных: задача либо дожимается и закрывается, либо возвращается в очередь и исполняется заново — и повтор обязан быть безопасным, как договорились в восемнадцатом уроке. Asynq обрабатывает остановку сам: ловит сигналы, перестаёт забирать новые задачи, дожидается активных хендлеров и возвращает в очередь неуложившиеся. Но цифры — зона ваша: ShutdownTimeout, таймауты задач и grace period оркестратора складываются в лестницу, и ошибка в ней превращает плавную остановку в kill -9 посреди транзакции. В этом уроке — механика остановки по шагам: что происходит при сигнале, чем Run отличается от Start, как устроен дожим активных задач и как рассчитать время, после которого деплой проходит без потерь и без лишних повторов.

Что происходит при SIGTERM#

Сигнальный обработчик asynq подписан на три сигнала: SIGTERM, SIGINT и SIGTSTP. TERM и INT — путь завершения: сначала вызывается Stop, затем Shutdown, после чего Run возвращает управление, и main завершается. TSTP — путь паузы: только Stop, процесс продолжает жить и держать соединения, о нём отдельно ниже. Stop переводит сервер в состояние stopped: процессор прекращает вытаскивать новые задачи из очередей — очередь копит их до следующего запуска или следующего пода. Активные хендлеры не прерываются: они продолжают работать до своего конца или до истечения лимита дожима, о котором дальше. Логи честно показывают каждый шаг: «Stopping processor» на входе, «Processor stopped» после, затем «Starting graceful shutdown» и «Waiting for all workers to finish...». Обратите внимание, что остановка двухфазная сознательно: сначала очередь замораживается на входе, потом разбирается остаток — так новые задачи не приходят, пока живые воркеры заняты дожимом старых. Практический вывод для локальной отладки: Ctrl+C в терминале — это SIGINT, и он запускает тот же полный цикл, что и деплойный TERM, поэтому остановку воркера можно репетировать без оркестратора — просто следите за логами. Если же процесс не реагирует на сигнал вовсе, проверьте, не запущен ли он через Start без вашего обработчика: в этом режиме asynq сигналы не ловит, и процесс умирает без всякого дожима.

Минимальный main с Run

GO
srv := asynq.NewServer(
    asynq.RedisClientOpt{Addr: "localhost:6379"},
    asynq.Config{Concurrency: 10},
)

mux := asynq.NewServeMux()
mux.HandleFunc(tasks.TypeWelcomeEmail, tasks.HandleWelcomeEmail)

if err := srv.Run(mux); err != nil {
    log.Fatal(err)
}

Run запускает сервер и блокируется до сигнала: TERM или INT запускают Stop и graceful Shutdown, после чего функция возвращает управление и main завершается. Ошибки старта — недоступный Redis, битый конфиг — возвращаются из Run сразу, поэтому log.Fatal ловит именно проблемы запуска. Для выделенного воркера этого main достаточно: остановка уже корректная.

Проверка: сигнал-пауза

Какой сигнал остановит забор новых задач, но оставит процесс жить?

Asynq graceful shutdown: Run против Start#

У запуска два режима, и выбор определяет, кто ловит сигналы. Run — самодостаточный: старт, ожидание сигнала, Stop и Shutdown внутри; возвращает ошибку только старта. Он рассчитан на выделенный воркер, где больше ничего нет. Start — фоновый: запускает те же горутины и немедленно возвращает управление. Он нужен, когда в одном процессе живёт ещё и HTTP-API — связка из двадцать первого урока: веб-сервер слушает порт, воркер обрабатывает задачи, и завершать их надо согласованно. С Start обработчик сигналов пишете вы: signal.Notify по SIGTERM, затем srv.Stop() — дожим активных задач, затем srv.Shutdown() и только после него остановка HTTP-сервера. Порядок важен: Shutdown обязан вызываться перед выходом — Stop лишь останавливает забор, и процесс без Shutdown не завершится корректно. Учтите и состояние сервера: уже закрытый сервер вернёт ErrServerClosed из повторных Start и Run — проверяйте errors.Is, если в вашей архитектуре сервер может останавливаться дважды. Для простого воркера Run покрывает всё; Start — инструмент для процессов, где у остановки есть хозяин.

Дожим активных задач: Stop, Shutdown и ShutdownTimeout#

Дожим — сердце плавной остановки. После Stop новые задачи не берутся, но активные хендлеры продолжают работать; Shutdown запускает для них таймер. Длительность задаёт ShutdownTimeout в конфиге сервера: если поле не задано или ноль, работает дефолт — восемь секунд. До истечения таймера всё развивается штатно: хендлер успел и вернул nil — задача закрывается успешной; вернул ошибку — обычный ретрай из шестого урока. Если же попытка не уложилась в ShutdownTimeout, процессор снимает её силой: контекст хендлера отменяется, а задача возвращается в pending — в логах это «Pushed task id back to queue». Никакой потери нет: задача подхватится другим воркером — скорее всего, свежеподнятым подом нового релиза. Но исполнение начнётся с нуля, и всё, что попытка успела сделать до отмены, обязано быть безопасным для повтора — восемнадцатый урок здесь обязательная база. Отсюда же понятна цена больших значений: ShutdownTimeout — верхняя граница времени деплоя, и каждый неуложившийся хендлер тормозит выкатку, сколько бы вас ни просили «подождать, сейчас доделает». Полезно знать и границы: дожим дожидается только тех, кто уже работает. Задачи, которые процессор ещё не успел вытянуть, остаются в очереди нетронутыми — для них остановка не видна. Поэтому прирост pending после деплоя — хороший датчик здоровья лестницы таймаутов: ноль после каждого выката значит, что числа подобраны верно, а постоянный рост — что дожим короче реальных хендлеров.

ShutdownTimeout под реальный хендлер

GO
srv := asynq.NewServer(
    asynq.RedisClientOpt{Addr: "localhost:6379"},
    asynq.Config{
        Concurrency:     10,
        ShutdownTimeout: 25 * time.Second,
    },
)

ShutdownTimeout — сколько сервер дожидается активных хендлеров перед тем, как снять попытку и вернуть задачу в очередь. Восемь секунд по умолчанию хватает письмам, но не платёжному хендлеру с внешними вызовами. Значение берётся чуть больше самого долгого штатного хендлера — и обязательно с запасом помещается в grace period оркестратора, иначе деплой оборвёт дожим.

Что станет с задачей, не уложившейся в дожим?

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

// asynq.Config{ShutdownTimeout: 10 * time.Second}
// хендлер задачи исполняется 40 секунд
// процесс получил SIGTERM, дожим закончился

Остановка воркеров в k8s: лестница таймаутов#

В k8s и systemd остановка воркеров — рутина каждого деплоя, и она строится по лестнице из трёх чисел. Нижняя ступень — таймаут задачи из одиннадцатого урока: потолок длительности одной попытки, он же обещание «длиннее этого задача не живёт». Средняя — ShutdownTimeout: сколько ваш сервер готов дожидаться активных хендлеров. Верхняя — terminationGracePeriodSeconds в манифесте пода или в юните systemd: сколько оркестратор ждёт после SIGTERM, прежде чем слать SIGKILL. Каждая ступень обязана помещаться в предыдущую с запасом: таймаут задачи больше ShutdownTimeout не имеет смысла — попытка всё равно снимется на дожиме; ShutdownTimeout вплотную к grace period оставляет процессу ноль секунд на собственное завершение — соединения с Redis закроются криво. Приятно, что несколько реплик деплоятся независимо: старый под останавливается и возвращает недоделанное в очередь, новый поднимается и разбирает её — снаружи это выглядит как непрерывная работа. Отдельно упомяну preStop-хуки: они не обязательны, asynq всё делает по сигналу, но полезны, когда перед остановкой надо дать балансировщику исключить под из ротации.

Лестница таймаутов остановки#

ПараметрГде живётРоль в остановке
Timeout задачиопция постановки, одиннадцатый урокпотолок длительности одной попытки
ShutdownTimeoutasynq.Config серверасколько дожидаться активных хендлеров перед снятием попытки
terminationGracePeriodSecondsманифест k8s или юнит systemdсколько оркестратор ждёт от SIGTERM до SIGKILL
Lease воркеравнутренний механизм asynqворкер умер жёстко — задачу вернёт recoverer

Таймауты задачи против времени на дожим#

Связка с одиннадцатым уроком здесь не теоретическая: Timeout(10*time.Minute) у задачи превращает дефолтный дожим в фарс — восемь секунд против десяти минут, попытка снимется почти наверняка и вернётся в очередь почти пустой. Варианта три. Первый — дробить длинную работу на шаги и проверять ctx.Done() между ними: хендлер, который между шагами смотрит на контекст, снимается вежливо, на границе атомарных шагов, и идемпотентность из восемнадцатого урока закрывает последствия. Второй — наращивать ShutdownTimeout до самого долгого штатного хендлера и следить, чтобы grace period по-прежнему покрывал его с запасом; цена — медленный деплой, потому что остановка ждёт именно этот хендлер. Третий — принять повторы как данность: короткий дожим, безопасные хендлеры, задача вернётся и доделается. Выбор зависит от цены повтора: для письма сгодится третий, для списания денег — только первый или второй. Что точно не работает — игнорировать вопрос: по умолчанию любой хендлер длиннее восьми секунд деплой будет рвать.

Чек-лист плавной остановки#

  • SIGTERM обрабатывается сам: Run ловит TERM, INT и TSTP — свой обработчик нужен только с Start.
  • Stop прекращает забор новых задач, активные дожимаются: очередь копит остальное.
  • Stop не завершает сервер: перед выходом обязателен Shutdown.
  • ShutdownTimeout покрывает самый долгий штатный хендлер; восемь секунд по умолчанию — отправная точка.
  • Лестница чисел: таймаут задачи ≤ ShutdownTimeout с запасом ≤ terminationGracePeriodSeconds.
  • Неуложившиеся задачи возвращаются в pending и исполняются заново: повторы должны быть безопасны.
  • Длинные хендлеры либо дробятся по ctx.Done(), либо получают увеличенный дожим — игнорировать нельзя.

Проверка: Stop без Shutdown

В коде со Start по сигналу вызвали только srv.Stop() и завершили main. Что не так?

Соберите ручной graceful

Допишите запуск и остановку сервера для web+воркера в одном процессе.

srv.(mux) // неблокирующий запуск // по SIGTERM из signal.Notify: srv.() // прекратить забор новых задач srv.() // дожать активных и закрыться

Соберите main web+воркера

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

Строки

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

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

Карточки: плавная остановка

Термин

Run(mux)

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

Блокирующий запуск: сервер стартует и ждёт сигнала; TERM и INT запускают Stop и Shutdown. Ошибки старта возвращаются сразу.

1 / 6

Плавная остановка собрана: сигналы обрабатывает сам asynq, Stop замораживает вход, Shutdown дожидается активных задач до ShutdownTimeout и возвращает неуложившиеся в очередь. Мини-эксперимент: поставьте задачу со сном на тридцать секунд, отправьте процессу SIGTERM и прочитайте логи по порядку — Stopping processor, дожим, Pushed task back to queue; поднимите воркер и увидите, что задача исполнилась с нуля. Формат прежний: код живёт локально с Go и Redis в Docker, песочница сайта показывает тематическую программу на чистом Go без Redis. Заведите себе деплойную привычку: после каждого выката сверяйте счётчики pending и retry в asynqmon до и после — ровные числа означают, что остановка воркеров настроена, а всплеск сразу подскажет, какой хендлер не влез в дожим. Дальше по курсу: двадцать четвёртый урок сведёт деплой-механику в чек-лист продакшена, а связка web+воркер через Start ждёт вас в двадцать первом. Одна репетиция SIGTERM локальным kill -TERM скажет о вашей настройке больше, чем любой чек-лист: дожим либо закрывается тихо, либо сразу называет хендлер, которому тесно.

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

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

Вывод

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

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

  • Деплой шлёт SIGTERM: asynq ловит TERM, INT и TSTP сам — обработчик сигналов нужен только при Start.
  • Stop прекращает забор новых задач; активные хендлеры продолжают работать.
  • Run блокируется до сигнала и делает всё сам; Start возвращается сразу — для web+worker в одном процессе.
  • Stop не завершает сервер: перед выходом обязателен Shutdown.
  • Shutdown дожидается активных задач до ShutdownTimeout — по умолчанию восемь секунд.
  • Неуложившиеся попытки снимаются, задачи возвращаются в pending и исполняются заново с нуля.
  • Лестница чисел: таймаут задачи ≤ ShutdownTimeout с запасом ≤ grace period оркестратора.
  • Возврат в очередь — это повтор: хендлеры обязаны быть идемпотентными.

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

Соберите деплой-эксперимент: задача со сном на тридцать секунд, ShutdownTimeout десять секунд, затем kill -TERM процессу. Проследите в логах полный цикл: Stopping processor, дожим, возврат задачи в очередь, выход. Поднимите воркер и убедитесь, что задача исполнилась заново с нуля. Потом поднимите ShutdownTimeout до сорока пяти секунд и убедитесь, что та же задача дожимается до успеха без повтора. В финале перепишите main на Start с собственным signal.Notify: по SIGTERM — Stop, затем Shutdown — это каркас для связки с HTTP-сервером из двадцать первого урока.

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

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

Как понять, что дожим проходит штатно, а не рвёт задачи?

Смотрите логи и счётчики. Нормальный цикл остановки заканчивается в логах фразой «All workers have finished» — хендлеры уложились сами. Если после каждого деплоя в очереди всплеск pending, дожим короче хендлеров: сверяйте ShutdownTimeout с самым долгим штатным хендлером и держите лестницу таймаутов целиком.

Нужно ли при деплое ждать опустошения очереди?

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

Нужно ли писать свой обработчик SIGTERM?

Только если сервер запущен через Start — например, рядом с HTTP-API: тогда по сигналу вызываются Stop и Shutdown вручную. С Run ничего писать не нужно: библиотека подписывается на TERM, INT и TSTP сама и проводит полный цикл остановки. Свой signal.Notify поверх Run конфликтовать не будет, но и пользы не даст.

Что произойдёт с задачей, если процесс убили SIGKILL?

Никакого graceful: ядро убивает процесс без обработчиков. Задача остаётся в active до истечения lease, затем recoverer вернёт её в retry — или в архив при исчерпании попыток. Это медленный и грубый путь против дожима, поэтому grace period оркестратора и ShutdownTimeout подбираются так, чтобы до SIGKILL дело не доходило.

Гарантирует ли дожим, что все задачи завершатся?

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

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

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

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

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

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

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

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