虽然讲清楚Ray远程调试的步骤很容易,但是希望能做到知其所以然,因此本文首先从VSCode的调试原理讲起。
VSCode是如何调试Python程序的
VSCode的调试架构是一个客户端-服务器模型,其客户端是VSCode,提供调试界面、发送调试命令。服务端则是调试器debugpy,管理 Python 调试会话,如断点注入、执行控制等。其中的通信协议是调试适配器协议( Debug Adapter Protocol, DAP)。这里就要重点介绍一下debugpy了。debugpy支持两种模式,launch和attach。
launch模式
launch模式就是我们一般使用的方式,即由IDE启动程序,此时debugpy 被自动注入,通过内部机制(如本地进程间通信 IPC) 与 Python 程序通信。launch模式适合本地开发调试。
此时你的lauch.json大概是这个样子:
|
|
attach模式
attach模式则是将调试器附加到一个已经运行的 Python 进程上进行调试的方法。通常情况下,我们在终端启动程序而不是用IDE,而debugpy通过在指定端口(默认是5678)启动一个本地 TCP 服务来监听客户端的连接,从而实现调试功能。
启动attach模式,需要在被调试代码中添加以下代码:
|
|
关键参数说明:
debugpy.listen(("0.0.0.0", 5678))
0.0.0.0
:允许任意 IP 连接(如果只允许本地连接,用"localhost"
)。5678
:默认调试端口(可自定义,但需与 VSCode 配置一致)。
debugpy.wait_for_client()
程序会在此处暂停,直到 VSCode 连接。
同时也要在launch.json中做类似以下的配置:
|
|
由于attach模式的灵活性(不依赖IDE),因此attach模式非常适合远程调试、Docker、分布式。这些情况在采用launch模式进行调试时会面临以下问题:
- 远程服务器:远程服务器上没有VSCode。
- Docker 容器:VSCode 无法 launch 容器内的 Python。
- 分布式训练:进程是由调度系统(不是 IDE)启动的,甚至多个节点。
当然,熟悉VSCode的读者知道,针对前两种情况,VSCode有相应的插件支持,即Remote - SSH 和Docker插件。我们以Remote - SSH 插件为例说明这两个插件的机制(对于Docker插件,把容器类比远程服务器即可):
- 本地 VSCode 通过 SSH 登录远程服务器;
- 它会在远程服务器上安装一个轻量的 VSCode Server;
- 之后你在本地 VSCode 操作的任何“launch”命令,其实是在远程服务器上启动 Python 脚本;
- 这样launch模式就可以正常用了,就像在本地一样
那么问题来了:Ray分布式调试有类似的插件使用launch吗?答案是,目前没有。这是因为分布式调试的问题是,Ray是多节点多进程框架,Worker 和 Driver 进程通常由 Ray 集群调度系统启动,IDE 无法统一启动所有节点。所以,Ray调试目前只能基于attach模式。幸运的是,Ray官方提供了一个基于debugpy attach调试的插件Ray Distributed Debugger(RDD),可以轻松的进行调试。
如何用RDD进行本地调试
在讨论如何用RDD进行远程调试前,我们先复习一下文档,看看如何用RDD进行本地调试,更重要的是,看看RDD是如何和Debugpy联系起来的。
调试步骤
步骤1:启动Ray的head节点
可以通过命令行:
|
|
也可以在python程序中使用:
|
|
步骤2:打断点
通过breakpoint()
在分布式代码中打上断点。官方给出了一个job.py
示例:
|
|
步骤3:运行Ray程序
|
|
运行后你应该会看到类似的三行:
这三个端口分别是:
- 6379:Ray的GCS (Global Control Store),使用Redis作为后端,用作 Ray 的全局状态存储和调度协调中心。
- 8265:是 Ray Dashboard 的默认端口。Ray Dashboard 是一个 Web UI,展示集群运行状态、任务执行、资源使用、日志等。更重要的是,虽然调试器会通过6379端口发现集群,但实际调试数据(如堆栈、指标)仍需从 8265 端口获取。
- 60693:这是RDD为某个Worker 启动的debugpy调试端口,也即debugpy需要去attach的端口,通常是随机的。
步骤4:配置RDD插件
- 打开插件面板,点击 “➕ Add Cluster”。
- 输入服务端的 Dashboard 地址:127.0.0.1:8265
步骤5:开始调试
- 当代码执行到
breakpoint()
时,会在 Ray Debugger 插件中显示暂停任务 - 点击 “▶ Start Debug” 启动调试器,进入远程交互调试界面。
多个 breakpoint() 怎么办?
Ray 是多进程框架,一个任务一个进程,因此调试多个 breakpoint()
的步骤如下:
- 第一个任务遇到breakpoint()时,attach VSCode调试器
- 调试完后,手动断开连接
- 任务继续跑,下一次遇到 breakpoint()时,再次通过插件attach
RDD做了什么?
可以看到,RDD相比自己启动debuggy的attach功能,实现了:
- 你不需要去写launch.json,因为RDD帮你处理好了
- 在每个 worker 中会自动启动一个 debugpy实例,并动态生成监听端口替代常用的5678端口,相当于:
|
|
- 你不需要去写侵入式的debugpy代码
如何用RDD进行远程调试
了解了debugpy的原理、RDD的本地调试原理,RDD的远程调试该多做哪些工作就很清楚了:调试端口转发。这里默认是使用了Remote-SSH插件进行的远程连接。
这是因为 Ray 中每个进程的调试端口(如60693)通常绑定在服务器上的 127.0.0.1,本地的 VSCode 根本访问不到这个地址,所以需要通过端口转发来把它“引流”到本地电脑。
具体的,在本地机器上运行:
|
|
这表示:
- 本地机器的5678端口被转发到远程服务器上的 127.0.0.1:60593
- 即使 Ray的debugpy监听的是 127.0.0.1,本地照样可以通过 127.0.0.1:5678 来访问它
如何更优雅的用RDD进行远程调试
按照上文中的方法,每次调试都需要端口转发,当有多个breakpoint()
时需要做多次转发,也是很麻烦的。这里推荐一个更优雅的方法:采用WireGuard或者Zerotier等工具将服务器与本地机器组成内网。流程详见RAY远程debug。
当组成内网后,服务器与本地机器相当于一个集群中的多机,因此仅需要保证调试端口可以被集群内所有机器访问,而不是只能被本地访问即可。
- 在shell中设置:
|
|
- 在python代码中修改:
|
|
当然,这样做也有缺点,即把调试端口暴露到公网带来了安全风险。
One More thing:直接用debugpy实现远程调试
我们可以看到RDD实际上是对debugpy的一层包装,之前我们提到过debugpy的attach模式本身就适用于远程调试,那么如果不用RDD插件,如何用debugpy实现Ray远程调试呢?
如果你使用的是Remote-SSH插件直接在服务器上进行远程开发(本地没有代码),那么直接用”attach模式”一节中提到的方法即可,因为不用RDD,根本就不会面临插件访问不到调试接口的问题。
在远程情形下,采用RDD的缺点是必须做端口转发;采用debugpy的缺点是必须指定端口、写侵入式代码以及launch.json文件。