Rivet Guard 路由与重试机制解析:Actor 代理网关的乐观路由、缓存失效与 503 重试协议
2026/9/17 10:22:19 网站建设 项目流程

Rivet Guard 路由与重试机制解析:Actor 代理网关的乐观路由、缓存失效与 503 重试协议

【免费下载链接】actorsRivet Actors are the primitive for stateful workloads. Built for AI agents, collaborative apps, and durable execution.项目地址: https://gitcode.com/GitHub_Trending/riv/actors

本文基于 Rivet 引擎内部文档 GUARD.md,系统讲解 Rivet Engine 中 Guard 组件的三大核心机制:基于缓存路由的乐观重试(Retry Behavior)、目标优先级路由逻辑(Routing Logic),以及 Actor 网关代理(Gateway Proxying)的请求流与 WebSocket 休眠(Hibernation)。读完本文,你将理解 Guard 如何在“Actor 正常运行”的常见场景下以最低延迟完成路由,又如何在 Actor 停止、迁移时通过 503 +x-rivet-error重试协议优雅恢复,并能对照 engine/packages/guard 与 engine/packages/guard-core 的源码验证每一处行为。

一、Guard 是什么:引擎的入口代理

Guard 是 Rivet Engine 的 HTTP/WebSocket 入口代理。从源码看,它的启动入口在 engine/packages/guard/src/lib.rs:start函数构造共享上下文后,分别生成三个关键闭包——routing_fn(路由决策)、cache_key_fn(路由缓存键)、cert_resolver(TLS 证书解析),最终交给通用代理框架 engine/packages/guard-core/src/proxy_service.rs 运行。

这意味着 Guard 的架构是“策略与执行分离”的:guard-core提供通用的代理、重试、缓存、准入控制执行引擎,而guard包负责“这个请求该去哪里”的具体路由策略(Actor、Runner、API Public、Envoy 等)。

二、Guard Core 的重试行为:乐观路由 + 缓存失效

这是 GUARD.md 的核心设计:重试机制使乐观路由与缓存失效成为可能。文档给出了三条设计原则:

  1. 带缓存路由的快路径:Guard 使用缓存的路由信息避免每次请求都做昂贵的数据库查询,提供低延迟的路由决策;
  2. 优雅的缓存失效:当 Actor 被停止、销毁或迁移到其他 Runner 时,缓存即成为“陈旧”状态。Guard 不主动失效缓存条目(那需要复杂的协调),而是让任何失败响应本身成为“缓存路由已失效”的信号;
  3. 重试时刷新服务发现:重试尝试忽略缓存、执行全新的数据库查询以发现 Actor 的当前位置,确保请求最终到达正确目的地。

这套方案优化的是常见情况(Actor 在运行、路由有效),同时妥善处理罕见情况(Actor 已迁移/停止),且不牺牲性能。

2.1 重试流程(Retry Flow)

首次请求(Attempt 1):

  1. 检查目标位置的路由缓存
  2. 若缓存路由存在,将请求发往缓存目标
  3. 请求成功 → 向客户端返回响应
  4. 请求以可重试错误失败 → 进入重试

重试尝试(Attempts 2-N):

  1. 等待指数退避延迟
  2. 忽略缓存,执行全新的数据库查询获取目标位置
  3. 将请求发往新发现的目标
  4. 请求成功 → 向客户端返回响应
  5. 请求失败且未达到最大尝试次数 → 重复重试流程
  6. 超过最大尝试次数 → 向客户端返回502 Bad Gateway

重试配置(文档声明 + 源码当前默认值):

配置项说明文档/源码现状
指数退避起始间隔,每次尝试翻倍(100ms, 200ms, 400ms…)文档以 100ms 为示例;当前配置默认值为DEFAULT_PROXY_RETRY_INITIAL_INTERVAL_MS = 150,见 engine/packages/config/src/config/guard.rs
最大尝试次数文档默认 3 次总尝试当前配置默认值为DEFAULT_PROXY_RETRY_MAX_ATTEMPTS = 7,由单元测试proxy_operational_defaults_preserve_existing_behavior锁定(guard.rs)
重试触发条件TCP 连接错误,或带x-rivet-error头的503 Service Unavailable见下文 2.2 的判定函数

