用户衍生进程的生命周期#

当你从 Ray worker 派生子进程时,你需要负责管理这些子进程的生命周期。然而,这并不总是可能的,特别是当 worker 崩溃或子进程是从库(如 torch dataloader)派生时。

为了避免用户派生的进程泄漏,Ray 提供了一些机制,当启动这些进程的 worker 退出时,可以杀死所有用户派生的进程。此功能可防止子进程(例如 torch)导致 GPU 内存泄漏。

我们有两个环境变量来处理 worker 退出时子进程的杀死问题

  • RAY_kill_child_processes_on_worker_exit(默认为 true):仅适用于 Linux。如果为 true,worker 退出时会杀死所有直接子进程。如果 worker 崩溃,此功能将不起作用。这不是递归的,孙子进程不会被此机制杀死。

  • RAY_kill_child_processes_on_worker_exit_with_raylet_subreaper(默认为 false):仅适用于 Linux 3.4 或更高版本。如果为 true,Raylet 会在 worker 退出后递归杀死由该 worker 派生的任何子进程和孙子进程。即使 worker 崩溃,此功能也有效。杀死操作会在 worker 死亡后 10 秒内发生。

在非 Linux 平台上,用户派生的进程不受 Ray 控制。用户需要负责管理子进程的生命周期。如果父 Ray worker 进程死亡,子进程将继续运行。

注意:此功能旨在作为杀死孤儿进程的最后手段。它不能替代正确的进程管理。用户仍应管理其进程的生命周期并正确清理。

Worker 退出时用户派生的进程被杀死#

以下示例使用 Ray Actor 派生一个用户进程。该用户进程是一个睡眠进程。 .. testcode

import ray
import psutil
import subprocess
import time
import os

ray.init(_system_config={"kill_child_processes_on_worker_exit_with_raylet_subreaper":True})

@ray.remote
class MyActor:
  def __init__(self):
    pass

  def start(self):
    # Start a user process
    process = subprocess.Popen(["/bin/bash", "-c", "sleep 10000"])
    return process.pid

  def signal_my_pid(self):
    import signal
    os.kill(os.getpid(), signal.SIGKILL)


actor = MyActor.remote()

pid = ray.get(actor.start.remote())
assert psutil.pid_exists(pid)  # the subprocess running

actor.signal_my_pid.remote()  # sigkill'ed, the worker's subprocess killing no longer works
time.sleep(11)  # raylet kills orphans every 10s
assert not psutil.pid_exists(pid)

启用此功能#

要启用 subreaper 功能,请在启动 Ray 集群时将环境变量 RAY_kill_child_processes_on_worker_exit_with_raylet_subreaper 设置为 true。如果 Ray 集群已在运行,则需要重新启动 Ray 集群才能应用更改。在运行时环境中设置 env_var 将不起作用。

RAY_kill_child_processes_on_worker_exit_with_raylet_subreaper=true ray start --head

另一种方法是在调用 ray.init() 时,通过添加 _system_config 来启用,如下所示

ray.init(_system_config={"kill_child_processes_on_worker_exit_with_raylet_subreaper":True})

⚠️ 注意:核心 worker 现在会回收僵尸进程,如果你需要等待 waitpid,请将其切换回去#

启用此功能后,worker 进程会成为 subreaper(详见下一节),这意味着可能会有一些孙子进程被重新分配父进程到该 worker 进程。为了回收这些进程,worker 将 SIGCHLD 信号设置为 SIG_IGN。这样 worker 在其子进程退出时就不会收到 SIGCHLD 信号。如果你需要等待子进程退出,则需要将 SIGCHLD 信号重置为 SIG_DFL

import signal
signal.signal(signal.SIGCHLD, signal.SIG_DFL)

底层原理#

此功能是通过在派生所有 Ray worker 的 Raylet 进程上设置 prctl(PR_SET_CHILD_SUBREAPER, 1) 标志来实现的。参见 prctl(2)。此标志使 Raylet 进程成为“subreaper”,这意味着如果一个后代子进程死亡,死亡子进程的子进程将重新分配父进程到 Raylet 进程。

Raylet 维护一个它派生的“已知”直接子进程 PID 列表,当 Raylet 进程收到 SIGCHLD 信号时,它就知道其某个子进程(例如 worker)已经死亡,并且可能存在被重新分配父进程的孤儿进程。Raylet 列出所有子进程 PID(ppid = raylet pid),如果一个子进程 PID 不是“已知”(即不在直接子进程 PID 列表中),Raylet 就认为它是孤儿进程,并通过 SIGKILL 将其杀死。

对于一个深层的进程创建链,Raylet 会逐步进行杀死操作。例如,在这样一个链中

raylet -> the worker -> user process A -> user process B -> user process C

the worker 死亡时,Raylet 会杀死 user process A,因为它不在“已知”子进程列表中。当 user process A 死亡时,Raylet 会杀死 user process B,依此类推。

一个边缘情况是,如果 the worker 仍然存活,但 user process A 死亡了,那么 user process B 会被重新分配父进程并面临被杀死的风险。为了缓解这种情况,Ray 也将 the worker 设置为 subreaper,这样它可以收养被重新分配父进程的进程。Core worker 不会杀死未知的子进程,因此像 user process B 这样比 user process A 寿命长的用户“守护”进程可以继续运行。但是,如果 the worker 死亡,用户守护进程会被重新分配父进程到 raylet 并被杀死。

相关 PR:Use subreaper to kill unowned subprocesses in raylet. (#42992)