RunPod 常见问题排查:Pod 起不来、抢不到 GPU、端口连不上
昨天跑完一轮微调,为了省钱把 Pod 停了。今天想接着调,点 Start,界面告诉你这台机器现在是 “Zero GPU Pods”。你盯着屏幕想了两秒:卡呢?我付过钱的那张卡去哪了?
这是 RunPod 上最典型的一类挫败感——不是配置写错了,而是你对它的调度模型有一个错误的默认假设。下面这些排查路径全部按官方文档的口径整理,文档没覆盖的症状我会直接写「官方文档没有明确说明」,不替它编一个”通常是因为”。排错文里那种自信满满的因果推断,比不写更害人。
一、先把”起不来”拆成三件不同的事
同一句”我的 Pod 起不来”,在文档里对应的其实是三个完全不同的故障域,定位手段也不一样:
- 根本没调度到 GPU——Pod 存在,但没有卡可挂。表现是 Zero GPU 或者提示你迁移。
- 容器起来了,里面的服务没起来——Pod 在控制台是 Running,但你打开的那个网页是 502 或者一片空白。
- 服务起来了,你连不上——进程在监听,但端口映射、绑定地址、协议这一层没对上。
对应三个入口,文档反复提到的就这么几个,记住它们比记住任何单个故障都值钱:Pod 详情页的 Telemetry 标签、Pod 的日志、以及 Connect 菜单里的 Web Terminal。
其中有一条特别容易被忽略:控制台显示 Running、旁边有个绿点,并不代表这个 Pod 可以用了。 文档在两处分别强调过同一件事——判断一个 Pod 是否真的就绪,看 Telemetry 有没有在收数据;即使 Pod 已经就绪,里面的具体服务(JupyterLab、你自己的 HTTP 服务)仍然可能还要几分钟才起来。很多人在这一步就开始改配置、重启、换机型,其实只是等得不够久。RunPod 的基本形态和两种交付方式如果还没理顺,可以先看 RunPod 是什么那篇再回来对号入座。
二、Zero GPU 与抢不到机型:机制不搞清楚,怎么试都是白试
这一段是全文最需要讲透的,因为它反直觉。
按文档的说法,你部署一个 Pod 时,它被分配到某一台具体物理机上的某一块 GPU,这在你的 Pod 和那台硬件之间建立了一个绑定关系。Pod 运行期间,那块卡是排他地留给你的——这也是为什么价格稳定、你的任务不会被打断。但反过来:你一停机,就把那块卡释放回去了,别人可以租。而你的 volume 存储还留在那台物理机上。
于是重启时的死结就出现了:卡被别人占了,而你的 Pod 因为数据在那台机器上,没法跑到别的机器去。文档特意点明了一句很重要的话——这不代表 RunPod 上这个型号的 GPU 没有了,只代表你那台特定物理机上没有空闲的了。 一台物理机上有多块 GPU,全被租满是常事。
这时候文档给的选项是这些:
- 以零 GPU 启动:官方明确说这主要是个数据恢复功能,让你能访问 Pod 的 volume 磁盘。它的 CPU 资源有限,不适合跑计算任务。正确用法是进去把数据备份或者传出来,然后终止这个 Pod。注意 502 那篇文档补了一个细节:零 GPU 状态下,界面上那些 Web 连接入口即使是亮着的也不工作——别把这个当成新的故障去查。
- 等一会儿再试:对方停机你就有卡了。但文档写得很老实:没有任何关于什么时候会有的保证。 具体要等多久、排在第几位,官方文档没有明确说明,控制台也不给可用性预测。
- 终止后重建:用同样的配置重新部署,新 Pod 会被调度到网络里任意一台有空闲卡的机器上。代价是 volume disk 上的东西跟着 Pod 一起没了。
- 自动迁移(Pod migration):文档标注这个功能处于 beta 阶段。它会用相同规格开一台新 Pod,找到有空闲卡的机器、开出实例、把旧 Pod 的数据搬过去。
迁移这条路有个必须提前知道的副作用:你会拿到一个新的 Pod ID 和新的 IP,因为 Pod ID 在架构上就是跟物理机绑定的。文档列出了会因此受影响的几种情况,每一条都是真实会炸的:API 调用里硬编码了 Pod ID;硬编码了形如 http://xxxx.proxy.runpod.net 的代理地址;防火墙或 VPN 里按 Pod ID 配了规则;防火墙或 VPN 里按 IP 配了规则;以及你给别人发过的服务地址——换了 Pod 就换了 URL。迁移本身要花多久,官方文档没有明确说明。
预防手段官方只给了一条,而且两篇文档都指向它:用 network volume。 它把数据从具体物理机上解耦出来,/workspace 落在一块独立的持久卷上,可以挂到任何一台 Pod。需要换机时直接开新 Pod 挂同一块卷,立刻就能在有空闲卡的机器上继续。存储这一层的持久化边界和它那几条硬约束(最要紧的是只能在创建 Pod 时挂载),单独一篇讲:RunPod 存储怎么选。
三、502、524 和”服务连不上”
502 的官方排查顺序是有讲究的,第一步不是看你的代码。
文档给的第一步是确认 Pod 到底有没有挂 GPU。进 Pod 设置看,Pods 页面上会显示挂载的卡(比如 1 x A6000 这种形式),没挂的话这个数是 0。前面说过零 GPU 是允许的,那种状态下 Web 界面的连接方式全都无效。这一步能筛掉一大批”我明明什么都没改”的 502。
第二步才是看 Pod 日志,在 Pod 设置里就能看,翻错误信息。第三步是文档专门列出来的一条,很多人不会想到:如果你用的是官方模板,界面可能不是开箱可用的,模板页面的 ReadMe 里写着需要你手动补的步骤。 每个模板的要求不一样,不看 ReadMe 就当成平台故障是最常见的误判。这三步走完还是 502,文档的建议就是联系官方支持了。
502 之外还有一个长得很像但根因完全不同的错误:524。要理解它得先看 HTTP 代理这条链路,文档画得很清楚:
User → Cloudflare → Runpod Load Balancer → Your Pod
链路上游有一个固定的单次连接时长上限,你的服务在这个时长内没响应,连接就会被关闭并返回 524(具体是多少秒见官方 expose-ports 文档,别按印象记)。所以 502 通常意味着”后面那个服务没在跑或者出错了”,524 意味着”它在跑,只是太慢了”。这两个的修法方向相反:前者去看日志和进程,后者要改你的接口设计。文档给的应对思路是:做一个返回进度的状态接口、用后台任务队列加状态轮询、把大操作拆小、或者先立刻返回一个 job id 让客户端稍后取结果。如果你的场景就是长连接,文档建议直接走 TCP 而不是 HTTP 代理。
再往下,端口这一层的机制必须先搞明白,否则排查方向全错。Pod 里的内部端口和外部能访问的端口通常不是同一个。 两种暴露方式:
- HTTP 代理:在部署时点 Edit Template、或者对已有 Pod 走 Pod 页面展开 → 左下角菜单 → Edit Pod,把端口填进 Expose HTTP Ports 字段(这个字段标签里带了个数上限)。也可以在控制台的 My Templates 里给模板配好。访问地址的形状是
https://[POD_ID]-[INTERNAL_PORT].proxy.runpod.net,注意 URL 里拼的是内部端口。 - TCP 直连:填到 Expose TCP Ports 字段,Pod 起来后在 Connect 菜单的 Direct TCP Ports 下面看分配到的公网 IP 和外部端口,形如
TCP port <IP>:<外部端口> -> :22。这里有两条坑:Community Cloud 的公网 IP 在 Pod 迁移或重启后可能变(Secure Cloud 相对稳定),而外部端口映射只要 Pod reset 就会变。任何把这两个值写死在配置里的做法,都是在给自己埋一个随机时间引爆的故障。
如果你的程序必须让外部端口和内部端口一致,文档给了一个看起来很怪的写法:在 TCP 配置里填大于 70000 的端口号。这不是合法端口号,只是一个信号,告诉 RunPod 给你分配内外一致的端口。分配结果同样在 Connect 菜单的 Direct TCP Ports 里看,程序里可以通过环境变量拿到,例如填了 70000 和 70001 就读 $RUNPOD_TCP_PORT_70000 和 $RUNPOD_TCP_PORT_70001。把它塞进启动配置,应用就能自适应实际分配到的端口。
文档列的端口类故障,按出现频率大致是这个顺序:
- 代理访问不到服务——你的服务绑在了
localhost或127.0.0.1。必须绑0.0.0.0。这一条在 Pod 侧和 Serverless 侧的文档里各写了一遍,是真正的头号原因。 - 524 超时——响应太慢,改设计或换 TCP。
- Connection refused——进程没起,或者没监听在你以为的那个端口上。
- 端口被占——Pod 里另一个服务已经在用了。
- 连接不稳——Community Cloud 的 IP 会变,客户端得自己写重连逻辑。
还有两条属于”不是 bug 是限制”的:Pod 不支持 UDP,只支持 TCP 和 HTTP,靠 UDP 的应用得改成 TCP;另外,需要 Pod 之间互通而不想暴露到公网的话,走 global networking——每个 Pod 拿到一个只有你账号内其他 Pod 能访问的私有地址,内部 DNS 名的形式是 POD_ID.runpod.internal,服务照样要绑 0.0.0.0。文档同时提醒 global networking 目前只对 NVIDIA GPU Pod 可用,且只在部分数据中心提供,部署时要选支持的数据中心。想验证通不通,文档给的办法是在一个 Pod 的 Web Terminal 里 ping POD_ID.runpod.internal(镜像里可能没有 ping,需要先 apt-get install -y iputils-ping)。
四、JupyterLab 的三个专门坑
这三个在文档里各占一篇,说明踩的人足够多。
空白页。 连接面板里 JupyterLab 显示 “Ready”,点进去是一片白。文档解释得很直接:那个 Ready 只代表 Jupyter server 的 /api/status 接口在响应 HTTP 请求,不代表 JupyterLab 已经起好了。可能的原因文档列了几条,而且明说可能同时中几条:Pod 本身还在初始化(即使显示 Running)、Jupyter 服务还在加载、浏览器或中间层缓存了一个坏响应、你到 Pod 之间的网络有问题、镜像或模板没在预期的端口/路径上启动 Jupyter。
对应的步骤按你看到的状态分两种。状态还是 Initializing,就等——文档给的量级是启动后先等上几十秒再打开。状态已经 Ready 但页面空白,顺序是:在空白页上再等一会儿;硬刷新(Windows/Linux 是 Ctrl+Shift+R,Mac 是 Cmd+Shift+R);用隐私/无痕窗口打开以排除缓存;还是空白就去看 Pod 日志,找 Jupyter 相关的报错,或者找到”Jupyter 已在 8888 端口运行”这类信息;有报错或者 Jupyter 压根没启动,就重启 Pod 再走一遍。
这里有一条藏得很深的坑:文档警告 RunPod 的 JupyterLab 健康检查只查 8888 端口。如果你在模板里把 Jupyter 换到了别的端口,官方的建议是改回 8888——否则那个 Ready 状态永远不会变绿,而你会一直以为是 Jupyter 起不来。
另外文档给了一条判据我觉得很实用:同一个 Pod 重启超过三次、JupyterLab 始终不出来,就别再当成瞬时启动延迟了,按模板/配置问题处理。 该确认的是:模板是否支持在文档所说的端口(通常是 8888)上跑 JupyterLab、模板配置里 Jupyter 需要的环境变量和启动命令是否填对。还有一句很硬的话要记住:社区模板 RunPod 不维护、也不提供客户支持,出问题要找模板作者或者去社区渠道。
Token 认证界面。 打开 JupyterLab 撞到 “Token authentication is enabled”,解法是去拿 token:控制台 Pod 页面点 Connect → 找到 Web Terminal 的 Start 按钮 → 点 Start 打开终端 → 运行 jupyter server list。输出里会有一行形如 http://localhost:8888/?token=xxxxxxxx,你要的就是 = 后面那串字符,复制回登录页填进 Token 字段。
名为 checkpoints 的文件夹打不开。 这是文档明确记录的一个已知问题:JupyterLab 把 “checkpoints” 当成保留字,点这个目录时它触发的是内部的 listCheckpoints 函数而不是打开目录,所以什么反应都没有。文档说这个坑最常撞到的就是 ML 模型目录和 ComfyUI 的安装目录。三个解法:终端里 mv checkpoints checkpoint 临时改名(mv 只是改名,不会删数据),用完再改回去;或者把文件下到别的目录再用 JupyterLab 拖拽进去——目录打不开但接受拖进来的文件;或者干脆所有操作都走终端,ls -la checkpoints/、cp、mv 都正常工作。
五、盘满了
文档给的定位顺序是固定的三步,照着走就行。
先 df -h 看总体。重点看两行:挂在 / 上的 overlay 就是容器的根目录,也就是 container disk;volume disk 或 network volume 默认挂在 /workspace。然后 du -sh . 看当前目录占了多少。最后揪大文件,文档给的命令是:
find /workspace -type f -exec du -h {} + | sort -rh | head -n 10
确认了就删,rm /path/to/file 删文件、rm -r /path/to/directory 删目录。文档在这里挂了一个警告框:这是永久删除,谨慎使用。
真正容易让人怀疑人生的是下一条:你在 JupyterLab 界面里删了文件,空间却没释放。因为它们进了隐藏的回收站目录。文档给的检查位置是 $HOME/.local/share/Trash/ 和 /workspace/.Trash*,存在就 rm -rf 清掉。这个坑的特征很好认——du 算出来的占用和你以为删掉的量差不上。
如果是持续不够用,而不是被垃圾文件占满,文档的建议就是上 network volume,别在 container disk 上跟自己较劲。顺带说一句,这也是为什么前面那篇存储的文章值得先读完:盘满、停机丢数据、Zero GPU 这三个看起来无关的故障,根子都在”你把东西放在了哪一层”。
六、主机维护与停机:这部分是平台的义务,也是你的义务
计划内维护的口径文档写得明确:需要在承载你 Pod 的机器上做计划维护时,RunPod 会提前通过邮件通知,让你有时间保存工作、备份数据或者迁到别的 Pod;维护窗口期间,Pod 不可用的那段时间不收费;等不了的话可以同时另开资源顶上。对维护窗口有疑问或者认为自己受了影响,可以带着 Pod ID 找官方支持。
非计划停机就没那么客气了:硬件故障和突然崩溃没有预告,文档说 RunPod 可能只能在停机发生之后通知你,会在问题被定位后尽快通知。怀疑自己被非计划停机影响,同样是带 Pod ID 找支持,问影响范围、时间线和当前状态。
然后是这一篇里最该被当成硬纪律的一段:Pod 默认使用临时的容器存储,一旦 Pod 被中断、重启、停止或终止,只存在容器存储上的数据就丢了。 文档给的三条保护措施是:一是挂 network volume,让数据跨重启和跨删除存活,官方称这是最可靠的办法;二是给长跑任务做 checkpointing,按任务长度决定存的频率(文档的量级是每小时到每几小时一次),主流框架都自带这个能力,文档直接给了 PyTorch、Hugging Face Transformers / Accelerate、PyTorch Lightning 的官方入口;三是按 3-2-1 原则做备份——三份副本、两种不同的存储类型、一份异地,可以用 runpodctl 或 Cloud Sync 自动化。
最后一句是官方自己的免责,抄下来贴在团队文档里都不过分:只存在临时容器盘上的数据,RunPod 不保证能恢复。
七、Serverless 侧:故障形态换了一套
Pod 侧的排错逻辑搬不到 Serverless 上,因为你不再拥有那台机器。这一侧的入口按症状分:
Worker 起不来或初始化失败,文档给了五步:看控制台里的端点日志;先确认你的 handler 在本地测试里是好的再部署(这条排在前面是有道理的,本地能复现的问题别拿云上的冷启动去调);确认依赖都装进了镜像;确认镜像和你选的 GPU 类型兼容;确认输入格式和 handler 的预期一致。
Worker 起来了但每个请求都失败,文档列的四类是:输入校验报错(handler 里加校验,日志里看期望格式)、依赖缺失、模型加载失败(查显存需求和模型路径)、权限问题(文件可读、目录可写)。
任务一直卡在 IN_QUEUE,三个方向:没有可用 worker(看 max_workers 配得够不够)、worker 被 throttled(在 Workers 标签页能看到)、以及冷启动本身——空闲一段时间后的第一个请求要等 worker 初始化,文档给的缓解方向是提高 min_workers 或者开 FlashBoot。
这里有一个非常值得单独记住的坑,因为它的表现会把你带向完全错误的方向:端点明明有多个 worker,几乎所有任务却都挤在一个 worker 上跑,其他的闲着,任务还卡在 IN_QUEUE。 文档说这可能是你用了受影响版本的 RunPod Python SDK——1.7.11 到 1.10.0 这几个版本在挂了 network volume 的端点上会破坏按 worker 的任务跟踪,导致大部分 worker 不再拉新任务,最常出现在用 network volume 的 ComfyUI worker 上。修法是 pip install --upgrade "runpod>=1.10.1",然后重新构建并重新部署镜像,代码和配置都不用改。如果你正在为”伸缩策略怎么调都不生效”抓头发,先去看一眼 SDK 版本。
任务超时:处理太久就调 executionTimeout,模型加载太慢就用 model caching 或者把模型打进镜像,ttl 太短就放宽到能覆盖排队加执行的总时长。任务失败:handler 里的未捕获异常(加 try/catch 并返回结构化错误)、OOM(模型或 batch 超了显存,减 batch 或换更大的卡)、超时。
端点被莫名缩容:文档说这是 RunPod 自动做的,两种情形——一是长期无请求,连续多日没有请求会先把 max workers 降低一档,再继续没有请求会降到 0,第一次下调时官方会发邮件;二是端点持续产出不健康(崩溃)的 worker,平台会主动缩容以停止计费、减少反复重启,同样发邮件。恢复办法是去控制台把 max workers 调回去,但如果原因是 worker 不健康,先修根因,否则还会被降一次。
冷启动太频繁:拉长 idle_timeout 让 worker 多留一会儿、把 min_workers 设成大于 0、或者接受”零散流量天然比稳定流量冷启动更多”这个事实。日志不见了:日志太多会触发限流(降低日志级别)、确认写的是 stdout/stderr 而不是只写文件、只有成功初始化的 worker 才有日志、超过保留期的日志会被自动清掉。
负载均衡型端点有两个专属症状:报 “No workers available” 说明 worker 没来得及初始化,可能是首个请求(重试即可)、worker 全忙(提高 max_workers)、或者 worker 在崩(看日志);请求到不了 worker,则要确认你的 HTTP 服务监听在 8000 端口(或你配置的端口)、绑的是 0.0.0.0 而不是 127.0.0.1、并且返回合规的 HTTP 响应。
用官方 vLLM worker 的还有几条专项:OOM 就把 GPU_MEMORY_UTILIZATION 往下调一档、把 MAX_MODEL_LEN 压小、或者换显存更大的卡;模型加载不了分三种——MODEL_NAME 和 Hugging Face 的模型 id 没有完全一致、门控模型没给有权限的 HF_TOKEN、模型本身不在 vLLM 支持列表里;OpenAI 兼容接口报错则按状态码分流,401 查 RUNPOD_API_KEY,404 查端点 URL 的形状是不是 https://api.runpod.ai/v2/ENDPOINT_ID/openai/v1,connection refused 是 worker 还没就绪。
实在定不下来,文档给的最后手段是 SSH 进 worker 实时调试,以及带上端点 ID 和错误细节联系官方支持。Serverless 这一侧的机制本身(端点、worker、冷启动、伸缩)另有一篇:RunPod Serverless 部署推理服务。
八、官方文档没有明确说明的部分
排错文最该诚实的地方是这里。下面这些是我在通读这几篇文档后确认官方没有给出明确说明的,遇到时请以控制台实际显示和账单为准,不要采信任何”一般来说”:
- 抢不到 GPU 时,要等多久才会有空闲,有没有排队位次——文档只说”没有保证”。
- 零 GPU 模式下具体能用到多少 CPU 和内存——文档只说”有限”。
- 自动迁移要花多长时间、失败了会怎样。
- 哪个数据中心此刻还有你要的机型,以及各数据中心的实际网络表现。
- 停机之后存储费用的确切结算周期。
- 任务在队列里的预期等待时长。
这几项我没有账号去逐条验证,按官方文档的说法只能到这个程度。写一个”通常是因为机房负载高”之类的解释很容易,但那会让你在错误的方向上浪费一个下午。
九、换机之前先确认的三件事
Zero GPU 一弹出来,大部分人的第一反应是终止重建。先停三秒,确认这三件:
第一,确认是”这个机型没有了”还是”你那台机器上没有了”。 这两件事的处理方式完全相反。文档明确说过,提示你迁移不代表平台上这个型号的卡都没了。如果只是那台物理机满了,终止重建或者迁移都能立刻拿到卡;如果是这个型号整体紧张,换机也解决不了,该考虑的是换机型或换数据中心。
第二,确认你的数据在哪一层。 container disk 停机即丢、volume disk 跟着 Pod 生死、network volume 独立存在——换机的代价完全由这一条决定。如果关键数据在 volume disk 上,先用零 GPU 模式把它传出来再终止,顺序错了没有第二次机会。
第三,确认有没有硬编码。 新 Pod 意味着新 ID、新 IP、新代理地址、新的 TCP 外部端口映射。把调用方配置、防火墙白名单、VPN 规则、别人手里的服务地址挨个过一遍,比换完机再挨个救火省事得多。
最后说一句可能不太讨喜的判断:这些故障里有相当一部分不是”排查能力”问题,是第一次开 Pod 时的决定问题。 存储放哪一层、用不用 network volume、走 HTTP 代理还是 TCP、Jupyter 用不用 8888——这些在创建表单里一次性定死的选项,直接决定了你后面会不会反复撞上 Zero GPU、盘满、连不上。真想少排错,成本最低的动作是在第一次上量之前按 RunPod 上手全流程把这几个选项想清楚,并且拿一台最小配置的机器把”停机再启动”这个动作真的跑一遍——它会诚实地告诉你,你的数据到底还在不在。至于要不要把生产服务押在某一家 GPU 云上,那是更前面一层的问题,可以对照 GPU 云租用那篇一起量。
算完账发现自建推理不划算?
先用托管端点把业务跑起来,量上来了再回头算自建的平衡点。