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 的核心设计:重试机制使乐观路由与缓存失效成为可能。文档给出了三条设计原则:
- 带缓存路由的快路径:Guard 使用缓存的路由信息避免每次请求都做昂贵的数据库查询,提供低延迟的路由决策;
- 优雅的缓存失效:当 Actor 被停止、销毁或迁移到其他 Runner 时,缓存即成为“陈旧”状态。Guard 不主动失效缓存条目(那需要复杂的协调),而是让任何失败响应本身成为“缓存路由已失效”的信号;
- 重试时刷新服务发现:重试尝试忽略缓存、执行全新的数据库查询以发现 Actor 的当前位置,确保请求最终到达正确目的地。
这套方案优化的是常见情况(Actor 在运行、路由有效),同时妥善处理罕见情况(Actor 已迁移/停止),且不牺牲性能。
2.1 重试流程(Retry Flow)
首次请求(Attempt 1):
- 检查目标位置的路由缓存
- 若缓存路由存在,将请求发往缓存目标
- 请求成功 → 向客户端返回响应
- 请求以可重试错误失败 → 进入重试
重试尝试(Attempts 2-N):
- 等待指数退避延迟
- 忽略缓存,执行全新的数据库查询获取目标位置
- 将请求发往新发现的目标
- 请求成功 → 向客户端返回响应
- 请求失败且未达到最大尝试次数 → 重复重试流程
- 超过最大尝试次数 → 向客户端返回
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_ms与proxy_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 Unavailable或504 Gateway Timeout(比文档描述的 503 更宽); - 错误头契约:
x-rivet-error头必须存在,且形如guard.<code>(以.分隔出组名)。仅当组名为guard且错误码属于可重试清单时才触发重试。可重试清单is_retryable_guard_http_error包含:service_unavailable、actor_ready_timeout、actor_wake_retries_exceeded、actor_stopped_while_waiting、tunnel_request_aborted、tunnel_message_timeout、tunnel_response_closed、gateway_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_cache为true时跳过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 Gateway、service_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 位于另一个数据中心,则进行跨数据中心路由
Runner(x-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_ms | 60,000 ms | 路由解析的整体兜底超时 |
route_dispatch_timeout_ms | 见配置定义 | 各路由模块派发超时 |
route_cache_ttl_ms | 6,000,000 ms(10 分钟) | 路由缓存 TTL |
route_pegboard_fetch_actor_timeout_ms | 5,000 ms | 拉取 Pegboard Actor 路由状态 |
route_pegboard_resolve_query_timeout_ms | 15,000 ms | 解析查询型 Actor 路由 |
upstream_request_timeout_ms | 30,000 ms | 上游响应头接收超时 |
四、Gateway 代理(Actor 路径路由)
GUARD.md 指出:Gateway(Guard 的一部分)是发往 Actor 的 HTTP 请求与 WebSocket 连接的代理。
4.1 三种路径匹配模式
路径解析的完整实现在 engine/packages/guard/src/routing/actor_path.rs,支持三种模式(与文档一致):
/gateway/{actor_id}/{...path}—— 直连/gateway/{actor_id}@{token}/{...path}—— 带令牌的直连(@后为访问令牌,parse_direct_actor_path)/gateway/{name}/{...path}?rvt-namespace=...&rvt-method=...—— 查询型路由,通过 key 查找解析到具体 Actor
查询型路由由rvt-前缀的查询参数驱动,这些参数被 Rivet 网关路由保留并在转发前剥离(Actor 永远看不到它们,actor_path.rs)。完整参数集定义在RvtParams结构体中:
| 参数 | 含义 | 约束 |
|---|---|---|
rvt-namespace | 命名空间 | 必填 |
rvt-method | 方法 | 仅支持get或getOrCreate |
rvt-key | Actor 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-input、rvt-region、rvt-crash-policy、rvt-runner会直接报QueryGetDisallowedParams错误(actor_path.rs)。路径解析还有单元测试覆盖:engine/packages/guard/tests/parse_actor_path.rs。
4.2 请求流:Runner 协议的多路复用
文档描述的请求流转链路,从源码结构可以完整印证:
- 客户端 WebSocket 连接到运行在 Rivet Engine 上的 WebSocket 监听器(即 Guard);
- Rivet Engine 通过runner 协议经 Runner 到 Guard 之间的 WebSocket 隧道,把 HTTP 请求 / WebSocket 消息传输给 Actor 所在 Runner 的对应 WebSocket;
- Runner 与 Guard 之间保持单条独立的 WebSocket 长连接,与任何客户端 WebSocket 连接解耦;
- 这条单连接多路复用该 Runner 上所有 Actor 的请求与 WebSocket 连接——
ProxyState中的in_flight_requests(scc::HashSet<RequestId>)与InFlightPermit机制正是为这条隧道上的请求 ID 分配/回收设计的(proxy_service.rs、utils.rs);
- Runner 将请求与 WebSocket 委托给 Actor 处理;
- Runner 通过同一条 WebSocket 以 runner 协议把 HTTP 响应 / WebSocket 消息发回 Rivet;
- Rivet 将 runner 协议消息转换回 HTTP 响应与 WebSocket 消息。
此外,Guard 在转发前会通过proxied_request_builder剥除x-rivet-target、x-rivet-actor、x-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 给出了完整生命周期:
- 客户端经 Rivet(由 Guard 管理)建立到 Actor 的 WebSocket 连接;
- Guard 检查 Actor 是否已唤醒,已唤醒则跳过下一步;
- 未唤醒则向其 workflow 发送 Wake 信号,使 Actor 分配到现有 Runner(serverless 场景则启动新 Runner 并分配);
- Guard 通过 runner 协议向 Runner 发送
ToClientWebSocketOpen; - Runner 回
ToServerWebSocketOpen确认——Runner 必须设置.canHibernate = true才能启用休眠; - 连接建立后,客户端消息经 Guard 代理到 Runner 并委托给 Actor;
- Actor 睡眠时,Runner 以
.hibernate = true发送ToServerWebSocketClose; - Guard 收到后开始休眠,期间不做任何处理;
- 此后: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),仅供参考