10月的这次故障,我印象太深了。凌晨快两点,手机被连续的告警震动吵醒,第一反应是“免费模型通道是不是挂了”,结果打开监控面板一看,成功率99.2%,HTTP 5xx几乎为零。要不是Hermes里那条“慢速累积”的规则被触发了,我们可能直到第二天早上业务方反馈“效果变差”才会发现,到那时候就真成了教科书级的“隐形翻车”。
先交代一下背景。我们这边对外提供模型接入网关,底层会对接多家模型服务商,其中有付费的,也有免费的。免费模型通道的优势很明显——成本低、适合大批量预研任务,但劣势也突出:不稳定、限流规则经常变、质量波动没有明显规律。为了解决这个问题,我们维护了一套自研的监控与自动容灾系统,代号叫Hermes。简单说,它负责盯着网关出口的所有调用链路,发现异常后能自动切流量,省去人工半夜爬起来操作。
这次记录的就是10月里对免费模型通道做的一次完整排查和Hermes容灾策略调优,顺便把排查思路、调优参数和踩过的坑都整理出来,给自己做个备忘,也给同样在做模型监控和网关容灾的朋友一些参考。
1. 故障现场还原:明明没挂,但就是不对劲
1.1 成功率的数字游戏
事故苗头最早出现在10月中旬。我们惯例做每周质量巡检时,发现免费模型通道的P95延迟从前两周的800毫秒涨到了1.6秒左右,翻了一倍。更诡异的是,请求成功率依然维持在99%以上,HTTP错误码极少,网关日志一片祥和。
这就是“隐形翻车”最典型的表现:业务可用的表象掩盖了下游服务质量的严重劣化。免费通道虽然没在功能上“挂掉”,但响应慢、输出内容偶发截断、部分请求返回的JSON结构异常,这些都是传统监控很难第一时间暴露的问题。
我们用Prometheus采集了网关的请求指标,当时除了延迟在涨,还有几个细节值得注意:
- 状态码200的请求占比没有明显下降,但请求的字节数分布变得“头重脚轻”——大量响应体集中在较短区间。
- 上游服务偶发返回“200”但响应体是错误模板的情况,这类响应如果不做内容解析,光看状态码根本发现不了。
- 重试率在悄悄上升,说明客户端侧已经感知到部分请求需要第二次甚至第三次重试才能成功。
这些指标单个看都不足以触发告警,但放在一起,就是免费的模型服务商那边已经出现明显劣化的信号。
1.2 为什么传统监控会漏掉这种问题
我们的监控体系其实不算差。统一网关会记录每次请求的QPS、延迟分位值、错误码分布、超时次数,而且都有对应的告警规则。但问题在于,这些规则都是“显性故障”导向的:
- 连续5次连接失败才触发熔断
- 超时率超过10%才告警
- HTTP 5xx数量超过阈值才拉群
这套逻辑对付“下游直接宕机、连接被拒、超时大量堆积”这类硬故障很有效,但对“服务还活着但质量下降”的软故障,几乎没有感知能力。软故障的特性和硬故障不一样,它不会让成功率瞬间崩塌,而是让成功率“边边角角”地往下掉,让延迟“慢慢悠悠”地往上涨,让响应体“偶尔”缺胳膊少腿。
所以排查这次问题,我最大的教训是:监控不能只盯“挂了没挂”,还要盯“变差了多少”。尤其免费通道,质量波动是常态,需要一个能持续量化劣化程度的机制。
这也是后来给Hermes增加“健康分”和“滑动窗口劣化检测”的直接导火索。
1.3 监控指标的整体设计
在讲后续排查之前,先把Hermes平时采集的核心指标列一下,后续的内容都会围绕这些指标展开。
| 指标名称 | 采集方式 | 作用 |
|---|---|---|
| 请求成功率 | 按状态码分组统计 | 识别硬故障和部分软故障 |
| 平均时延与P95时延 | 分位值统计 | 感知性能劣化 |
| 响应体大小分布 | 按字节数分桶 | 发现响应被截断或异常缩短 |
| JSON解析错误率 | 网关层内容解析 | 发现“200假成功” |
| 客户端重试率 | 网关记录重试标记 | 侧面反映真实可用性 |
| 错误码分布 | 按错误类型聚合 | 定位上游限流、参数报错等 |
这六类指标,能覆盖大部分“看起来还活着,其实已经不太行”的场景。特别是JSON解析错误率和响应体大小分布,这次就是靠它们锁定的问题。
2. 顺着数据一点一点挖根因
2.1 先看错误,再看响应内容
拿到故障现象后,我第一步是打开网关错误码聚合面板。意外的是,5xx没有明显增多,但422的占比从平时的0.2%涨到了1.1%,同时401的数量也有小幅上升。
422表示“请求被理解,但参数校验失败”,在免费模型的接口上很常见。我们一样发同样的参数,为什么突然冒出这么多422?我怀疑是上游服务商更新了参数校验逻辑或者切换了版本。顺手把收费模型通道的同一批请求日志拉出来对比,发现同样的请求参数、同样的模型名配置,在免费通道上422率远超在收费通道上。
这说明问题不在我们的网关侧,而是免费模型服务商那边悄悄改了规则。结合10月刚好是很多平台做服务调整的月份,这个猜测在时间点是合理的。但调试没法依赖猜测,得继续验证。
2.2 参数兼容的坑:一样的参数,不一样的解释
我抽取了延迟高的请求样本,检查请求体的具体字段。重点对比这几个:
max_tokens:我们传的是2048,免费通道部分模型上限只有1024,超限会直接报422。temperature:部分免费模型只支持0到1.0,但我们根据任务需求会传1.2,收费通道能接受,免费通道直接拒绝。top_p:有的免费模型版本对这个参数的处理逻辑有变化,传了反而会被判为非法。
代码层面确认下来,网关对免费和收费通道用的是同一套参数模板,没有做兼容性区分。这确实是个隐患,只是之前一直没暴露。平时请求量少、参数简单,上游宽松处理,大家都觉得没问题;等到上游某天收紧了校验,问题就集中爆发。
所以排查结论第一条:免费通道必须单独做参数模板,不能复用收费通道的默认参数。这不是能力问题,是兼容策略问题。
2.3 限流和配额变化也被“平均”掩盖了
除了参数校验,限流也是免费通道的常见雷区。我看那天的数据,每秒的请求数没有超过我们预估配额曲线,但错误码里出现了几类不常见的返回,包括“请求过于频繁”的提示被包装在200响应里,需要解析内容才能识别。
这里有个隐蔽的点:网关层面统计的成功率,实际是按HTTP状态码判定的。只要状态码是200,就算成功。但“请求过于频繁”的提示如果被打包成200,网关根本不知道它其实是个失败请求,成功率的统计就被污染了。这是隐形翻车的另一个重要构成。
解决办法是给Hermes增加内容级状态判定:网关在返回前,会将响应体做一个轻量解析,检查是否存在特定的错误标识。如果命中,即使状态码是200,也会标记为“伪成功”,纳入健康评估。这个操作会增加一点CPU开销,但和收益相比完全值得。
2.4 延迟上升为什么会“传染”到整个网关
免费通道的P95延迟涨到1.6秒后,另一个麻烦被放大了:连接池占用变长。我们的网关对上游的连接是有上限的,一个请求迟迟不返回,连接就迟迟不释放。免费通道流量占比又高,占用的连接数多了,其他正常通道的可用连接就被挤占,导致整体吞吐跟着下降。
这就解释了为什么那段时间我们偶尔会遇到“网关整体变慢”的反馈,明明免费通道只占30%左右的流量,却拖慢了全局的响应。延迟劣化的影响不局限于链路本身,它会通过连接池和线程池蔓延到整个系统。
这也是后来调整容灾策略时,我坚持要把“延迟权重”放进食谱的原因。只要P95持续高位,无论成功率多高,都要启动降级。
3. Hermes自动容灾的调整过程
3.1 原有容灾策略为什么不够用
Hermes之前已经有一套自动容灾逻辑,思路比较简单:
- 某个通道连续N次请求失败(连接错误或5xx),标记为“不健康”,阈值默认5次。
- 熔断后,新请求默认切到备用通道,直到通道恢复。
- 恢复条件:连续成功N次后再放量回切。
这套逻辑在硬故障下挺好用,但这次暴露了三个明显缺陷:
缺陷一:起点太晚,且没有质量维度的判断。必须要等到失败次数累计到阈值才会触发容灾。可这次的情况是“成功率很高但延迟和质量都在变差”,失败次数根本积不起来,所以容灾一直没有启动。不是它不想干活,是根本没有能被它感知的信号。
缺陷二:重试叠加导致雪崩。当时网关对失败请求是有自动重试机制的,免费通道已经出现延迟劣化,重试会进一步压榨通道的吞吐能力,反而让延迟更高。自动容灾和自动重试如果互不知晓,很容易形成负反馈循环。
缺陷三:恢复探测是盲目的。切到备用通道后,Hermes会定期用探测请求去验证原通道是否恢复,但探测频率和阈值都是固定的,没有根据故障级别动态调整。有时候上游只恢复了30%,探测请求恰好成功,就觉得“恢复”了,结果把流量切回去又引发第二轮劣化。
3.2 健康分模型:把多维信号算成一个数
针对上面的问题,我给Hermes设计了一套“健康分”评估机制。核心思路是:不再用单一的成功率或错误码判断通道好坏,而是把多个指标加权合成一个0到100的分数,低于阈值才触发容灾。
健康分的大致计算方式如下:
$$ score = 100 - (w_1 \times fail_rate_penalty + w_2 \times latency_penalty + w_3 \times malformed_penalty + w_4 \times retry_penalty) $$
其中各项罚分规则为:
fail_rate_penalty:基于5xx和连接错误比例计算,每出现1%扣2分。latency_penalty:当P95延迟超过基线1.5倍时开始扣分,每超过10%扣3分。malformed_penalty:基于JSON解析失败和伪成功比例计算,每1%扣5分,这是权重最高的。retry_penalty:基于网关内部重试率计算,超过5%后开始扣分。
每次评估的窗口默认是10分钟,滑动更新,避免瞬时抖动把分数打得太低。实际代码并不复杂,核心就是按时间窗口聚合指标,再套用罚分规则。
一个简单的计算示例(伪代码):
func HealthScore(m *Metrics) float64 { score := 100.0 score -= 2.0 * m.FailRate * 100 // 1%约等于2分 score -= 3.0 * (m.P95Latency / m.Baseline - 1) score -= 5.0 * m.MalformedRate * 100 // 伪成功/解析失败,权重最大 score -= 2.0 * max(0, m.RetryRate*100-5) return math.Max(0, score) }有了健康分,容灾触发条件就灵活多了。以前是“失败到一定次数”,现在是“健康分降到阈值以下”,而且切入切出都能用百分比流量控制,不用一刀切。
3.3 四级降级流程
新的容灾流程分成四级,每一级都有明确的触发条件和动作,避免一上来就全量切换造成更大波动。
第一级:预热池接管部分流量。当免费通道健康分降到75到85之间,Hermes会从备用池里拉一个新入口作为“预热通道”,切5%的流量过去先试探。这一级的目的是让备用通道提前热起来,而不是等真正故障再冷启动。
第二级:主链路降载。健康分降到60到75之间,免费通道流量上限被压到50%,另外50%全部切到预热池。此时业务基本无感知,只是成本会略微上升。
第三级:全量容灾切换。健康分低于60,免费通道全量熔断,所有流量切到备用池或收费通道。同时把自动重试功能临时关掉,防止重试风暴。
第四级:人工接管确认。连续故障超过30分钟,或出现上游规则大改的迹象,系统不再自动回切,保留现场并通知值班开发确认原因后再操作。
这四级流程不是我们拍脑袋定的,而是根据这次故障中实际损耗推算出来的。免费通道的流量占比约为30%,如果第一级就开始切5%,即使备用通道质量一般,也足以稀释掉大部分坏流量,不会造成整体服务质量崩塌。
3.4 回切逻辑:别急着把流量全拉回来
原方案的另一个问题是回切太激进。新方案里,原通道恢复后,Hermes会先放10%的流量回去,观察15分钟。如果健康分稳定在80以上,再逐步增加到30%、50%、100%。这期间如果健康分再次掉头向下,就立刻回到上一级。
实际调优时我把观察窗口设得比较长,因为免费通道的服务质量波动是锯齿形的,短窗口容易误判。15分钟是折中点,既能及时抓住趋势变化,又不会因为一次抖动就来回切换。
这里分享一个实操细节:回切时一定要看“恢复质量”而不只是“是否恢复”。之前我们就是只看探测请求成没成功,结果把流量切回去后,用户立刻遇到新一轮超时。区别就在于,探测请求是一个轻量调用,而真实业务请求的负载会高很多,通道的承载能力需要在真实流量下验证。
4. 调优后的配置参数与操作记录
4.1 热配置参数清单
这次调整后,Hermes的相关配置丰富了不少,全部支持运行时热更新,不需要重启网关。整理一份我们当前用的参数清单,仅供参考,具体数字要根据自己的业务流量和成本预算调整。
| 配置项 | 参数值 | 说明 |
|---|---|---|
| 评估窗口 | 10分钟 | 滑动窗口,聚合指标计算健康分 |
| 健康分高分阈值 | 85 | 高于85视为健康 |
| 健康分低分阈值 | 60 | 低于60全量熔断 |
| 预热池流量比例 | 5% | 第一级降载切换到备用池的比例 |
| 恶意恢复定性阈值 | 30分钟 | 连续不健康超过30分钟自动转人工 |
| 回切初始比例 | 10% | 原通道恢复后首批放量 |
| 回切观察窗口 | 15分钟 | 每一档流量放量后需要观察的时间 |
| 自动重试开关 | 条件开启 | 免费通道健康分低于70时自动关闭重试 |
这个清单看起来平平无奇,但每项的选取其实都吃过亏。比如回切观察窗口,刚开始我们设的是5分钟,结果切了3次都因为误判导致二次切换。改成15分钟后,误判率明显下降。原因很简单,免费通道的延迟波动周期往往在5到10分钟左右,窗口太短根本看不出趋势。
4.2 实际切换时的流量变化
以这次10月的故障为例,我们实际执行的切换节奏是这样的:
- 第1分钟:发现健康分跌到72,触发二级降载,免费通道流量压到50%,另外50%转到备用池。
- 第3分钟:备用池表现稳定,P95约400毫秒,继续维持这个配比,观察了10分钟。
- 第15分钟:免费通道健康分跌破60,触发三级全量切换,所有流量进入备用池,免费池保持空跑。
- 第20分钟:确认上游规则变化,关闭免费通道自动重试,防止重试风暴。
- 后续2小时:Hermes每隔10分钟探测一次免费池恢复情况,确认稳定后才逐步回切。
整个过程业务侧几乎是无感的,只有少量请求出现响应变慢的情况,被网关的重试机制消化掉了。这就是自动容灾的意义——不让用户感知到故障,或者至少把感知范围缩到最小。
4.3 告警分层重置
除了容灾参数,我还把告警重新分了三层,减少深夜无效告警的数量。
告警级别A(即时处理):健康分低于60持续5分钟,或伪成功比例超过3%,或全量容灾切换动作发生。这类必须立即响应。
告警级别B(尽快处理):健康分在60到75之间波动,P95延迟超基线1.5倍超过15分钟,或422错误率超过1%。这类不用半夜爬起来,但需要白天处理。
告警级别C(观察跟踪):预热池流量使用情况、免费通道参数模板命中率、响应体大小分布偏移等。这类只进周报,不进告警群。
分层之后,值班体验好了很多。以前是“响个不停分不清轻重”,现在深夜能收到的最严重告警基本都是有实际影响的级别,处理效率提升了不少。
5. 这类故障的排查路径与常见误区
5.1 排查顺序建议
如果接下来你们也遇到类似“看起来没挂但实际有问题”的模型通道故障,我建议按以下顺序排查:
- 先看异常分布,不看总数。成功率99%在总数上没有意义,但拆到具体模型、具体入口、具体错误类型,差异就出来了。
- 看延迟和错误占比的联动。延迟涨、重试率涨、422涨,三个信号同时出现,基本可以判断上游在改规则或者过载。
- 解析响应体,不只看状态码。这是发现“200成功假象”的唯一途径。
- 对比同参数在备用通道的表现。我们当时就是靠着收费通道同样参数完全正常,才锁定了免费通道的兼容性问题。
- 不要急着重启或发新版本。先调整流控和参数模板,很多时候不需要动代码就能恢复。
这套顺序适用于大多数模型网关类故障,尤其是涉及免费或低成本通道的场景。核心逻辑是:先用数据缩小范围,再采取动作,避免靠猜去解决问题。
5.2 免费通道特有的坑
用免费模型通道,有一些特有的坑是即便认真做监控也不一定第一时间能想到的:
第一个坑,参数阈值被静默修改。免费通道的模型版本更新不像付费服务那样有完整的通知机制,有时候字段含义变了也没人知道。比如max_tokens的有效范围,可能从1.1版本开始就悄悄缩水了,等你的业务量变大才暴露。应对办法是给免费通道做一份单独的请求参数模板,并用定时任务每天探活一次,探测时覆盖关键参数的边界值。
第二个坑,限流隐藏级别的判断。免费通道的限流不一定体现在HTTP 429上,可能是延迟升高、请求排队、偶发“200错误响应体”。单纯看错误码分布会低估限流的影响。应对办法是监控响应体关键字,把“请求被拒绝”之类的文本作为错误信号计算。
第三个坑,免费通道的“性价比”陷阱。免费并不等于不要钱,它消耗的是你的运维排障精力和延迟容忍度。如果你的业务是对延迟延敏感的类型,免费通道降级对用户体感的影响会很明显。我的建议是,免费通道承担非关键任务和低峰期流量,核心业务必须走稳定的收费通道并配置自动降级。
5.3 长期维护建议
排查完这次故障后,我们顺手做了一组自动化工具,算是对这次调优的补充:
- 每周自动对比免费通道与收费通道的响应体样本,检查字段结构是否发生偏移。
- 每天定时用边界参数探测免费通道,验证参数模板是否仍然适用。
- 把Hermes的容灾动作全量记录到审计日志里,方便事后做故障复盘。
这三件事看着不起眼,但长期用下来能省大量排障时间。免费的模型服务本身没有SLA,你不能指望它主动通知你“我要变更了”,只能靠自己的监控去感知,提前做好预案。
回顾整个10月的这次调优,我最大的体会是:模型监控的难点不是工具不够多,而是对“劣化”这件事的理解不够深。一个服务挂掉,监控一定能发现;但一个服务越变越差,如果监控体系只盯着“挂没挂”,那它就永远发现不了。给Hermes增加健康分模型和分级容灾流程,本质上就是让我自己从“被动响应故障”转向“主动管理质量”。这个思路延续下来,后面我们接入更多模型服务商时,新通道上线后做的第一件事就是配置健康分基线,而不是等着出了问题再救火。