遥测:上游状态码被压平、三分之二失败行没有归因列、21 条错误详情为空 #19

Open
opened 2026-08-26 22:17:33 +08:00 by iomgaa · 0 comments
Owner

一句话

上游过载回的 529 传到调用方手里变成了 bad_response_status_code,而三分之二的失败遥测行
连是哪个模型都没记。2026-08-26 那一跑为了搞清「gemini 那条渠道到底怎么了」查了两个多小时,
最后是靠去中转后台翻日志才知道上游在回 529——这两条缺口各自贡献了一半的弯路。

口径

  • 数据来自 PolyGateway 1.3.0 写的 llm_calls 表,窗口 2026-08-26 07:04 ~ 14:11 UTC,共 4372 行
  • 调用方 CHSAnalyzer3,一次真实压测(23 位患者、7 小时),不是构造的复现
  • 涉及的四条渠道:qwen3.7-plus / gpt-5.5 / gemini-3.1-pro 走中转 newapi.iomgaa.online
    monkey-ocr 走内网

一、上游的状态码在中转那一层被压平,PolyGateway 原样透传,于是丢了

gemini-3.1-pro 110 次调用错 46 次,其中 35 条错误正文是同一句:

503 | {"error":{"message":"openai_error","type":"bad_response_status_code","param":"","code":"bad_response_status_code"}}

上游实际回的是 529(过载)。这个数是去中转后台翻日志才拿到的,
从 PolyGateway 记的这一行里看不出来——429、500、503、529 到这边长得一模一样。

这两种的处理方式是相反的:配额打满该退避重排,上游过载该换渠道或者等。
分不开就只能一律当瞬时错误重试,而重试正是过载时最不该做的事。

压平这一步是中转做的,PolyGateway 只是原样收下。但PolyGateway 是唯一有机会
把「我实际收到的 HTTP 状态码」单独记一列的地方
——它手里有响应对象,中转的 body
再怎么糊,status line 是它自己看见的。现在这个数只以字符串形式出现在 error 的开头
503 | {...}),要用得先 split。

建议llm_calls 加一列存实收的 HTTP 状态码,以及一列存上游 body 的原文
(现在 body 是拼进 error 的,但只有出错时才有,且和我们自己的前缀混在一起)。

二、三分之二的失败行没有归因列

333 条失败行里,218 条的 modelprovidersource_name 三列全是空串
这批是 PolyGateway 自己的渠道级拒绝:

[179] nursing_extract_primary 网关暂时不可用: circuit_open
[ 17] nursing_extract_arbiter 网关暂时不可用: retry_exhausted
[ 11] classify 网关暂时不可用: retry_exhausted
[  4] nursing_extract_primary 网关暂时不可用: retry_exhausted
[  3] nursing_extract_arbiter 网关暂时不可用: circuit_open
[  2] cancelled
[  1] nursing_extract_reviewer 网关暂时不可用: retry_exhausted
[  1] table_extract 网关暂时不可用: retry_exhausted

渠道名明明就在 error 那个自由文本里,却没有进任何一个结构化列。
后果是「哪条渠道熔断了几次」只能拿 LIKE 去凑,而 circuit_open 那 182 条
恰恰是判断一条渠道健康与否最要紧的信号——排查时我是先按 model 分组,
得到的结论是「gemini 只错了 46 次」,直到发现还有 218 条无归因行才知道
真实的失败规模是三倍。

熔断时具体用哪个 source 可能还没选出来,但渠道是知道的

建议:给渠道名一个自己的列。source/model/provider 选不出来时留空可以接受,
渠道名留空不行。

三、21 条错误的详情是空的

[11] 'nursing_primary 网络错误: '
[ 8] 'nursing_arbiter 超时: '
[ 1] 'nursing_arbiter 网络错误: '
[ 1] 'nursing_reviewer 网络错误: '

冒号后面什么都没有。一条超时不说自己撞的是哪个上限、撞的时候等了多久;
一条网络错误不说是连接失败还是读到一半断了。

这一条不是理论问题。这一跑里判断「12:22 前后中转不可达三分钟」这件事,
靠的是恰好有几条 网络错误 带上了 httpx 的原文:

网络错误: All connection attempts failed
网络错误: peer closed connection without sending complete message body (incomplete chunked read)

而另外 11 条同类的是空的。同一类异常,有的带原文有的不带,
说明是某条路径上 str(exc) 拿到空串就直接拼进去了。

建议:拿不到 str(exc) 时退回到异常类名。超时那一类还应该把撞上的是哪个上限
(连接超时 / 读超时 / 流活性 / 总时长)和实际等了多久记进去。

附:这次排查里被证伪的几条猜想

留个记录,免得下次重复查。这几条在 llm_calls 现有的列上都查得出来,
说明这张表本身的骨架是对的,缺的是上面那三处。

  • 熔断拒绝是瞬时的。 182 条 circuit_open 的时延中位 452 ms,
    179 条 nursing_extract_primary 里 176 条在 10 秒内。
    怀疑过「先在并发闸上等满槽位、再被熔断拒掉」,不成立。
  • 失败和并发无关。 逐次算「开工瞬间在飞的同模型调用数」,
    失败的均值 0.50、成功的 0.41,最大都是 1。
  • 失败和入参大小无关。 失败的 messages 平均 5820 字符,成功的 6447,
    失败的反而更小,区间完全重叠。
  • 不是本机网络。 走内网的 monkey-ocr 全程 1343 次只错 4 次(0.3%)。
