☰
Finagle 重试指标(Retries Metrics)全解析:从 Requeue 到 RetryBudget 的度量体系
2026/9/25 10:38:45 网站建设 项目流程
  • 后端
  • RPC框架

【免费下载链接】finagle

A fault tolerant, protocol-agnostic RPC system

项目地址:https://gitcode.com/gh_mirrors/fi/finagle
点击查看免费下载

本文聚焦 Twitter 开源 RPC 框架 Finagle 中与请求重试相关的全部指标(metrics)。这些指标由 Retries 模块 统一追踪,覆盖自动重排(requeue)、策略化重试(retry)、动态预算(budget)等核心机制。阅读完本文,你将能准确区分retries、requeues、tries三组易混淆的统计口径,理解每个指标在客户端调用链上的确切产生位置,并能够基于这些指标监控与调优客户端的重试行为。

一、重试指标背后的两种机制:Requeue 与 Retry

Finagle 对失败请求的自动重试分为两类,理解二者的区别是读懂所有指标的前提:

  • Requeue(重新排队):由RequeueFilter自动执行,仅针对被判定为安全重试的失败(例如请求尚未完整写入远端服务的WriteException)。重试次数由一个动态预算控制。
  • Retry(策略化重试):由RetryFilter根据用户配置的RetryPolicy执行,适用于通过ClientBuilder等 API 显式配置了重试策略的客户端。

两者的预算都来自同一个动态预算组件 RetryBudget,因此在监控时会出现指标名部分重合(如budget_exhausted),需要结合产生指标的过滤器来区分来源。

重要语义:应用层失败(application level failures)不会被计入这些重试统计。这一点对 Thrift 这类"协议内携带异常"的协议尤其关键——Thrift 应用异常被封装在正常响应中返回,而非以失败形式暴露给重试层。

二、指标速览:8 个统计项的分类与含义

Finagle 的重试指标统一以retries为 scope 前缀,按类型可分为三类:

类型指标全名含义
Stat(分布统计)retries按RetryPolicy实际执行的重试次数分布
Statretries/requeues_per_request单个请求被重新排队的次数分布
Counter(计数)retries/requeues请求被重新排队的累计次数
Counterretries/budget_exhausted预算被耗尽的累计次数
Counterretries/request_limit单个逻辑请求达到重试上限的累计次数
Counterretries/not_open可重试但因底层Service非Open而未重试的次数
Counterretries/cannot_retry可重排但因底层ServiceFactory非Open而未重排的次数
Gauge(实时值)retries/budget当前可用重试预算余额

三、逐项指标详解与源码印证

3.1retries:策略化重试次数分布(Stat)

retries是一个 stat,记录按照RetryPolicy重试的次数。它由RetryFilter在每次请求终结时写入,核心逻辑位于 RetryFilter.scala:

private[this] val retriesStat = statsReceiver.stat("retries")

在 RetryFilter.scala 的 issueRequest 中,无论重试最终成功还是预算耗尽,都会执行retriesStat.add(count),其中count是本次请求已实际发生的重试次数:

  • 策略判定不再重试时(case None),直接累加当前count;
  • 预算耗尽时(tryWithdraw()返回false),累加count后将该失败标记为NonRetryable(FailureFlags.asNonRetryable),防止上游再次尝试。

3.2retries/requeues与retries/requeues_per_request:自动重排的两面

  • retries/requeues(Counter):请求被自动重新排队的累计次数。在 RequeueFilter.scala 中定义为statsReceiver.counter("requeues"),每次真正发起一次重排(无论是否带延迟)都会incr()。
  • retries/requeues_per_request(Stat):每次请求被重新排队次数的分布。对应 RequeueFilter.scala 第 57 行 的statsReceiver.stat("requeues_per_request"),在responseFuture中每次终结响应时requeueStat.add(attempt)。

哪些失败"已知安全、可被重排"?由 RetryPolicy.scala 中的RetryableWriteException提取器 判定,规则如下:

  • 被标记为Interrupted或NonRetryable的失败:不重排(请求已被丢弃或明确不可重试);
  • 被标记为Retryable的Failure:重排;
  • 包装在WriteException中的异常:重排(请求未完整写出即失败,重放安全);
  • 其余:不重排。

RequeueFilter内部通过Requeueable提取器(RequeueFilter.scala 第 177-180 行)复用这一判定。

