Так уж получилось, что появилась необходимость досрочно останавливать уже запущенный сайдкик-воркер. И, как уже всем причастным известно, запущенную задачу невозможно остановить штатными средствами — этого просто не предусмотрено архитектурой. И когда сайдкик-задача начала уже выполняться, то ее уже ничто не остановит. Конечно же, в интернетах тут же нашлось решение с убиением руби-процесса и с отменой перезапуска оного, но это решение по очевидным причинам не может устраивать ни разработчиков приложения, ни разработчиков сайдкика.
В итоге решение появилось откуда не ждали и оказалось крайне простым и очевидным. В общем и целом идею можно сформулировать очень коротко: сайдкик-процесс должен сам себя убивать, а мы лишь должны сказать ему когда это сделать. Весь код, что раньше просто запускался в сайдкике, будем запускать в отдельном трэде внутри процесса сайдкика. И, параллельно ему запустим трэд, который будет следить за указаниями извне чтобы сайдкик процесс убил себя сам. Получив указания убить себя, следящий тред убивает соседа и убивается сам.
В общем и целом такой подход не ограничивается только лишь при использовании сайдкика и долгоработающая фигня может быть где угодно. Поэтому давайте абстрагируемся от сайдкика и попробуем обобщить наш подход.
Во-первых нужно решить каким образом мы будем сообщать треду-убийце новость, что нужно умереть. И в случае с руби лучше всего воспользоваться внешним механизмом передачи сообщений и редис в данном случае подойдет идеально. И лучше встроенный в редис механизм передачи сообщений не использовать по нескольким причинам главная из которых — отсутсвие отложенного чтения. Если по каким-либо причинам трэд-киллер пропустит сообщение, то наш процесс убийства может вообще не состоятся. Допустим, мы попросим сайдкик-процесс умереть очень быстро — сразу после того, как его запустим. Есть шанс, что трэд-киллер еще вообще не будет существовать.
Итак, процесс проверки сообщения достаточно простой и прямолинейный:
until $redis.del("sidekiq:killer:#{self.identificator}") == 1 do
sleep 0.1
end
main_thread.kill
А метод identificator
будет отвечать за уникальную составляющую ключа в редисе.
Основной же трэд с кодовым именем "жертва" после своей естественной смерти должен оставить убедится, что киллер не будет его ждать вечно:
begin
self.perform_without_thread(*args)
rescue
$redis.set("sidekiq:killer:#{self.identificator}", 1)
end
Опять же, как настоящий киллер, наш трэд-убийца должен быть уверен, что жертва мертва:
until !!main_thread.status == false do
sleep 0.1
end
Ну, и в конце концов наш общий сайдкик-процесс дожидается окончания работы всех двух процессов:
[watcher_thread, main_thread].each(&:join)
А теперь давайте соединим вышеописанный код вместе, чтобы получить целостный модуль для прямого добавления в существующие долгоиграющие процессы:
module SidekiqKiller
def perform_with_thread(*args)
main_thread = Thread.new do
begin
self.perform_without_thread(*args)
rescue
$redis.set("sidekiq:killer:#{self.identificator}", 1)
end
end
watcher_thread = Thread.new do
until $redis.del("sidekiq:killer:#{self.identificator}") == 1 do
sleep 0.1
end
main_thread.kill
until !!main_thread.status == false do
sleep 0.1
end
end
[watcher_thread, main_thread].each(&:join)
end
alias_method_chain :perform, :thread
end
В нашей системе механизм приказного самоубийства воркеров использовался для принудительной остановки воркера отдельно запущенного экземпляра стейджинг-сервера. Механизм, конечно, безотказный, но слишком дорогой по памяти. В итоге эту часть системы переписали на чистом руби, оптимизировав и избавившись от сайдкика и сопутствующих библиотек.
Какой из этого можно сделать вывод? Да никакого, кроме того, что все мы смертны. Еще, наверное то, что, не стоит так делать, если нет уж очень острой необходимости.
Комментарии (16)
Dreyk
26.07.2016 10:20Килять треды — не самая лучшая затея
aratak
26.07.2016 11:09Очень рад, что вы согласны выводами статьи. К большому сожалению лучшего способа, чем килять рядом запущенный тред, который знает, что его могут в любое время кильнуть, найдено не было. Может быть есть какие-нибудь идеи?
Dreyk
26.07.2016 11:14+1я вначале тоже неправильно понял, что вы называете процессом.
ну я для себя решил так, что если, допустим, воркер не идемпотентный (как того прям все требуют, но мы-то знаем, что это не всегда реально), то я просто выключаю конкретно ему ретрай и просто грохаю весь процесс сайдкикаaratak
26.07.2016 20:54Выключение ретрая и убиение процесса сайдкика переводит эту джобу в состояние "fail", а хотелось бы считать это успешным завершением.
Кроме того, при обилии такого рода задач, запуск нового сайдкик-процесса с поднятием всего окружения делает это неоправданно дорогой манипуляцией.
iqiaqqivik
26.07.2016 11:58-1Я всегда рассуждаю так: если мне надо убить запущенный sidekiq-worker, значит я облажался с архитектурой и мне нужно подкрутить логику вокруг этого воркера, а не доставать из кармана 45-й.
aratak
26.07.2016 12:18И вы совершенно правы. В случае, описанным в статье пришлось вовсе отказаться от сайдкика.
iqiaqqivik
26.07.2016 12:40-1Случай в статье не описан, но я убежден, что отказ от сайдкика — такой же беспомощный костыль, как и убийство воркера.
Envek
26.07.2016 20:41Интересно, можно ли наподобие этого сделать механизм автоматического перезапуска всего процесса Sidekiq после выполнения тяжёлой по памяти задачи (чтобы вернуть память ОС, потому что та память, которую Ruby съел, он уже не отдаст до самой своей смерти)?
Сейчас у нас для этой задачи monit запряжён, рестартует sidekiq'а спустя полчаса после того, как тот станет потреблять больше полугига.
aratak
26.07.2016 20:48Для этого у сайдкика есть официальный API по работе с процессами, хотя я согласен с вами, что перезапуск извне выглядит эффективнее и надежнее.
iqiaqqivik
27.07.2016 09:43> та память, которую Ruby съел, он уже не отдаст до самой своей смерти
Ruby отдаст все, кроме того, что было аллоцировано под новые Ruby Heaps. Писать код так, чтобы количество отъеденных RValues (от которого напрямую зависит размер локального хипа) — довольно просто, при некоторой сноровке.Dreyk
27.07.2016 10:54по моим наблюдениям, со временем руби-процесс таки отдает практически все назад. Можете подсказать, где толково описано про RValues и Heap?
iqiaqqivik
27.07.2016 12:27+3https://ruby-hacking-guide.github.io/gc.html — про gc
https://blog.engineyard.com/2010/mri-memory-allocation-a-primer-for-developers — тоже полезно, чуть более прикладной вариант
http://stackoverflow.com/a/20608455/2035262 — совсем вкратце, мой ответ на SO про это
Надо бы заметку написать, но руки не доходят.
Nakilon
01.08.2016 19:07-1> сайдкик-процесс должен сам себя убивать, а мы лишь должны сказать ему когда это сделать
pipes? sockets? mmap?
> И в случае с руби лучше всего воспользоваться внешним механизмом передачи сообщений и редис в данном случае подойдет идеально.
> author: CTO at…
Рельсисты такие рельсисты…
Fikys
Привет, не очень понимаю смысла статьи, если у сайдкика есть API для работы с процессами. Был бы рад, если бы ты объяснил в чем преимущество твоего подхода :)
aratak
Я немного сбил вас с толку, называя запущенный воркер "сайдкик-процессом". Воркер работает внутри настоящего сайдкик-процесса и всю статью я пытаюсь остановить запущенный воркер, а процесс не трогать, так как ему еще предстоит обработать новые воркеры. API показывает как работать с запущенными процессами сайдкика, и команда
#stop!
останавливает весь сайдкик процесс, а не отдельно запущенный воркер.Вот разница при работе с процессами и в моем случае.
Сначала запустим процесс и вызовем метод
stop!
, как это предлагают сайдкикеры.Повторный запуск сайдкика приводит к тому, что он пытается перезапустить процессы, которые были помечены статусом "fail".
Теперь давайте используем воркера из статьи и килять будем воркер, а не процесс:
Во втором случае процесс готов обрабатывать следующую задачу, в отличие от первого, где он вообще выключается со всеми работающими процессами.