批量推理、多模态预处理等长作业失败后,可以通过 Ray Data checkpoint 保留已提交的处理进度。重新提交同一个作业时,系统在读取输入后过滤已提交 ID,继续计算剩余记录。
这个机制要解决的问题很具体。Ray 自带的任务级重试能兜住偶发的 worker 抖动,但整个作业失败退出之后,重提就是整条管道从头再跑一遍——几小时的计算白做。checkpoint 记的是"哪些记录已经处理完"这件事,不是处理结果;结果仍然完整落在你指定的输出目录里。恢复时框架加载已提交的 ID 集合,在读取输入之后把它们过滤掉,只把剩余记录送进处理逻辑和写出环节。过滤发生在读取之后,所以恢复仍然要把输入完整读一遍,省下来的是后续的计算与写出。
本文适用于 EMR Serverless Spark 产品中的 Ray 作业(Ray 版本 >= err-1.2.0),使用包含 CheckpointConfig 和 OSS 适配的产品运行时。
1. 准备输入与存储路径
适合使用 checkpoint 的管道是:输入可重读,每条记录独立处理,输出保留稳定、非空且全局唯一的 id。例如,读取一批文本或图片,逐条预处理或推理,再写出结果。
这三个条件不是风格建议,各自对应恢复路径上的一环:输入可重读,是因为恢复轮仍然要扫一遍输入才能拿到待处理的记录;记录独立处理,是因为进度是按记录粒度提交的,"某条记录已完成"这种说法无法描述聚合或跨行依赖;ID 稳定且唯一,是因为整个恢复判断只依赖一件事——这个 ID 之前出现过没有。
为每个业务批次分配独立的输出和 checkpoint 路径,并确保执行节点具备 OSS 访问权限。以下用 run-001 表示同一个业务批次,失败重提时保持不变。
oss://<bucket>/snapshots/input-001/ 输入快照
oss://<bucket>/jobs/run-001/output/ 处理结果
oss://<bucket>/jobs/run-001/checkpoint/ 已提交进度
主要节省后续计算与写出;恢复时仍需读取输入。 把一次失败重提拆开看,各阶段是这样:
| 执行阶段 | 不启用 checkpoint | 启用后重提(假设上一轮已完成 70%) |
|---|---|---|
| 读取输入 | 全量 | 仍为全量 |
| 逐条处理 / 推理 | 全部重算 | 只算剩余约 30% |
| 结果写出 | 全部重写 | 只追加剩余结果 |
| 人工介入 | 手工记录进度,或整批重跑 | 原命令重提即可 |
如果处理逻辑本身不便宜,这张表就是 checkpoint 的价值所在;如果瓶颈在读输入,它帮不上忙,这一点后面还会展开。
2. 在处理代码中启用 Checkpoint
下面示例假设输入 Parquet 包含唯一的 id 和字符串类型的 text 两列,用文本长度计算展示完整的数据流。将 process_batch 替换为业务预处理或模型推理时,保留原始 ID。保存为 job.py:
import numpy as np
import ray
from ray.data.checkpoint import CheckpointConfig
def process_batch(batch):
return {
"id": batch["id"],
"text_length": np.asarray(
[len(text) for text in batch["text"]], dtype=np.int64
),
}
ray.init(address="auto")
ctx = ray.data.DataContext.get_current()
ctx.checkpoint_config = CheckpointConfig(
id_column="id",
checkpoint_path="oss://<bucket>/jobs/run-001/checkpoint",
delete_checkpoint_on_success=False,
)
ds = ray.data.read_parquet("oss://<bucket>/snapshots/input-001")
ds = ds.map_batches(process_batch, batch_format="numpy", batch_size=64)
ds.write_parquet("oss://<bucket>/jobs/run-001/output", mode="append")
这段脚本的四个动作值得逐个交代:ray.init(address=“auto”) 让脚本接入运行时已经拉起的 Ray 集群;CheckpointConfig 挂在全局的 DataContext 上,之后不需要在每个算子处重复声明;map_batches 里就是你的预处理或推理逻辑,示例用文本长度演示;最后 write_parquet 才真正触发执行——Ray Data 是惰性的,前面两步只是在拼 DAG。
三个配置决定了恢复方式:
| 配置 | 建议 |
|---|---|
id_column | 使用输入已有的唯一 ID,处理过程中保留其值与类型。 |
checkpoint_path | 同一业务批次及其失败重提复用同一路径,不与其他批次共享。 |
delete_checkpoint_on_success=False | 成功后先保留进度,待结果确认和后续业务完成后再按保留策略清理。 |
这里显式使用 mode="append",让恢复轮继续追加剩余结果。保留 checkpoint 时,不要清空或覆盖原输出。
CheckpointConfig 还带有 override_filesystem、write_num_threads 等可选参数,前者用于传入自定义的 OSS 文件系统和凭证,后者控制写出进度记录的线程数,常规场景保持默认即可。另外,checkpoint 相关接口在 Ray 中仍标记为 beta,跨运行时大版本升级时,建议先在测试批次上回归验证一次。
3. 正常提交,失败后原样重提
按产品支持的方式提交 job.py。如果使用 Ray Jobs CLI,先按控制台调用信息配置网关及身份认证,再从脚本目录执行:
# ray job submit 前必须设置 ray address 和 headers
export RAY_ADDRESS='https://emr-spark-ray-gateway-cn-hangzhou.aliyuncs.com'
export RAY_JOB_HEADERS='{"ray-token": "<RAY_TOKEN>"}'
ray job submit --working-dir . -- python job.py
作业失败后,先处理导致失败的问题,并确认旧作业及写任务已经结束。保持输入快照、处理逻辑、模型版本、输出路径与 checkpoint 路径不变,重新执行同一条提交命令即可。读取进度和过滤已提交 ID 由框架完成,无需手工记录已完成行或增加业务侧恢复阶段。
若修复会改变输出语义,例如更换模型或修改处理逻辑,应作为新批次运行,使用新的输出与 checkpoint 路径,避免混用不同版本的结果。
4. 使用时注意这几件事
-
ID 稳定且唯一。 不在每次运行时随机生成 ID,不在处理中丢弃或改写 ID。聚合、跨行依赖及一对多输出需要单独设计恢复语义。
-
路径按批次隔离。 同批次失败重提复用路径,新批次使用新路径;不要让多个作业同时写同一组输出和 checkpoint。
-
成功后管理进度。 本例保留 checkpoint,以便完成后重提仍能过滤已提交 ID。清理后再向原输出目录追加,可能产生重复结果。API 默认自动清理对应 Dataset 执行成功,不代表整个业务流程完成。
-
按实际规模评估资源。 恢复会加载已提交 ID;大规模 ID 集合需要预留内存。UDF 调用外部服务或写入数据库时,业务仍需自行保证幂等。
常见问题
1、checkpoint 目录里存的是什么?
已提交记录的 ID 和必要的进度元数据,不是处理结果——结果完整落在输出目录。所以它的空间占用主要随已完成记录数增长,通常远小于输入和输出数据本身。
2、为什么恢复时还要把输入读一遍?
因为过滤发生在读取之后:框架得先拿到记录,才知道它提交过没有。由此有两点推论:checkpoint 省不掉读取阶段的开销,如果瓶颈恰好在读输入(比如 OSS 上是海量小文件),得从输入的组织方式入手优化;"输入可重读"是前提而不是建议,输入被删除或覆盖之后,恢复就无从谈起。
3、重提之后结果会重复吗?
正常路径不会,已提交的记录不会再被处理——前提是 checkpoint 还在、输出目录没被清空、写入保持 append。反过来,手工清掉 checkpoint 又往原输出目录追加,重复几乎是必然的。另外,Ray Data 不保证输出顺序,checkpoint 也不改变这一点,需要有序结果的话把排序放到下游。
4、失败瞬间正在处理的那批记录会怎样?
会被重新处理。进度并非逐条实时落盘,最后一段尚未提交的记录在重提后会再算一次。这正是外部副作用必须幂等的原因:写数据库用 upsert、调用模型服务时设计好去重或重试策略,都能让这次重算变得无害。
5、只修了 bug,需要新开批次吗?
判断标准是输出语义有没有变化。让原本会崩溃的记录能正常算出结果、调整并发、加日志,这类改动可以沿用原路径恢复,已提交的部分不受影响。换了模型、改了预处理口径、调整了字段含义,则应新开批次——否则同一个输出目录里会混着两个版本的结果,而且没有任何字段能把它们区分开。
6、什么情况下不该用 checkpoint?
跑几分钟就结束的短作业,恢复的开销未必比直接重跑小。含跨行依赖的处理,比如按某个键聚合、join、一条输入产出多条输出,记录级的"已完成"无法表达正确的恢复语义,需要另行设计。如果处理本身很便宜、读取特别贵,checkpoint 能省下的那部分本来就很有限。