## 一句话 上游过载回的 529 传到调用方手里变成了 `bad_response_status_code`,而三分之二的失败遥测行 连是哪个模型都没记。2026-08-26 那一跑为了搞清「gemini 那条渠道到底怎么了」查了两个多小时, 最后是靠去中转后台翻日志才知道上游在回 529——这两条缺口各自贡献了一半的弯路。 ## 口径 - 数据来自 PolyGateway 1.3.0 写的 `llm_calls` 表,窗口 2026-08-26 07:04 ~ 14:11 UTC,共 4372 行 - 调用方 CHSAnalyzer3,一次真实压测(23 位患者、7 小时),不是构造的复现 - 涉及的四条渠道:`qwen3.7-plus` / `gpt-5.5` / `gemini-3.1-pro` 走中转 `newapi.iomgaa.online`, `monkey-ocr` 走内网 ## 一、上游的状态码在中转那一层被压平,PolyGateway 原样透传,于是丢了 `gemini-3.1-pro` 110 次调用错 46 次,其中 35 条错误正文是同一句: ``` 503 | {"error":{"message":"openai_error","type":"bad_response_status_code","param":"","code":"bad_response_status_code"}} ``` 上游实际回的是 529(过载)。这个数是去中转后台翻日志才拿到的, **从 PolyGateway 记的这一行里看不出来**——429、500、503、529 到这边长得一模一样。 这两种的处理方式是相反的:配额打满该退避重排,上游过载该换渠道或者等。 分不开就只能一律当瞬时错误重试,而重试正是过载时最不该做的事。 压平这一步是中转做的,PolyGateway 只是原样收下。但**PolyGateway 是唯一有机会 把「我实际收到的 HTTP 状态码」单独记一列的地方**——它手里有响应对象,中转的 body 再怎么糊,status line 是它自己看见的。现在这个数只以字符串形式出现在 `error` 的开头 (`503 | {...}`),要用得先 split。 **建议**:`llm_calls` 加一列存实收的 HTTP 状态码,以及一列存上游 body 的原文 (现在 body 是拼进 `error` 的,但只有出错时才有,且和我们自己的前缀混在一起)。 ## 二、三分之二的失败行没有归因列 333 条失败行里,218 条的 `model`、`provider`、`source_name` 三列**全是空串**。 这批是 PolyGateway 自己的渠道级拒绝: ``` [179] nursing_extract_primary 网关暂时不可用: circuit_open [ 17] nursing_extract_arbiter 网关暂时不可用: retry_exhausted [ 11] classify 网关暂时不可用: retry_exhausted [ 4] nursing_extract_primary 网关暂时不可用: retry_exhausted [ 3] nursing_extract_arbiter 网关暂时不可用: circuit_open [ 2] cancelled [ 1] nursing_extract_reviewer 网关暂时不可用: retry_exhausted [ 1] table_extract 网关暂时不可用: retry_exhausted ``` 渠道名明明就在 `error` 那个自由文本里,却没有进任何一个结构化列。 后果是「哪条渠道熔断了几次」只能拿 LIKE 去凑,而 `circuit_open` 那 182 条 恰恰是判断一条渠道健康与否最要紧的信号——排查时我是先按 `model` 分组, 得到的结论是「gemini 只错了 46 次」,直到发现还有 218 条无归因行才知道 真实的失败规模是三倍。 熔断时具体用哪个 source 可能还没选出来,但**渠道是知道的**。 **建议**:给渠道名一个自己的列。source/model/provider 选不出来时留空可以接受, 渠道名留空不行。 ## 三、21 条错误的详情是空的 ``` [11] 'nursing_primary 网络错误: ' [ 8] 'nursing_arbiter 超时: ' [ 1] 'nursing_arbiter 网络错误: ' [ 1] 'nursing_reviewer 网络错误: ' ``` 冒号后面什么都没有。一条超时不说自己撞的是哪个上限、撞的时候等了多久; 一条网络错误不说是连接失败还是读到一半断了。 这一条不是理论问题。这一跑里判断「12:22 前后中转不可达三分钟」这件事, 靠的是恰好有几条 `网络错误` 带上了 httpx 的原文: ``` 网络错误: All connection attempts failed 网络错误: peer closed connection without sending complete message body (incomplete chunked read) ``` 而另外 11 条同类的是空的。同一类异常,有的带原文有的不带, 说明是某条路径上 `str(exc)` 拿到空串就直接拼进去了。 **建议**:拿不到 `str(exc)` 时退回到异常类名。超时那一类还应该把撞上的是哪个上限 (连接超时 / 读超时 / 流活性 / 总时长)和实际等了多久记进去。 ## 附:这次排查里被证伪的几条猜想 留个记录,免得下次重复查。这几条在 `llm_calls` 现有的列上都查得出来, 说明这张表本身的骨架是对的,缺的是上面那三处。 - **熔断拒绝是瞬时的。** 182 条 `circuit_open` 的时延中位 452 ms, 179 条 `nursing_extract_primary` 里 176 条在 10 秒内。 怀疑过「先在并发闸上等满槽位、再被熔断拒掉」,不成立。 - **失败和并发无关。** 逐次算「开工瞬间在飞的同模型调用数」, 失败的均值 0.50、成功的 0.41,最大都是 1。 - **失败和入参大小无关。** 失败的 `messages` 平均 5820 字符,成功的 6447, 失败的反而更小,区间完全重叠。 - **不是本机网络。** 走内网的 `monkey-ocr` 全程 1343 次只错 4 次(0.3%)。
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: iomgaa/PolyGateway#19