服务重试:设置调用重试策略,自动重试,自动Tracing

Created By RV, and licensed with Creative Commons "CC BY-NC-ND 4.0"

目的与场景

在生产环境中,服务调用,失败是常态。

例如,服务重启中、网络抖动、瞬时过载……。

在这一章,讲解如何为服务调用设置重试策略:调用失败时自动重试,直到成功或耗尽次数。

Kree4X支持调用重试,同时还默认支持了自动Tracing

最后一次重试时,系统自动开启调用链路追踪,使用Tracing系统自动跟踪调用过程。

关于Tracing的能力,参阅:追踪:KreeX Tracing

一. 概念

1. 总调用次数:times

通过服务存根的retry()接口,设定服务调用允许总执行次数。

serviceStub.retry(times, interval)times 表示总调用次数

times是包含首次调用在内的总次数

serviceStub.retry(3, 200) // 总共最多调用3次(首次 + 2次重试),间隔200ms

2. 重试策略: Retrier

默认的 retry(times, interval) 是设定固定间隔(FixedInterval)Retrier的一个语法糖。

等同于:retry(new FixedInterval(times,interval))

可使用 Retrier 配置多种重试策略:

  • 固定间隔:每次重试间隔相同
  • 固定退避(fixedBackoff):固定间隔 + 随机抖动,分散瞬时故障压力
  • 指数退避(exponentialBackoff):间隔指数增长,缓解持续过载
  • 线性退避(linearBackoff):间隔线性增长
  • 因子递增(factorIncrease):间隔按倍数增长
  • 摆动间隔(shuttleInterval):间隔在范围内摆动

Retrier的详情,参阅:零零碎碎的稀奇古怪之Retrier

3. 自动Tracing

Kree4X,默认关闭全局Tracing,但自动开启“服务调用失败重试”,支持自动Tracing。

原因只有一个,性能。

最后一次重试会自动开启Tracing,调用最终失败时,能通过Tracer可查阅最后一次尝试的完整调用栈、时间线。

二. 示例代码

在下边的示例中,我们模拟一个不稳定服务

  • node-a:注册两个服务
    • broken:永远失败(模拟宕机)
    • flaky:前2次调用失败,第3次成功(模拟启动中)
  • node-b:调用方,设置不同的重试策略,观察重试行为

1. 搭建拓扑

// 不稳定服务存根:前2次失败、第3次成功,可重置计数
function createFlakyService () {
  const stub = {
    calls: 0,
    ping () {
      stub.calls++
      if (stub.calls <= 2) throw new Error('still warming up')
      return 'pong'
    },
    reset () { stub.calls = 0 }
  }
  return stub
}

const flaky = createFlakyService()
const nodeA = create('node-a', 'Unstable provider')
nodeA.register('broken', { ping () { throw new Error('always broken') } })
nodeA.register('flaky', flaky)
nodeA.listen('tcp://127.0.0.1:8060')

const nodeB = create('node-b', 'Retry caller')
nodeB.attach('tcp://127.0.0.1:8060')

await nodeA.start()
await nodeB.start()

2. 无重试:调用直接失败

未设置重试策略时,一次失败即抛错:

const broken = nodeB.service('broken').timeout(5000)
try {
  await broken.ping()
} catch (e) {
  // 调用失败
}

3. 固定间隔重试:retry(times, interval)

flaky 设置 retry(3, 200):最多3次总调用,间隔200ms。前2次失败自动重试,第3次成功:

flaky.reset()
const flakyFixed = nodeB.service('flaky').timeout(5000)
flakyFixed.retry(3, 200)
const pongFixed = await flakyFixed.ping()  // pong,服务端共收到3次调用

4. Retrier实例:指数退避

Retrier 提供若干静态工厂方法,直接传入实例即可:

import { Retrier } from '@kree4js/commons-retrier'

flaky.reset()
const flakyBackoff = nodeB.service('flaky').timeout(5000)
flakyBackoff.retry(Retrier.exponentialBackoff(100).times(3))
const pongBackoff = await flakyBackoff.ping()  // pong

间隔依次约为100ms → 200ms → 400ms,指数增长。

5. 回调形式:链式配置

retry(callback) 传入一个回调,在回调中链式配置 Retrier

flaky.reset()
const flakyChain = nodeB.service('flaky').timeout(5000)
flakyChain.retry(r => r.times(3).fixedBackoff(100, 0))
const pongChain = await flakyChain.ping()  // pong

采用固定退避:间隔固定100ms + 0ms抖动(fixedBackoff(interval, jitter))。

6. 清除重试策略

retry() 无参调用,则会清除已设置的重试策略。

