Step 5 Preview 接流式回包时,长回答中间经常有几秒到几十秒没有任何数据帧。如果客户端没配读取超时,或者把读取超时当成“整段响应的总时长”来设,连接就会在中间被断开;反过来,如果断开后无条件重试,已经吐给用户或已经发出计费请求的那部分内容可能再来一遍。要落到配置上的其实就两件事:把连接超时和读取超时分开设,把重试限制在明确的错误类型上。
流式回包建议分两个超时:连接超时管建连,通常几秒;读取超时管两次数据块之间的空闲间隔,用它兜住“长时间无数据”。重试只对建连失败、未收到任何数据块时的读取超时、限流和部分服务端错误开放,一旦已经消费到片段就不建议整段重试。是否重复输出或重复计费,用请求ID在两边日志里核对确认。具体阈值和错误码含义需要结合所用 SDK 与服务端实现确认。
在客户端设置流式读取超时
先区分两个参数的作用范围:连接超时(connect timeout)管的是 TCP/TLS 建连阶段,超了就表示没连上;读取超时(read timeout,有些客户端叫空闲超时)管的是“上一次读到数据到现在”的间隔,只要陆续有数据块进来,计时器就会重置。多数流式客户端里后者是空闲超时,不是整段响应的总时长,这一点要结合你所用 SDK 的说明确认,别混用。
# 通用接入骨架,字段名按你所用 HTTP 客户端替换
client_config = {
"connect_timeout": 5, # ①连接超时:建连阶段,通常设几秒
"read_timeout": 60, # ②读取超时:两次数据块之间的最大间隔
"stream": True,
}
resp = send(url, payload, stream=True)
for chunk in resp.iter_chunks(): # 每收到一个数据块,②的计时器重置
handle(chunk)把②设得比典型的最长“思考停顿”略大一些,不要设成整段回答的预期时长。先按业务里最常见的停顿估一个值,再在日志里观察实际停顿分布去调,而不是一次定死。
观察长时间无数据返回时的错误类型
出现长时间无数据时,先分清楚是“读取超时”还是“连接被断开”,两者处理方式不同。前者通常由你配的②触发,后者可能来自服务端主动结束、中间链路重置或网络抖动。记录时保留四样东西:时间戳、请求ID、异常类名、SDK 抛出的原始错误码或 errno。
# 日志里按原样记录,不要自己改写错误码含义
[stream] start request_id=abc123
[stream] recv chunk bytes=12
[stream] 32s no data
[stream] ERROR type=ReadTimeout code=<原始错误码> msg=<原样粘贴>
# 或另一次:
[stream] ERROR type=ConnectionReset msg=<原样粘贴>先抄下原始值,再对照所用 SDK 或服务端的文档确认语义。不同客户端的错误码定义并不通用,不要把 A 库里某个码的含义套到 B 库上;也不要在没查证前,凭经验给一个错误码下结论。
给重试加条件
重试最容易出问题的地方,是“已经收到一部分内容之后再整段重试”。判断时优先看现象,而不是只看错误码:
| 现象 | 建议处理 | 说明 |
|---|---|---|
| 连接超时,建连阶段失败 | 可小次数重试 | 没有产生任何已消费内容 |
| 读取超时,且一个数据块都没收到 | 可重试 | 重试不会造成重复输出 |
| 读取超时,但已经收到部分片段 | 不建议整段重试 | 可能重复输出或重复计费,先确认能否续传 |
| 参数或鉴权类 4xx | 不重试 | 重试结果通常一样 |
| 限流类响应 | 退避后有限重试 | 按响应头给出的等待时间处理 |
| 服务端 5xx | 有限重试 | 建议带退避并设次数上限 |
重试策略里至少要有三样:次数上限、退避间隔、以及“已经收到多少字节”的记录。错误码含义以你所用 SDK 和服务端定义为准,上表的判断顺序是先看现象、再看码。
用请求ID去重服务端响应
给每次用户请求生成一个请求ID(或在调用链上游传入),放在请求头或 body 里一起发出。如果服务端支持幂等键,重试时携带同一个请求ID,它应当只按首次请求计一次;服务端日志通常按这个ID可检索。
headers = {
"X-Request-Id": req_id,
"Idempotency-Key": req_id, # 以服务端实际支持的字段名为准
}
# 事后核对:
# grep -c "request_id=abc123" client.log
# grep -c "request_id=abc123" service.log校验方式是比对同一次用户操作的客户端重试次数与服务端收到的同ID记录数,以及计费侧是否出现两条同ID记录。若服务端不支持幂等,客户端能做的只是减少“已产生输出后再重试”的场景,这点需要结合你的服务端实现确认,不能只靠客户端假设。
回放一次超时场景验证配置生效
用一个本地 mock 或可控的中间层人为制造停顿,比等线上偶发要可靠。把读取超时临时设小,观察是否按预期触发、重试是否被限制住。
# 模拟:第二个数据块前停住,超过 read_timeout=5
import time
def mock_stream():
yield b"chunk-1"
time.sleep(10) # 大于前面配的 read_timeout
yield b"chunk-2"- 把读取超时临时设为 5 秒,mock 里 sleep 10 秒,发一次流式请求。
- 记录客户端抛出的异常类型与原始错误码,和前面日志格式对齐。
- 打开重试并把次数上限设为 2,再跑一次,数日志里出现了几次重试、每次从第几个数据块开始。
- 把两次收到的片段拼接起来,检查是否出现重复内容。
- 把重试关掉再跑一次,确认行为与配置一致。
验证通过的标准是:超时按配置的阈值触发、重试次数不超过上限、没有在已产生输出后整段重试。若 mock 的表现与真实链路不一致,以真实链路的日志为准,不要用本地结果去推断线上行为。