1. 从一次线上告警说起请求失败次数和错误率为什么对不上凌晨两点监控大盘突然弹出一条告警某个核心接口的失败请求数在五分钟内从个位数飙升到四位数。值班的同学第一反应是“完了服务挂了”赶紧拉群、翻日志、准备回滚。结果折腾了半小时才发现真正因为服务端异常导致的失败只有几十次剩下的大部分是客户端主动取消、参数校验不通过、以及一批爬虫在疯狂试探不存在的资源路径。失败次数确实很高但错误率算下来还不到百分之三业务方那边甚至没感觉到任何抖动。这个场景我遇到过不止一次也是很多前端和后端同学在联调、排障、做监控时最容易混淆的一个点API 异常、前端请求失败、错误率这三者之间并不是简单的等号关系。请求失败次数是一个绝对量错误率是一个相对比例而“异常”这个词在不同语境下指向的东西完全不一样。把这三个概念混在一起看轻则误判故障等级重则触发不必要的回滚把本来稳定的系统折腾出真问题。这篇内容我想把这件事彻底讲清楚。不管你是刚入行的前端还是已经写了几年接口的后端或者是负责稳定性建设的同学只要你在日常工作中需要看监控、排查线上问题、设计接口的错误处理逻辑这些内容都能直接拿去用。我会从概念拆解开始讲到失败次数的统计口径、错误率的分母到底该怎么选、前端拿到不同状态码时该怎么理解最后给出一套可以直接落地的排查思路和监控配置建议。全程用大白话加实际案例尽量不堆术语保证你看完能自己动手复现一遍。2. 先把概念掰开API 异常、请求失败、错误率到底各指什么2.1 API 异常不等于接口报错它是个更大的集合很多人一看到“API 异常”这四个字脑子里浮现的就是接口返回了 500。但实际上在工程实践里API 异常涵盖的范围要宽得多。我习惯把它分成四层来看第一层是传输层异常。请求根本没到达服务端或者响应根本没回到客户端。比如 DNS 解析失败、TCP 连接超时、TLS 握手失败、连接被重置。这一层的问题在前端表现往往是net::ERR_CONNECTION_TIMED_OUT或者Failed to fetch在监控里可能连一条服务端日志都没有。第二层是协议层异常。请求到了服务端但 HTTP 层面的语义出了问题。比如返回了 4xx 状态码表示客户端请求本身有问题返回了 5xx表示服务端处理出了问题。这一层是大多数监控系统能抓到的部分。第三层是业务层异常。HTTP 状态码是 200响应体里却写着{code: 40001, message: 余额不足}。从 HTTP 协议角度看这是成功响应从业务角度看这是一次失败。很多团队的监控只统计 HTTP 状态码结果这类异常被完全漏掉。第四层是语义层异常。请求成功、业务码也成功但返回的数据不符合预期。比如期望返回一个数组却返回了 null期望字段是数字却是字符串。这类问题前端不做校验根本发现不了但它确实是一种异常。把这四层分清楚之后你就会发现“API 异常”这个词本身太笼统了不同角色说这句话时指的东西可能完全不同。后端说异常通常指第二层和第三层前端说异常往往包含第一层和第四层而监控系统说异常大概率只覆盖了第二层的一部分。2.2 前端请求失败的判定标准比你想的要复杂前端判断一次请求“失败”依据的是什么很多人脱口而出看状态码呗不是 200 就是失败。这个答案只对了三分之一。实际在前端代码里一次请求的失败判定至少涉及三个层面。第一个层面是Promise 的 reject。用 fetch 的时候只有网络层面的错误才会让 Promise 进入 rejected 状态比如断网、跨域被拦截、请求被 abort。服务端返回 500 的时候fetch 的 Promise 依然是 fulfilled 的只是response.ok是 false。这一点坑过无数新手我见过有人写了fetch(url).then(...).catch(...)以为 catch 能兜住所有错误结果 500 的时候 catch 根本没触发。第二个层面是HTTP 状态码。通常我们会把status 400视为失败但这里也有讲究。401 未授权和 403 禁止访问很多时候是预期内的比如用户没登录时接口返回 401前端应该跳登录页而不是报错。404 也可能是预期内的比如查询一个不存在的资源。把这些都算进“失败”错误率自然就虚高了。第三个层面是业务状态码。响应体里的code字段不等于成功码时前端也应该判定为失败。但问题是不同接口的成功码可能不一样有的用 0有的用 200有的用 success。如果前端没有统一封装很容易漏判。我一般建议团队在请求库层面做统一拦截把这三层判定收敛到一个地方。下面是一个简化后的判定逻辑用 JavaScript 写async function request(url, options) { try { const response await fetch(url, options); // 传输层成功进入协议层判定 if (!response.ok) { // 401/403 交给业务层处理不算硬失败 if (response.status 401 || response.status 403) { return { ok: false, type: auth, status: response.status }; } return { ok: false, type: http, status: response.status }; } const data await response.json(); // 业务层判定 if (data.code ! 0 data.code ! 200) { return { ok: false, type: business, code: data.code, message: data.message }; } return { ok: true, data }; } catch (err) { // 传输层失败 return { ok: false, type: network, error: err.message }; } }这段代码的关键在于它把失败分成了 network、http、auth、business 四种类型。只有分类型统计后面算错误率的时候才能按需过滤。如果所有失败都混在一起算那错误率这个指标基本没有参考价值。2.3 错误率的分子和分母选错了全盘皆输错误率的计算公式看起来很简单错误率 失败次数 / 总请求次数。但魔鬼全在细节里。先说分子。失败次数要不要包含 4xx要不要包含业务失败要不要包含客户端主动取消的请求我的经验是分子必须和分母的口径匹配而且要和监控的目的匹配。如果你关心的是服务端稳定性那分子应该只包含 5xx 和服务端主动抛出的业务异常4xx 是客户端的问题不该算在服务端头上。如果你关心的是用户体验那分子应该包含所有让用户感知到失败的请求包括网络超时和业务失败。再说分母。总请求次数是包含所有请求还是只包含有效请求举个例子某个接口被爬虫扫了十万次全部返回 404。如果分母把这十万次都算进去那错误率看起来很低因为分子只有几百但实际上真实用户的请求可能只有一千次其中失败了两百次真实错误率是百分之二十。这就是典型的分母被污染。我通常建议按下面的口径来统计不同场景用不同的组合监控目的分子包含分母包含适用场景服务端稳定性5xx 服务端业务异常所有到达服务端的请求后端告警、SLA 统计用户体验网络失败 4xx 5xx 业务失败前端发起的有效请求前端监控、体验优化接口健康度5xx 超时排除爬虫和探活后的请求接口级监控业务成功率业务码非成功业务相关的请求业务方报表这张表建议直接抄走和团队对齐口径的时候拿出来能省掉大量扯皮。我见过太多团队因为分子分母定义不一致同一个接口在不同报表上错误率差了十倍开会的时候各说各话。3. 为什么失败次数高错误率却可能很低3.1 分母足够大时绝对量会骗人这是最直观的一个原因。假设一个接口每天有一千万次调用失败次数是一万次。一万这个数字听起来很吓人但错误率只有千分之一完全在可接受范围内。反过来一个内部管理接口每天只有一百次调用失败五次错误率就是百分之五虽然绝对量很小但比例上已经需要关注了。这里涉及一个统计学上的基本认知绝对量和相对比例反映的是不同维度的问题。绝对量反映的是影响面错误率反映的是稳定性。一个高流量接口的绝对失败数很高但可能只是正常波动一个低流量接口的绝对失败数很低但可能意味着功能已经坏了。我在做容量规划和告警配置时通常会同时看两个指标并且设置不同的阈值。绝对量用来判断是否需要立即介入错误率用来判断系统是否处于健康状态。比如失败次数超过一千且错误率超过百分之五才触发 P1 告警只有其中一个超标触发 P3 通知即可。3.2 重试机制会把失败次数放大但不影响真实错误率很多前端请求库和网关都带自动重试。一个请求失败了自动重试两次如果三次都失败那监控里就会记录三次失败。但实际上这只是一个用户的一次请求失败。我做过一个统计某个接口开启了三次重试之后监控里的失败次数涨了将近三倍但用户侧的实际失败率几乎没有变化。因为大部分失败是偶发的网络抖动重试一次就成功了。如果只看失败次数会误以为系统变差了看错误率才发现一切正常。这里的关键是重试产生的失败记录在计算错误率时应该做去重或者折算。常见的做法有两种一种是在请求头里带一个唯一的 trace id统计时按 trace id 去重另一种是只统计最终失败的请求中间的重试不计入。两种方式各有优劣前者实现简单但需要日志系统支持后者需要在客户端做聚合。注意如果你的监控系统直接统计网关日志里的失败条数而网关又开了重试那失败次数一定是虚高的。排查问题时先确认这一点能省掉很多无用功。3.3 客户端主动取消和超时算不算失败要看场景用户打开一个页面还没加载完就跳走了这时候前端会 abort 掉正在进行的请求。这些请求在监控里可能被记录为失败但它们并不是真正的错误。类似的还有超时。前端设置了 3 秒超时服务端其实在 3.5 秒的时候正常返回了但前端已经放弃了。这次请求算失败吗从用户体验角度算从服务端角度不算。我的处理方式是在请求库层面给主动取消的请求打上特殊标记统计错误率时把这些排除掉。具体做法是在 abort 的时候传一个 reason拦截器识别到这个 reason 就不计入失败。这样算出来的错误率才更接近真实的服务质量。3.4 业务失败和系统失败混在一起指标就失真了前面提到过HTTP 200 但业务码失败的情况非常普遍。如果监控系统只统计 HTTP 状态码这类失败完全不会被计入错误率会偏低。反过来如果前端把所有业务失败都算进错误率而业务失败里有很多是用户输入错误导致的比如密码错误、验证码错误那错误率又会偏高。我一般建议把业务失败再细分。用户输入导致的失败比如参数校验不通过归为“用户错误”不计入系统错误率系统原因导致的失败比如库存不足、服务降级归为“系统错误”计入错误率。这样区分之后指标才能真正反映系统的健康状况。4. 前端拿到不同响应时到底该怎么理解4.1 网络层错误请求根本没出去或者没回来前端最常见的网络层错误有这么几种。Failed to fetch通常意味着请求被浏览器拦截了可能是跨域、可能是混合内容、也可能是请求被扩展程序阻断。net::ERR_CONNECTION_TIMED_OUT是连接超时说明 TCP 握手阶段就失败了。net::ERR_NAME_NOT_RESOLVED是 DNS 解析失败。这些错误的共同点是服务端可能完全不知道这次请求发生过。所以在排查时不能只盯着服务端日志看要从前端侧收集信息。我通常会在请求库的 catch 分支里把navigator.onLine、performance.getEntriesByType(resource)里的相关记录一起上报这样能快速判断是网络问题还是服务问题。4.2 4xx 错误大部分是客户端的问题但不全是4xx 状态码的语义是“客户端请求有误”但实际工作中很多 4xx 是服务端配置或者接口设计导致的。400 Bad Request 最常见的原因是请求体格式不对。比如前端发了 JSON服务端期望 form-data或者必填字段缺失。这类问题在联调阶段很常见上线后如果突然增多通常是前端发版引入了 bug。401 Unauthorized 和 403 Forbidden 要区分开。401 是没认证403 是认证了但没权限。前端拿到 401 应该跳登录拿到 403 应该提示无权限。这两个都不应该算作系统错误。404 Not Found 除了资源不存在还可能是路由配置错误。我遇到过前端请求的路径多了个斜杠服务端路由匹配不上全部返回 404。这种问题在监控里表现为 404 突增但错误率可能因为分母大而看不出来。429 Too Many Requests 是限流。这个要特别注意它说明客户端请求太频繁了可能是前端有循环调用也可能是被恶意刷了。429 应该计入错误率因为它确实影响了正常用户。4.3 5xx 错误这才是真正需要告警的5xx 表示服务端处理请求时出了问题。500 是通用错误501 是未实现502 是网关错误503 是服务不可用504 是网关超时。这里面 502 和 504 特别值得关注。它们通常意味着网关后面的服务出了问题可能是服务崩溃了也可能是响应太慢导致网关超时。这两个错误在监控里应该单独统计因为它们反映的是基础设施层面的问题。我一般会把 5xx 的错误率阈值设得比较低比如超过百分之一就告警。因为 5xx 意味着服务端有 bug 或者资源不足是必须立即处理的。4.4 业务码错误最容易被漏掉的一类业务码错误藏在 200 响应里监控系统如果不解析响应体根本发现不了。这类错误的排查难度也最大因为从 HTTP 层面看一切正常。我的做法是在网关或者 BFF 层做统一解析把业务码提取出来打到日志里。这样监控系统就能基于业务码做统计了。如果团队用的是 ELK 或者类似方案可以在日志采集阶段加一个解析规则把code字段提取为独立字段。下面是一个 Logstash 的配置片段用来提取业务码filter { if [path] ~ /api/ { json { source message target parsed } if [parsed][code] { mutate { add_field { business_code %{[parsed][code]} } } } } }提取出来之后就可以在 Kibana 或者 Grafana 里按 business_code 做聚合算出真实的业务错误率。5. 一套可直接落地的排查与监控方案5.1 前端埋点把失败原因分门别类上报前端埋点是整个方案的基础。如果前端只上报“成功”和“失败”后面什么都分析不了。我建议至少上报这几个字段请求 URL、HTTP 状态码、业务码、失败类型、耗时、重试次数、是否主动取消。失败类型按前面说的分四类network、http、business、auth。上报的时候用 sendBeacon 或者图片打点避免阻塞页面卸载。function reportFailure(info) { const payload { url: info.url, status: info.status || 0, businessCode: info.code || null, failType: info.type, duration: info.duration, retryCount: info.retryCount || 0, aborted: info.aborted || false, timestamp: Date.now() }; navigator.sendBeacon(/monitor/report, JSON.stringify(payload)); }这个埋点方案的好处是数据落到后端之后可以按任意维度聚合。想看网络失败率就过滤 failTypenetwork想看业务失败率就过滤 failTypebusiness非常灵活。5.2 服务端日志把请求链路串起来服务端这边关键是给每个请求打上唯一的 trace id并且把关键信息结构化输出。我通常会在网关层生成 trace id透传到下游服务每个服务在日志里都带上这个 id。日志格式建议用 JSON方便采集和解析。至少包含这些字段trace_id、path、method、status、business_code、duration、upstream_status、error_message。有了 trace id 之后前端上报的失败可以和服