const brokenNoRetry = nodeB.service('broken').timeout(5000)
brokenNoRetry.retry()  // 清除,等价于不重试
try {
  await brokenNoRetry.ping()
} catch (e) {
  // 调用失败,不再自动重试
}

7. 自动Tracing

最后一次重试,自动开启Tracing。

调用结束后,从服务存根的 lastTracer 属性取Tracer,查看完整的调用链路:

flaky.reset()
const flakyTrace = nodeB.service('flaky').timeout(5000)
flakyTrace.retry(3, 200)
const pongTrace = await flakyTrace.ping()  // pong,3次尝试

const tracer = flakyTrace.lastTracer
const actions = Array.from(tracer._timeline, t => t.action)
// Tracer.Construct → FindServices.Start → FindServices.Done → _Invoke.Start → ...

Tracer时间线完整记录了最后一次尝试的每个阶段:服务查找、候选选定、调用发起、结果归约等。

三. 须强调的细节

1. times是总调用次数,不是重试次数

所有的调用,都是“试图调用”。

首次调用,就是首次试图调用。

所以,设定的times,是总调用次数。

retry(3, 200) = 首次 + 2次失败重试,最多执行3次调用。

2. 成功即停止

调用成功,重试不执行。

重试策略,只在失败时生效。

3. 重试间隔vs调用超时

服务调用,默认超时30s。可使用serviceStub.timeout(ms) 调整。

重试间隔应小于服务调用超时,否则重试没有意义。

4. 自动Tracing只在最后一次重试开启

  • 前N次失败尝试:不开启Tracing
  • 最后一次尝试,自动开启Tracing

最终失败时,Tracer中保留了完整的调用过程。

5. 失败不一定需要重试

重试适合瞬时故障:重启中、网络抖动、过载。

若服务持续不可用(如宕机、协议不匹配),重试只会浪费资源,此时Fail-Fast才是常识。

6. 幂等性

重试意味着同一调用可能执行多次。

服务方法应具备幂等性、或者原子性(或至少可安全重复执行),对非原子性的服务,多次重试可能造成副作用:例如,重复下单、重复扣款……。

四. 涉及到的API

1. 设置重试策略

/**
 * 清除重试策略(等价于不重试)。
 *
 * @returns {this} 当前存根,支持链式。
 */
retry(): this

/**
 * 固定间隔重试。
 *
 * 注意:times是总调用次数,不是重试次数。
 * 例如retry(3, 200)表示最多3次总调用,间隔200ms。
 *
 * @param {number} times - 总调用次数(含首次尝试)。
 * @param {number} [interval] - 重试间隔(毫秒),默认固定间隔。
 * @returns {this} 当前存根,支持链式。
 */
retry(times: number, interval?: number): this

/**
 * 使用Retrier实例(可配置退避、超时等)。
 *
 * @param {Retrier} retrier - 配置好的Retrier实例。
 * @returns {this} 当前存根,支持链式。
 */
retry(retrier: Retrier): this

2. 设置服务调用超时

/**
 * 设置服务调用的超时时间(毫秒)。
 *
 * @param {number} [ms=30000] - 超时时间,默认30000。
 * @returns {this} 当前存根,支持链式。
 */
serviceStub.timeout(ms?): this

3. Retrier静态工厂

// 常用静态工厂,返回配置好的Retrier实例
Retrier.times(n)                    // 最多n次
Retrier.fixedInterval(ms)           // 固定间隔
Retrier.fixedBackoff(ms, jitter)    // 固定退避(间隔+抖动)
Retrier.linearBackoff(inc, jitter)  // 线性退避
Retrier.factorIncrease(factor)      // 因子递增
Retrier.exponentialBackoff(jitter)  // 指数退避
Retrier.shuttleInterval(step, jitter) // 摆动间隔
Retrier.infinite()                  // 无限次数(配合timeout使用)

4. Retrier实例方法

// 链式配置
retrier.times(n).fixedInterval(ms)
retrier.name('name')          // 命名,便于日志识别
retrier.taskTimeout(ms)       // 单次调用超时,默认2s
retrier.timeout(ms)           // 全部重试总超时,默认120s
retrier.noTimeout()           // 禁用总超时

// 事件监听
retrier.onStart(fn)           // 开始
retrier.onRetry(fn)           // 一次尝试开始
retrier.onSuccess(fn)         // 一次尝试成功
retrier.onFailure(fn)         // 一次尝试失败
retrier.onTimeout(fn)         // 总超时
retrier.onTaskTimeout(fn)     // 单次超时
retrier.onMaxRetries(fn)      // 达到最大次数
retrier.onStop(fn)            // 手动停止
retrier.onCompleted(fn)       // 全部结束

五. 可运行代码

完整示例代码,参见:12-retry-policy.mjs

results matching ""

    No results matching ""