Сайдкик-самоубийца

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


    image


    В итоге решение появилось откуда не ждали и оказалось крайне простым и очевидным. В общем и целом идею можно сформулировать очень коротко: сайдкик-процесс должен сам себя убивать, а мы лишь должны сказать ему когда это сделать. Весь код, что раньше просто запускался в сайдкике, будем запускать в отдельном трэде внутри процесса сайдкика. И, параллельно ему запустим трэд, который будет следить за указаниями извне чтобы сайдкик процесс убил себя сам. Получив указания убить себя, следящий тред убивает соседа и убивается сам.


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


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


    Итак, процесс проверки сообщения достаточно простой и прямолинейный:


      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

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


    Какой из этого можно сделать вывод? Да никакого, кроме того, что все мы смертны. Еще, наверное то, что, не стоит так делать, если нет уж очень острой необходимости.


    image

    Поделиться публикацией

    Похожие публикации

    Комментарии 16
      +2

      Привет, не очень понимаю смысла статьи, если у сайдкика есть API для работы с процессами. Был бы рад, если бы ты объяснил в чем преимущество твоего подхода :)

        0

        Я немного сбил вас с толку, называя запущенный воркер "сайдкик-процессом". Воркер работает внутри настоящего сайдкик-процесса и всю статью я пытаюсь остановить запущенный воркер, а процесс не трогать, так как ему еще предстоит обработать новые воркеры. API показывает как работать с запущенными процессами сайдкика, и команда #stop! останавливает весь сайдкик процесс, а не отдельно запущенный воркер.


        Вот разница при работе с процессами и в моем случае.


        Сначала запустим процесс и вызовем метод stop!, как это предлагают сайдкикеры.


        INFO: Starting processing, hit Ctrl-C to stop
        TestProcessWorker JID-e0d48711e21cd37c90e98186 INFO: start
        
        # в этом месте я вызвал метод `stop!` на процессе из соседней вкладки
        
        INFO: Shutting down
        INFO: Terminating 49 quiet workers
        INFO: Pausing up to 30 seconds to allow workers to finish...
        WARN: Terminating 1 busy worker threads
        WARN: Work still in progress [#<struct Sidekiq::BasicFetch::UnitOfWork queue="queue:default", message="{\"class\":\"TestProcessWorker\",\"args\":[1],\"retry\":false,\"queue\":\"default\",\"jid\":\"e0d48711e21cd37c90e98186\",\"created_at\":1469519195.722636,\"enqueued_at\":1469519195.722796}">]
        INFO: Pushed 1 messages back to Redis
        TestProcessWorker JID-e0d48711e21cd37c90e98186 INFO: fail: 121.278 sec

        Повторный запуск сайдкика приводит к тому, что он пытается перезапустить процессы, которые были помечены статусом "fail".


        Теперь давайте используем воркера из статьи и килять будем воркер, а не процесс:


        INFO: Starting processing, hit Ctrl-C to stop
        TestKillerWorker JID-e0d48711e21cd37c90e98186 INFO: start
        
        # в этом месте кладем в редис соответствующий киллер-ключ
        
        TestKillerWorker JID-e0d48711e21cd37c90e98186 INFO: done: 0.396 sec

        Во втором случае процесс готов обрабатывать следующую задачу, в отличие от первого, где он вообще выключается со всеми работающими процессами.

        0
        Килять треды — не самая лучшая затея
          0

          Очень рад, что вы согласны выводами статьи. К большому сожалению лучшего способа, чем килять рядом запущенный тред, который знает, что его могут в любое время кильнуть, найдено не было. Может быть есть какие-нибудь идеи?

            +1
            я вначале тоже неправильно понял, что вы называете процессом.

            ну я для себя решил так, что если, допустим, воркер не идемпотентный (как того прям все требуют, но мы-то знаем, что это не всегда реально), то я просто выключаю конкретно ему ретрай и просто грохаю весь процесс сайдкика
              0

              Выключение ретрая и убиение процесса сайдкика переводит эту джобу в состояние "fail", а хотелось бы считать это успешным завершением.


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

          –1
          Я всегда рассуждаю так: если мне надо убить запущенный sidekiq-worker, значит я облажался с архитектурой и мне нужно подкрутить логику вокруг этого воркера, а не доставать из кармана 45-й.

            0

            И вы совершенно правы. В случае, описанным в статье пришлось вовсе отказаться от сайдкика.

              –1
              Случай в статье не описан, но я убежден, что отказ от сайдкика — такой же беспомощный костыль, как и убийство воркера.

            0

            Интересно, можно ли наподобие этого сделать механизм автоматического перезапуска всего процесса Sidekiq после выполнения тяжёлой по памяти задачи (чтобы вернуть память ОС, потому что та память, которую Ruby съел, он уже не отдаст до самой своей смерти)?


            Сейчас у нас для этой задачи monit запряжён, рестартует sidekiq'а спустя полчаса после того, как тот станет потреблять больше полугига.

              0

              Для этого у сайдкика есть официальный API по работе с процессами, хотя я согласен с вами, что перезапуск извне выглядит эффективнее и надежнее.

                0
                А холодный перезапуск всего юнита выглядит вообще как панацея.
                0
                > та память, которую Ruby съел, он уже не отдаст до самой своей смерти
                Ruby отдаст все, кроме того, что было аллоцировано под новые Ruby Heaps. Писать код так, чтобы количество отъеденных RValues (от которого напрямую зависит размер локального хипа) — довольно просто, при некоторой сноровке.
                  0
                  по моим наблюдениям, со временем руби-процесс таки отдает практически все назад. Можете подсказать, где толково описано про RValues и Heap?
                    +3
                    https://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 про это

                    Надо бы заметку написать, но руки не доходят.
                –1
                > сайдкик-процесс должен сам себя убивать, а мы лишь должны сказать ему когда это сделать

                pipes? sockets? mmap?

                > И в случае с руби лучше всего воспользоваться внешним механизмом передачи сообщений и редис в данном случае подойдет идеально.
                > author: CTO at…

                Рельсисты такие рельсисты…

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

                Самое читаемое