第2章 异常调用的定义与分类
先说我个人的理解。异常调用是什么?说白了就是一次请求、一次方法调用、或者一次服务交互,返回了不符合预期的结果。
嗯,别把它想得太玄乎。你写了个函数调第三方API,对方没响应,这就叫异常调用。你往数据库里插一条数据,结果磁盘满了插不进去,这也叫异常调用。
我刚开始做后端时,总觉得异常就是"报错"。后来踩过坑才发现——有些异常根本不报错,只是默默返回了个错误码,或者干脆卡死在那。
异常调用的本质
从流程上看,一次调用由四个阶段组成:
- 发起 —— 发送请求或执行代码
- 处理 —— 系统内部执行逻辑
- 返回 —— 把结果传回来
- 超时控制 —— 等待的边界
任何一个环节偏离了正常路径,就产生了异常调用。我有一次在生产环境排查问题,发现某个调用耗时从20ms突然飙到2s,但业务没报错。这就是典型的异常——性能异常。
核心观点:异常调用 ≠ 程序崩溃。只要不符合预期,都算异常。
常见分类:四维视角
我在实际项目中总结了一套分类法,按触发原因分为四大类。你想想看,不管多复杂系统的调用异常,基本都能归到这四类里。
| 分类 | 典型场景 | 我遇到过的案例 |
|---|---|---|
| 超时类 | HTTP连接超时、数据库查询超时、RPC调用超时 | 曾经有个接口设置了30s超时,结果数据库慢查询把连接池打满了 |
| 错误码类 | HTTP 4xx/5xx、业务错误码、内部异常码 | 我记得最离谱的是某云SDK返回200但body里有个"fail"字段 |
| 资源耗尽类 | 内存溢出、磁盘满、连接池无空闲、线程池拒绝 | 线上服务突然所有请求都超时,查半天发现是文件描述符用光了 |
| 数据异常类 | 空指针、类型转换失败、数据不一致、返回null | 上游传了个空字符串过来,下游查数据库直接抛SQL语法错误 |
超时类:最常见的隐形杀手
超时是我在监控面板里最不想看到的指标。它不像错误码那么明显,但带来的伤害往往更大。
为什么会这样?因为超时通常不会触发业务异常流程,而是让调用方一直等着。单个等几秒没事,一旦并发上来,线程池、连接池瞬间就满了。
我个人习惯给所有外部调用都设超时,而且分三级:
- 连接超时 —— 3秒,连不上就放弃
- 读取超时 —— 10秒,等响应不回来就放弃
- 整体超时 —— 30秒,整条链路的总时间
我曾经在一个金融项目中看到过一个血淋淋的例子:同一个接口给第三方调,对方设置了10分钟超时。结果某个客户端网络抖动,所有请求都卡在服务端,最终导致整个集群雪崩。
实战技巧:超时时间不是越短越好。太短会导致频繁重试,太长又会堆积资源。建议根据P99耗时再加50%的buffer。
错误码类:看上去最清楚,其实最坑
说到错误码,很多人觉得——返回码不是错误就是成功,有啥坑?
你想想看,HTTP 200就代表正常吗?不一定。我遇到过某微服务框架,内部用200包装所有响应,真正的业务结果放在body里的status字段。200 => 业务成功,但status可能是个错误码。
更常见的坑是:错误码没有统一规范。有的系统用-1表示失败,有的用1表示成功,有的用空字符串表示业务异常。我建议你先把所有调用的错误码映射关系画清楚,尤其是跨团队的系统。
我曾经因为错误码没对齐,把业务失败的请求当成功处理了,导致库存多卖了一倍。那次教训让我养成了一个习惯——永远不要信任上游的错误码格式,一定要加一层校验。
注意:不要只检查HTTP状态码。很多框架的"成功"是传输层面的成功,业务层面的状态需要单独解析。
资源耗尽类:看不见的极限
资源耗尽是我认为最难检测的异常类型。为什么?因为它在爆发前通常没有明显征兆。
内存是一点点涨上去的,连接池是一个个被占满的,磁盘是一步步写满的。等到监控告警响起来的时候,往往已经快挂了。
常见的资源耗尽类型:
- 内存:堆内存泄漏、直接内存溢出、GC开销过高
- 连接:数据库连接、HTTP连接、Redis连接、消息队列连接
- 文件描述符:日志文件没关、临时文件不清理
- 线程:线程池耗尽、死锁、无限创建线程
- 磁盘空间:日志爆炸、临时文件堆积、数据库WAL增长
我记得有一次线上告警,所有接口响应都大于10秒。我赶紧看监控,CPU、内存、磁盘都正常。最后查了连接池指标才发现——连接池的活跃数已经到了上限200,其他请求在排队等待。
嗯,这里要注意:很多常规监控(CPU、内存)其实反映不了资源耗尽。你得单独盯着连接池、线程池、文件描述符这些中间件的内部指标。
数据异常类:最容易被忽略的"软"异常
数据异常跟前面的三类不一样。它通常不会让系统报错,但会让业务逻辑跑偏。
举个例子:你调用一个获取用户信息的接口,它返回了一个空对象。你的代码没判空,直接用user.getName(),妥妥的空指针。这种异常其实属于数据异常,而不是错误码异常。
更隐蔽的是数据不一致。比如订单状态在数据库里是"已支付",但第三方支付平台告诉我"支付失败"。这种矛盾就是数据异常。
我建议你在每个关键调用的后面,都加一个数据校验器。校验器不一定要很复杂,可以只是检查返回对象的关键字段是否有值,或者值是否在合法范围内。
数据异常的特点:不报错、不超时、不耗尽资源,但会导致后续业务逻辑全部错位。
知识结构图:异常调用分类全景
下面这张图把四种异常分类以及它们之间的关系画出来了。你可以把它当作一个检查清单。
小结:如何快速定位异常类型
我在排查问题时,会按这个顺序问自己:
- 调用有没有返回?→ 没有返回就是超时类
- 返回了,但状态码是不是错误?→ 错误码类
- 返回正常,但系统资源指标是否异常?→ 资源耗尽类
- 资源正常,但数据逻辑不对?→ 数据异常类
这四步基本能覆盖90%的场景。剩下那10%,往往是你压根没想到的边界情况——比如网络包乱序导致的数据错乱,这就需要更深的排查手段了。
我建议你在写代码时就预设好每一个调用会属于哪个分类,然后针对性地写监控逻辑。不要等到出了线上事故再回头补。
分类只是第一步。下一类知识——怎么检测这些异常、用什么指标、怎么设阈值,那就是第3章的内容了。嗯,先消化好这一章,把基础打牢。