Динамическая многопроцессорная и сигнальная система Python

97
9

У меня есть настройка python multiprocessing (т.е. рабочие процессы) с пользовательской обработкой сигналов, что не позволяет работнику чисто использовать multiprocessing. (См. Подробное описание проблемы ниже).


Настройка


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


Здесь он повторно связывает свой собственный signal только для печати Master teardown; фактически полученные сигналы распространяются по дереву процессов и должны обрабатываться самими рабочими. Это достигается путем повторного связывания сигналов после появления рабочих.


class Midlayer(object):
def __init__(self, nprocs=2):
self.nprocs = nprocs
self.procs = []

def handle_signal(self, signum, frame):
log.info('Master teardown')
for p in self.procs:
p.join()
sys.exit()

def start(self):
# Start desired number of workers
for _ in range(nprocs):
p = Worker()
self.procs.append(p)
p.start()

# Bind signals for master AFTER workers have been spawned and started
signal.signal(signal.SIGINT, self.handle_signal)
signal.signal(signal.SIGTERM, self.handle_signal)

# Serve forever, only exit on signals
for p in self.procs:
p.join()


Основы классов рабочий multiprocessing.Process и реализует свой собственный run() -метод.


В этом методе он подключается к распределенной очереди сообщений и навсегда проверяет очередь на элементы. Навсегда должно быть: пока рабочий не получит SIGINT или SIGTERM. Работник не должен немедленно уходить; вместо этого он должен завершить любой расчет, который он делает, и будет уходить после этого (один раз quit_req установлен на True).


class Worker(Process):
def __init__(self):
self.quit_req = False
Process.__init__(self)

def handle_signal(self, signum, frame):
print('Stopping worker (pid: {})'.format(self.pid))
self.quit_req = True

def run(self):
# Set signals for worker process
signal.signal(signal.SIGINT, self.handle_signal)
signal.signal(signal.SIGTERM, self.handle_signal)

q = connect_to_some_distributed_message_queue()

# Start consuming
print('Starting worker (pid: {})'.format(self.pid))
while not self.quit_req:
message = q.poll()
if len(message):
try:
print('{} handling message "{}"'.format(
self.pid, message)
)
# Facade pattern: Pick the correct target function for the
# requested message and execute it.
MessageRouter.route(message)
except Exception as e:
print('{} failed handling "{}": {}'.format(
self.pid, message, e.message)
)


Проблема


Пока для базовой настройки, где (почти) все работает нормально:


    Мастер-процесс порождает нужное количество работников
    Каждый рабочий подключается к очереди сообщений
    Как только сообщение опубликовано, один из рабочих получает его
    Шаблон фасада (с использованием класса с именем MessageRouter) направляет полученное сообщение в соответствующую функцию и выполняет его

Теперь для задачи: Целевые функции (где message получает направленный факетом MessageRouter) может содержать очень сложную бизнес-логику, и поэтому может потребоваться многопроцессорная обработка.


Если, например, целевая функция содержит что-то вроде этого:


nproc = 4
# Spawn a pool, because we have expensive calculation here
p = Pool(processes=nproc)
# Collect result proxy objects for async apply calls to 'some_expensive_calculation'
rpx = [p.apply_async(some_expensive_calculation, ()) for _ in range(nproc)]
# Collect results from all processes
res = [rpx.get(timeout=.5) for r in rpx]
# Print all results
print(res)

Затем процессы, порожденные Pool, также перенаправят обработку сигналов для SIGINT и SIGTERM на рабочую функцию handle_signal (из-за распространения сигнала на поддерево процесса), по существу печатающей Stopping worker (pid: ...) и не останавливаясь вообще. Я знаю, что это происходит из-за того, что я повторно привязал сигналы для рабочего до того, как его дочерние процессы порождаются.


Вот где я застрял: Я просто не могу установить сигналы рабочих после нереста его дочерних процессов, потому что я не знаю, генерирует ли он некоторые из них (целевые функции маскируются и могут должны быть написаны другими), а также потому, что работник остается (как было разработано) в своем опросе. В то же время я не могу ожидать реализации целевой функции, которая использует multiprocessing для повторного связывания своих собственных обработчиков сигналов с любыми значениями по умолчанию.


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


Я что-то пропустил? У тебя есть какой-нибудь совет? Я был бы очень рад, если бы кто-нибудь мог дать мне подсказку о том, как решить недостатки моего дизайна здесь!

спросил(а) 2021-01-27T23:41:01+03:00 2 месяца, 2 недели назад
1
Решение
75

Я смог сделать это, используя Python 3 и set_start_method(method) с 'forkserver'. Другой способ Python 3 > Python 2!


Где "this" я имею в виду:


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

