Bencher Runner 协议


runner 二进制文件 和 API 服务器通过单个 WebSocket 连接通信。 本参考描述该协议:交换的消息、它们驱动的 Job 生命周期, 以及超时和重连如何防止 Job 卡住。它是 Self-Hosted Runner 指南的深入补充。

使用 runner up 操作 Runner 并不需要了解这些内容; 提供它们是为了透明,也是为了任何围绕 API 构建工具的人。


连接

Runner 在其整个生命周期内与 API 服务器保持单个 WebSocket 连接。 同一连接同时处理 Job 分配和 Job 执行, 并且它在多个 Job 之间保持打开,从而避免为每个 Job 重连和握手。

  • 端点: /v0/runners/{runner}/channel
  • 认证: 建立连接时,Runner 密钥以 Authorization: Bearer bencher_runner_<key> 头的形式发送。
  • 消息大小: 每条消息都受服务器 request_body_max_bytes 限制的约束(同时应用于最大消息和帧大小)。超过此限制的消息,例如携带大量 stdoutstderr 或输出文件的 completed 负载,会在 WebSocket 协议层被拒绝。

每条消息都是一个带有 event 字段的 JSON 对象,该字段标识其类型。


Runner 消息

从 Runner 发送到服务器的消息。

Event描述负载
readyRunner 空闲并请求一个 Job可选的 poll_timeout(1-900 秒)和 runner 元数据(osarchversion
runningJob 设置完成,基准测试正在启动
heartbeat周期性存活信号(约每秒一次)
completed基准测试成功完成job(Job UUID)和 results(每次迭代的输出)
failed基准测试失败job(Job UUID)、resultserror
canceled确认来自服务器的取消job(Job UUID)

服务器消息

从服务器发送到 Runner 的消息。

Event描述负载
ack确认收到的消息可选的 job(Job UUID)
job将认领的 Job 分配给 Runner认领的 Job:其 Spec、Job 配置,以及一个短期有效的 OCI 拉取令牌
no_job轮询超时到期且没有可用的 Job
cancelJob 已取消或超时;停止执行
updateRunner 应自我更新到新版本versionurl(下载 URL)和 checksum(SHA-256)

job 消息中的 OCI 拉取令牌在 Job 被认领时生成,且从不存储。 它的作用域限定在 Job 所属的单个 project,仅可拉取,且短期有效, 因此一个被攻陷的 Runner 只能拉取其所认领 Job 对应 project 的 Image。


连接流程

连接后,Runner 进入空闲轮询循环, 持续发送 ready 直到服务器分配一个 job (或在轮询超时时返回 no_job,或在有新版本可用时返回 update)。 一旦拿到 Job,Runner 发送 running, 在基准测试执行期间流式发送 heartbeat 消息, 并以终止性的 completedfailed 消息结束。 服务器用 ack 确认每条消息, 连接保持打开,因此 Runner 会返回空闲循环以处理下一个 Job。

如果 Job 被取消,服务器会用 cancel 回复某个 heartbeat。 Runner 停止基准测试并回复 canceled,服务器随后确认它。

API ServerRunnerAPI ServerRunneralt[Job available][Poll timeout][Update available]loop[Idle / polling]loop[Benchmark executes]Connect with runner keyConnectedready (os, arch, version)job (Spec, config, OCI token)no_jobupdate (version, url, checksum)runningackheartbeatack (or cancel)completed (job, results)ack

Job 生命周期

每个 Job 在被认领、执行和处理时,会经过一组固定的状态。

触发条件
pendingclaimedRunner 认领该 Job
pendingcanceled用户取消该 Job
claimedrunningRunner 发送 running
claimedfailedRunner 发送 failed,或心跳超时
claimedcanceled用户取消该 Job
runningcompletedRunner 发送 completed
runningfailedRunner 发送 failed,或心跳超时
runningcanceled用户取消该 Job,或超过了硬性 Job 超时
completedprocessed服务器成功处理结果
failedcompletedRunner 重新发送 completed,覆盖心跳超时导致的失败

processedcanceled 是终止状态。 completedfailed 是准终止状态: completed 在结果被解析后转换为 processed, 而 failed 在 Runner 重新发送 completed 时转换为 completed。 每次转换在其数据库更新上都使用状态过滤器, 因此被并发修改的 Job 会被重新读取而不是被覆盖。

runner claims

user cancels

running

failed / timeout

user cancels

completed

failed / timeout

cancel / hard timeout

results recovered

results parsed

pending

claimed

canceled

running

failed

completed

processed


超时与恢复

三种互补的机制确保 Job 永远不会卡住, 即使 Runner 崩溃或丢失连接。

心跳超时

在连接打开期间,读取超时会检测到已连接但保持沉默的 Runner。 只有有效的协议消息才会重置计时器; 无效的 JSON、ping/pong 帧和二进制消息不会。 超时时,运行时间超过其超时加宽限期的 Job 会被标记为 canceled, 否则被标记为 failed(与 Runner 的联系已丢失)。

硬性 Job 超时

服务器强制执行一个独立于 Runner 行为的硬性最大执行时长, 因此有缺陷或被攻陷的 Runner 无法通过发送心跳而无限运行。 当超过限制(Job 超时加宽限期)时, Job 会被标记为 canceled,且 Runner 会收到一条 cancel 消息。

断连恢复

如果在 Job 仍在进行中时连接断开, 服务器会在心跳超时之后安排一次检查。 如果 Runner 已重连并恢复心跳,则 Job 继续; 否则该 Job 会被标记为 failed,如果它已超过硬性超时则标记为 canceled。 在启动时,服务器还会恢复孤立的 claimed Job、 为进行中的 Job 重新安排超时, 并重新处理那些结果已存储但尚未解析的 completed Job。

重连与结果交付

重连是受支持的,且是幂等的。 为已在运行的 Job 重新发送 running 只会刷新其存活状态, 而重新发送终止性的 completedfailedcanceled 消息始终是安全的。 终止性消息携带 Job UUID 并会收到 ack; 如果在 ack 到达之前连接断开, Runner 会存储结果,并在下次连接时、进入空闲之前重新发送它。 Runner 实际的 completed 结果甚至可以覆盖心跳超时导致的 failed 状态。

不自动重试

failed Job 不会被自动重试。 失败的基准测试是一种信号,而不是要隐藏的错误, 因此是否重新运行它由你决定。


Job 输出

当 Runner 发送 completedfailed 时, 完整的输出会存储在与容器 Image 相同的 OCI 存储后端中, 路径为 {project}/output/v0/jobs/{job}

存储的输出包含一个按迭代划分的 results 数组,并在失败时包含一个 error 字符串。 每次迭代都记录其 exit_codestdoutstderr, 以及任何收集到的输出文件到其内容的映射。 在输出被存储后,服务器对结果运行 基准测试工具适配器, 将 Metric 和 Alert 解析到 Report 中,并将 Job 转换为 processed

当使用 GET /v0/projects/{project}/jobs/{job} API 查询 Job 时会返回该输出。 约束 WebSocket 消息的同一个 request_body_max_bytes 限制 也限定了 Runner 可以交付的输出大小。



Published: Fri, June 19, 2026 at 8:00:00 AM UTC