注意:文档描述的是设计意图与早期参数,仓库当前默认值(150ms 起始间隔、7 次总尝试)可通过proxy_retry_initial_interval_msproxy_retry_max_attempts两个配置项覆盖——两者在 engine/packages/config/src/config/guard.rs 中均要求最小值为 1,并有参数校验逻辑。

2.2 源码中的重试判定:should_retry_request_inner

重试触发条件的精确实现在 engine/packages/guard-core/src/utils.rs:

pub(crate) fn should_retry_request_inner(status: StatusCode, headers: &hyper::HeaderMap) -> bool { (status == StatusCode::SERVICE_UNAVAILABLE || status == StatusCode::GATEWAY_TIMEOUT) && headers .get(X_RIVET_ERROR) .and_then(|value| value.to_str().ok()) .and_then(|value| value.split_once('.')) .is_some_and(|(group, code)| group == "guard" && is_retryable_guard_http_error(code)) }

由此可以确认文档中的两点细节:

  • 状态码门槛:实际代码要求响应状态为503 Service Unavailable504 Gateway Timeout(比文档描述的 503 更宽);
  • 错误头契约x-rivet-error头必须存在,且形如guard.<code>(以.分隔出组名)。仅当组名为guard且错误码属于可重试清单时才触发重试。可重试清单is_retryable_guard_http_error包含:service_unavailableactor_ready_timeoutactor_wake_retries_exceededactor_stopped_while_waitingtunnel_request_abortedtunnel_message_timeouttunnel_response_closedgateway_response_start_timeout(utils.rs)。

另一个触发路径是TCP 连接错误:在 HTTP 请求处理循环中,只有err.is_connect()为真(连接层错误)且未达最大尝试次数时才会重试,其他上游错误直接包装为UpstreamError(proxy_service.rs)。

2.3 “失败即失效”的实现:重试时忽略缓存

文档所述“重试忽略缓存”在 proxy_service.rs 中有直接对应:

// Resolve target again, this time ignoring cache. This makes sure // we always re-fetch the route on error let ResolveRouteOutput::Target(new_target) = self.state.resolve_route(req_ctx, true).await? else { bail!("resolved route does not match Target"); }; target = new_target;

resolve_route的第二个参数ignore_cachetrue时跳过route_cache查询、直接调用路由函数(proxy_service.rs)。退避延迟由calculate_backoff计算,公式为initial_interval * 2^(attempt-1)(utils.rs),与文档描述的指数退避一致。

另外值得说明的一点:从当前源码结构看,路由结果写缓存的代码处于注释状态(TODO: Disable route caching for now, determine edge cases with gateway,proxy_service.rs),即“快路径缓存”目前是保留能力而非始终启用的行为——这与 GUARD.md 描述的“缓存 + 失效信号”模型是同一套机制的两个阶段,阅读源码时不应误解为缓存未实现。

2.4 上游服务如何触发 Guard 重试

按文档,希望触发 Guard 重试的服务必须返回:

  • 503 Service Unavailable状态码
  • x-rivet-error: <error>

源码印证了该契约是双向的:Guard 自身向外生成错误响应时,err_into_response会在响应中写入x-rivet-error: {group}.{code}头(utils.rs),其中retry_attempts_exceeded(重试耗尽)映射为502 Bad Gatewayservice_unavailable映射为503——正好对应文档中“重试耗尽返回 502”的行为。

三、Guard Router 的路由优先级

文档规定请求按如下优先级路由。对照 engine/packages/guard/src/routing/mod.rs 中create_routing_function的实现,实际顺序为:路径型路由(Gateway / Runner / Envoy)优先于目标头路由,无目标头时兜底到 API Public,全部不匹配则返回NoRoute错误(映射为404 Not Found,见 utils.rs 的错误映射表)。

3.1 基于目标的路由(x-rivet-target头)