3.3retries/budget与retries/budget_exhausted:动态预算的两个观测点

  • retries/budget(Gauge):当前可用重试预算余额的实时值。定义在 Retries.scala 第 266-267 行:
private[this] val budgetGauge = statsReceiver.addGauge("budget") { retryBudget.balance }

Gauge 的生命周期与ServiceFactory绑定,在close()时通过budgetGauge.remove()移除。

  • retries/budget_exhausted(Counter):预算被耗尽的次数。它有两个产生源头:

    1. RetryFilter中定义于 RetryFilter.scala 第 79-80 行:当策略判定应重试、但retryBudget.tryWithdraw()失败时incr(),并将响应标记为NonRetryable;
    2. RequeueFilter中定义于 RequeueFilter.scala 第 55 行:当退避序列耗尽(backoffs.isExhausted)或retriesRemaining尚有余额但预算取款失败时incr()。

3.4retries/request_limit:单个请求的重试次数上限

retries/request_limit(Counter)统计"逻辑请求的重试尝试达到上限"的次数。逻辑请求对应的上限由maxRetriesPerReq决定——在 RequeueFilter.scala 第 156 行:

val maxRetries = Math.ceil(maxRetriesPerReq * retryBudget.balance).toInt

即:单个请求允许的最大重排次数 =maxRetriesPerReq× 当前预算余额,目的是防止单个请求消耗不成比例的预算。在 Retries.scala 第 114 行 中该比例被设为MaxRequeuesPerReq = 0.2(即预算的 20%)。

