我们首先明确几个概念:
在Ray中,当我们运行ray start命令时,会启动一个Ray节点,包括多个后台进程,其中核心的是raylet进程(负责资源管理和任务调度)。
使用subprocess.call或subprocess.Popen执行命令时,会创建一个子进程来执行该命令。
我们分两种情况:主节点和从节点。
主节点(组主节点)的进程创建流程:
main进程(运行Python脚本的进程)执行:
python
return_code = subprocess.call([‘ray’, ‘start’, ‘–head’, ‘–port=6379’, ‘–num-cpus=4’])
这里,main进程会创建一个子进程,我们称之为ray_start_head进程,该进程执行ray start --head …命令。
ray_start_head进程会启动Ray的头节点,这包括:
一个raylet进程(负责资源管理、任务调度等)
可能还有其他一些后台进程(如Redis、监控等,取决于Ray的配置)
当ray start命令执行完成(即Ray头节点启动成功后),ray_start_head进程就会退出。注意,ray start命令默认是启动Ray节点然后退出,不会保持运行。但是,它启动的Ray后台进程(如raylet)会继续运行。
然后,main进程继续执行,调用ray.init(),这会连接到刚刚启动的Ray头节点。
从节点(非主节点)的进程创建流程:
main进程执行:
python
process = subprocess.Popen([‘ray’, ‘start’, ‘–block’, f’–address={master_addr}:6379’, ‘–num-cpus=4’])
这里,main进程创建了一个子进程,我们称之为ray_start_worker进程,该进程执行ray start --block …命令。
注意,这里使用了–block参数,它表示ray start命令会阻塞运行,直到该Ray节点被关闭。
ray_start_worker进程会启动一个Ray工作节点,并连接到主节点的地址(master_addr:6379)。同样,它会启动一个raylet进程(以及其他后台进程)作为工作节点。
由于使用了–block,ray_start_worker进程会一直运行,直到我们手动关闭这个工作节点(例如通过ray stop或主节点关闭导致工作节点关闭)。
然后,main进程会继续执行其他任务(如下载模型、设置环境等),然后调用process.wait()。
process.wait()等待的是ray_start_worker进程(即执行ray start --block命令的进程)结束。而ray_start_worker进程结束的条件是:Ray工作节点被关闭。
当Ray工作节点被关闭(例如,主节点完成了任务并关闭了集群,或者出现了错误),ray_start_worker进程就会结束,然后process.wait()返回。
之后,main进程检查主节点的返回码,然后退出。
总结进程树:
主节点:
main进程(Python脚本)
ray_start_head进程(执行ray start --head,启动后很快退出)
raylet进程(以及其他Ray后台进程)作为头节点运行
从节点:
main进程(Python脚本)
ray_start_worker进程(执行ray start --block,一直运行直到工作节点关闭)
raylet进程(以及其他Ray后台进程)作为工作节点运行
注意:
在主节点中,我们使用subprocess.call,它会等待ray start命令完成(即头节点启动完成)后再继续。
在从节点中,我们使用subprocess.Popen,它不会等待,而是立即返回,然后我们可以在后台启动Ray工作节点的同时,继续执行其他任务(如下载模型)。然后我们通过process.wait()来等待Ray工作节点结束。
另外,从节点在process.wait()之后还会检查主节点的状态,如果主节点异常退出(返回码非0),则从节点会抛出异常,可能会触发重启。
这样设计的目的是让从节点在后台启动Ray工作节点,同时进行一些本地准备工作(如下载模型),然后等待Ray工作节点运行。当Ray工作节点退出时(通常是因为主节点关闭了集群),从节点再根据主节点的状态决定是否要重启。
希望这样详细的解释能帮助你理解整个过程。
是的,您的理解基本正确,但我需要更详细地澄清整个进程创建链。让我为您完整地描述所有创建的进程:
进程创建的全流程
主节点(组主节点)的进程链:
1. main (Python脚本进程,RANK=0或组主节点) │ ├─ 2. ray_start_head (subprocess.call创建的进程) │ │ │ ├─ 3. raylet (Ray的核心后台进程,主节点) │ │ │ ├─ 4. gcs_server (全局控制服务) │ │ │ ├─ 5. redis_server (Ray的状态存储) │ │ │ ├─ 6. dashboard (监控面板) │ │ │ └─ 7. log_monitor (日志监控) │ └─ 8. main继续执行,调用ray.init()连接到本地Ray集群详细说明:
- main进程:运行Python脚本的原始进程
- ray_start_head进程:由
subprocess.call(['ray', 'start', '--head', ...])创建,执行ray start命令 - raylet进程:Ray的核心守护进程,负责资源管理和任务调度
- 在主节点上作为头节点运行
- 监听6379端口等待工作节点连接
- 其他Ray服务进程:Ray自动创建的后台服务
关键点:subprocess.call()是阻塞的,它会等待ray start --head命令执行完成。而ray start --head命令在执行时会:
- 创建所有必要的Ray后台进程(raylet, gcs_server等)
- 等待这些进程启动成功
- 然后退出(ray_start_head进程结束)
- 但Ray的后台进程(raylet等)会继续运行
从节点(非主节点)的进程链:
1. main (Python脚本进程,RANK=非主节点) │ ├─ 2. ray_start_worker (subprocess.Popen创建的进程) │ │ │ ├─ 3. raylet (Ray工作节点进程) │ │ │ ├─ 4. log_monitor (日志监控) │ │ │ └─ 5. 其他Ray工作节点进程 │ ├─ 6. main并行执行:下载模型文件 │ ├─ 7. main并行执行:设置vLLM环境 │ └─ 8. process.wait() → 等待ray_start_worker进程结束进程间通信关系:
主节点: 从节点: main进程 main进程 | | |---创建---> ray_start_head进程 |---创建---> ray_start_worker进程 | | |---创建---> raylet(头节点) <------网络连接------> raylet(工作节点) (监听6379端口) (连接到主节点:6379) | | |---创建---> 其他Ray服务 |---创建---> 其他工作节点服务process.wait()等待的到底是什么?
您的理解是正确的:process.wait()等待的是从节点的ray_start_worker进程,而不是raylet进程。
关键区别:
# 主节点使用subprocess.callreturn_code=subprocess.call(['ray','start','--head',...])# call()会阻塞,直到ray_start_head进程结束# ray_start_head进程:创建Ray后台进程后就会退出# 从节点使用subprocess.Popenprocess=subprocess.Popen(['ray','start','--block',...])# Popen()立即返回,不阻塞# process.wait() 等待ray_start_worker进程结束--block参数的关键作用:
['ray','start','--block','--address=master:6379']有--block参数:
ray start命令会保持运行(不退出)- 它会监控Ray工作节点的状态
- 只有当Ray工作节点被正常关闭时,
ray start命令才会退出 - 所以
ray_start_worker进程会一直运行,直到Ray集群关闭
没有--block参数:
ray start命令在创建工作节点进程后就会立即退出ray_start_worker进程很快结束process.wait()会立即返回- 但Ray工作节点(raylet)会变成孤儿进程继续运行
完整的生命周期时序图:
时间轴: 主节点 从节点 t0: main进程启动 main进程启动 ↓ ↓ t1: subprocess.call(['ray', get_group_master_info() 'start','--head',...]) (获取主节点地址) 创建ray_start_head进程 ↓ t2: ray_start_head进程创建 subprocess.Popen(['ray', raylet等后台进程 'start','--block',...]) ↓ 创建ray_start_worker进程 t3: ray_start_head进程退出 ray_start_worker进程创建 (因为ray start完成) raylet工作节点 ↓ ↓ t4: main调用ray.init() main并行下载模型 连接到本地raylet ↓ ↓ main设置vLLM环境 t5: 等待从节点连接 ↓ while len(ray.nodes())... process.wait() ↓ (阻塞,等待ray_start_worker) t6: 所有从节点连接成功 ↓ 继续执行训练任务 ray_start_worker保持运行 ↓ (因为--block参数) t7: 训练任务完成 ↓ ↓ main等待中... t8: 关闭Ray集群 ↓ ray.shutdown() ray_start_worker收到关闭信号 ↓ ↓ t9: 主节点raylet关闭 从节点raylet关闭 ↓ ↓ t10: ray_start_worker进程结束 (--block不再阻塞) ↓ t11: process.wait()返回 ↓ t12: main检查主节点返回码 ↓ t13: main进程退出(exit(0))为什么这样设计?
1.进程控制与资源管理
ray_start_worker作为监督进程,确保Ray工作节点正常运行- 如果直接让
ray start退出,Ray工作节点会成为孤儿进程,难以管理
2.同步机制
process.wait()提供了一个同步点:只有当Ray集群关闭时,从节点才继续执行后续检查- 避免了从节点过早检查主节点状态
3.错误传播
- 如果Ray工作节点崩溃,
ray start --block会检测到并退出 process.wait()会返回非零退出码,可以从节点处理错误
4.优雅关闭
- 当主节点关闭集群时,所有工作节点能收到信号并有序关闭
- 避免了僵尸进程或资源泄漏
重要注意事项:
1.进程继承关系
# Ray的工作节点进程(raylet等)不是main进程的直接子进程# 它们是ray_start_worker进程的子进程# 当ray_start_worker进程结束时,其子进程(raylet)也会被终止# 这就是为什么需要--block:# 它确保ray_start_worker进程保持运行,从而保护raylet进程不被意外终止2.信号处理
# 如果从节点的main进程收到SIGTERM信号,它需要:# 1. 首先终止ray_start_worker进程# 2. ray_start_worker会终止其子进程(raylet等)# 3. 然后main进程可以安全退出3.多层级进程管理
操作系统视角的进程树: - main (pid 1000) - ray_start_worker (pid 1001, --block模式) - raylet (pid 1002) - log_monitor (pid 1003) 当需要关闭时: 1. kill ray_start_worker (pid 1001) 2. ray_start_worker会终止子进程1002, 1003 3. ray_start_worker进程结束 4. process.wait()返回 5. main进程退出这种设计虽然复杂,但提供了可靠的分布式进程管理,确保Ray集群中的所有节点都能正确启动、运行和关闭。
那为什么主节点的ray_start_head进程直接推出, 不担心ray_let 成为孤儿进程吗
这个问题其实涉及Ray的设计。在Ray中,当我们在命令行执行ray start --head时,它会启动一些后台守护进程(如raylet、gcs_server等),然后命令本身会退出。这些守护进程被设计为脱离当前shell会话,在后台运行。这是Ray的常见用法,类似于许多其他服务(如nginx、redis等)的启动方式。
为什么主节点的ray_start_head进程退出后,raylet不会成为孤儿进程?
这是因为Ray使用了进程组和会话管理,确保这些守护进程在启动它们的进程退出后仍然继续运行。具体来说:
双fork技术:很多服务程序使用双fork来脱离父进程,使得子进程被init进程(或systemd)接管,从而不会成为孤儿进程。Ray可能也使用了类似的技术。
setsid:创建新的会话,使得进程独立于启动它的终端。
Ray的进程管理:Ray可能使用了自己的进程管理机制,例如使用特殊的父进程(如监控进程)来管理这些守护进程。
但是,在我们提供的代码中,主节点使用subprocess.call调用ray start --head,这是一个阻塞调用,会等待ray start命令执行完毕。而ray start --head在启动完Ray的守护进程后就会退出,不会一直运行。因此,主节点的Python脚本(main进程)会在subprocess.call返回后继续执行。
那么,Ray的守护进程(如raylet)是否真的成为了孤儿进程?实际上,它们通常会被init进程(PID 1)接管,或者由Ray的进程监控机制管理。在Ray的设计中,这些守护进程应该是独立于启动它们的进程的,因此即使启动进程退出,它们也能继续运行。
但是,这里有一个需要注意的点:如果主节点的Python脚本(main进程)退出,那么由它启动的Ray守护进程是否会被终止?这取决于这些守护进程是否已经与启动进程分离。在Ray中,它们通常是分离的,所以不会。
然而,在实际生产环境中,我们通常希望Ray集群在主程序退出后仍然可以继续运行(例如,为了支持交互式任务或多次提交任务)。因此,Ray的设计允许启动命令退出,而守护进程继续运行。
那么,为什么从节点使用–block参数呢?
从节点的ray start命令使用–block参数是为了让ray start命令保持运行,直到Ray工作节点被关闭。这样,从节点的Python脚本可以通过process.wait()来等待Ray工作节点结束。这样做的好处是:
从节点的Python脚本可以知道Ray工作节点何时结束,从而可以执行一些清理操作或重新启动。
可以避免从节点的Ray工作节点在不需要时继续运行,造成资源浪费。
所以,主节点和从节点的ray start命令行为不同:
主节点:ray start --head 启动守护进程后退出,让守护进程在后台运行。
从节点:ray start --block --address=master:6379 启动守护进程并阻塞,直到工作节点被关闭。
这种设计使得主节点的Ray集群可以长期运行,而从节点可以通过阻塞来控制生命周期。
但是,这里有一个潜在的问题:如果主节点的Python脚本退出,那么如何关闭主节点的Ray集群?通常,我们可以通过ray stop命令来停止,或者主节点Python脚本在退出前调用ray.shutdown()。在我们的代码中,主节点在subprocess.call之后调用了ray.init(),这并不会关闭Ray集群,而是连接到已启动的Ray集群。然后,当主节点完成训练任务后,可以调用ray.shutdown()来关闭Ray集群,这会导致所有从节点的Ray工作节点也关闭。
总结:
主节点的ray_start_head进程退出后,Ray守护进程(如raylet)不会成为孤儿进程,因为它们被设计为独立运行,可能被init进程接管或由Ray的进程监控机制管理。
从节点使用–block参数是为了让ray start命令阻塞,从而可以通过process.wait()来等待工作节点结束,实现生命周期同步。
注意:在实际部署中,可能需要使用进程管理工具(如systemd、supervisor等)来管理Ray集群,确保其稳定运行和正确关闭。但是,在容器化环境中,通常使用单个容器作为一个节点,通过容器的生命周期来管理进程。在我们的代码中,从节点在Ray工作节点结束后会退出进程,这符合容器化部署的常见模式。
您提出了一个非常好的问题!主节点的ray_start_head进程退出而raylet不会成为孤儿进程,这是因为Ray使用了专门的进程管理机制。让我详细解释:
1. Ray的进程管理设计
Ray使用了"双fork"技术来确保守护进程(如raylet)在父进程退出后仍然正常运行:
进程创建流程的详细分解:
1. main进程(Python脚本) │ └─ 2. ray_start_head进程(执行`ray start --head`) │ ├─ 3. ray_start_head fork() → 创建中间进程 │ │ │ ├─ 4. 中间进程调用setsid()创建新会话 │ │ │ ├─ 5. 中间进程fork() → 创建raylet进程 │ │ │ │ │ ├─ 6. raylet进程脱离中间进程 │ │ │ (通过setsid和双重fork) │ │ │ │ │ └─ 7. raylet被init/systemd接管 │ │ │ └─ 8. 中间进程立即退出 │ └─ 9. ray_start_head进程退出(subprocess.call返回)2. 为什么raylet不会成为孤儿进程?
技术原理:
# Ray的C++源代码简化逻辑(raylet启动):intmain(){//第一次fork pid_t pid=fork();if(pid>0){//父进程(中间进程)立即退出 exit(0);}elseif(pid==0){//子进程(raylet)继续//创建新会话,脱离终端 setsid();//第二次fork(双fork技术) pid_t pid2=fork();if(pid2>0){exit(0);//中间父进程退出}elseif(pid2==0){//最终的raylet进程//现在被init/systemd接管(PID1) run_raylet();//运行raylet主循环}}}进程关系变化:
启动时: main(1000) → ray_start_head(1001) → 中间进程(1002) → raylet(1003) 启动完成后: raylet(1003) 现在被 init/systemd(PID 1) 接管 父进程ID变为1,不再是1002 中间进程(1002)和ray_start_head(1001)都已退出3. 对比:主节点 vs 从节点
主节点(无--block):
# ray start --head 的默认行为# 1. 启动所有必要的Ray守护进程# 2. 使用双fork技术使它们脱离# 3. 父进程(ray_start_head)立即退出# 4. 守护进程(raylet等)继续运行,被init/systemd接管# 验证方法:$ ps-ef|grep raylet raylet10031010:00?00:00:01/usr/local/bin/raylet# ↑ 父进程ID是1(init/systemd),不是启动它的进程从节点(有--block):
# ray start --block --address=...# 1. 启动Ray工作节点进程# 2. 但父进程(ray_start_worker)保持运行# 3. 它监控工作节点的状态# 4. 当需要关闭时,它负责清理# 验证方法:$ ps-ef|grep ray main20001999010:00?00:00:00/usr/local/bin/ray start--block...raylet20012000010:00?00:00:01/usr/local/bin/raylet# ↑ 父进程ID是2000(ray_start_worker),不是14. 为什么设计差异?
设计考量:
主节点(长期运行的服务): - 应该独立于启动脚本存在 - 即使Python脚本崩溃,Ray集群应继续运行 - 适合生产环境,作为服务运行 从节点(临时工作节点): - 生命周期与任务绑定 - 当主节点任务完成时,工作节点应该退出 - 需要监督进程(ray_start_worker)来管理生命周期实际执行差异:
主节点命令(在shell中执行的效果):
# 执行后立即返回$ ray start --head --port=6379LocalnodeIP:192.168.1.100... Ray runtime started.# ↑ 命令立即返回,但Ray进程在后台运行# 检查进程$psaux|grepray user10030.00.1... /usr/local/bin/raylet user10040.00.1... /usr/local/bin/gcs_server# 父进程都是1(init/systemd)从节点命令(在shell中执行的效果):
# 执行后会阻塞$ ray start --block --address=192.168.1.100:6379 Connecting to existing Ray cluster... Ray workernodestarted.# ↑ 命令会阻塞在这里,直到收到停止信号# 此时按Ctrl+C会停止所有Ray工作节点进程5. 进程状态验证
创建测试脚本:
# test_ray_process.pyimportsubprocessimporttimeimportos# 主节点模式print("=== 主节点模式(无--block)===")proc=subprocess.Popen(['ray','start','--head','--port=6380'])time.sleep(2)# 查看进程树os.system(f'pstree -p{proc.pid}2>/dev/null || echo "进程可能已退出"')os.system('ps aux | grep raylet | grep -v grep')proc.wait()print(f"ray start进程退出码:{proc.returncode}")# 从节点模式print("\n=== 从节点模式(有--block)===")proc2=subprocess.Popen(['ray','start','--block','--address=localhost:6380'])time.sleep(2)os.system(f'pstree -p{proc2.pid}')os.system('ps aux | grep raylet | grep -v grep')proc2.terminate()# 模拟停止proc2.wait()6. 实际容器/环境中的差异
在容器环境中:
# 主节点容器: # 启动脚本执行:ray start --head # 然后退出,但Ray进程继续运行 # 容器不会退出,因为raylet是PID 1的前台进程 # 从节点容器: # 启动脚本执行:ray start --block # 脚本阻塞,容器持续运行 # 当主节点关闭时,从节点收到信号,ray start退出 # 然后容器脚本继续执行后续清理并退出为什么这样设计?
- 服务化:主节点作为长期服务
- 生命周期管理:从节点需要被管理
- 资源清理:确保工作节点正确释放资源
- 故障隔离:主节点脚本崩溃不影响Ray服务
7. 关键验证
验证主节点raylet的父进程:
# 启动主节点$ ray start --head --port=6379&[1]2000# 查找raylet进程$psaux|grepraylet user20030.10.2... /usr/local/bin/raylet# 查看父进程$cat/proc/2003/stat2003(raylet)S12003...# 父进程ID是1!# 查看启动进程状态$jobs[1]+ Done ray start --head --port=6379# 已经完成,但raylet仍在运行验证从节点raylet的父进程:
# 启动从节点$ ray start --block --address=localhost:6379&[1]3000# 查找raylet进程$psaux|grepraylet|grep-v"grep"user30030.10.2... /usr/local/bin/raylet# 查看父进程$cat/proc/3003/stat3003(raylet)S30003003...# 父进程ID是3000(ray_start进程)# 查看进程树$ pstree -p3000ray(3000)───raylet(3003)───{raylet}(3004)总结
主节点的ray_start_head进程退出而raylet不会成为孤儿进程的原因:
- 双fork技术:Ray使用经典的双fork技术使守护进程脱离
- init/systemd接管:raylet最终被PID 1的进程接管
- 设计意图:主节点作为服务应独立于启动脚本
- 生命周期差异:
- 主节点:长期服务,独立运行
- 从节点:临时工作,需要监督
这种设计确保了Ray集群的高可用性:即使启动脚本崩溃,Ray服务本身仍能继续运行。而对于工作节点,则需要紧密控制生命周期,避免资源泄漏。