Actor 服务x-rivet-target: actor):

  • 必需头:x-rivet-actor: <actor_id>—— 具体 Actor 实例的 UUID
  • 可选头:x-rivet-addr: <address>—— Actor 位置的直连地址覆盖
  • 行为:路由到该具体 Actor 实例;若 Actor 位于另一个数据中心,则进行跨数据中心路由

Runnerx-rivet-target: runner):

  • 用途:将 WebSocket 连接路由到 Pegboard runner 服务
  • 目标:配置的 Pegboard 服务地址(pegboard.lan_host:pegboard.port
  • 场景:Runner 与编排系统之间的 WebSocket 通道

3.2 API 路由(无 target 头)

x-rivet-target头时,请求路由到公开 API 服务(api_public.lan_host:api_public.port)。对应实现是 api_public.rs 的默认分支(api_public_default阶段),原始请求路径在上游请求中保持不变。

3.3 WebSocket 的协议头替代

对 WebSocket 连接,目标不通过x-rivet-target头而是通过Sec-Websocket-Protocol头传递。源码解析逻辑在 routing/mod.rs:将协议列表按逗号拆分,寻找rivet_target.前缀的项作为目标,另有rivet_actor.rivet_token.rivet_skip_ready_wait等协议前缀用于传递 Actor ID、令牌与跳过就绪等待的标志。这与 GUARD.md 中“rivet_target.actor,rivet_actor.{actor_id}形式的逗号分隔点分对”的描述一致。

3.4 路由阶段的超时围栏

每个路由模块的派发都被phase_timeout包裹(routing/mod.rs),超时会生成RouteDispatchTimeout错误并计入guard_route_phase直方图指标。相关超时配置及默认值(均可在 config/guard.rs 中覆盖)包括:

配置项默认值用途
route_timeout_ms60,000 ms路由解析的整体兜底超时
route_dispatch_timeout_ms见配置定义各路由模块派发超时
route_cache_ttl_ms6,000,000 ms(10 分钟)路由缓存 TTL
route_pegboard_fetch_actor_timeout_ms5,000 ms拉取 Pegboard Actor 路由状态
route_pegboard_resolve_query_timeout_ms15,000 ms解析查询型 Actor 路由
upstream_request_timeout_ms30,000 ms上游响应头接收超时

四、Gateway 代理(Actor 路径路由)

GUARD.md 指出:Gateway(Guard 的一部分)是发往 Actor 的 HTTP 请求与 WebSocket 连接的代理。

4.1 三种路径匹配模式

路径解析的完整实现在 engine/packages/guard/src/routing/actor_path.rs,支持三种模式(与文档一致):

  1. /gateway/{actor_id}/{...path}—— 直连
  2. /gateway/{actor_id}@{token}/{...path}—— 带令牌的直连(@后为访问令牌,parse_direct_actor_path)
  3. /gateway/{name}/{...path}?rvt-namespace=...&rvt-method=...—— 查询型路由,通过 key 查找解析到具体 Actor

查询型路由由rvt-前缀的查询参数驱动,这些参数被 Rivet 网关路由保留并在转发前剥离(Actor 永远看不到它们,actor_path.rs)。完整参数集定义在RvtParams结构体中:

参数含义约束
rvt-namespace命名空间必填
rvt-method方法仅支持getgetOrCreate
rvt-keyActor key 组件逗号分隔的字符串列表
rvt-pool/rvt-runner计算池名getOrCreate时必填(pool优先)
rvt-input创建输入getOrCreate允许;Base64URL 编码且需通过 CBOR 校验
rvt-region区域getOrCreate允许
rvt-crash-policy崩溃策略取值restart/sleep/destroy
rvt-token访问令牌可选
rvt-skip-ready-wait跳过就绪等待布尔值

注意get方法下传入rvt-inputrvt-regionrvt-crash-policyrvt-runner会直接报QueryGetDisallowedParams错误(actor_path.rs)。路径解析还有单元测试覆盖:engine/packages/guard/tests/parse_actor_path.rs。

4.2 请求流:Runner 协议的多路复用

文档描述的请求流转链路,从源码结构可以完整印证:

  1. 客户端 WebSocket 连接到运行在 Rivet Engine 上的 WebSocket 监听器(即 Guard);
  2. Rivet Engine 通过runner 协议经 Runner 到 Guard 之间的 WebSocket 隧道,把 HTTP 请求 / WebSocket 消息传输给 Actor 所在 Runner 的对应 WebSocket;
    • Runner 与 Guard 之间保持单条独立的 WebSocket 长连接,与任何客户端 WebSocket 连接解耦;
    • 这条单连接多路复用该 Runner 上所有 Actor 的请求与 WebSocket 连接——ProxyState中的in_flight_requestsscc::HashSet<RequestId>)与InFlightPermit机制正是为这条隧道上的请求 ID 分配/回收设计的(proxy_service.rs、utils.rs);
  3. Runner 将请求与 WebSocket 委托给 Actor 处理;
  4. Runner 通过同一条 WebSocket 以 runner 协议把 HTTP 响应 / WebSocket 消息发回 Rivet;
  5. Rivet 将 runner 协议消息转换回 HTTP 响应与 WebSocket 消息。

