
整整两天我盯着日志里的API超时和连接重置试遍了所有能想到的代码层方案——调大超时、重建连接池、加重试退避、换HTTP版本问题依旧。到第三天上午我用一台完全独立的服务器打了个测试请求才发现这次故障真不在我这边上游服务的API网关在特定区域、特定时间段内持续丢包跨地域请求成功率差了近20个百分点。这篇就把完整的排查链路记录下来从第一天的盲目改代码到最终用探针数据锁定外部故障中间的经验希望对做接口联调、处理线上偶发故障的后端开发、运维同学有直接参考价值。这套定位问题归属的流程不依赖特定技术栈任何语言、任何框架都能拿去用。1. 故障现场断连到底长什么样1.1 第一波告警超时率从0.2%飙到12%先从事故现场说起。周一晚上七点半监控群突然刷屏核心服务模块调用第三方内容审核API的超时率从平时的0.2%一路涨到12%以上紧接着告警升级大批HTTP 502和connection reset by peer开始往外冒。我们的业务背景是一个用户上传内容的实时审核场景服务端同步调用上游开放平台的内容审核API正常P99耗时稳定在180ms左右。所谓P99就是1000个请求里最慢的10个也不超过180毫秒。这个基线平时没人注意但故障排查时它就是冲锋号——一旦超过说明系统一定发生了变化。刚开始故障的表现不是全挂而是间歇性抽风这一分钟成功率正常下一分钟超时率飙到15%再过两分钟又恢复了。一会好一会坏的故障最折磨人你在抓问题的时候它偏偏正常等刚放松警惕它又来一下。当时我翻了日志看到几类报错混在一起[WARN] read timeout after 10000ms, urlhttps://api.example.com/v1/check [ERROR] connection reset by peer, hostx.x.x.x:443 [ERROR] connect timeout to x.x.x.x:443正是这种混杂的报错模式让我一开始误判成是代码层的超时与连接管理问题。毕竟前两天正好改过HTTP客户端配置按谁最近改动谁背锅的团队潜规则嫌疑首先落在我自己头上。1.2 三类报错混在一起慢是慢断是断排障之前有必要把现象分清楚慢和断完全不同。我看到的报错可以归成三类第一类是读超时。TCP连接建好了请求发出去了对方一直没有返回直到本地设置的readTimeout到期。第二类是连接被重置。对方在数据传输中途直接断开连接抓包能看到RST包而不是正常的FIN挥手。第三类是建连失败。请求连TCP握手都没完成socket层就报connection timeout。这三类混在一起表象上都是调用失败但背后原因不一样。如果只是慢多数是资源不足、带宽拥塞如果出现大量RST多半是中间链路或对端网关主动做了某种重置动作——比如限流触发、防火墙拦截、或者节点异常重启。这也是我事后复盘时的第一个关键线索现象里RST占比太高从一开始就不太像传统的服务响应慢。但当时我满脑子都是自己代码有问题完全没往外部想。2. 第一轮排查把所有代码层嫌疑都试了一遍2.1 调大超时反而帮了倒忙我做的第一件事是检查超时参数。我们用Java的OkHttp原来的配置是连接超时5秒、读超时10秒、写超时10秒。第一直觉就是会不会是超时设太短上游在高峰期响应稍微慢一点就被本地掐断然后断掉的连接堆着没人管最后雪崩我把读超时从10秒改成30秒上线观察半小时。结果意外的是超时率没怎么降反而有一小部分请求真的吊死在了30秒才回来。这说明对面并不是响应很慢但最终会返回而是压根没打算返回。超时调大只会让所有挂住的请求占满线程池把一个小故障放大成服务整体不可用。这个教训值得单独说改超时之前先想清楚你在治什么病。超时是兜底策略不是治疗手段。一个合理的超时配置应该配合你的重试预算、线程池大小、业务容忍度去算而不是凭感觉往上加。比如我们后来定的方案是连接超时3秒、读超时8秒、总调用时间上限10秒重试预算另算这样单个请求最坏情况下也不会占住资源超过10秒。2.2 连接池与HTTP版本改了但毫无波澜第二个重点怀疑对象是连接池。排查时发现OkHttp默认对单个路由最多5个并发连接闲置连接5分钟回收。我想当然觉得会不会是连接不够用请求排队等着占连接导致大量超时于是把最大连接数调到50并顺手改了keep-alive行为。结果配置上线后超时率没有任何改善倒是监控里看到服务器上TIME_WAIT状态的连接暴涨了几倍。这说明问题根本不是连接不足。之后我又做了关闭HTTP/2改用HTTP/1.1的对比实验结果同样没有变化。虽然这些实验都白做了但说句公道话排查的排除法就是这个过程——你只有把一个一个变量都验证过后面给上游发工单时才敢理直气壮地写我们已排除客户端配置因素。在没做这些实验之前你说问题在你那边是没有说服力的。所以这一段我不想简单用白干概括它是整个证据链里最耗时间但也最必要的一环。2.3 重试策略成了故障期的帮凶第三个坑是重试。原来的重试策略很粗暴失败后最多重试2次无退避立即重发。平时接口偶发失败这种策略确实能提高成功率但等到上游半死不活的时候等于故障期间我们自己的调用量瞬间翻三倍。日志里能看到同一个请求ID在几秒内出现三次调用最后一次是在对端已恢复后才成功的。这里顺便放一下我们之前和现在的重试逻辑对比// 之前的粗暴重试失败立刻重发没有等待 for (int i 0; i 3; i) { try { return client.newCall(request).execute(); } catch (IOException e) { // 立即重试 } } // 现在的指数退避 抖动 long backoff Math.min(1000L * (1 attempt), 10000L); backoff ThreadLocalRandom.current().nextLong(0, 500); Thread.sleep(backoff);把重试改成指数退避加抖动之后我们这边发出去的流量降下来了但故障期间的成功率依然是波浪形。也就是从这一刻开始我隐约意识到代码层能做的都做了问题不在这里。这里也强调一个重试的正确设计思路重试次数上限2到3次退避基数1到2秒指数递增最好再加随机抖动避免多个客户端在同一时刻涌上来同时要确保业务接口是幂等的否则重试会制造脏数据。2.4 DNS和TLS排雷又是两个无效动作代码层排查得差不多后我把目光放到DNS和TLS上。生产环境用的是云厂商的DNS服务器我发现目标API域名的TTL很短怀疑频繁解析导致异常。为了验证我把域名直接用hosts固定指向一个解析结果绕过DNS问题依旧。再查TLS抓包看到一部分连接卡在ClientHello之后没有下文像是TLS握手被中断。我尝试调整TLS版本和加密套件组合依然无改善。到这里可以确定我们应用的出站配置、DNS解析、TLS协商都没有问题。排查方向只剩两条路中间网络链路或上游服务端。这正是从应用层钻进网络层的信号。3. 网络层真相从应用层钻进抓包与MTR3.1 tcpdump里的两类异常信号从应用层钻进网络层后我先在一台出问题的服务器上跑抓包tcpdump -i eth0 host x.x.x.x and port 443 -s 0 -w /tmp/capture.pcap大概抓了90秒故障复现期间的数据非常有代表性。正常连接应该是三次握手、TLS握手、HTTP数据交换、四次挥手故障期间则主要出现两种异常。第一种是大量重复ACKDup ACK和TCP重传。同一个序列号的包在短时间内重传三到五次最后对端以RST收场。这说明链路中间存在明显丢包可能是一个物理链路或路由节点在做黑洞。第二种是TCP握手阶段的RST。连握手都完不成而且这样的RST成片出现不太像业务超载超载通常是排队而不是直接重置更像是有网关安全策略在主动掐断新建连接——比如并发数限制、限流规则、或者边缘节点的健康检查误杀。怎么区分呢我的经验是看RST的位置数据收发中途的RST优先怀疑链路丢包刚建连就RST优先怀疑对端入口限流特定时间点同时大量RST基本可判定是网关或节点在抖。3.2 MTR把丢包定位到特定网络区域tcpdump只能看到我们到目的地之间的表现要看具体哪一跳出问题我用MTR跑了一段持续监测mtr -t -r -c 300 x.x.x.xMTR会持续发送探测包并统计每一跳的丢包率和RTT比traceroute更接近真实业务路径。跑的结果是全程约十几个节点大部分丢包率为0只有两个节点随着故障时间窗口出现了20%到40%的丢包而且这两个节点正好处在网络区域边界。这里有个容易误判的坑中间路由节点丢包不全等于它的故障有些核心设备本身就对ICMP探测包的优先级较低业务流量可能照样畅通。所以要盯的是最后一跳目标服务器所在网络的丢包率。我们这次最后一跳的丢包率和中间异常节点几乎同步这就坐实了整条链路在这个时间段是真实丢包。至于丢包是上游机房网络限流还是中间链路节点的问题仅凭MTR还不能最后定性于是我再做了对照实验。3.3 对照实验矩阵换机器、换地域、换探针排障的核心思维不是猜而是不断改变一个变量、观察结果是否跟着变。我设计了这样一组对照实验实验变量目的结果同机房换一台新机器排除本机资源与本地配置因素现象不变排除本机问题换华东、华南轻量服务器改变出口地域华东、华南正常仅华北受影响用独立监控平台的探针发起请求完全隔离我方环境特定地域节点偶发失败时间窗与业务告警重合这张表是整个排查过程最重要的产出。有了它结论不只是我觉得是上游的问题而是同一份代码、同一个请求换一个地域出口成功率从82%变回98%我们没有改任何东西唯一变化的是网络路径。这个说服力比一百句抱怨都强。有一点要特别说明做对照实验时每次只改一个变量。如果同时改两个变量结果变了你都说不清楚是哪个造成的。我们第一轮排查就是犯了同时调超时又调连接池的错误后面才学乖拆开做。4. 确凿证据上游API网关在特定区域丢包4.1 独立探针报告让数据自己说话到这一步我写了一个独立的探测脚本每5秒发起一次真实业务请求分别从华北、华东、华南三个区域各跑1000次记录成功率、P95耗时和错误分布。30分钟后的数据是这样的华北出口成功率82.6%P95耗时1460ms错误以RST和读超时为主华东出口成功率98.9%P95耗时205ms华南出口成功率98.5%P95耗时224ms同一种API同一套请求内容唯一区别就是地域出口结果却差了16个百分点。再结合之前工单里的反馈基本可以确认上游API整体服务是正常的但他们某个区域的接入网关或边缘节点确实存在连续、间歇性的故障。同时我也拉了一个外部状态监控服务业界主流的拨测平台都行的数据做佐证。这里有个技巧如果服务商提供了面向客户的状态页或服务健康JSON接口定期抓下来存库没有的话就在自建拨测工具里配置一个关键API健康检查每30秒探一次。多个独立数据源的时间窗口重合之后给上游提工单就是一套铁证。4.2 服务状态页的教训绿着不代表没问题在等待上游确认的时间里我盯着他们的status page刷新了无数遍自始至终都显示绿色All Systems Operational。这件事给了我一个重要教训状态页只能作事后参考不能作为实时的故障依据。原因有三。一是检测阈值不同大平台通常看全球平均成功率某一两个区域的异常波动很容易被平均值抹平。二是边缘节点与中心控制面的信息差API网关全球多节点部署状态页展示的是整体健康度单个节点抽风对全局数字来说只是小数点后第三位的扰动。三是有不少平台的SRE流程是先确认再发布在故障没被完全定位之前不会主动标记incident避免引起客户恐慌。所以排障时一定记住状态页绿着不等于对方的服务真的没出问题你的用户都失败成一片了你更需要相信自己的数据和工单渠道而不是那个页面。4.3 一份让上游无法推诿的工单工单来回折腾了快一天。第一轮回复永远是模版请联系我们检查网络配置请确认API Key是否正确您的请求量可能触发限流。这时候别生气继续把证据栈叠上去。我整理了一份比较管用的工单内容模板现在一直保留着第一段故障时间窗口、受影响区域、失败特征一句话概括。第二段我们做过的客户端排障清单超时、连接池、重试、DNS、TLS都列出来证明锅不在我们。第三段独立探针报告直接附上三地的成功率对比表。第四段请求URL、请求头、关键报错和pcap抓包文件的下载链接提前传到对象存储。第五段明确请求请对方重点排查xx区域接入网关在指定时间段的健康状态。这样一份工单对方基本不用二次返工内部可以直接转给自己的网络团队。我们这边提完以后他们当晚就确认了特定区域的一个边缘接入点存在不稳并告知可以短期内切换接入地址绕开故障。5. 复盘与防御问题不在我不等于我可以躺平5.1 48小时都耗在哪先入为主是最大成本故障从发现到最终确认前后约48小时。真正有价值的排查操作压缩一下其实大约半天。剩下的时间主要耗在三个地方。第一是我先入为主的变更有罪假设。前24个小时全在改超时、连接池、重试这些代码参数没有停下来先分类慢与断。第二是一直在刷新状态页和等工单把判断依据寄托在别人身上。第三是没有第一时间拉起跨地域探针——如果第二天上午就做至少能提前6到8个小时锁到区域性问题。复盘时我把这点写成了一条团队规范遇到第三方依赖故障第一优先是采集证据第二才是猜测原因。采集证据最快的方式永远是多个源头同时打点而不是盯着一块日志硬看。数据是最后的法官这句话在这场排查里被验证得淋漓尽致。5.2 快速三分法客户端、链路、服务端经过这次的折腾我沉淀了一套自己的快速定性流程无论是自研服务问题还是第三方API问题都能用第一步抓短时包先分类目的是解决失败到底是建连失败、读超时还是RST。第二步看失败的时间分布是持续性的还是集中在特定时间段如果是特定时间段把本地监控的时间戳对齐。第三步做跨地域对照起一台其他区域的机器或者用一个拨测平台的探针同一时间段各打一批请求。如果其他区域正常基本锁定为特定区域的网络路径问题如果所有区域都挂优先怀疑API服务端全局故障。第四步查自己的依赖配置确认API Key、网关地址、代理设置和DNS没有问题把我们侧正常单列成证据。第五步提交证据工单把上述内容一次性甩给上游请对方检查对应区域的接入节点。这套方法我建议每个团队都写成SOP文档尤其是对接了一堆外部API的业务真到故障发生时才去回忆流程是会手忙脚乱的。5.3 四层防御外部依赖生病时业务不瘫痪问题不在我是归因的结论但产品对外部API的依赖是不可消除的现实。我现在的看法是第三方API对业务来说永远是需要防的雷而不是协议里承诺的可靠性。基于这次经历我们做了这样几层防御。第一层是超时与重试预算。给所有第三方调用设置独立的超时矩阵核心是总调用时间上限不能把所有请求都吊死在同一根超时绳上。重试统一使用指数退避加抖动并且限定总重试预算防止雪崩时自我放大。第二层是熔断器。使用Resilience4j时的配置是10秒窗口内失败率超过50%熔断器打开后续请求快速失败并落本地兜底每5秒释放一个探测请求对端恢复后自动半开、关闭。如果这次故障前我们就配好熔断那波性能雪崩是可以直接拦住的。第三层是降级预案。内容审核场景里临时方案是先把待审核内容送进本地缓冲队列等上游恢复后异步补偿实在承担不了风险就切换成功能降级模式人工抽检并记录。重要的不是降级方案多完美而是故障的时候你有一条路可以走。第四层是跨区域热备接入。不少平台都会提供多区域的接入地址我们在配置中心里维护了主备两组地址按权重分配流量。监控到主区域成功率低于阈值时自动切走部分流量。这次如果提前有这个配置用户侧几乎不会有感觉顶多监控里看到一条已自动切换接入区域的日志。5.4 可观测性改造一次调用拆成五个时间切片最后聊一聊事后做的可观测性改造。以前对接第三方API时监控只有成功率和平均耗时两个指标连失败发生在哪个阶段都不知道。现在我们把一次调用拆成了可观测的多个时间切片DNS解析耗时、TCP建连耗时、TLS握手耗时、首字节时间TTFB、总耗时连接复用情况是否使用了连接池中的老连接目标地址命中的域名、IP、区域接入点错误分类超时、RST、TLS失败、HTTP状态码这些指标拿来干什么举个例子如果某个时段TTFB异常高问题往往在服务端处理链路如果TCP建连耗时和TLS握手耗时一起飙升问题大概率在网络路径如果普通耗时平稳但成功率低可能要看重试和限流。加了这些维度之后任何人打开Dashboard都能一眼判断自己代码、网络链路、外部服务三者的嫌疑顺序不用再翻着日志猜。告警规则也做了调整。之前只对成功率低于99%告警故障时被刷屏到麻木现在改成多级、持续性告警比如成功率低于99.9%持续5分钟是warning低于99%持续5分钟才是critical并且把外部依赖告警和业务告警分开值班人看到告警就能区分该切流量了还是该看代码了。这次的经历等于用两天两夜的不眠换了一套可以反复使用的外部依赖故障排查手册。以后再碰到API调用老是断我反而不会先慌了先把现象分清楚再拉跨地域数据最后用证据说话。最反直觉的一点是——越是急着改代码越容易错过真正的病灶越是先把现象剖清楚越能在最短时间内把问题归属定下来。这里的甩锅不是推卸而是技术判断到位该认的自己认不该认的用铁证说话。这套流程现在是我们团队的默认SOP希望你用的时候不用再熬那两个晚上。