RPC 容错#

添加到 Ray Core 的所有 RPC 都应该是容错的,并使用可重试的 gRPC 客户端。理想情况下,它们应该是幂等的,或者至少,非幂等性应该被记录下来,并且客户端必须能够考虑重试。如果您不熟悉什么是幂等性,请考虑一个将“hello”写入文件的函数。重试时,它会再次写入“hello”,导致“hellohello”。这不是幂等的。为了使其幂等,您可以在再次写入“hello”之前检查文件内容,确保多次相同函数调用后的可观察状态与单次调用后的状态相同。

本指南将通过一个 RPC 的案例研究,该 RPC 之前不容错或不幂等,以及如何修复它。在本指南结束时,您应该了解添加新 RPC 时需要注意的事项以及用于验证容错性的测试方法。

案例研究:RequestWorkerLease#

问题#

在此修复之前,由于 Raylet 中的处理程序不是幂等的,因此无法使 RequestWorkerLease 可重试。

这是因为一旦授予了租约,它们就会被视为已占用,直到调用 ReturnWorker。在此 RPC 被调用之前,工作进程及其资源从未返回到可用工作进程和资源池中。Raylet 假定原始 RPC 及其重试都是新的租约请求,并且无法对它们进行去重。

例如,考虑以下操作序列

  1. 通过 RequestWorkerLease 请求新的工作进程租约(Owner → Raylet)。

  2. 响应丢失(Raylet → Owner)。

  3. 重试租约的 RequestWorkerLease(Owner → Raylet)。

  4. 现在授予了两组资源和工作进程,一组用于原始租约,一组用于重试。

在重试时,Raylet 应该检测到租约请求是重试,并将已授予的租约工作进程地址转发给 Owner,这样就不会授予第二个租约。

解决方案#

为了实现幂等性,在 PR #55469 中添加了一个名为 LeaseID 的唯一标识符,它允许对传入的租约请求进行去重。一旦授予了租约,它们就会被跟踪在 leased_workers 映射中,该映射将租约 ID 映射到工作进程。如果新的租约请求已存在于 leased_workers 映射中,系统就会知道该租约请求是重试,并用已授予的租约工作进程地址进行响应。

隐藏问题:长轮询 RPC#

网络瞬时错误可能随时发生。对于大多数 RPC,它们在一个 I/O 上下文执行中完成,因此防范请求或响应是否失败就足够了。然而,有一些 RPC 是长轮询的,这意味着一旦 HandleX 函数执行,它不会立即响应客户端,而是依赖于将来的某个状态变化来触发响应回客户端。

对于 RequestWorkerLease 来说就是这种情况。租约在所有参数都被拉取之前不会被授予,因此系统在拉取过程结束之前无法响应客户端。如果客户端在 Raylet 端的服务器逻辑执行期间断开连接并发送重试,会发生什么?leased_workers 映射只跟踪已授予的租约,而不跟踪正在授予的租约。这导致在 lease_dependency_manager 中触发了一个 RAY_CHECK,因为系统在服务器逻辑执行时无法去重租约请求重试。

具体来说,请考虑以下操作序列

  1. 通过 RequestWorkerLease 请求新的工作进程租约(Owner → Raylet)。

  2. Raylet 正在异步拉取租约的租约参数。

  3. 重试租约的 RequestWorkerLease(Owner → Raylet)。

  4. 租约尚未授予,因此它通过了幂等性检查,Raylet 无法去重租约请求。

  5. 因为 Raylet 尝试再次拉取同一租约的参数,所以 RAY_CHECK 被触发。

最终的修复是考虑到服务器逻辑可能仍在执行,并跟踪租约在授予租约的各个阶段。在任何阶段,系统都应该能够去重请求。

对于任何长轮询 RPC,您都应该 **特别注意** 幂等性,因为客户端的重试不一定会等待响应被发送。

可重试的 gRPC 客户端#

可重试的 gRPC 客户端在 RPC 容错项目期间得到了更新。本节将介绍它的工作原理以及需要注意的一些陷阱。

如需基本介绍,请阅读 retryable_grpc_client.h 注释

工作原理#

