Jina Client 瞬态错误处理指南:指数退避重试与 continue_on_error 实战
Jina Client 瞬态错误处理指南:指数退避重试与 continue_on_error 实战
瞬态错误(Transient Errors)是分布式推理系统中客户端与服务端之间最常见的不稳定因素,往往由网络抖动、服务重启或数据库等依赖短暂不可用引起。Jina 的 Client.post() 内置了一套基于指数退避(Exponential Backoff)的重试机制,并通过 continue_on_error 支持在执行器出错时继续发送剩余请求。阅读本文后,你将掌握 Jina 客户端重试参数的完整语义、gRPC 流式/一元 RPC 两种模式下的重试差异,以及处理大输入与长耗时任务的推荐配置。
瞬态错误概述:可重试、可忽略与不可恢复
瞬态错误大多源于客户端与目标服务器之间的网络问题,或服务器与其依赖(如数据库)之间的通信故障。Jina 官方文档 transient-errors.md 明确将其划分为三类处理方式:
- 忽略:当某个由生成器(generator)或操作序列产生的结果与整体成功无关时,可以忽略该错误;
- 重试:在有限次数内重试,假设恢复逻辑会在重试间隙修复瞬态问题;
- 接受失败:当操作无法被成功完成时,接受这一事实并将错误上抛给调用方。
Jina 为第一类"可重试"场景提供了两条互补的路径:客户端侧的 post() 重试参数(解决"客户端 ↔ 网关"之间的连通性问题),以及网关侧的 retries 参数(解决"网关 ↔ 执行器"之间的通信问题)。本文依次展开。
post() 重试参数:指数退避的完整语义
Client.post()(定义于 jina/clients/mixin.py)接受以下四个重试控制参数:
| 参数 | 默认值 | 语义 |
|---|---|---|
max_attempts |
1 |
发送尝试的总次数,包含原始请求。例如 max_attempts=5 表示 1 次原始请求 + 4 次重试 |
initial_backoff |
0.5 |
第一次重试前的随机延迟上限(秒) |
max_backoff |
2 |
指数增长后的退避时间上限(秒),防止延迟无限膨胀 |
backoff_multiplier |
1.5 |
退避时间的指数增长因子 |
这四个参数共同构成一个带随机抖动(jitter)的指数退避策略:第 n 次尝试发生在 random(0, min(initial_backoff * backoff_multiplier**(n-1), max_backoff)) 秒之后。即首次重试的延迟落在 [0, initial_backoff] 区间,此后每次延迟的上限按 backoff_multiplier 指数放大,直到被 max_backoff 封顶。
底层实现:wait_or_raise_err 与退避计算
重试的核心逻辑集中在 jina/clients/base/retry.py,其中 _wait_time() 精确实现了上述公式:
def _wait_time(attempt, backoff_multiplier, initial_backoff, max_backoff):
if attempt == 1:
wait_time = initial_backoff
else:
wait_time = random.uniform(
0,
min(initial_backoff * backoff_multiplier ** (attempt - 1), max_backoff),
)
return wait_time
同步/异步两个入口 sync_wait_or_raise_err 与 wait_or_raise_err 的行为一致:若 attempt == max_attempts,则调用 _raise_last_attempt() 将底层异常重新抛出(并附带 jina-client-attempts 元数据,记录最终尝试次数);否则按上述时间 sleep 后允许下一次尝试。
重试针对的异常类型包括:grpc.aio.AioRpcError、aiohttp.ClientError、asyncio.CancelledError 以及 Jina 自定义的 InternalNetworkError(定义于 jina/excepts.py,用于表达网关与执行器之间的内部通信失败)。
gRPC 流式与一元 RPC 的重试行为差异
post() 的 stream 参数(默认 True)决定 gRPC 调用方式:True 时调用服务端流式 RPC(JinaRPC.Call,见 jina/clients/base/stream_rpc.py),False 时调用一元 RPC(JinaSingleDataRequestRPC.process_single_data,见 jina/clients/base/unary_rpc.py)。不同的 inputs 类型与 stream 组合,重试的接管方完全不同:
场景一:stream=True + Generator/Iterable 输入(手动重试)
当 inputs 是生成器或可迭代对象时,结果必须被消费后才能发现流中的错误,因此内置的 gRPC 服务端重试虽被配置但无法保证生效,需要调用方手动执行重试循环。官方文档给出了完整示例,下面是对应仓库实测可用的完整代码(测试验证见 tests/unit/clients/python/test_client_errors.py):
from jina import Client
from docarray import BaseDoc
from jina.clients.base.retry import wait_or_raise_err
from jina.helper import run_async
client = Client(host='grpc://localhost:12345')
max_attempts = 5
initial_backoff = 0.8
backoff_multiplier = 1.5
max_backoff = 5
def input_generator():
for _ in range(10):
yield BaseDoc()
for attempt in range(1, max_attempts + 1):
try:
response = client.post(
'/',
inputs=input_generator(),
request_size=2,
timeout=0.5,
)
assert len(response) == 1
except ConnectionError as err:
run_async(
wait_or_raise_err,
attempt=attempt,
err=err,
max_attempts=max_attempts,
backoff_multiplier=backoff_multiplier,
initial_backoff=initial_backoff,
max_backoff=max_backoff,
)
这里的 run_async()(定义于 jina/helper.py)是 Jina 提供的通用 asyncio.run 封装,兼容 Notebook 中已存在事件循环的场景;wait_or_raise_err 在未达到 max_attempts 时负责退避等待,达到上限后直接抛出错误。
场景二:stream=True + Document/DocList 输入(内部重试)
当 inputs 是单个 Document 或 DocList 时,重试由客户端内部接管。在 jina/clients/mixin.py 的 _with_retry() 中可以看到:当输入为 Document/DocumentArray、客户端为 gRPC 且 max_attempts > 1、stream=True 时,客户端会自行循环 max_attempts 次,捕获 AioRpcError、InternalNetworkError 和 ConnectionError 后调用 wait_or_raise_err 退避重试。
场景三:stream=False(一元 RPC,内部重试)
当 stream=False 时,客户端调用一元 RPC,重试完全由内部处理:unary_rpc.py 中每个请求的 _with_retry 都会循环 1..max_attempts,逐个捕获 AioRpcError 与 InternalNetworkError 并调用 wait_or_raise_err。这是最"省心"的模式,适用于文档推荐的对大输入做切片重试的场景。
gRPC 服务端重试配置的注入
值得注意的底层细节:post() 的重试参数会同时被用于设置 gRPC 的 retry service options,以提升 gRPC 自身重试条件满足时的成功率。该逻辑位于 jina/clients/base/grpc.py 的 client_grpc_options():当 max_attempts > 1 时,会构造包含 maxAttempts、initialBackoff、maxBackoff、backoffMultiplier 以及可重试状态码 ["UNAVAILABLE", "DEADLINE_EXCEEDED", "INTERNAL"] 的 methodConfig JSON,并通过 grpc.enable_retries=1 与 grpc.service_config 注入 channel 选项。
同时必须明确 gRPC 官方的一个事实约束:内置 gRPC 重试的能力范围有限,仅在特定条件下生效,其完整设计约束见 gRPC 官方的 A6 客户端重试设计文档(https://github.com/grpc/proposal/blob/master/A6-client-retries.md)。因此在流式模式下,仍应遵循"场景一"的手动重试以保证正确性。
continue_on_error:执行器出错时继续发送
除网络瞬态错误外,Jina 还区分了一类执行器逻辑错误——即 Executor 在响应中返回的错误,如业务逻辑在操作执行期间抛出的异常。post() 的 continue_on_error 参数(默认 False)专门处理此类错误:
- 设为
True时,Client 会记录错误但继续发送剩余请求,不会因单个请求失败而中断整个批次; - 该参数仅适用于 Executor 引起的异常;对于网络连通性问题(
AioRpcError、aiohttp.ClientError、asyncio.CancelledError、InternalNetworkError等网关与执行器通信期间的瞬态错误),异常仍会被直接抛出,continue_on_error不会吞掉它们。
在源码层面,continue_on_error 被透传到客户端的 _get_results(见 jina/clients/base/grpc.py),并最终传给 callback_exec(用于 on_done/on_error/on_always 回调分发)与 RequestStreamer,从而决定单个请求失败后是继续消费输入迭代器还是终止。
网关侧 retries 参数:Gateway ↔ Executor 通信
continue_on_error 解决的是"执行器业务错误",而网关与执行器之间的瞬态错误则由 Gateway 的 retries 参数控制重试次数。该参数在 docs/concepts/orchestration/gateway-args.md 中定义为"每个 gRPC 调用的重试次数,若 <0 则默认 max(3, num_replicas)",默认值为 -1。
更完整的失败重试策略(含本地部署、Kubernetes 有无 service mesh 三种部署形态下的每 Executor 重试规则)可参考 docs/concepts/orchestration/handle-exceptions.md:本地多副本部署会"每个副本至少尝试一次、总计至少三次";Kubernetes 无 service mesh 时由于 gRPC 负载均衡限制只能"同一副本重试三次";引入 service mesh 后可将重试分散到不同副本。
大输入与长耗时任务的重试建议
针对大批量输入或长耗时操作,Jina 文档给出的推荐配置是:
- gRPC 客户端将
stream设为False,让一元 RPC 由客户端在内部按max_attempts、initial_backoff、backoff_multiplier、max_backoff执行重试,避免在流式模式下因结果必须被消费才能发现错误而导致的重试失效; - 设置合适的
request_size,将大输入切分为更小的请求批次,使单个请求的重试在服务器端开销更低、更易成功(request_size <= 0表示所有输入合并为一个请求,见 jina/clients/mixin.py)。
from jina import Client
client = Client(host='grpc://localhost:12345')
response = client.post(
'/',
inputs=large_doc_list, # DocList 输入
request_size=64, # 每批 64 个 Document
stream=False, # 一元 RPC,内部重试
max_attempts=5,
initial_backoff=0.8,
backoff_multiplier=1.5,
max_backoff=5,
)
HTTP 与 WebSocket 客户端同样支持上述重试参数(见 jina/clients/base/http.py 中 HTTPClientlet 对 max_attempts 等参数的接收),适合网络环境不稳定的跨地域部署。
重试后的结果处理:回调函数
重试最终成功或失败之后,如何感知结果?Jina 通过 on_done、on_error、on_always 三个回调函数完成结果分发(其内部执行入口为 jina/clients/helper.py 中的 callback_exec):
on_done:请求被成功解析时调用;on_error:请求被拒绝(失败)时调用;on_always:无论成功与否都会调用。
关于在重试场景下通过回调处理成功与失败的完整用法,可参考 callbacks.md。
关键结论速查
- 区分错误类型再决定策略:网络类瞬态错误(
AioRpcError/ClientError/CancelledError/InternalNetworkError)走客户端重试或网关retries;执行器业务错误走continue_on_error;无关紧要的操作错误可直接忽略。 - 记住退避公式:第 n 次尝试发生在
random(0, min(initial_backoff * backoff_multiplier**(n-1), max_backoff)),首次即initial_backoff封顶区间,实现见 jina/clients/base/retry.py。 - 流式 + 生成器输入必须手动重试:用
wait_or_raise_err+run_async手写循环(见上文完整示例)。 - 大输入推荐
stream=False+ 合适的request_size,让一元 RPC 内部重试接管。 - gRPC 内置重试有适用前提:gRPC 服务端重试被配置但不保证在流式场景生效,详情见 gRPC A6 设计文档。