用户衍生进程的生命周期#
当你从 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)