此外,Guard 在转发前会通过proxied_request_builder剥除x-rivet-targetx-rivet-actorx-rivet-token等内部头并追加X-Forwarded-For(utils.rs),保证 Actor 侧看到的是干净的上游请求。

4.3 WebSocket 休眠(Hibernation)

GUARD.md 最后一节指出:Gateway 支持为 Actor 实现可休眠 WebSocket——客户端 WebSocket 连接保持打开的同时允许 Actor 进入睡眠,当 WebSocket 上无流量时用量降为 0;一旦有消息发送到 Gateway,Actor 会被自动唤醒。

配套的 HIBERNATING_WS.md 给出了完整生命周期:

  1. 客户端经 Rivet(由 Guard 管理)建立到 Actor 的 WebSocket 连接;
  2. Guard 检查 Actor 是否已唤醒,已唤醒则跳过下一步;
  3. 未唤醒则向其 workflow 发送 Wake 信号,使 Actor 分配到现有 Runner(serverless 场景则启动新 Runner 并分配);
  4. Guard 通过 runner 协议向 Runner 发送ToClientWebSocketOpen
  5. Runner 回ToServerWebSocketOpen确认——Runner 必须设置.canHibernate = true才能启用休眠;
  6. 连接建立后,客户端消息经 Guard 代理到 Runner 并委托给 Actor;
  7. Actor 睡眠时,Runner 以.hibernate = true发送ToServerWebSocketClose
  8. Guard 收到后开始休眠,期间不做任何处理;
  9. 此后:Actor 因其他来源被唤醒则回到第 6 步(不再发ToClientWebSocketOpen);客户端在休眠期发送消息则回到第 2 步;客户端关闭 WebSocket 则唤醒 Actor(若未运行)并发送ToClientWebSocketClose

在源码侧,guard-core通过is_ws_hibernate(识别guard.websocket_service_hibernate错误码,utils.rs)与custom_serve.rs中的HibernationResult处理休眠的挂起/恢复路径;休眠期间每个休眠 WebSocket 还运行一个 keepalive 循环,定期向 UDB 写入活跃标记,客户端关闭时清除该值,Runner 收到CommandStartActor时也会携带仍在活跃的休眠请求信息。

五、小结

GUARD.md 描述的是一个“先快后准”的代理设计:快路径靠缓存路由降低数据库压力,正确性靠“失败即失效 + 重试时刷新服务发现”保证;路由层通过x-rivet-target头、Sec-Websocket-Protocol协议项与/gateway/...路径三种信号把请求分流到 Actor、Runner 与 API Public;而 Runner 协议上的单连接多路复用与可休眠 WebSocket,则让 Actor 的长连接成本可以压到零。所有关键行为均可在 engine/packages/guard(路由策略)与 engine/packages/guard-core(代理执行、重试、准入)两个 crate 中逐条对照验证。

【免费下载链接】actorsRivet Actors are the primitive for stateful workloads. Built for AI agents, collaborative apps, and durable execution.项目地址: https://gitcode.com/GitHub_Trending/riv/actors

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询