【问题标题】:Avoid killing children when parent process is killed父进程被杀死时避免杀死子进程
【发布时间】:2018-03-18 17:52:26
【问题描述】:

我在基于 flask 的 Web 应用程序中使用库 multiprocessing 来启动长时间运行的进程。执行此操作的函数如下:

def execute(self, process_id):
    self.__process_id = process_id
    process_dir = self.__dependencies["process_dir"]
    self.fit_dependencies()
    process = Process(target=self.function_wrapper, name=process_id, args=(self.__parameters, self.__config, process_dir,))
    process.start()

当我想在这个 Web 应用程序上部署一些代码时,我重新启动了一个服务,该服务重新启动 gunicorn,由 nginx 提供服务。我的问题是,此重新启动会杀死此应用程序启动的所有子进程,就好像 SIGINT 信号已发送给所有子进程一样。我怎么能避免呢?

编辑: 阅读this post 后,看来这种行为是正常的。答案建议改用 subprocess 库。所以我重新提出我的问题:如果我想在 python 脚本中启动长时间运行的任务(它们是 python 函数)并确保它们能够在父进程中生存 OR 确保父进程进程(这是一个 gunicorn 实例)会在部署中存活下来吗?

最终编辑:我选择了@noxdafox 答案,因为它更完整。首先,使用进程排队系统可能是这里的最佳实践。然后作为一种解决方法,我仍然可以使用 multiprocessing 但在函数包装器中使用 python-daemon 上下文(请参阅here ans here)。最后,@Rippr 建议将 subprocess 与不同的进程组一起使用,这比使用 multiprocessing 分叉更干净,但涉及启动独立函数(在我的情况下,我从导入的库)。

【问题讨论】:

  • 一目了然,我会尝试将此方法从烧瓶应用程序中分离出来,使其成为一个完全独立的进程。这样,子进程就不会随着烧瓶应用程序而死。例如,您可以将这项工作委托给 celery 应用程序。
  • 谢谢。芹菜似乎是主要推荐的选择。但这涉及向我的项目添加更多依赖项,我想避免这种情况。真的没有办法从 python 脚本中启动一个完全独立的进程吗?
  • 我还是不明白为什么会这样。孩子们应该成为 init 的孩子,直到他们终止他们的代码。
  • 取决于您与该进程通信的需求,根据您使用的是 Python2 还是 Python3 以及您使用的平台,实现可能会有所不同。查看this question,它可能会为您提供一些答案。
  • 我处理与文件的通信。我只是想了解为什么孩子们被关闭而不是成为孤儿..

标签: python nginx flask gunicorn python-multiprocessing


【解决方案1】:

我会建议您反对您的设计,因为它很容易出错。更好的解决方案是使用某种排队系统(RabbitMQCeleryRedis、...)将工作人员与服务器分离。

不过,您可以尝试以下几个“技巧”。

  1. 将您的子进程变成 UNIX 守护进程。 python daemon 模块可以作为一个起点。
  2. 指示您的子进程忽略SIGINT 信号。如果子进程拒绝终止,服务编排器可能会通过发出SIGTERMSIGKILL 信号来解决这个问题。您可能需要禁用此类功能。

    为此,只需在 function_wrapper 函数的开头添加以下行:

    signal.signal(signal.SIGINT, signal.SIG_IGN)
    

【讨论】:

  • 谢谢,这是一个很好的总结。您能否详细说明“这很容易出错”部分?
  • 你觉得subprocess的使用怎么样?
  • 正如我所说,一些框架试图防止子进程泄漏。因此,即使忽略SIGINT,您也可能会看到子进程被硬杀死。有更好的解决方案可以实现您想要实现的目标:例如,您可以通过 docker-composesupervisor 编排微服务。 systemd 也可以满足您的目的。这些系统旨在完成您尝试使用裸 python 实现的目标。
  • 要使用subprocess 运行python 函数,您需要将其写入单独的文件并通过Popen 调用它。这将增加相当多的开销,因为您基本上是在启动一个全新的 Python 解释器。 multiprocessing 相反(在 UNIX 上)巧妙地分叉父进程,这要快得多。
【解决方案2】:

除了@noxdafox 的优秀answer,我认为您可以考虑这个替代方案:

subprocess.Popen(['nohup', 'my_child_process'], preexec_fn=os.setpgrp)

基本上,子进程被杀死是因为它们与父进程属于同一进程组。通过添加preexec_fn=os.setpgrp 参数,您只是请求子进程在它们自己的进程组中生成,这意味着它们不会收到终止信号。

解释取自here

【讨论】:

  • 感谢您对进程组的解释。为了适应我的情况,能够执行以下操作将是完美的:multiprocessing.Process(function_wrapper, args, preexec_fn=os.setpgrp)。
  • 不幸的是,multiprocessing 尚不支持进程组,尽管它的实现有一个存根,check this
【解决方案3】:

最终,这个问题归结为对部署的意义的误解。在 Python 等非企业语言中(与 Erlang 等企业语言相比),通常认为部署消除了运行进程的任何前述人工制品。因此,如果您的旧子/函数在执行新部署后实际上并未终止,这显然是一个错误。

要扮演魔鬼的拥护者,从您的问题/规范中甚至不清楚您对部署的实际期望是什么——您只是希望您的旧“功能”永远运行吗?这些功能是如何开始的?谁应该知道这些“功能”是否在给定的部署中被修改,以及它们是否应该重新启动以及以什么方式?在 Erlang/OTP(与 Python 无关)中对这些问题进行了很多考虑,因此,当您使用像 Python 这样甚至没有设计的语言时,您不能简单地期望机器能够读懂您的想法对于这样的用例。

因此,将长时间运行的逻辑与其余代码分开并适当地执行部署可能是一个更好的选择。正如另一个答案所提到的,这可能涉及直接从 Python 中生成一个单独的 UNIX daemon,或者甚至可能使用完全独立的逻辑来处理这种情况。

【讨论】:

  • 这里的部署是指Web应用有时需要更新(新代码替换旧代码)。但我希望应用程序启动的进程继续运行,因为它们是原子计算,即与网络应用程序本身无关。将它们转换为 UNIX 守护程序对我来说似乎是一个不错的选择。
  • python 不是专门为此设计的这一事实并不是什么大问题。有很多方法可以设计这样的进程管理,但正如我的问题所暗示的那样,这是一件棘手的事情,因为它与内部 UNIX 结构高度相关。