对应地,在 RequeueFilter.scala 第 122-127 行 的决策逻辑中:

} else { if (retriesRemaining > 0) budgetExhaustCounter.incr() // 还有配额,但预算取款失败 → budget_exhausted else requestLimitCounter.incr() // 配额用尽 → request_limit responseFuture(attempt, t).transform(FailureFlags.asNonRetryable) }

即:当剩余重试配额retriesRemaining > 0但预算取款失败时记budget_exhausted;当配额本身用尽(retriesRemaining <= 0)时记request_limit。两者都以NonRetryable终结本次失败。

3.5retries/not_open与retries/cannot_retry:放弃重试的两个状态门槛

这两个计数器分别描述"判定可重试/可重排,但因下游状态非Open而放弃"的场景:

  • retries/not_open(Counter):在 Retries.scala 第 268-269 行 中定义。产生于**服务获取(service acquisition)**阶段——svcFactory的applySelf尝试获取服务失败时(Retries.scala 第 281-314 行):若失败属于RetryableWriteException且还有重试配额,但当前status != Status.Open,则放弃重试并notOpenCounter.incr()。

  • retries/cannot_retry(Counter):在 RequeueFilter.scala 第 58 行 中定义。产生于**请求应用(request application)**阶段:当响应为可重排失败、但底层service.status != Status.Open时(RequeueFilter.scala 第 101-103 行),canNotRetryCounter.incr()并直接返回原始失败。

两者的共性在于:Status.Open是 Finagle 对下游资源可用性的统一判定(依据具体协议栈配置,可能是所有可用端点或单个会话),状态非Open时重试已无意义,因此放弃并以原失败返回。

四、tries与retries:逻辑请求 vs 物理请求

文档特别提示了一个易混淆点:通过ClientBuilder构建的客户端,还有一组以tries为 scope 的指标,它们来自StatsFilter。

  • tries系列指标:代表逻辑请求(logical requests),即应用发起的一次调用;
  • retries系列指标:代表物理请求(physical requests),即包含所有重试/重排在内的实际网络请求。

对于使用StackAPI(即client.withStack(...)等现代构建方式)的客户端,若希望复现ClientBuilder的这一行为,可以手动将服务用 scope 为tries的StatsFilter包裹,从而在日志/监控中同时看到"应用视角"和"网络视角"的两组统计。

五、预算(RetryBudget)如何运作

所有重试都受 RetryBudget 约束,其设计目标是抑制进程内多客户端并发重试导致的放大效应(retry amplification)。核心接口只有三个:

  • deposit():存入信用额度(通常在每次请求发出时调用);
  • tryWithdraw():尝试取出一次重试额度,成功返回true并扣减,失败返回false且余额不变;
  • balance:当前可立即发起的重试次数。

默认预算由 RetryBudget.apply() 创建,参数为:

参数默认值含义
ttl10 秒deposit()存入的额度约在ttl后过期(合法区间 1~60 秒)
minRetriesPerSec10每秒最低重试储备,保障刚启动或低 QPS 客户端的基本重试能力
percentCanRetry0.2允许重试的比例,即约 20% 的请求可被重试(deposit与withdraw的比率)

底层实现是TokenBucket.newLeakyBucket(ttl, reserve, nowMillis)的令牌桶(TokenRetryBudget,见 RetryBudget.scala 第 79-95 行),并使用ScaleFactor = 1000.0的比例因子以整数运算支持percentCanRetry > 1的场景。此外还提供了两个特殊实现:RetryBudget.Empty(永不重试,余额恒为 0)与RetryBudget.Infinite(余额恒为 100,总是允许重试)。

在客户端栈中,RequeueFilter与RetryFilter会共享同一个预算。为避免重复计账,Retries.scala 中的WithdrawOnlyRetryBudget包装器会吞掉第二个过滤器的deposit()调用(只取不存),确保一次请求只存入一次额度;若未显式配置该参数,则默认会为每个客户端新建一个未共享的预算(Retries.scala 第 145-150 行 注释说明了这一细节)。

六、配置入口:从 Stack 参数到过滤器组装

对于使用StackAPI 的客户端,重试行为通过 Retries 模块 的两个 Stack 参数配置:

  • Retries.Policy:决定"哪些"失败可重试,默认RetryPolicy.Never(不重试)。常用策略可直接复用 RetryPolicy 对象 中的现成值,如RetryPolicy.tries(numTries)(上限次数 + 5ms~200ms 抖动退避)、WriteExceptionsOnly、TimeoutAndWriteExceptionsOnly、ChannelClosedExceptionsOnly,或通过RetryPolicy.combine(...)组合多个策略、用limit(maxRetries)动态限制上限。
  • Retries.Budget:决定"多少次"可重试,包含retryBudget与requeueBackoffs(仅作用于自动重排的退避序列,默认Backoff.const(Duration.Zero),即立即重试)。

组装逻辑(Retries.scala 第 207-230 行):

  • 当retryPolicy为Never时,仅装配RequeueFilter(自动重排);
  • 否则装配RetryExceptionsFilter+ 只取不存的RequeueFilter(withdrawsOnly = true),此时重排与策略化重试共享预算但不重复存款。

此外,服务获取阶段的自动重试有独立的固定预算:Effort = 25(Retries.scala 第 24 行),即在获取服务失败时最多尝试 25 次,且每次成功获取后requeuesCounter.incr(),失败且状态非Open时记not_open。

七、测试验证与观测建议

重试指标的语义在仓库测试中有完整印证,可参考:

  • RequeueFilterTest.scala:验证requeues、requeues_per_request、budget_exhausted、request_limit、cannot_retry等计数在预算耗尽、请求上限、状态非Open等场景下的精确行为;
  • RetryFilterTest.scala:验证retriesstat 与budget_exhausted的取值;
  • RetriesTest.scala:验证模块级参数(Policy/Budget)与过滤器组装的整体行为。

实际观测建议:

  1. 区分口径:应用侧先看tries(逻辑请求)判断调用量,再看retries/*(物理请求)判断重试开销;二者差距越大,说明重试放大越明显;
  2. 关注预算:retries/budget持续走低或retries/budget_exhausted快速上涨,说明系统正处于高失败率下的重试风暴边缘,预算正在发挥限流作用;
  3. 排查放弃原因:request_limit上涨说明单请求配额过小或预算被过度消耗;not_open/cannot_retry上涨则指向下游Status非Open(如连接池耗尽、熔断器打开),此时应优先排查负载均衡与连接池配置,而非单纯调高重试次数。

正确理解这 8 个指标,是监控 Finagle 客户端重试健康度的基础:它们共同构成了一套从"是否重试"(requeues/cannot_retry/not_open)到"重试多少"(budget/request_limit/budget_exhausted)再到"重试成本"(retries/requeues_per_request)的完整可观测体系。

  • 后端
  • RPC框架

【免费下载链接】finagle

A fault tolerant, protocol-agnostic RPC system

项目地址:https://gitcode.com/gh_mirrors/fi/finagle
点击查看免费下载

相关推荐

上一篇:m3u8下载神器:终极免费工具,永久保存直播视频的完整方案
下一篇:终极Windows优化神器:WinUtil完整指南 - 一键搞定所有Windows管理任务

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

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

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

立即咨询