可重试的 gRPC 客户端工作原理如下:

  • 使用可重试的 gRPC 客户端发送 RPC。

  • 如果客户端遇到 gRPC 瞬时网络错误,它会将回调推入队列。

  • 会定期执行几项检查:

    • 廉价的 gRPC 通道状态检查:这会检查 gRPC 通道 的状态,以查看系统是否可以再次开始发送消息。默认情况下,此检查每秒进行一次,但可以通过 check_channel_status_interval_milliseconds 进行配置。

    • 潜在的昂贵的 GCS 节点状态检查:如果指数退避期已过且通道仍然关闭,系统会调用 server_unavailable_timeout_callback_。此回调设置在客户端池类中(raylet_client_poolcore_worker_client_pool)。它会检查客户端是否订阅了节点状态更新,然后检查本地订阅者缓存,看是否收到了来自 GCS 的节点死亡通知。如果客户端未订阅或缓存中没有该节点的任何状态,它会向 GCS 发送 RPC。请注意,对于 GCS 客户端,server_unavailable_timeout_callback_ 在调用后会终止进程。这发生在 gcs_rpc_server_reconnect_timeout_s 秒(默认为 60)之后。

    • 每个 RPC 的超时检查:有一个 超时检查,每个 RPC 都可以自定义,但由于它总是为每个 RPC 设置为 -1(无穷大),因此在功能上被禁用,这在 core_worker_client.h 中有说明。

  • 随着每个失败的 RPC 增加,指数退避期会增加,这与失败的 RPC 类型无关。退避期会达到一个最大值,您可以通过 core_worker_rpc_server_reconnect_timeout_max_sraylet_rpc_server_reconnect_timeout_max_s 配置选项为 core worker 和 raylet 客户端自定义。如上所述,GCS 客户端没有最大退避期。

  • 通道检查成功后,指数退避期会重置,队列中的所有 RPC 都会重试

  • 如果系统成功收到节点死亡通知(通过订阅或直接查询 GCS),它将销毁 RPC 客户端,并将每个回调以 gRPC Disconnected 错误 发布到 I/O 上下文。

重要注意事项#

有几点需要牢记:

  • 按客户端排队:每个可重试的 gRPC 客户端对于客户端是唯一的(对于 core worker 客户端是 WorkerID,对于 Raylet 客户端是 NodeID),而不是 RPC 的类型。如果您首先提交 RPC A,由于瞬时网络错误而失败,然后向同一客户端提交 RPC B,由于瞬时网络错误而失败,队列将包含两个项目:RPC A 然后 RPC B。没有按 RPC 分开的队列,而是按客户端分开的队列。

  • 客户端级别超时:每个超时都需要等待前一个超时完成。如果 RPC A 和 RPC B 在短时间内连续提交,那么 RPC A 总共将等待 1 秒,RPC B 总共将等待 1 + 2 = 3 秒。不同的 RPC 没有区别,被视为相同。原因是瞬时网络错误不是 RPC 特定的。如果 RPC A 遇到网络故障,您可以假设 RPC B,如果发送到同一客户端,也会遇到相同的故障。因此,RPC 等待的时间是队列中所有先前 RPC 超时及其自身超时的总和。

  • 析构函数行为:在 RetryableGrpcClient 的析构函数中,系统会通过发布它们的 I/O 上下文来使所有待处理的 RPC 失败。这些回调理想情况下不应修改客户端类(如 RayletClient)持有的状态。如果绝对必要,它们必须以某种方式检查客户端是否仍然存活,例如使用弱指针。PR #58744 中有一个示例。应用程序代码还应考虑 Disconnected 错误

测试 RPC 容错#

Ray Core 具有三层 RPC 容错和幂等性测试。

C++ 单元测试#

对于每个 RPC,都应该有一种 C++ 幂等性测试形式,该测试调用 HandleX 服务器函数两次,并检查每次输出的结果是否相同。应考虑 HandleX 服务器函数调用之间的不同状态更改。例如,在 RequestWorkerLease 中,编写了一个 C++ 单元测试来模拟重试发生在初始租约请求卡在参数拉取阶段的情况。

Python 集成测试#

对于每个 RPC,如果易于实现,最好有一个 Python 集成测试。对于某些 RPC,使用 Python API 完全确定性地测试它们具有挑战性,因此充分的 C++ 单元测试可以起到良好的替代作用。因此,这更像是一个锦上添花,因为集成测试也充当了用户如何遇到幂等性问题的示例。

主要的测试机制使用 RAY_testing_rpc_failure 配置选项,该选项允许您:

  • 在不发送 RPC 的情况下,立即使用 gRPC 错误触发 RPC 回调(模拟请求失败)。

  • 一旦响应从服务器到达,使用 gRPC 错误触发 RPC 回调(模拟响应失败)。

  • 立即使用 gRPC 错误触发 RPC 回调,但同时将 RPC 发送到服务器(模拟正在进行的故障,对于长轮询 RPC,重试应该会命中正在执行服务器代码的服务器)。

有关更多详细信息,请参阅 Ray 配置文件中的注释,网址为 ray_config_def.h

混沌网络发布测试#

IP 表停机#

IP 表停机方法涉及 SSH 到每个节点并使 IP 表停机一小段时间(5 秒)以模拟瞬时网络错误。IP 表脚本在后台运行,在测试脚本执行期间,会定期(60 秒)造成网络停机。

对于核心发布测试,我们在 PR #58868 中将 IP 表停机方法添加到了所有现有的混沌发布测试中。

注意

最初考虑了 Amazon FIS。但是,它有一个 60 秒的最小值,由于配置设置导致了节点死亡,这很难调试,因此 IP 表方法更简单、更灵活。