连接被拒和超时,是两条完全不同的消息
一句话总结
在“API 不工作”的报障中,信息量最大的不是日志,而是失败消息的准确措辞。
为什么需要这些知识
客户说“API 挂了”时,这句话背后可能藏着五种完全不同的情况。而具体是哪一种,通常仅凭一行失败消息就能判断。
Connection refused——这并不是坏消息。它证明数据包已经到达目的地并返回。也就是说,路由、防火墙、NAT 等沿途的所有关卡都已经通过,剩余原因可以缩小到目标主机内部:进程已经停止、正在监听其他端口,或没有绑定在 0.0.0.0 上,而是只绑定在 127.0.0.1 上。
Connection timed out——这几乎没有提供任何信息。它只表示无人应答:可能是防火墙静默丢包,可能是没有路由,也可能是对端过载。生产环境中的防火墙几乎总是采用静默丢弃策略,因此,被防火墙阻断时出现的不是 refused,而是 timeout。 所以看到 refused 后再去排查防火墙,其实是在重新检查一个已经通过的关卡。
Name or service not known——没有任何数据包发往目标服务器。防火墙日志里没有记录才是正常现象,调查方向应转向名称解析。
如果再结合失败前经过的时间,判断就会更有把握。几毫秒内结束,说明一次往返已经完成(refused);大约 2 分 7 秒后才中断,则说明内核已经耗尽所有 SYN 重传。如果应用超时设置为 30 秒,实际却在 127 秒后才失败,这表明该配置并未生效。
它是如何运作的
连接建立后,下一步要看状态码。这里最常被误诊的是 502 和 504。
502 并非没有收到响应,而是收到了无效响应。 即使响应已经返回,只要代理无法解析,也会产生 502。因此,面对 502 时延长超时时间毫无作用。 未能在时限内收到响应才是 504。
另外,在对接文档不完善的 API 时,还有三件必须确认的事。
如何判断分页结束。 如果有 has_next 这样的显式标志,就以它为准;如果没有,就一直请求,直到收到空数组。常见错误是只依据第一页的 total 计算页数,结果漏掉真正的最后一页。这是忘记处理除法余数的经典失误。
如何处理临时故障。 503 或网络错误通常会进入重试流程。但必须区分哪些操作可以重试,哪些不可以。查询重复多少次结果都一样,而重试创建请求却可能制造重复数据。4xx 表示请求本身有问题,不修改请求,无论发送多少次都会得到同样结果,因此不应重试。
文档未说明的缺失值有多少。 这是生产环境里最常让人栽跟头的问题。规范中标为必填的字段,在实际响应中经常以空字符串出现;若把这样的值直接纳入汇总,就会悄无声息地算出错误数字。
在实际工作中会是什么样
因此,对接新 API 时的第一项工作不是写代码,而是做一次全量调查。完整遍历所有页面,统计总记录数、合计值,以及每个字段的缺失数量。
一次这样的调查,可以省去之后数周的调试。如果第一周就提出“总共 60 条记录,其中 6 条的 region 为空,这些记录该如何处理?”,以后就不会收到“按地区汇总的销售额对不上”的报障。
让依赖外部 API 的代码更安全
通过全量调查了解对接对象之后,下一步就是确保即使该 API 出现波动,我们也不会随之崩溃。外部 API 不由我们修复,因此唯一的防线是把假设明确写进代码。
不要原样相信收到的数据。 我们已经见过规范标为必填的字段实际却为空。因此,应在读取数据的位置检查类型和取值范围;遇到不符合条件的记录时跳过,但要记录跳过的数量。 静默丢弃会让人日后无法查明合计值不一致的原因,而整体报错则会让一条坏数据拖停全部处理。
务必设置超时。 很多库没有默认超时,或者默认值是无限。一次没有超时的调用会永久占住工作线程,直接重现前面提到的连接池耗尽。如果能分别设置建立连接和等待响应的时间,就应拆开设置。
只对幂等操作重试。 如前所述,查询执行多次仍然相同,创建操作却不是如此。此外,如果重试间隔不加入随机抖动,对端短暂波动后恢复时,所有客户端就会在同一时刻蜂拥而至,再次将其压垮。
保留原始响应。 留下原始数据后,即使日后修正了解析规则,也可以重新处理。若只保存解析结果,那么发现规则有误时,原始数据早已不复存在。保存数据的成本通常远低于重新获取数据的成本。
最后,要设置一种能够察觉对端变化的机制。当响应的字段构成或记录数量与平时差异很大时发出告警,就能在 API 悄然变更时先于用户发现问题。外部 API 会在没有预告的情况下发生变化;即使对方发过预告邮件,那封邮件通常也不会送到我们手里。
下一次实操要做什么
你将对接一个全部文档只有一页 Wiki 的订单 API,确认版本,遍历所有页面以计算总记录数和合计值,统计文档未说明的缺失值,并通过重试成功访问不稳定的端点。