凌晨跑批量代码改造任务,日志里最扎眼的不是模型返回的报错,而是一行几乎没有堆栈信息的提示:stream disconnected before completion。前面几十行增量输出戛然而止,进度条停在一个尴尬的百分比上,进程既不退出也不继续。这个报错在 Codex 这类编码助手、CLI 工具以及任何开着流式开关的 SDK 调用里都相当高频,它既可能是客户端读取逻辑写得不够细致,也可能是链路上某一层把长连接掐掉。这篇内容就围绕这句话展开:它到底在表达什么、断在哪一环、客户端该怎么改、重试该怎么设计、哪些默认参数是隐形雷区。无论你是刚接 SDK 跑通第一个流式 Demo,还是已经在维护一套批量代码生成流水线,下面这些细节应该都能直接拿去用。
1. 先把报错翻译成人话:它在说哪一段出了问题
1.1 这句话的准确含义与三个高频触发现场
先把字面意思拆开。流式响应的本质是服务端在一个 HTTP 长连接上,按顺序不断写入增量片段,最后写入一个明确的结束标记(多数实现是data: [DONE]这一行,或者一个带finish_reason的收尾片段)。客户端只有在读到结束标记之后,才认为这次请求完整结束。stream disconnected before completion的意思非常直白:客户端的读取循环还没见到结束标记,连接就断开了——可能是对端关闭,可能是本地读取超时,也可能是中间某一层把空闲连接回收了。所以它不是“模型拒绝回答”,也不是“参数填错”,而是传输层面没走完。
这个区分很重要,因为它直接决定排查方向。很多人第一反应是换模型、改提示词、降温度,全是南辕北辙。真正该看的是:断在第几个片段、断之前的最后一段数据长什么样、距离上一个片段隔了多久。
从我这几年接触到的场景看,触发点集中在三类。第一类是长输出任务,比如让模型一次性重构一个几百行的模块,输出 token 数量大、生成时间长,中途任何一次空闲检测都可能命中。第二类是高并发批量任务,几十个流同时在跑,连接池被打满,新请求排队,排队时间被算进了客户端超时。第三类是环境变化,笔记本合盖休眠后唤醒、无线网络切换、容器被调度到另一台宿主机,这些都会让已有的长连接变成“僵尸连接”——本地以为还活着,实际对端早就没了。
还有一类容易被忽略:SDK 版本与流式开关的组合问题。有些版本在流式模式下默认的读取超时偏短,或者对分块数据的解析方式有变化,升级后原本能跑通的脚本突然开始报这句话。遇到这种情况,先别急着改业务代码,把 SDK 版本钉住、用最小脚本复现一次,往往比在业务层加一堆重试更省事。
1.2 为什么流式比普通请求脆弱这么多
普通请求是一次完整的往返:发出去、等、收完、关。中间层即使有缓冲,也只是把整个响应攒齐一次性给你,最多慢一点,不会“断在半路”。流式完全不一样,它把一个长连接拆成几十上百次小写入,每一次写入之间都有间隔,而每一段间隔都是一次被超时机制命中的机会。
这里有个容易被误解的点:很多中间层的空闲超时是按“两次数据之间的间隔”算的,不是按“整个请求总时长”算的。所以你会看到一种很迷惑的现象——一个本该跑三分钟的任务,在一分钟出头就断了。原因不是总时长超限,而是模型在某一段“思考”时间较长,超过三十秒没吐新片段,中间层的空闲回收先动手了。
另一个不对称在于缓冲。普通的响应体被中间层攒一攒无所谓,反正最后一次性给你。但流式响应一旦被攒,客户端就收不到实时片段,感知上就是“卡住了”,然后客户端的读取超时先触发,主动断开,报出来的还是这句话。所以排查流式问题,永远要同时怀疑两头:是不是对端没发,是不是中间层攒住了没转,是不是本地等不及先走了。
还有一个隐藏成本是连接复用。为了性能,HTTP 客户端普遍会保持长连接复用。复用的连接如果在上一次使用后已经半关闭(对端发了 FIN,本地还没感知),下一次请求写进去就会失败,或者读到空。这类问题在低频调用时不明显,一旦并发上来就集中爆发,而且报错信息往往就是这种“没走完就断了”的形态。
2. 断点定位:先搞清楚是哪一层的锅,再动手改
2.1 一条流式请求要穿过的五段路
不要一上来就改代码。先把链路画清楚,从你的进程到模型服务,大致会经过:本地进程 → 本机网络栈 → 企业出口网关或负载均衡 → 服务接入层 → 推理服务。这五段里,前两段你能完全控制,第三段通常有运维同学管,后两段只能靠观测。
每一层的断连特征其实不太一样,认准特征能省掉大量试错:
| 链路层级 | 典型断连特征 | 首选的验证动作 |
|---|---|---|
| 客户端读取逻辑 | 断在固定片段数附近,或固定秒数后 | 打印每个片段的到达时间戳 |
| 本机网络栈 | 切换网络、休眠唤醒后集中出现 | 换一个网络环境跑最小脚本 |
| 出口网关或负载均衡 | 空闲约 30 秒或 60 秒必断,规律性极强 | 让服务端按固定间隔发心跳观察是否还断 |
| 服务接入层 | 高并发时成片失败,低并发正常 | 把并发从 1 开始逐个加 |
| 推理服务 | 特定长提示词或特定参数组合下复现 | 固定提示词,只改输出长度上限 |
这张表我自己用了很久。判断“是不是空闲超时”有个很土但极有效的办法:写一个测试脚本,让服务端每隔固定时间发一个空的心跳行,看断连时间点是否跟着后移。如果后移了,几乎可以确定是空闲回收;如果纹丝不动,那更可能是读取超时或者缓冲区的问题。
2.2 用三十行脚本把断点钉死
排查阶段最忌讳在业务代码里加日志,因为业务代码有太多干扰因素。我的习惯是单独写一个最小脚本,只做三件事:发起流式请求、记录每个片段的到达时刻、记录结束原因。下面这段是 Python 版本的骨架,用官方 SDK 和裸 HTTP 两种方式都可以照这个思路写:
import time, httpx, json url = "https://api.example.com/v1/chat/completions" headers = {"Authorization": "Bearer " + "YOUR_KEY", "Content-Type": "application/json"} payload = {"model": "code-model", "stream": True, "messages": [{"role": "user", "content": "重构下面这个函数……"}]} t0 = time.time() last = t0 count = 0 # 关键:read 超时必须显式给,默认值往往是坑 timeout = httpx.Timeout(connect=10.0, read=120.0, write=10.0, pool=10.0) try: with httpx.Client(timeout=timeout) as client: with client.stream("POST", url, headers=headers, json=payload) as resp: print("status:", resp.status_code, "首包耗时: %.2fs" % (time.time() - t0)) for line in resp.iter_lines(): if not line: continue now = time.time() count += 1 gap = now - last last = now print("第 %d 段 间隔 %.3fs 长度 %d" % (count, gap, len(line))) if line.strip() == "data: [DONE]": print("正常结束,总耗时 %.2fs" % (now - t0)) break except Exception as e: print("异常类型:", type(e).__name__, "已收片段:", count) print("异常信息:", str(e)[:200])跑一遍,输出会告诉你三件事。首包耗时能区分是排队慢还是传输慢;片段间隔分布能定位到哪一秒开始异常,如果所有间隔都在几十毫秒,只有一个间隔是三四十秒,那基本就是那一次空闲检测;已收片段数则告诉你模型到底生成到了哪一步——如果片段数很少就断了,问题偏连接建立;如果快收完才断,问题偏尾部超时。
裸 HTTP 能复现、SDK 不能复现,或者反过来,这个信息同样值钱,说明问题出在封装层而不是链路层。我碰到过一次,裸脚本跑得好好的,换成 SDK 调用就断,最后发现是 SDK 内部对响应做了额外的缓冲处理,读取超时的计算方式和裸 HTTP 不一样。
2.3 一份可以贴到工位上的自检清单
排查顺序建议固定下来,形成肌肉记忆,这样下次再遇到不会慌:
- 换最小脚本,先排除业务代码干扰。
- 看首包耗时,超过十秒说明排队或建连有问题。
- 统计片段间隔的分布,找出那个异常的间隔值。
- 把读取超时调大,如果断连时间点跟着变化,问题在本地。
- 加服务端心跳,如果断连时间点后移,问题在中间层空闲回收。
- 把并发降到 1,如果不再复现,问题在连接池或服务侧限流。
- 换网络环境,如果换了就好,问题在本机网络栈。
这七步走完,九成情况下你不需要再猜了。剩下那一成,通常是多个因素叠加,比如中间层空闲回收和本地读取超时数值恰好接近,两个机制同时生效,看起来就毫无规律。
3. 客户端改造:把“收完再处理”彻底改成“边收边处理”
3.1 正确的流式读取写法长什么样
报这个错的代码,八成有这样的特征:把响应体一次性读完再解析、用会缓冲的中间件包了一层、或者干脆没有设置读取超时。流式读取的正确姿势有几个硬性要求,缺一不可。
第一,必须逐块消费。Python 里用iter_lines()或iter_bytes(),Node 里拿response.body.getReader()循环read(),不要用await response.text()或者.json()。后者会把整个响应缓冲起来,流式就退化成了普通请求,不仅失去实时性,还更容易因为体量过大触发别的超时。
第二,必须显式设置读取超时。注意这里的关键:读取超时在很多客户端里指的是“两次读操作之间的最大间隔”,不是总时长。这意味着你把读取超时设成 120 秒,一个跑十分钟但每分钟都有数据的流是不会被掐断的。理解这一点,调参才有方向。
第三,必须处理增量解码。这是最容易被忽略的一条。响应体是字节流,一个中文字符在 UTF-8 里占三个字节,完全可能被切在两个片段之间。如果你直接对每个片段做.decode('utf-8'),遇到半截字符会抛异常,异常一出,整个读取循环就断了,报出来还是那句话。
import codecs, json decoder = codecs.getincrementaldecoder("utf-8")() buffer = "" for chunk in resp.iter_bytes(): # 增量解码:半截字符会被暂存到下一次 text = decoder.decode(chunk, final=False) buffer += text while "\n" in buffer: line, buffer = buffer.split("\n", 1) line = line.strip() if not line or not line.startswith("data:"): continue body = line[5:].strip() if body == "[DONE]": break try: obj = json.loads(body) # 单行内的 JSON 解析失败才需要处理 except json.JSONDecodeError: continue # 半截数据直接丢掉,等下一块补齐 delta = obj.get("choices", [{}])[0].get("delta", {}) if delta.get("content"): print(delta["content"], end="")Node 那边对应的是new TextDecoder("utf-8"),并在decode时传{ stream: true }。这个参数不加,同样会在多字节字符被切断时出问题。这类 bug 有个共同特点:平时跑英文提示词一点事没有,一换中文就偶发报错。因为这个现象,它经常被误判成“中文编码不支持”,其实是解析逻辑的问题。
3.2 三个超时参数怎么定,给一套能算的数
客户端超时一般分三层:连接超时、两次读之间的间隔超时、整体请求超时。很多人只设了一个“超时”就完事,结果三个语义被混在一起,调哪个都不对。
连接超时好定,局域网内给 5 到 10 秒,跨区域给 15 到 20 秒,超过这个数说明网络本身有问题,早断早重试更划算。
间隔超时需要用数据算。正常的流式输出,服务端每生成一个 token 就推一次,即使有攒批,间隔通常也在几十毫秒到一两秒之间。所以间隔超时给 30 秒已经非常宽松了。但有些实现会在开始正式输出前有一个较长的“首包等待”,如果服务端不额外发心跳,这段等待可能被误判成空闲。这种情况我一般单独拆出一个首包超时,给 60 秒,比间隔超时宽松,两者分开配置,问题就能精确定位。
整体超时反而最需要估。算法很简单:拿你的最大输出 token 数,除以模型的实际输出速率,再加上排队和首包的固定开销。举个例子,最大输出设成 4096 token,实测输出速率约每秒 40 到 60 token,取保守的 40,就是 102 秒;排队按 20 秒算,首包按 5 秒算,合计约 127 秒。再乘一个 1.5 的安全系数,整体超时给到 200 秒比较合适。如果你的业务会跑上万 token 的长任务,那整体超时要给到十分钟以上,这种情况下更该做的是缩小单次任务粒度,而不是无限拉长超时。
顺便说一个反直觉的经验:整体超时设得过长,反而更容易遇到这个报错。因为长时间不结束的连接更容易被中间层的空闲回收盯上,而且一旦断了,你要多等很久才知道失败。所以我更倾向于“短任务 + 多次拼接”,而不是“一次跑完”。
3.3 半截数据的处理,决定了你是优雅降级还是全盘崩溃
流式读取过程中,随时可能停下。停在半个 JSON 上是常态,不是异常。所以整个读取循环必须有“数据不完整也能收尾”的能力。
具体做法是维护一个字符串缓冲,每次追加新数据后按换行切分,只有完整的行才送进解析器,剩下的留在缓冲里等下一块。遇到JSONDecodeError不要抛,直接跳过那一行——因为很可能它确实是半截。同时要统计“跳过了多少行”,如果这个数字一直涨,说明你的切分逻辑有问题,比如服务端用了\r\n而你只按\n切,导致每行尾巴都多一个\r把 JSON 解析搞崩。
还有一种情况是断在结束标记之前。这时候你手上已经有一批完整的增量了,正确的做法是把它们拼起来当作部分结果保留,而不是全部丢弃。我在批量任务里会把已生成的内容写进临时文件,同时记录状态为“未完成”,后续由重试逻辑接手。这样一来,即使断连发生在 90% 的位置,那 90% 的成果也不会白费。这个改动看起来不起眼,但在跑上千个任务的时候,节省的算力和时间非常可观。
4. 重试设计:不是所有中断都值得再来一次
4.1 先分清哪些错误该重试,哪些重试就是灾难
一看到报错就包一层for i in range(3): try...,是很多脚本的通病。问题是这句话涵盖的情况太杂,有的是网络抖动,重试立刻就好;有的是参数错误,重试一百次也一样;还有的是限流,立刻重试等于火上浇油。
我一般按错误性质分类处理:
| 错误性质 | 典型表现 | 是否重试 | 处理方式 |
|---|---|---|---|
| 连接意外关闭 | 片段中途断开,无状态码 | 是,限次 | 带退避重试,最多 3 次 |
| 读取超时 | 固定秒数后断开 | 是,但先调参 | 先排查间隔超时再重试 |
| 服务端 5xx | 响应头有 500、502、503 | 是,带退避 | 按指数退避,避开雪崩 |
| 限流 429 | 响应头带重试建议 | 是,按建议等待 | 优先遵循响应头的等待时间 |
| 参数错误 400 | 明确的字段校验信息 | 否 | 直接修参数,重试无意义 |
| 鉴权失败 401、403 | 密钥无效或权限不足 | 否 | 修配置 |
| 上下文超长 | 提示词或输出超限 | 否 | 拆任务,不是重试问题 |
这张表里最值得说的是限流。很多服务在限流时会返回一个建议等待时长,直接用它比自己算退避更准。我见过不少脚本无视这个字段,自己按固定 1 秒重试,结果一直在限流区打转,看起来像“网络不好”,实际是自己把路堵死了。
4.2 退避参数怎么算,抖动量为什么必须有
指数退避的基本形式是等待时长 = 基数 × 2^重试次数。基数取 1 秒,重试五次得到 1、2、4、8、16 秒,累计 31 秒。这个量级适合大多数交互式场景——用户能忍,服务端也缓过来了。
但只做指数退避是不够的,必须加抖动。原因很直白:如果你同时跑了 50 个任务,它们在同一秒失败,那么第一次退避后会在同一秒集体重试,第二次又在同一秒集体重试。峰值一点没降,只是被推后了。加上 0 到 500 毫秒的随机抖动,这些请求就会被打散到半秒的时间窗里,压力曲线立刻平缓。
import random, time def backoff(attempt, base=1.0, cap=30.0, jitter=0.5): raw = min(cap, base * (2 ** attempt)) return raw + random.uniform(0, jitter) for attempt in range(5): try: run_stream_once() break except RetryableError: if attempt == 4: raise time.sleep(backoff(attempt))上限cap也别省。没有上限的话,到第八次重试就是 256 秒,一个批量任务会因为这个卡很久,还不如早点失败让上层调度。我一般把上限压在 30 秒左右,重试次数不超过 5 次,超过就交给任务队列重新排队。
还有一个细节:重试要有幂等键。如果你调用的接口支持传入一个请求标识,务必每次逻辑任务用同一个标识,这样即使上一次其实已经成功、只是响应没收到,服务端也能识别出这是重复请求,不会重复计费或重复生成。这个机制在断连重试场景下几乎是必需品。
4.3 断点续写:让已经生成的部分不白费
重试能解决“这次没成功”,但解决不了“白花了一次算力”。断点续写的思路是:把已经收到的增量存下来,重新发起时不让模型从头开始,而是接着写。
实现上有两种常见做法。一种是上下文拼接,把已生成的部分作为对话历史里的助手消息,再补一句“继续”,让模型从断点接着输出。这种做法实现简单,但要注意两点:一是新生成的开头容易和旧内容重复,需要在拼接时做去重;二是模型可能“忘记”之前的风格,需要把关键约束再强调一遍。
另一种更彻底的做法是分段生成,从一开始就不追求一次生成全部内容。比如重构一个两百行的函数,先让它输出结构提纲,再按函数逐个生成,每段之间做校验和拼接。这种做法把单次任务时长压了下来,断连概率大幅下降,而且每一段的失败可以独立重试,不影响已经完成的部分。我现在更倾向这种,虽然前期要多写一点编排逻辑,但长期跑批量的收益很明显。
续写还有个坑要提醒:重复片段检测。模型接着写的时候,经常会把断点前最后一两行重写一遍。如果你的下游要做语法校验或者直接写文件,重复内容会导致语法错误。我的处理方式是对比新旧拼接处的若干字符,如果发现重叠就裁掉一段。这个逻辑很土,但实测比任何花哨的算法都管用。
5. 服务端与中间层:那些默认值才是真正的隐形杀手
5.1 缓冲和超时的默认配置,几乎全都是为普通请求准备的
如果你有条件碰服务端配置,这一节价值最大。绝大多数中间层的默认行为是为普通短请求优化的,对流式一点都不友好。
先说缓冲。很多接入层默认会开启响应缓冲,把后端的输出攒到一定量再转发,目的是减少小包、提升吞吐。对流式来说这是灾难——客户端收不到实时片段,读取超时会先炸。解决办法很明确:对返回事件流类型(text/event-stream)的响应,关掉这一层的响应缓冲,并把对应的控制头带上,让上游知道不要攒。同时在响应头里加Cache-Control: no-cache和Connection: keep-alive,避免被当成普通响应处理。
再说空闲超时。很多接入层的连接空闲超时默认是 30 秒或 60 秒,这个值对普通请求绰绰有余,对流式却经常不够——模型在两段输出之间“想事情”超过这个时长,连接就被回收。我遇到过的几次规律性断连,都是卡在整 60 秒,一查就是默认空闲回收。这里的选择有两个:要么把这个超时调大,要么让服务端定期发心跳。
还有压缩。有些层默认对响应做压缩,压缩需要攒数据才能有效率,天然和流式冲突。流式响应最好不要走压缩,或者至少让压缩层知道要逐块刷出。这个问题比较隐蔽,因为表现不是断连,而是“首包特别慢”。
5.2 心跳与保活:成本最低的一个改动
如果你的服务端能改,加心跳是投入产出比最高的一招。做法很简单:在正式内容开始之前和输出的间隙,每隔 15 到 20 秒写入一行注释性质的空事件,比如一行以冒号开头的行,客户端解析时直接忽略。这一行内容不占多少流量,但足以让所有中间层的空闲计时器归零。
心跳间隔怎么选?要比链路上最小的那个空闲超时小一半以上。如果你不确定最小值是多少,就从 15 秒开始试。我见过用 20 秒心跳跑通的链路,最小空闲超时是 45 秒,留了一倍多的余量,比较稳妥。如果心跳设成 25 秒而链路超时是 30 秒,那就等于没加,稍微抖一下还是断。
客户端这边也要配合:解析时要明确知道“注释行不是数据”,不能把它当 JSON 去解析。很多人加了心跳之后反而报错变多,就是因为客户端把:开头的行送进了 JSON 解析器。顺手做件事,把心跳行的接收次数也统计一下,这个数字能直接反映链路是否健康。
5.3 日志埋点:下次再出问题,三分钟能定位
最后一个改造是日志。流式请求的日志和普通请求完全不是一套东西,普通请求记一个耗时和状态码就够了,流式必须记这些:首包耗时、片段总数、最大片段间隔、结束原因、已生成内容长度。
这五个指标的组合诊断能力很强。首包慢说明排队或建连有问题;片段总数比预期少说明生成被提前终止;最大间隔异常大说明有空闲超时风险;结束原因可以区分是正常结束、对端关闭还是本地超时;内容长度则直接决定要不要走续写逻辑。
我还会额外记一个连接复用标记,标记这次请求用的是新连接还是复用连接。排查过几次之后你会发现,某些断连是集中出现在复用连接上的,这时候方向就非常明确了——去查连接池的保活配置,而不是去查网络。
6. 常见问题速查表与几段踩坑实录
6.1 现象、原因、处置,一张表对照着查
| 现象 | 最可能的原因 | 排查动作 | 处置 |
|---|---|---|---|
| 每次都在固定秒数断开 | 中间层空闲回收 | 加心跳看断点是否后移 | 调大空闲超时或加心跳 |
| 首包很慢随后就断 | 建连或排队超时 | 看首包耗时 | 调大连接超时,降并发 |
| 中文输出必现、英文正常 | 多字节字符被切断 | 检查是否逐块解码 | 改用增量解码 |
| 高并发成片失败,单发正常 | 连接池打满或限流 | 并发从 1 逐步加 | 限流 + 加抖动退避 |
| 休眠唤醒后集中报错 | 复用僵尸连接 | 看连接复用标记 | 缩短连接存活时间、加健康检查 |
| 尾部快结束才断 | 整体超时刚好卡住 | 对比整体超时设置 | 按输出量重算整体超时 |
| 日志里片段数正常但报错 | 没识别结束标记 | 打印最后几行原始数据 | 补上结束标记判断逻辑 |
| 加了心跳后报错变多 | 心跳行被当数据解析 | 检查解析分支 | 过滤注释行 |
这张表我建议直接放进团队的排查手册里。流式问题看起来玄学,实际上九成以上都能落到上面这几种。
6.2 几个当时折腾了很久、现在看起来很傻的坑
第一个坑是用打印调试把流式搞成了非流式。早期我在代码里加了一堆print做调试,结果输出全被缓冲在一行里,看起来像“流式没生效”。折腾了半天才发现是输出缓冲的问题,标准输出在重定向到文件时默认是块缓冲的,得手动刷。这件事之后我养成习惯:调试流式,一律把每段的时间戳和长度打出来,不靠肉眼观感判断。
第二个坑是把读取超时当成总超时。当时有个任务跑两分钟必断,我把“超时”从 60 秒调到 300 秒,还是断。后来才搞明白,那个参数控制的是两次读之间的间隔,而我的问题在于中途有一段四十多秒没有新片段——调大总时长完全没用,得调间隔超时或者加心跳。这个误解在初学者里非常普遍,值得单独强调一遍。
第三个坑是重试没加抖动,把限流打成了雪崩。批量任务跑两百个代码转换,第一批失败了三十个,退避一秒后这三十个同时重试,全部撞在限流阈值上,然后退避两秒再次集体重试。最后整批任务跑了一小时。加上随机抖动之后,同样的任务量四十分钟就跑完了。这个教训我记了很久:并发问题的解药往往不是更少的重试,而是更散的节奏。
第四个坑是半截 JSON 直接抛异常。这个问题的痛苦在于它是偶发的,跑十次可能只报一次,很难复现。后来把解析失败的次数也打进日志,才发现每次失败前都有一次异常长的片段——原来是缓冲区边界把一行数据切开了。改成按行缓冲、解析失败就跳过之后,这个问题再没出现过。
6.3 关于这个话题,我自己的几条稳定做法
踩了这么多坑之后,我现在写任何流式调用的代码,都会固定做几件事,几乎不假思索。写出来给各位参考,也可以直接拿去当模板。
一是所有超时参数全部显式指定,绝不留空靠默认。连接、间隔、整体三层分开写,旁边加注释说明为什么是这个数。二是增量解码器必带,不管当前任务是不是中文,一律用增量方式解码,成本几乎为零,能避免一整类偶发问题。三是每次请求都记录五个指标,首包耗时、片段数、最大间隔、结束原因、内容长度,写进结构化日志,出问题直接查。四是重试永远带抖动和上限,并且区分可重试与不可重试。五是部分结果一定落盘,宁可多写一次临时文件,也不让已经生成的内容随断连一起丢掉。
还有一条比较虚但我觉得挺重要的:别把长任务当成默认选项。能拆成三步做完的事,就不要想办法让一步跑完。流式的所有麻烦,本质上都来自“连接要活很久”这件事;任务粒度降下来,连接寿命自然降下来,一半的问题压根不会发生。这个思路比任何参数调优都有效,只是需要前期多写一点编排代码。
如果你的场景里这个报错还在反复出现,建议先别急着加更多重试,回到第 2.3 节那七步自检走一遍,把断点位置钉死。绝大多数情况下,答案就在那几个指标里,只是之前没人把它们记下来。