Перейти к содержанию

Устойчивые сервисы: таймауты, повторы и корректное завершение

Начало урока

Устойчивые сервисы: таймауты, повторы и корректное завершение

Как удержать Node-сервис в рабочем состоянии, когда его зависимости, сеть и оркестратор ведут себя плохо одновременно.

Главная идея: устойчивый сервис не предполагает, что его база данных, кэш, нижестоящие API или машина, на которой он работает, надёжны. Он считает частичный отказ нормальным штатным состоянием и построен так, чтобы деградировать, восстанавливаться и перезапускаться без потери запросов и порчи состояния. Инструменты для этого образуют небольшой, хорошо изученный набор: ограниченные таймауты, дисциплинированные повторы с задержкой, предохранители (circuit breakers), честные проверки жизнеспособности и готовности и корректное завершение по SIGTERM.
К концу вы сможете:
  • дренировать выполняющуюся работу и завершаться чисто, когда оркестратор присылает SIGTERM;
  • отличать проверку жизнеспособности (liveness) от проверки готовности (readiness) и знать, какая из них управляет трафиком;
  • ставить жёсткий таймаут на каждый исходящий вызов зависимости с помощью AbortController;
  • повторять только идемпотентную работу с экспоненциальной задержкой и джиттером -- и знать, когда повторять НЕЛЬЗЯ;
  • оборачивать ненадёжную зависимость в предохранитель, чтобы один больной бэкенд не уронил вас.

Эта тема опирается на Архитектуру микросервисов (как сервисы декомпозируются) и Логирование и наблюдаемость (как вы видите происходящее). Здесь мы сосредоточены исключительно на выживании при частичном отказе. Предполагается, что вы уже уверенно владеете промисами, async/await и циклом событий Node.

1. Корректное завершение: SIGTERM -- это просьба, а не убийство

На платформе контейнеров (Kubernetes, ECS, Nomad или просто docker stop) перезапуск -- рутина: новый деплой, уменьшение масштаба, вывод узла из работы или проваленная проверка здоровья -- всё это останавливает ваш процесс. Платформа делает это в два шага. Сначала она посылает SIGTERM -- вежливое "пожалуйста, завершайтесь и выходите". Затем, после льготного периода (30 секунд по умолчанию в Kubernetes), если вы всё ещё работаете, она посылает SIGKILL, который нельзя перехватить, заблокировать или обработать, и который завершает вас мгновенно.

Наивный сервис игнорирует SIGTERM. Действие Node по умолчанию для этого сигнала -- немедленный выход, поэтому каждый обслуживаемый в этот момент запрос обрывается на середине ответа, открытые транзакции БД брошены, а клиент видит сброс соединения. Корректное завершение означает перехват SIGTERM и по порядку:

  1. Перестать принимать новую работу -- вызвать server.close(), который прекращает слушать новые соединения, но позволяет выполняющимся запросам завершиться.
  2. Провалить готовность -- начать возвращать "не готов", чтобы балансировщик перестал направлять новый трафик на этот экземпляр.
  3. Дренировать выполняющиеся запросы -- дождаться завершения текущих обработчиков, ограничившись таймаутом.
  4. Закрыть зависимости -- завершить пулы БД, сбросить логи, отключиться от брокера сообщений.
  5. Выйти с кодом 0.

Аналогия. SIGTERM -- это просьба магазину закрыться. Аккуратный магазин запирает переднюю дверь, чтобы новые покупатели не входили, обслуживает уже находящихся внутри, подбивает кассу, затем гасит свет. Неаккуратный магазин просто вырубает питание, когда покупатели на середине оплаты. SIGKILL и есть отключение питания -- с ним ничего нельзя поделать, именно поэтому вы хотите закончить до его прихода.

Критическая деталь: к дренированию нужно добавить жёсткий дедлайн. Если медленный запрос никогда не завершится, бесконечное ожидание означает, что SIGKILL в итоге всё равно убьёт вас на середине работы -- худшее из обоих миров. Дайте дренированию, скажем, 25 секунд (комфортно меньше 30с льготного периода платформы) и вызовите process.exit(1), если оно превышено, залогировав, какие запросы оставались открытыми.

Ещё одна тонкость в Kubernetes: есть гонка между удалением платформой вашего пода из эндпоинтов Service и отправкой SIGTERM. В течение краткого окна балансировщик может слать вам трафик уже после начала завершения. Обычное решение -- поспать пару секунд после получения SIGTERM (или сначала провалить готовность и выждать) перед вызовом server.close(), чтобы удаление эндпоинта успело распространиться.

Продолжите этот урок на YourAIZone

В полном уроке — объяснение целиком, разобранные примеры, частые ошибки, тесты с мгновенной проверкой и ИИ-репетитор. Начните сразу как гость или создайте аккаунт, чтобы сохранять прогресс.

В полном уроке

  • Объяснение целиком
  • Разобранные примеры
  • Частые ошибки и как их избежать
  • Тестовые вопросы: 15
  • ИИ-репетитор, который отвечает на вопросы по теме
  • Отслеживание прогресса для ученика, родителей и учителя