Поведение на Ctrl-C:


    Процесс диспетчера ожидает выхода рабочих.
    рабочие запускают свои обработчики сигналов (возможно, установлен флаг stop и продолжают выполнение, чтобы закончить работу, хотя я не беспокоился в своем примере, я просто присоединился к ребенку, которого знал), а затем вышел.
    все дети рабочих немедленно умирают.

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

Чтобы беспощадно подняться из документов:


Когда программа запускается и выбирает метод запуска forkserver, запускается процесс сервера. С тех пор всякий раз, когда требуется новый процесс, родительский процесс подключается к серверу и запрашивает его для нового процесса. Процесс вилочного сервера является однопоточным, поэтому безопасно использовать os.fork(). Никакие ненужные ресурсы не наследуются.

Доступно на платформах Unix, которые поддерживают передачу файловых дескрипторов над Unix-каналами.



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


Код во всей красе:


from multiprocessing import Process, set_start_method
import sys
from signal import signal, SIGINT
from time import sleep

class NormalWorker(Process):

def run(self):
while True:
print('%d %s work' % (self.pid, type(self).__name__))
sleep(1)

class SpawningWorker(Process):

def handle_signal(self, signum, frame):
print('%d %s handling signal %r' % (
self.pid, type(self).__name__, signum))

def run(self):

signal(SIGINT, self.handle_signal)
sub = NormalWorker()
sub.start()
print('%d joining %d' % (self.pid, sub.pid))
sub.join()
print('%d %s joined sub worker' % (self.pid, type(self).__name__))

def main():
set_start_method('forkserver')

processes = [SpawningWorker() for ii in range(5)]

for pp in processes:
pp.start()

def sig_handler(signum, frame):
print('main handling signal %d' % signum)
for pp in processes:
pp.join()
print('main out')
sys.exit()

signal(SIGINT, sig_handler)

while True:
sleep(1.0)

if __name__ == '__main__':
main()

ответил(а) 2021-01-27T23:41:01+03:00 2 месяца, 2 недели назад
76

Поскольку мой предыдущий ответ был только python 3, я думал, что также предлагаю более грязный метод для удовольствия, который должен работать как на python 2, так и на python 3. Не Windows, хотя...


multiprocessing просто использует os.fork() под обложками, поэтому поменяйте его на reset обработку сигнала в дочернем элементе:


import os
from signal import SIGINT, SIG_DFL

def patch_fork():

print('Patching fork')
os_fork = os.fork

def my_fork():
print('Fork fork fork')
cpid = os_fork()
if cpid == 0:
# child
signal(SIGINT, SIG_DFL)
return cpid

os.fork = my_fork


Вы можете вызвать это в начале метода выполнения ваших процессов Worker (чтобы вы не влияли на Менеджер), и поэтому убедитесь, что любые дети будут игнорировать эти сигналы.


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

ответил(а) 2021-01-27T23:41:01+03:00 2 месяца, 2 недели назад
75

Нет четкого подхода к решению проблемы в том виде, в котором вы хотите продолжить. Я часто нахожусь в ситуациях, когда мне приходится запускать неизвестный код (представленный как функции точки входа Python, который может сбиться с некоторой странности C) в средах многопроцессорности.


Вот как я подхожу к проблеме.


Основной цикл


Обычно основной цикл довольно прост, он извлекает задание из некоторого источника (HTTP, Pipe, Queb Queue..) и отправляет его в пул работников. Я уверен, что исключение KeyboardInterrupt правильно обработано для выключения службы.


try:
while 1:
task = get_next_task()
service.process(task)
except KeyboardInterrupt:
service.wait_for_pending_tasks()
logging.info("Sayonara!")

Рабочие


Рабочих управляет пул работников из multiprocessing.Pool или из concurrent.futures.ProcessPoolExecutor. Если мне нужны более продвинутые функции, такие как поддержка тайм-аута, я либо использую billiard, либо pebble.


Каждый работник игнорирует SIGINT, как рекомендовано здесь. SIGTERM остается по умолчанию.

Сервис


Служба управляется либо systemd, либо supervisord. В обоих случаях я уверен, что запрос завершения всегда поставляется как SIGINT (CTL + C).


Я хочу сохранить SIGTERM как аварийное завершение, а не полагаться только на SIGKILL. SIGKILL не переносится, и некоторые платформы не реализуют его.


"Я прошу, чтобы это было просто"


Если все сложнее, я бы рассмотрел использование фреймворков, таких как Luigi или Celery.


В целом, изобретать колесо на таких вещах довольно вредно и дает мало удовлетворения. Особенно, если кому-то придется смотреть на этот код.


Последнее предложение не применяется, если ваша цель - узнать, как это делается, конечно.

ответил(а) 2021-01-27T23:41:01+03:00 2 месяца, 2 недели назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема