1. 现象描述与初步猜测
1.1 从“两天排查”说起
这个标题写出来我自己都想笑。上上周四,我们的订单回调任务又开始在凌晨三点报警,日志里一堆Connection reset by peer和Read timed out。我本来以为是新上线的Java接口又没处理好连接池,结果整整排查了两天,最后把抓包记录甩给API服务商,对面才承认是他们某区域网关机房在高峰期主动断连接。这篇文章就来复盘一下,我是怎么一步步把问题从自己身上剥离干净,证明“问题真不在我这边”的。
先说下背景:我们有一个自动化脚本,每天定时通过Python调用某家大模型平台的接口做文本归类,同时还有一个同事用VS Code里的Continue插件直接请求DeepSeek API做代码提示。两边在同一个时间段都开始频繁断连,表现是请求发出去后要等很久才超时,或者刚连上就被踢下来,错误信息还不统一,一会儿Connection reset,一会儿Read timed out,偶尔还会冒出来一个RemoteDisconnected。因为现象跨度太大,第一反应就是“我这边出问题了”。
1.2 第一轮自查清单
按惯性思维,我先列了一个自查清单,把最可能出问题的地方全捋了一遍。这里建议大家都养成习惯:遇到API断连,不要急着改业务代码,先把下面这些基础项确认过再动手。
- 代码层面:超时时间是否设置得太短?有没有显式关闭连接?是不是每次请求都新建了一个Session,导致TCP握手开销大,又容易出现半开连接?
- 服务器层面:系统负载高不高?TCP
Time-Wait状态是不是堆积了?ulimit -n文件描述符是否足够? - 网络层面:服务器DNS解析是否正常?出口IP有没有被对方拉黑?有没有经过一层不稳定的代理或负载均衡?
我全都过了一遍。代码用的是requests库,设置了timeout=(5, 10),每次调用后关闭连接;服务器是4核8G的云主机,负载常年不到0.3;DNS用dig查过,解析结果正常并且稳定;出口IP也换过,问题依旧。最气人的是,我用curl -v手动请求同一个接口,第一次能通,第二次就卡住,第三次直接Connection reset,而我什么代码都没有跑,还是在同一台服务器上。这就很说明问题了:至少不是我的业务逻辑导致。
2. 系统排查流程
2.1 抓包:让证据说话
排查到第二天上午,我开始认真抓包。用的是最土但最有效的组合:tcpdump抓网络层数据包,curl -v看HTTP层的交互过程。命令很简单:
sudo tcpdump -i eth0 host api.xxx.com -w api_problem.pcap同时另开一个窗口用curl重复请求:
for i in $(seq 1 20); do curl -sS -o /dev/null -w "attempt $i: http_code=%{http_code} time_total=%{time_total}\n" \ -H "Authorization: Bearer YOUR_KEY" \ https://api.xxx.com/v1/chat/completions -d '{"prompt":"test"}' 2>&1 done抓下来的数据包用Wireshark打开,几个关键信息非常有意思:TCP三次握手非常快,SYN包发出去对端几十毫秒就回了SYN-ACK,说明网络链路和服务器本身是通的。但问题出在第四次交互上——当客户端发完HTTP请求之后,对端没有正常返回HTTP响应,而是直接回了一个RST包。这说明连接被对端主动重置,不是网络丢包导致的重传失败。因为我这边包都收到了,是对方在应用层主动把连接断掉。
与此同时,我注意到一个细节:curl -v显示的Connected to api.xxx.com后面,有时候会卡住几秒才进入TLS handshake,有时候TLS握手成功后,读取响应的阶段又卡住。这种“能连上但读不到数据”的典型表现,通常都指向两个方向:要么是服务端在接收完请求后处理超时,要么是对端网关在中间掐断了连接。
2.2 分段排查:从本地到远端每一跳
为了排除中间网络设备的问题,我对目标域名跑了mtr(更高级的traceroute),持续超过1000个包,观察每个节点的丢包率和响应抖动。
mtr -rw -c 1000 --no-dns api.xxx.com数据出来之后,中间路由的丢包率基本都是0%,只有最后一跳偶尔出现0.2%的丢包。这个0.2%其实也属于正常范围,因为最后一跳是目标服务器本身,它对自己发过来的ICMP应答会有一定优先级限制。如果中间任何一跳丢包率超过5%,那我们还能怀疑是运营商骨干网的问题,但这里没有。所以我基本可以排除“我们去机房的路上有干扰”这个假设。
接下来我尝试了不同端口验证。因为HTTPS走的是443端口,我用tcping测试了443端口的连接耗时,同时也测了22端口(SSH)做对比:
tcping api.xxx.com 443 tcping api.xxx.com 22结论是443端口连接成功率比22端口差很多,而且443端口还经常连接成功之后立即被断开。既然同一个IP地址、同一个网络路径,只是端口不同,表现完全不同,那问题一定出在监听443的服务上,也就是目标API的网关或负载均衡层,而不是基础网络。
2.3 排除代码与配置干扰
很多人会说我是不是本地代理环境有问题。为了堵住同事的嘴,我直接用一台全新的、没有配置任何代理的裸机跑最小复现脚本,只用来测试,脚本短得可怜:
import requests url = "https://api.xxx.com/v1/chat/completions" headers = { "Authorization": "Bearer test-key", "Content-Type": "application/json" } data = { "model": "example-model", "prompt": "hello", "max_tokens": 32 } session = requests.Session() for i in range(10): try: r = session.post(url, headers=headers, json=data, timeout=(5, 15)) print(i, r.status_code, r.text[:50]) except Exception as exc: print(i, type(exc).__name__, str(exc))这台裸机上既没有跑业务服务,也没有自定义路由或防火墙策略,Python环境也是刚装好的。结果连续十次请求中,有四次超时,三次Connection reset,三次成功。这已经足够说明问题了:重度复现的条件完全不依赖我们现有环境。
2.4 横向对比多API
同一个脚本,我顺手把市面上常用的几家API都测了一遍,包括阿里云百炼、Kimi、豆包、讯飞星火和DeepSeek。我把它们的鉴权方式、请求URL和参数统一封装了一下,然后对每个API连续请求50次,统计成功率、平均响应时间、超时次数。
最终结果很直观:同一台服务器、同一段网络环境,阿里云百炼和Kimi的稳定性很好,50次请求只有一两次超时;豆包和DeepSeek在高峰期会出现间歇性失败;而出问题的那个API服务商,成功率只有60%上下。这又给了我一个非常有力的证据:如果是我本地网络或服务器的问题,不可能只对某一家API生效,其他家一点事都没有。这跟人去医院看病是一个道理,你换个科室测同一项指标都正常,那就是原科室的设备或流程出问题了。
3. 核心问题定位:对端服务或网络路径问题
3.1 数据对比:什么时间段断?断的频率?
既然锅大概率不在我这边,我就开始记录断连的规律。我把每一次请求的时间戳、耗时和错误类型都写进日志,然后隔一会儿统计一次。连续跑了约15小时后,数据里的规律非常明显。
- 凌晨2点到6点:成功率高达99%,几乎没有断连。
- 上午10点到下午4点:成功率95%左右,偶发超时。
- 晚上8点到11点:成功率直接掉到75%,
Connection reset频发。
这个曲线刚好和对方API服务的客户端活跃时段重合。为了验证是不是我们自己服务器被攻击,我检查了服务器上的连接数、CPU负载和网络流量,数据都正常。而且我用同一个脚本去请求另一家API,在同样的晚上时间段,成功率还是稳定在95%以上。于是我有理由相信,对方的网关在业务高峰时出现了过载或恶意限流。
同时,我去查了对方官方状态页和运维公告,发现当天晚上确实有“服务异常”和“请求延迟增加”的提示,虽然第二天早上他们更新说“已恢复”,但我们的实际观察是仍然不稳定。这种“嘴上说恢复,实际还在抖”的情况并不少见,所以不能只看状态页,要自己持续监控。
3.2 是限流还是故障?如何区分
这里要分享一个区分经验:限流通常是有意识的,服务端一般会返回429 Too Many Requests或503 Service Unavailable,并且带上Retry-After头部;但故障导致的断连往往表现为连接层重置,或者请求发出后长时间没有响应,超时后客户端主动断开。
从抓包数据看,我们的问题几乎都是对端发了RST,而HTTP响应码很少出现。这说明请求根本没到达后端的业务逻辑层,在网关层就被丢弃或拒绝。你可以把网关想象成餐厅门口的迎宾员,如果只是人太多,迎宾员会告诉你“没位子,请晚点再来”(对应429);但如果迎宾员自顾不暇,门口乱成一团,客人就根本进不了门,也不会有人给一个明确答复(对应RST)。显然,我们遇到的更像后者。
为了进一步确认,我尝试降低并发数到每5秒一次,同样会断;把请求body减小到十几个字节,还是会断。这说明不是我们的单次请求参数触发了WAF之类的规则,而是网关对所有客户端都在执行某种粗粒度策略。
3.3 最终确认:问题不在我这边
抱着试试看的心理,我提交了工单,附带了三样证据:
- 最小复现脚本输出的错误日志;
tcpdump抓包文件(关键部分截图);mtr长时间统计报告。
客服一开始还是老一套,让我检查“本机防火墙、DNS、代理设置”,甚至怀疑我用作弊性的并发工具。我回复说:同一脚本请求阿里云百炼和Kimi都正常,你们是唯一失败的;并且我的抓包里严格显示TCP三次握手成功,是你们在收到HTTP请求后主动返回RST。你可以去查服务器上的TCP连接记录或者网关日志,看有没有来自我这个出口IP的入网数据。结果他们再回复时,终于承认“经排查是我们华北区域某网关集群出现异常,已触发自动运维”。这时候距离我开始处理,已经超过了48小时。
4. 如何让别人相信“问题不在我这边”
4.1 用最小复现脚本留档
很多开发者在排查问题时喜欢直接在项目里加日志,改超时参数,加一堆重试逻辑,最后把自己代码改得面目全非还没解决。我的习惯是先隔离出一个最小复现脚本,这个脚本只做一件事情:循环调用接口并记录结果。它不依赖项目代码,不依赖复杂的数据库和中间件,保证别人拿到同样环境也能复现。
最小脚本的输出尽量包含:
- 每次请求的时间戳(精确到毫秒)
- HTTP状态码(如果有)
- 异常类型和消息
- 响应耗时
- 请求ID(如果服务商返回了)
用Python写可以这样:
import time import requests from requests.exceptions import RequestException URL = "https://api.example.com/v1/your_endpoint" HEADERS = {"Authorization": "Bearer your_api_key", "Content-Type": "application/json"} PAYLOAD = {"query": "test"} for attempt in range(50): t0 = time.time() try: resp = requests.post(URL, headers=HEADERS, json=PAYLOAD, timeout=10) elapsed = time.time() - t0 print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] attempt {attempt}: {resp.status_code}, time={elapsed:.3f}s, body={resp.text[:100]}") except RequestException as exc: elapsed = time.time() - t0 print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] attempt {attempt}: ERROR, time={elapsed:.3f}s, type={type(exc).__name__}, msg={exc}") time.sleep(1)注意这里的timeout=10是连接和读取的总超时,如果接口本身响应慢,会误报超时。但我们要抓的是“明明连接成功了,读取时被对端重置”的情况,所以超时设置可以稍微宽松一点,比如(5, 20)。
4.2 收集证据链
跟服务商拉扯的时候,光说“我的代码没问题”是没用的,对方只会让你自查。你需要拿出一个完整的证据链,让对面无话可说。我的证据链一般按这几步组织:
- 网络连通性证明:
mtr -rw -c 1000 <host>的输出,展示中间链路没有明显丢包。 - TCP交互证明:
tcpdump抓包文件,重点展示TCP三次握手成功,随后对端发送RST或延迟很久不响应。 - 接口对照实验:同一脚本、同一时间窗口,请求其他API成功而请求该API失败。
- 服务状态截图:对方官方状态页当时标记的异常时间点,跟我本地时间轴对得上。
把这些东西打包成一个压缩文件,附上简要说明文档,在工单里一次性发出去。大多数人连第一步都懒得做,你跟他说“我给你抓包了”,他至少会把你往“认真排查过”的队列里排。
4.3 和对方客服有效沟通的技巧
给API服务商提工单也有技巧。错误的大白话:“你们的API有问题,老是断。”基本不会得到有效响应。有效的提法应该是:
我们于2025年某月某日20:13至22:30期间,调用贵方
/v1/chat/completions接口,出现大量Connection reset和Read timed out,成功率约75%。我们已用最小脚本在纯净环境复现,相同脚本调用其他API服务正常。抓包显示TCP握手成功,对端在收到HTTP请求后返回RST,问题疑似发生在贵方网关或负载均衡层。请求ID示例:xxx-xxx-xxx(如果有)。可以提供抓包文件和日志。
这样对方知道你已经做了功课,他们能直接跳过多轮无效沟通,直接去查网关。我自己这次就是,工单来回了两轮后,对方直接拉了内部团队处理,效率高很多。
5. 同类场景的避坑经验
5.1 “API调用老是断”常见的5个原因
虽然这次问题确实不在我这边,但说实话,我自己平时遇到的大多数“联动断开”问题,有相当比例还是自身配置不合理。这里我把常见原因按出现频率排了个序,方便你对照排查。
| 原因分类 | 典型现象 | 自查方向 |
|---|---|---|
| 超时设置过短 | 请求稍微慢一点就直接Timeout | 检查timeout参数,区分连接超时和读取超时 |
| 连接池未复用 | 频繁建立新TCP连接,导致握手开销大、端口耗尽 | 用requests.Session()或http.client长连接;Java里使用连接池管理HttpClient |
| DNS解析不稳定 | 解析结果随机变慢,或返回多个IP但某些IP不通 | 用dig多次查看TTL,尝试固定IP或换用HTTPDNS |
| 本地代理或防火墙干预 | 请求时通时不通,延迟抖动严重 | 临时关闭代理、云盾/IP白名单、杀软,再跑最小脚本 |
| 服务商限流或网关故障 | 高频时段固定失败,并且多客户端共同失败 | 查看官方状态页、对比多个API服务、抓包观察RST信号 |
在排查时,我建议先按表格里的前四项过一遍,如果全排除了,再往服务商那边靠,不要下意识就降级到“对方问题”的心态。否则排查容易变成甩锅,反而找不到真正的根因。
5.2 排查工具清单与常用命令
工欲善其事,必先利其器。整理一些我实际用过且觉得高效的命令,以后遇到了直接用。
- curl 带输出详情:适合快速验证,
curl -v https://api.example.com/path可以看到TLS握手、HTTPS状态等。加-w '%{http_code} %{time_total}\n'能拿到耗时。 - tcpdump 抓包:
tcpdump -i eth0 -w /tmp/api.pcap host api.example.com,抓完用Wireshark打开看有无RST。 - mtr 持续监测:
mtr -rw -c 1000 --no-dns api.example.com,比traceroute更准,能输出丢包率。 - tcping 端口测试:
tcping api.example.com 443,纯TCP连接测试,不受HTTP影响。 - strace 跟踪系统调用:
strace -f -e trace=network python3 test.py,看程序每一步系统调用的返回值,对定位connect和read阶段的超时很有帮助。 - netstat 查看连接状态:
netstat -antlp | grep :443,查看ESTABLISHED和TIME_WAIT数量的变化,判断连接池是否正常。
如果你用的是Java开发API接口供外部调用,特别要注意连接池参数。我之前写过一个Java应用的接口,内部用Apache HttpClient,默认连接池最大连接数太小,底层连接空闲回收时间过长,结果上游调用方频繁报连接超时。后来把setMaxTotal和setDefaultMaxPerRoute调大,又设置了setValidateAfterInactivity,情况立刻好转。这是很典型的“自己这边的问题,但是表现成API不稳定”。
5.3 针对大模型API与低代码平台的额外提醒
最近关于“DeepSeek API如何调用”“阿里云百炼API调用示例”“Kimi API调用”“豆包如何调用API接口”这类问题特别多,很多朋友是刚接触大模型API的。新手常犯的毛病是:看着文档里给的curl示例能通,但放到自己的Python脚本或Java程序里就开始断。
这里我额外提醒几个点:
- 鉴权参数不要拼接错。DeepSeek用的是
Authorization: Bearer <key>,Kimi也是这样,但有些老平台要用api_key字段。配置好之后先用官方在线调试工具试通再写代码。 - 注意请求超时与流式响应。调用大模型API时,如果开启
stream,服务端会分多次返回数据,你的代码要能处理“接收中”的情况,不要因为一时没数据就断开。读取超时时间至少要给到60秒或更长。 - 低代码平台调用API时要留意出口IP。很多低代码平台会给你一个共享出口IP,这个IP可能因为别人滥用而被目标API服务商限流。表现就是你做了很简单的请求也老断,但换了本地环境就很稳。出现这种情况,可以试着在低代码平台里配置自定义DNS,或者联系平台运维查一下出口IP是否在对方黑名单里。
- 本地调用大模型API的时候,第一次冷启动特别慢。所以单独设置较长的连接超时(5秒)和读取超时(60~120秒)是必要的。如果你读超时只有30秒,碰上模型推理时间超过30秒的请求,必然会报
Read timed out,但换个请求小一点的内容又正常。这个也容易被误判为“服务端断连”。 - 把Dify工作流转成API给其他软件调用,本质上你是做了一层“API二次封装”。你要同时考虑上游大模型API的稳定性、中间工作流的处理耗时,以及下游客户端的超时设置。我建议你在你的API层给下游一个明确的
Retry-After或503响应,而不是直接断开连接,否则下游会以为你系统崩了。
5.4 经验:遇到“无缘无故断连”先别改代码
最后说个实在的经验。遇到API断连,最忌讳的事是“瞎改”。你可能把这几次超时归咎于超时阈值太小,于是把超时从10秒改到30秒;又发现还是断,于是加重试;重试之后发现原来是一次性的偶发错误,却因为回调里没有幂等设计导致重复数据;最后越扯越深,甚至动了业务逻辑。而真正的原因可能只是对端某个节点在特定时间段的负载抖动。
我的建议是,先花10分钟做对照组实验:同一请求连续发50次或100次,统计成功率。如果成功率不是100%,哪怕只是98%,都说明这条API链路本身存在间歇性故障,不是偶然噪声。这时候再决定要不要改超时或重试逻辑。
有一次我排查一个Python调用讯飞星火API的问题,就是因为间歇性断连,我在代码里加了复杂的重试和缓存机制,结果不仅没解决问题,还让请求变得更慢。后来冷静下来,把重复调用改成session,然后抓包确认是对端需要携带app_id参数放在请求头里,少传一个就会概率性断开。这毛病改完后,稳定性直线上升。所以遇到“断连”先别着急加防御代码,先通过“最小脚本+抓包+横向对比”三步法确定责任域。
排查API断连没有玄学,就是缺什么补什么,一层一层剥开。希望我这个折腾两天才搞定的案例,能让你下次花20分钟就定位问题。想更快,可以直接把本文里提到的命令和脚本复制过去用,按顺序跑一遍,大概率能少走很多弯路。