Статьи

Лицензия на SIGKILL

Силуэт секретного агента. Нет прозрачности и градиенты не используются.

Шниман, Ричард Шниман.

Каждая программа хочет жить вечно. Что происходит, когда программа вынуждена завершать работу до того, как она будет завершена, и зачем нам это делать?

Unix Signals

Не стесняйтесь пропустить, если вы знакомы с сигналами.

В Unix процессы могут связываться друг с другом с помощью заранее определенных сигналов. Вы можете увидеть список сигналов Unix здесь . Эта способность общаться чрезвычайно важна в процессно-ориентированной программе. Например, веб-сервер Puma может добавить параллелизм, порождая дочерние «рабочие» процессы. Он принимает запросы в главный процесс, а затем передает их следующему доступному дочернему элементу. Если система, в которой выполняется главный процесс Puma, должна быть закрыта или перезапущена, мы не просто хотим, чтобы все текущие запросы были остановлены на своем пути. Вместо этого мы хотим, чтобы дочерние работники завершили обработку запроса, если могли, очистили все внешние подключения или временные файлы, которые они могли сгенерировать, а затем завершили работу. Система может безопасно сделать это, отправив сигнал родительскому «главному» процессу, который затем, в свою очередь, будет отправлен дочерним процессам.

Возможно, вы видели фильм « Наследие Трона» . Фильм начинается с взлома хакера в корпоративной сети. Генеральный директор видит это и ловко отвечает, набрав в терминале команду $ kill -9 . Эта команда kill в linux (и Mac OS X) отправляет сигнал номер 9, который является SIGKILL , процессу. SIGKILL означает «конец без очистки». Это похоже на использование CTRL + ALT + DELETE в Windows (хотя Windows не POSIX- совместима и не поддерживает процессы).

Когда нам нужно, чтобы наши длительные процессы корректно завершались, сигнал SIGKILL слишком силен. Этот сигнал вынуждает процессы немедленно выходить из системы и может вывести вашу систему из строя. Что вы должны использовать вместо этого? Сигнал SIGTERM (сигнал № 15) является «сигналом завершения». Это говорит программе, что она должна остановить то, что она делает, и очистить перед выходом.

Живи и дай умереть

Когда Ruby получает сигнал SIGTERM он вызывает ошибку SignalException . В Ruby исключение может произойти в любой момент во время выполнения процесса, поэтому критический код очистки всегда должен использовать блок ensure .

 begin # do something ensure # clean up something end 

Здесь есть заметные предостережения, такие как исключение, которое может быть вызвано, когда блок обеспечения уже запущен, поэтому мы не всегда можем рассчитывать на его выполнение. Для более глубокого изучения ошибок в Ruby я рекомендую Avdi Exceptional Ruby . При этом все еще рекомендуется использовать блоки ensure для защиты вашего кода.

Поскольку у нас уже есть это безопасное поведение, Ruby использует его при возникновении SignalException . Чтобы убедиться в этом, мы можем написать тривиальный скрипт:

 Thread.new do begin while true sleep 1 end ensure puts "ensure called" end end current_pid = Process.pid signal = "SIGTERM" Process.kill(signal, current_pid) 

Когда вы запустите это, вы увидите:

 ensure called Terminated: 15 

Вы заметите, что в дополнение к «обеспечить вызов» мы также получаем номер сигнала, который был использован для выхода из процесса (15, что соответствует SIGTERM ). Ухоженная. Такое поведение действительно удобно, так как любая программа, которая имеет обработку ошибок, уже готова к корректному завершению. Помещая чувствительные операции в блок ensure , мы повышаем вероятность того, что программа будет делать правильные вещи. В конце концов, блоки ensure вызываются, и программа завершается. Обратите внимание, что если вы перезапустите программу с SIGKILL , она выйдет с другим номером, и мы не получим вывод из блока ensure .

Завтра не умрет никогда

Я уверен, что на вашем компьютере было разочаровывающее приложение, которое не зависало, сколько бы раз вы ни нажали кнопку «закрыть». Некоторые упрямые программы никогда не завершатся, независимо от того, сколько раз вы отправляете им SIGTERM . Это может произойти, когда программа застревает, пытаясь привести себя в порядок. Мы можем легко воспроизвести это:

 thread = Thread.new do begin while true sleep 1 end ensure while true puts "ensure called" sleep 1 end end end current_pid = Process.pid signal = "SIGTERM" Process.kill(signal, current_pid) 

Вывод будет выглядеть так:

 ensure called ensure called ensure called ensure called ensure called # ... ensure called 

Он никогда не закончится, пока машина не будет перезапущена или SIGKILL будет отправлен. Вместо этого тривиального примера легко представить, что ваша Ruby-программа ожидает завершения запроса к базе данных или сетевого вызова. Если он завис и ваша программа никогда не получит ответ, он никогда не завершится. Вот почему всегда важно создавать код, чувствительный к таймауту, хотя будьте осторожны с timeout.rb .

Важно отметить, что все блоки в области будут вызываться при возникновении SignalException . Это означает, что, помимо вашего собственного кода, будут вызываться все коды в любых зависимостях. Если ваша система зависает при выходе и вы не можете определить ошибочный ensure блок, который вы зафиксировали, это может быть из библиотеки, которую вы используете.

Скажи (д-р) нет сигнальной ловушке

Другой способ предотвратить выход из программы — использовать Signal.trap . Когда вы запустите этот код, сигнал будет зафиксирован, и программа не выйдет.

 Signal.trap('TERM') do puts "Die Another Day" end current_pid = Process.pid signal = "SIGTERM" Process.kill(signal, current_pid) 

Когда вы выполняете программу, вы получаете вывод "Die Another Day" но она продолжает выполняться. Можно поймать и повторно поднять один и тот же сигнал , однако это очень большой молоток. Мы не можем зависеть от сигнала, посылаемого в программу, и не можем полагаться на запуск этого кода в блоке. Хуже того, когда мы получаем сигнал, система нуждается в том, чтобы мы очистились и вышли как можно быстрее. Лучше всего использовать блоки ensure всегда, когда это возможно, и прибегать к перехвату сигналов только тогда, когда это действительно необходимо.

Из России с любовью и сигналами

До сих пор мы рассматривали, как ваш код Ruby обрабатывает сигналы, но как вы узнаете, какие сигналы отправлять? Перед любым перезапуском или отключением вы должны отправить SIGTERM для очистки, а затем отслеживать процесс, чтобы убедиться, что он отключается в разумные сроки. Если этого не произойдет, отправьте SIGKILL чтобы остановить процесс, завершив любые бесконечные блоки обеспечения. Вы должны отметить, что когда ваш процесс не выходит из SIGTERM это может означать, что, когда вы принудительно завершаете процесс, вы прерываете какую-то важную работу или процесс очистки. Компания Heroku , в которой я работаю, выполняет эти шаги каждый раз, когда вы развертываете или перезапускаете свое приложение. Если по какой-либо причине ваше приложение не выйдет вовремя, система выдает R12 — Exit Timeout и записывает ошибку в представлении панели мониторинга, чтобы вы могли исследовать ее позже.

Хотя это трудно осмыслить, исключение может остановить всю вашу программу в любое время, поэтому приятно знать, что для ensure необходимо добавить ensure в местах, где они уже есть, есть все. Работаете ли вы в секретной службе Ее Величества или в ИТ-отделе Казино Рояль, вы можете взять Квант милосердия, зная, что ваши программы могут выйти изящно.