ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Ray 反模式剖析:带外序列化 ray.ObjectRef 引发的对象泄漏与提前回收问题

Ray 反模式剖析:带外序列化 ray.ObjectRef 引发的对象泄漏与提前回收问题 Ray 反模式剖析带外序列化 ray.ObjectRef 引发的对象泄漏与提前回收问题【免费下载链接】rayRay is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads.项目地址: https://gitcode.com/gh_mirrors/ra/ray在 Ray 中ray.ObjectRef采用分布式引用计数来管理对象生命周期Ray 会持续钉住pin底层对象直到系统不再持有任何引用。本文聚焦 Ray Core 官方文档中的经典反模式——带外序列化out-of-band serializationray.ObjectRef——分析其背后的分布式引用计数原理、它为何会导致对象被提前回收或内存泄漏并给出可落地的检测手段RAY_allow_out_of_band_object_ref_serialization0环境变量与正确的替代写法。读完本文你将能识别并修复代码中所有绕过 Ray 引用追踪的 ObjectRef 序列化点避免诡异的任务挂起hang与磁盘溢出spilling问题。一、背景Ray 的分布式引用计数与对象钉住机制Ray 的任务、Actor 与对象存储object store共同构成一个分布式系统ray.ObjectRef是该系统中的分布式引用计数句柄。其核心生命周期规则是Ray 会钉住ObjectRef指向的底层对象直到系统中所有对该引用的引用都不再被使用当指向该对象的全部引用消失后Ray 对其进行垃圾回收GC把对象从系统中清理掉。从源码看这种钉住能力由 Core Worker 提供的add_object_ref_reference实现即向本地 Core Worker 注册一个引用计数防止对象被驱逐。引用计数机制的底层实现在 serialization.py 中体现为带内in-band与带外out-of-band两种序列化路径的分流带内序列化in-bandObjectRef作为任务参数、任务返回值或作为另一个对象的一部分被序列化时Ray 能感知并追踪它。源码注释明确写道此 ObjectRef 正被存储在一个对象中将其 ID 加入该对象包含的 ID 列表以便只要外层对象在作用域内内层对象的值就保持存活serialization.py。带外序列化out-of-band用户在业务代码里直接调用pickle.dumps(obj_ref)或ray.cloudpickle.dumps(obj_ref)或让ObjectRef被远程函数闭包捕获。此时引用逃逸出 Ray 的追踪范围Ray 无法获知该引用的存活状态。官方文档明确指出该反模式的核心危害out-of-band-object-ref-serialization.rstTLDR避免序列化ray.ObjectRef因为 Ray 无法判断何时对底层对象进行垃圾回收。二、为什么序列化 ObjectRef 是反模式两条灾难路径当用户代码对ray.ObjectRef做带外序列化时引用计数被绕过可能走向两种截然不同的灾难1. 使用普通pickle对象被提前回收任务意外挂起用标准库pickle.dumps(obj_ref)序列化时Ray 完全不知道这个引用的存在也不会因此增加任何引用计数。序列化得到的字节串即使被传回、反序列化并尝试ray.get()Ray 也可能早已把底层对象当成无引用垃圾回收掉。最终表现为ray.get()无限等待或抛出GetTimeoutError任务莫名挂起且极难排查。2. 使用ray.cloudpickle对象被钉住整个 Worker 生命周期引发泄漏与磁盘溢出与普通pickle不同ray.cloudpickle是 Ray 自带的序列化器能够识别ObjectRef并主动为其注册引用。但为了安全它采取了一种代价高昂的策略只要检测到带外序列化就把对象钉住直到对应的 owner worker 死亡。源码 serialization.py 的注释写得很直白如果这是带外序列化例如直接调用 cloudpickle或被远程函数/Actor 捕获那么通过添加一个永远不会被移除的本地引用将对象钉住整个 Worker 的生命周期。所谓钉住即该对象永远无法从对象存储中被驱逐evict。一旦此类代码被高频执行例如在循环里反复cloudpickle.dumps引用钉住的对象会不断累积形成 Ray 对象泄漏最终挤占对象存储内存并触发磁盘 spill拖垮集群性能。三、反模式代码示例与运行验证官方文档配套的可运行示例位于 anti_pattern_out_of_band_object_ref_serialization.py它一次性演示了上述两条灾难路径与检测手段完整代码如下import ray import pickle from ray._private.internal_api import memory_summary import ray.exceptions ray.init() ray.remote def out_of_band_serialization_pickle(): obj_ref ray.put(1) import pickle # object_ref is serialized from user code using a regular pickle. # Ray cant keep track of the reference, so the underlying object # can be GCed unexpectedly, which can cause unexpected hangs. return pickle.dumps(obj_ref) ray.remote def out_of_band_serialization_ray_cloudpickle(): obj_ref ray.put(1) from ray import cloudpickle # ray.cloudpickle can serialize only when # RAY_allow_out_of_band_object_ref_serialization1 env var is set. # However, the object_ref is pinned for the lifetime of the worker, # which can cause Ray object leaks that can cause spilling. return cloudpickle.dumps(obj_ref) print( serialize object ref with pickle ) result ray.get(out_of_band_serialization_pickle.remote()) try: ray.get(pickle.loads(result), timeout5) except ray.exceptions.GetTimeoutError: print(Underlying object is unexpectedly GCed!\n\n) print( serialize object ref with ray.cloudpickle ) # By default, its allowed to serialize ray.ObjectRef using # ray.cloudpickle. ray.get(out_of_band_serialization_ray_cloudpickle.options().remote()) # you can see objects are still pinned although its GCed and not used anymore. print(memory_summary()) print( serialize object ref with ray.cloudpickle with env var RAY_allow_out_of_band_object_ref_serialization0 for debugging ) try: ray.get( out_of_band_serialization_ray_cloudpickle.options( runtime_env{ env_vars: { RAY_allow_out_of_band_object_ref_serialization: 0, } } ).remote() ) except Exception as e: print(fException raised from out_of_band_serialization_ray_cloudpickle {e}\n\n)运行结果解读依次运行上述三段验证可以看到pickle.dumps(obj_ref)路径反序列化后调用ray.get(pickle.loads(result), timeout5)抛出ray.exceptions.GetTimeoutError并打印Underlying object is unexpectedly GCed!——底层对象已经被提前回收这正是导致任务意外挂起的根因。ray.cloudpickle.dumps(obj_ref)路径默认环境变量未设置时序列化被允许但调用ray._private.internal_api.memory_summary()打印对象存储内存摘要时可以看到该对象虽然逻辑上已不再被使用却仍处于 pinned 状态即钉住在对象存储中构成泄漏。开启检测路径通过runtime_env的env_vars把RAY_allow_out_of_band_object_ref_serialization设为0后序列化时立即抛出异常详见下一节。四、检测手段RAY_allow_out_of_band_object_ref_serialization0为了帮助开发者发现代码中是否存在该反模式Ray 提供了开关环境变量RAY_allow_out_of_band_object_ref_serialization0当该变量为0关闭时只要ray.cloudpickle检测到带外序列化ray.ObjectRef就会抛出ray.exceptions.OufOfBandObjectRefSerializationException异常异常类定义于 exceptions.py并附带极具诊断价值的错误消息。异常消息中的关键信息从 serialization.py 的抛错代码可以看出该异常消息包含被序列化的ObjectRef的十六进制 IDobject_ref.hex()明确的修复指引若确实需要放行可设置RAY_allow_out_of_band_object_ref_serialization1但同时警告对象将被钉住整个 Worker 生命周期并可能导致 Ray 对象泄漏调用点callsite定位异常会打印调用点但前提是开启RAY_record_ref_creation_sites1来记录引用创建位置否则会提示 Disabled. Set RAY_record_ref_creation_sites1。因此排障时建议同时设置RAY_allow_out_of_band_object_ref_serialization0 RAY_record_ref_creation_sites1重要使用前提环境变量必须提前设置有两个细节需要特别注意默认值为开启该环境变量在 serialization.py 中通过ray_constants.env_bool(RAY_allow_out_of_band_object_ref_serialization, True)解析默认值是True即默认允许带外序列化——这正是该反模式容易悄悄潜入代码的原因。不可动态修改该变量在模块导入时ray.init()之前就被读取并缓存无法在运行时动态改变。测试用例 test_serialization.py 的注释专门指出这一点Use ray.remote as a workaround because RAY_allow_out_of_band_object_ref_serialization cannot be set dynamically。因此若想全局开启检测需在启动 Ray 进程前设置环境变量例如RAY_allow_out_of_band_object_ref_serialization0 ray start --head或启动 Python 前 export若只想让某个远程任务受控可以采用示例代码中的方式通过runtime_env的env_vars注入这会为对应任务创建独立 Worker 环境。测试用例佐证仓库中的单元测试完整验证了开关两端的语义test_serialization.pytest_cannot_out_of_band_serialize_object_ref在RAY_allow_out_of_band_object_ref_serialization0下无论是把ref捕获进另一个远程函数闭包还是直接cloudpickle.dumps(ray.put(1))都会抛出OufOfBandObjectRefSerializationExceptiontest_can_out_of_band_serialize_object_ref_with_env_var在1下同样的操作可以正常完成。这说明闭包捕获与显式cloudpickle.dumps是触发该反模式的两大典型来源检测开关对两者均有效。五、正确做法让 ObjectRef 始终留在 Ray 的引用追踪范围内修复该反模式的原则只有一条不要让ObjectRef以字节形式逃逸出 Ray 的追踪体系。具体建议如下1. 需要跨进程传递引用时交给 Ray 的带内通道作为任务/ Actor 方法的参数或返回值传递Ray 内部会自动做带内序列化并维护引用计数这是最常用的正确姿势把ObjectRef放入另一个对象再ray.put如第一节所述Ray 会把内层引用登记到外层对象的依赖列表中保证外层对象存活期间内层值不被回收在远程函数内部解引用直接在任务体里对ObjectRef调用ray.get()取回值再把值传出去而不是把引用传出去。2. 禁止手动pickle/cloudpickle序列化引用不要为了缓存、落盘或发送到外部系统而把ray.ObjectRef用pickle.dumps/cloudpickle.dumps序列化。即便序列化的是值而非引用也建议确认对象图中不嵌套任何ObjectRef。3. 如果确实需要跨集群/外部系统传递先ray.get(obj_ref)取出真实值或把大对象落盘/上传到外部存储在外部系统间传递值本身或值的稳定标识到达目的端后再重新ray.put生成新的ObjectRef。切勿直接搬运引用字节。4. 定期用memory_summary体检对象存储ray._private.internal_api.memory_summary()示例代码中已使用可打印当前对象存储中各对象的大小、引用状态PINNED_IN_MEMORY等与 owner 信息。若发现大量本应被回收却长期 pinned 的对象往往是带外序列化泄漏的信号应结合RAY_record_ref_creation_sites1回看创建调用点。六、小结反模式自查清单检查项说明代码中是否存在pickle.dumps(obj_ref)会导致底层对象被提前 GC引发GetTimeoutError与任务挂起代码中是否存在ray.cloudpickle.dumps(obj_ref)默认被允许但对象会被钉住整个 Worker 生命周期造成泄漏与磁盘 spill远程函数闭包是否意外捕获了外层ObjectRef同样属于带外序列化需显式避免或改造是否设置了检测开关调试阶段设置RAY_allow_out_of_band_object_ref_serialization0并配合RAY_record_ref_creation_sites1对象存储中是否有异常 pinned 对象用memory_summary()体检定位泄漏源该反模式是 Ray Core Patterns 系列文档中的一篇完整清单见 patterns/index.rst与之相关的还有 ray-get-loop.rst、nested-ray-get.rst、return-ray-put.rst 等对象引用与任务编排反模式它们共同构成一套系统的 Ray 最佳实践。记住最核心的一句话ray.ObjectRef是 Ray 分布式系统内部的句柄不是可以随意序列化搬运的普通数据——尊重它的引用计数生命周期才能让对象存储的回收与驱逐机制真正为你工作。【免费下载链接】rayRay is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads.项目地址: https://gitcode.com/gh_mirrors/ra/ray创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表