Graceful shutdown: откуда берутся пятисотки при каждой выкатке
«При деплое немного ошибок» — не свойство природы, а следствие одного решения: что делает процесс, получив SIGTERM. Урок измеряет обе ветки на настоящем сервере под нагрузкой: немедленный выход обрывает все запросы в работе, дренаж не обрывает ни одного и стоит остаток начатой работы.
Полное техническое изложение
TL;DR
Остановка сервиса — это последовательность действий, а не мгновение. Процесс, которому велели остановиться, может либо исчезнуть сразу, либо сначала перестать принимать новую работу, закончить начатую и только потом выйти. Кто выходит сразу — обрывает всё, что было у него в руках; клиент получает разрыв вместо ответа, и решает это код процесса, а не платформа.
Отсюда главное следствие: «немного ошибок при каждой выкатке» — свойство кода, а не судьба. Измерено на одном и том же сервере, под одной нагрузкой и с сигналом в один и тот же момент: немедленный выход дал 20 оборванных запросов из 20, аккуратная остановка — 0 из 20. Разница только в обработчике.
Дальше — то, что отличает знающего от читавшего. Аккуратная остановка —
это два действия по порядку: сначала закрыть слушающий сокет, потом дождаться
уже начатого; обратный порядок под нагрузкой не сходится. Цена — остаток
начатой работы, а не льготный срок: измерено 164 мс против 2 при обработке в
300 мс и сигнале на её середине. Возможен дренаж только потому, что SIGTERM
перехватывается, а SIGKILL — нет, и отсюда граница измеренного нуля: 0 из 20
получилось на этом сервере, при этой нагрузке и потому, что вся начатая работа
уложилась в льготный срок. Дренаж обязан быть ограничен по времени; то, что не
успело, завершит SIGKILL, и обрывы вернутся.
- сервис — это процесс: его кто-то запускает и когда-нибудь останавливает;
- обработка запроса занимает время — между «запрос пришёл» и «ответ отправлен» есть промежуток;
- выкатка новой версии означает, что старую в какой-то момент останавливают.
SIGTERM,SIGKILL, льготный срок, коды выхода 143 и 137;- что такое слушающий сокет и очередь
accept, в которой соединения ждут приложение.
Что здесь на самом деле спрашивают
Лестница обычно такая:
- «Что происходит при остановке контейнера?» — разминка, отсылающая к
первому уроку линии:
SIGTERM, льготный срок,SIGKILL. - «Почему при выкатке появляются ошибки?» — здесь начинается содержание.
- «Что такое graceful shutdown?» — вопрос, на который часто отвечают «дождаться завершения», не уточняя чего именно.
- «В каком порядке закрывать?» — вопрос, отделяющий тех, кто это делал.
- «Что делать с долгими запросами, которые не влезают в льготный срок?» — вопрос про границы метода.
- «А балансировщик?» — вопрос про то, что дренаж начинается раньше сигнала.
Числа получены прогоном bench/shutdown/drain.py и
bench/shutdown/practice.py: настоящий сервер на loopback, двадцать
одновременных клиентов, обработка 300 мс, сигнал приходит на её середине.
База: остановка — это последовательность, а не мгновение
Прежде чем говорить о сигналах и дренаже, стоит назвать обычными словами, что вообще значит «остановить сервис». Потому что все ошибки в этой теме происходят от того, что остановку считают одним действием.
Остановка, устроенная по-человечески, — это четыре действия по порядку:
- перестать принимать новую работу — новые запросы больше не берутся, они уходят туда, где их обслужат;
- закончить начатую — те запросы, что уже в руках, доводятся до ответа;
- закрыть то, чем пользовались — соединения с базой, файлы, соединения с соседними сервисами;
- выйти.
Процесс, у которого этой последовательности нет, делает только четвёртое действие. И вот главный вопрос урока: что происходит с работой, которая была в руках, если сразу выполнить четвёртый шаг?
Ответ простой и неприятный: ничего не происходит — её просто больше нет. Процесса нет, значит, отвечать некому: клиент, который уже отправил запрос и ждёт, получает разрыв вместо ответа. Запрос при этом мог быть выполнен наполовину, и клиент об этом не узнает.
Отсюда и берутся «немного пятисоток при каждой выкатке». Это не свойство выкаток: это то, что процесс сделал со своей работой в момент, когда его попросили уйти.
Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Всё дальнейшее — про то, как процессу вообще сообщают об остановке, почему порядок первых двух действий важнее, чем кажется, и почему на всю эту вежливость отведён конечный срок.
Механизм 1: что именно обрывается
Первый режим — то, что делает процесс, у которого обработчика нет вовсе (первый урок линии) или у которого он написан как «выйти немедленно»:
1. EXIT ON SIGTERM, THE WAY A PROCESS WITHOUT A HANDLER DOES
------------------------------------------------------------
requests broken 20 of 20
time from SIGTERM to exit, ms 2
wait status: killed by signal 15
the same as a shell reports it 143
Двадцать из двадцати. Не «немного пятисоток», а все запросы, которые были в работе в этот момент. Их доля от общего потока мала только потому, что момент короткий: обрываются не проценты запросов, а те, кому не повезло попасть в секунду остановки.
Отсюда первое уточнение, которое стоит делать в разговоре: «0,1 % ошибок при деплое» — это не характеристика надёжности, а произведение длительности обработки на частоту выкаток. Тот же код при вдвое более долгих запросах даст вдвое больше ошибок, ничего не изменив в себе.
Механизм 2: дренаж — это два действия, а не одно
Второй режим отличается обработчиком. Он делает две вещи по очереди:
- закрывает слушающий сокет — новые соединения больше не принимаются;
- дожидается, пока закончится работа по уже принятым, и только потом выходит.
2. STOP ACCEPTING, FINISH WHAT IS STARTED, THEN EXIT
----------------------------------------------------
requests broken 0 of 20
time from SIGTERM to exit, ms 164
exit code 0
Ноль из двадцати. И заметьте цену: 164 мс против 2 — это остаток той работы, которая уже шла. Сигнал пришёл на середине трёхсотмиллисекундной обработки, значит дожидаться пришлось около половины.
Здесь важно назвать границу этого нуля, иначе из него получится неверное правило. Ноль оборванных — результат этого прогона: этот сервер, эта нагрузка, обработка в 300 мс и льготный срок, в который вся начатая работа поместилась с запасом. «Дренаж не обрывает ничего» — не свойство дренажа, а описание случая, когда работа успела закончиться. Правило формулируется от границы: дренаж доводит до конца только то, что укладывается в отведённое ему время, и потому он обязан быть ограничен по времени сам. Ниже видно, почему.
Порядок двух действий важен, и в этом суть вопроса на собеседовании. Если сначала «дождаться завершения», а закрывать сокет потом, то за время ожидания придут новые соединения — и дренаж не закончится никогда, пока идёт нагрузка. Если закрыть сокет и сразу выйти, обрываются те же самые запросы, что и в первом режиме.
Почему это вообще возможно, известно из первого урока: SIGTERM
перехватывается. Вторая половина того же правила — граница метода:
SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
SIGKILL и SIGSTOP нельзя перехватить, заблокировать или проигнорировать.
То есть дренаж работает только внутри льготного срока. Всё, что не успело
закончиться, будет оборвано, и никакой обработчик этого не изменит. Отсюда и
цена бесконечного ожидания: обработчик, который ждёт «сколько понадобится», не
получает больше времени — он получает SIGKILL в конце срока, и тогда
обрывается всё, что оставалось в работе. Ноль оборванных запросов держится не
на терпении обработчика, а на том, что работа успела закончиться до этого
момента.
Механизм 3: почему закрытие сокета — не то же самое, что отказ
Тонкость, которую стоит знать: закрыв слушающий сокет, вы перестаёте принимать новые соединения, но соединения, уже стоящие в очереди ядра, никуда не деваются. Из прошлого урока:
It extracts the first connection request on the queue of pending connections for
the listening socket.
Он извлекает первый запрос на соединение из очереди ожидающих соединений для слушающего сокета.
Соединения копятся в очереди ядра, а не приложения. Значит, в момент остановки существуют три разные группы запросов, и обходятся с ними по-разному:
- в работе — их доводит дренаж;
- в очереди
accept, но ещё не принятые — при закрытии слушающего сокета они будут сброшены; клиент получит разрыв, хотя приложение о них не знало; - ещё не пришедшие — они пойдут в другую реплику, если балансировщик уже знает, что эта уходит.
Отсюда третье действие, которое к самому процессу не относится: сначала вывести реплику из балансировки, и только потом останавливать. Дренаж внутри процесса не спасает от запросов, которые к нему всё ещё направляют.
Глубже: работа, которая не влезает в льготный срок
Дренаж ограничен льготным сроком, а значит, есть работа, которая в него не поместится: длинные выгрузки, потоковые ответы, большие транзакции. Здесь метод не спасает, и это стоит сказать самому.
Три способа, каждый со своей ценой.
Разрешить прервать. Если операция идемпотентна и клиент повторит её (урок про ретраи), обрыв стоит одного повтора. Это самый дешёвый вариант, и он же самый частый в жизни.
Сделать её возобновляемой. Ответ по частям с указанием позиции — клиент продолжит с того места, где оборвалось. Дороже в коде, дешевле в эксплуатации.
Вынести из запроса. Длинная работа переезжает в фоновую задачу, а запрос только ставит её в очередь. Тогда остановка процесса не обрывает ничего важного — а перезапуск фоновой задачи становится отдельным механизмом со своими свойствами.
Как отвечать на собеседовании
Короткий ответ: graceful shutdown — это два действия по порядку: перестать
принимать новые соединения и доработать уже принятые; и работает он только
потому, что SIGTERM перехватывается. Измерено на одном и том же сервере под
одинаковой нагрузкой: без обработчика оборвано 20 запросов из 20, с дренажем —
ноль, а цена дренажа — 164 мс против 2, то есть остаток уже начатой работы.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
Хороший ответ отличают три вещи. Первая — вы называете порядок: сокет
закрывается первым, иначе дренаж не кончится под нагрузкой. Вторая — вы
говорите про балансировщик: реплику выводят из ротации до сигнала, потому что
дренаж не отменяет запросы, которые к ней всё ещё направляют. Третья — вы
знаете границу: дренаж живёт внутри льготного срока, а SIGKILL не
перехватывается, поэтому долгую работу надо либо делать возобновляемой, либо
выносить из запроса.
И одна вещь, на которой легко переусердствовать. Сказать «с дренажем обрывов
не бывает» — значит выдать один прогон за свойство метода. Ноль из двадцати
получен на конкретном сервере, при конкретной нагрузке и при работе, которая
уложилась в льготный срок. Точная формулировка звучит так: дренаж доводит до
конца ровно то, что успевает уложиться в отведённое ему время, поэтому его
самого ограничивают по времени — иначе его завершит SIGKILL, и обрывы
вернутся.
Дальше спросят
В каком порядке закрывать: сначала дождаться или сначала закрыть сокет?
Сначала закрыть слушающий сокет, потом дождаться. Обратный порядок под нагрузкой не сходится: пока вы ждёте завершения текущих запросов, приходят новые, и дренаж не кончается, пока не кончится нагрузка.
В измеренном режиме дренажа обработчик делает ровно это: закрывает сокет и затем ждёт, пока счётчик активных обработок дойдёт до нуля. Отсюда и ноль оборванных запросов при остановке за 164 мс.
Достаточно ли дренажа внутри процесса?
Нет: он не влияет на то, кому балансировщик отправляет новые запросы. Пока реплика числится живой, к ней продолжают идти соединения, и закрытие слушающего сокета превращает их в разрывы.
Поэтому порядок при выкатке шире: сначала вывести реплику из ротации, дать
балансировщику это заметить, и только потом посылать SIGTERM. Внутри процесса
дренаж закрывает свою часть — начатую работу.
Что произойдёт с запросами, которые не успеют за льготный срок?
Они будут оборваны: по истечении срока приходит SIGKILL, и его нельзя ни
перехватить, ни заблокировать, ни проигнорировать. Ни один обработчик тут не
поможет — процесс просто перестаёт существовать.
Поэтому для долгой работы дренаж не решение, а отсрочка. Решают три вещи: идемпотентность и повтор, возобновляемый ответ или вынос работы из запроса в фоновую задачу.
Насколько дренаж замедляет выкатку?
На остаток уже начатой работы, а не на весь льготный срок. Измерено: 164 мс против 2 при обработке в 300 мс и сигнале на её середине — то есть примерно на половину одного запроса.
Ошибка в рассуждении, которую это исправляет: «дренаж стоит тридцать секунд» неверно. Тридцать секунд — это предел ожидания, а не цена; реальная цена равна времени самого долгого из запросов, которые были в работе.
Частые заблуждения
небольшой процент ошибок при выкатке неизбежен
Измерено: на одном и том же сервере, под той же нагрузкой и с тем же моментом сигнала — 20 оборванных запросов из 20 без дренажа и 0 из 20 с ним. Разница только в обработчике SIGTERM, то есть это свойство кода, а не выкатки. Ноль при этом не гарантирован методом: он получился потому, что вся начатая работа уложилась в льготный срок. Что не уложится — оборвёт SIGKILL.
graceful shutdown — это «дождаться завершения запросов»
Это два действия, и порядок в них решает всё: сначала закрыть слушающий сокет, потом дождаться. В обратном порядке под нагрузкой дренаж не кончится никогда — пока вы ждёте, приходят новые соединения.
дренаж стоит весь льготный срок
Он стоит остаток начатой работы. Измерено: 164 мс против 2 при обработке в 300 мс и сигнале на её середине. Льготный срок — это предел ожидания, а не цена: процесс уходит, как только доработал.
если внутри процесса есть дренаж, реплику можно останавливать в любой момент
Дренаж не влияет на балансировщик. Пока реплика числится живой, к ней идут новые соединения, и закрытие слушающего сокета превращает их в разрывы. Сначала выводят из ротации, дают это заметить — и только потом посылают сигнал.
дренаж спасает и долгие запросы, надо только поставить льготный срок побольше
До определённого предела — да, но граница жёсткая: по истечении срока приходит SIGKILL, а его cannot be caught, blocked, or ignored
(нельзя перехватить, заблокировать или проигнорировать). Долгую работу делают возобновляемой или выносят из запроса, а не удлиняют ожидание до бесконечности.
код выхода 0 при остановке означает, что всё прошло хорошо
Он означает ровно обратное: процесс не вышел сам, а был убит сигналом. Измерено: при немедленном выходе ожидание вернуло -15, то есть смерть от пятнадцатого сигнала (оболочка показывает это как 128 + 15 = 143), и двадцать оборванных запросов; при дренаже — код 0 и ноль оборванных. Но код ничего не говорит о запросах: он говорит только о том, кто и как завершил процесс.
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
abrupt = run("abrupt")
drained = run("drain")
print(abrupt["broken"])
print(drained["broken"])
print(abrupt["exit"])Практика · оцените
Проверка знаний
Сервер обрабатывает двадцать запросов и выходит немедленно по SIGTERM. Сколько запросов оборвётся?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Остановка сервиса — это последовательность действий, а не мгновение. Процесс, которому велели остановиться, может либо исчезнуть сразу, либо сначала перестать принимать новую работу, закончить начатую и только потом выйти. Кто выходит сразу — обрывает всё, что было у него в руках; клиент получает разрыв вместо ответа, и решает это код процесса, а не платформа.
- Отсюда главное следствие: «немного ошибок при каждой выкатке» — свойство кода, а не судьба. Измерено на одном и том же сервере, под одной нагрузкой и с сигналом в один и тот же момент: немедленный выход дал 20 оборванных запросов из 20, аккуратная остановка — 0 из 20. Разница только в обработчике.
- Дальше — то, что отличает знающего от читавшего. Аккуратная остановка — это два действия по порядку: сначала закрыть слушающий сокет, потом дождаться уже начатого; обратный порядок под нагрузкой не сходится. Цена — остаток начатой работы, а не льготный срок: измерено 164 мс против 2 при обработке в 300 мс и сигнале на её середине. Возможен дренаж только потому, что
SIGTERMперехватывается, аSIGKILL— нет, и отсюда граница измеренного нуля: 0 из 20 получилось на этом сервере, при этой нагрузке и потому, что вся начатая работа уложилась в льготный срок. Дренаж обязан быть ограничен по времени; то, что не успело, завершитSIGKILL, и обрывы вернутся.
На самом деле
- Измерено: на одном и том же сервере, под той же нагрузкой и с тем же моментом сигнала — 20 оборванных запросов из 20 без дренажа и 0 из 20 с ним. Разница только в обработчике
SIGTERM, то есть это свойство кода, а не выкатки. Ноль при этом не гарантирован методом: он получился потому, что вся начатая работа уложилась в льготный срок. Что не уложится — оборвётSIGKILL. - Это два действия, и порядок в них решает всё: сначала закрыть слушающий сокет, потом дождаться. В обратном порядке под нагрузкой дренаж не кончится никогда — пока вы ждёте, приходят новые соединения.
- Он стоит остаток начатой работы. Измерено: 164 мс против 2 при обработке в 300 мс и сигнале на её середине. Льготный срок — это предел ожидания, а не цена: процесс уходит, как только доработал.
- Дренаж не влияет на балансировщик. Пока реплика числится живой, к ней идут новые соединения, и закрытие слушающего сокета превращает их в разрывы. Сначала выводят из ротации, дают это заметить — и только потом посылают сигнал.
- До определённого предела — да, но граница жёсткая: по истечении срока приходит
SIGKILL, а его cannot be caught, blocked, or ignored. Долгую работу делают возобновляемой или выносят из запроса, а не удлиняют ожидание до бесконечности. - Он означает ровно обратное: процесс не вышел сам, а был убит сигналом. Измерено: при немедленном выходе ожидание вернуло
-15, то есть смерть от пятнадцатого сигнала (оболочка показывает это как128 + 15 = 143), и двадцать оборванных запросов; при дренаже — код 0 и ноль оборванных. Но код ничего не говорит о запросах: он говорит только о том, кто и как завершил процесс.
Что разобрано
- Что здесь на самом деле спрашивают
- База: остановка — это последовательность, а не мгновение
- Механизм 1: что именно обрывается
- Механизм 2: дренаж — это два действия, а не одно
- Механизм 3: почему закрытие сокета — не то же самое, что отказ
- Глубже: работа, которая не влезает в льготный срок
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
Источники и что читать дальше
2 ИСТОЧНИКА
- signal(7), Linux man-pages 6.7Официальная документация. Почему остановка вообще начинается с SIGTERM, а не с SIGKILL: в таблице сигналов у SIGTERM умолчательное действие `Term`, и он перехватывается, а про второй сказано прямо: «SIGKILL and SIGSTOP cannot be caught, blocked, or ignored» (SIGKILL и SIGSTOP нельзя перехватить, заблокировать или проигнорировать). Дренаж возможен ровно потому, что первый сигнал можно обработать.https://man7.org/linux/man-pages/man7/signal.7.html
- listen(2) и accept(2), Linux man-pages 6.7Официальная документация. Почему «перестать принимать» и «перестать отвечать» — разные действия: `accept` «extracts the first connection request on the queue of pending connections for the listening socket» (извлекает первый запрос на соединение из очереди ожидающих соединений для слушающего сокета), то есть соединения копятся в очереди ядра независимо от того, читает ли их приложение. Отсюда порядок дренажа: сначала закрыть слушающий сокет, потом доработать уже принятое.https://man7.org/linux/man-pages/man2/accept.2.html