ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Ping、Telnet、Curl分层排障:网络层到应用层的接口超时定位

Ping、Telnet、Curl分层排障:网络层到应用层的接口超时定位

凌晨两点被电话叫醒,说某个接口一直超时,值班的人第一反应是“网络出问题了吧”,然后ping一下是通的,就卡在那里不知道下一步了。这种场面我见过太多次。Ping、Telnet、Curl这三个东西几乎是每个做后端、运维、测试的人第一天就会听说的命令,但真正能把它们分层用明白的人并不多。绝大多数人止步于“ping 得通就是好的”,结果在一堆“看起来通、实际不通”的问题上反复打转。

这篇东西想讲清楚一件事:这三个工具根本不是同一层的东西,它们各自能看到的信息、能证伪的假设、以及会骗你的地方,差别非常大。Ping 回答的是“这台机器还活着吗”,Telnet 回答的是“这个端口有人守着吗”,Curl 回答的是“这个服务在应用层能不能把活干完”。三者是从下往上、逐层剥离的三个观测点。我会把每个命令的关键参数、输出怎么读、什么时候它的结论不可信、以及踩过的坑都摊开讲,适合刚入行的同学照着抄作业,也适合干了几年但排查全靠经验直觉的老手重新梳理一遍。内容全部是我在真实排查场景里反复验证过的做法,命令都是在 Linux 和 macOS 上实测可跑的,Windows 下的差异我会单独标注。

1. 先把三层观测点分清楚,再谈命令怎么敲

很多排查效率低,根本原因是脑子里没有分层模型,抓到一个命令就敲,敲完看结果,结果符合预期就觉得没问题,不符合预期就开始乱猜。Ping、Telnet、Curl 的价值不在于它们各自能不能用,而在于它们串起来构成了一条从网络层到应用层的验证链路,每一层的结果都能帮你砍掉一半的怀疑范围。

1.1 三个命令分别站在哪一层

先把定位摆清楚,后面的所有讨论都建立在这个基础上。

Ping工作在 IP 层之上,用的是 ICMP 协议。它发给目标一个 Echo Request(类型 8),对方回一个 Echo Reply(类型 0)。整个过程不涉及任何端口,不需要目标上跑着什么服务,只要目标主机的协议栈还在、路由可达、中间设备没有拦截 ICMP,就能收到回应。这意味着 Ping 通只能证明“这台机器在网络上是活的”,它对“你关心的那个服务到底活着没有”一无所知。

Telnet在这里被当成端口探测器用,这是它的一个“副业”。真正的 telnet 协议是明文的远程终端协议,现在几乎没人拿它做正经登录了,但它的客户端有一个很好用的行为:telnet 目标IP 端口会尝试和目标端口建立 TCP 连接。连上了就打印 Connected,连不上就报错或者干等。所以它测的是TCP 三次握手能不能完成,比 Ping 高了一层,能验证“端口上有没有进程在监听”。

Curl是应用层的客户端,它会把完整的 HTTP/HTTPS 请求发出去,拿到响应。它测的是“这个服务能不能正确处理一个真实请求”,包含了 TCP 连接、TLS 握手(如果是 HTTPS)、请求发送、服务端处理、响应返回的全过程。它能看到的东西最多,但也最容易把底层问题伪装成上层问题——比如 DNS 解析慢,表现出来就是“接口很慢”,你不看分段时间根本发现不了。

我用一个生活化的类比来串一下:Ping 相当于你在楼下喊一嗓子“有人吗”,有回声就说明楼里有人;Telnet 相当于你走到某扇具体的门前敲门,有人应门说明这个房间有人;Curl 相当于你把一份文件从门缝塞进去,等对方处理完把结果递出来。三个动作验证的东西完全不同,缺一环都可能漏判。

1.2 什么场景该按什么顺序敲

顺序不是随便定的,核心原则是从便宜到贵、从粗到细。Ping 最便宜,一个包几十字节,几乎不消耗什么资源;Telnet 要建 TCP 连接,稍贵一点;Curl 要发完整请求,可能触发业务逻辑、写日志、查数据库,最贵。所以正常的排查顺序永远是 Ping → Telnet → Curl,除非你已经明确知道问题在应用层。

但顺序不是教条,实际排查里更常见的是根据现象倒推该从哪一层切入。比如:

现象描述建议起手命令理由
完全连不上,超时无响应Ping先确认主机是否可达,排除路由问题
报“Connection refused”Telnet已经有明确错误码,说明网络通、端口没监听
能连上但接口返回 5xxCurl网络层大概率没问题,问题在应用或上游依赖
请求很慢但最终成功Curl 加分段计时需要区分是 DNS、建连、还是服务端处理慢
偶发失败,重试就好Ping 长时间跑 + Curl 循环疑似丢包或负载不均

这个表我建议直接贴在排查手册里,比写一大段文字描述管用得多。它解决的是“遇到问题第一反应敲什么”这个最实际的困惑。

注意:这张表的前提是你在排查一台你有权探测的目标。对不属于自己管辖的地址做频繁扫描或大包探测,在多数环境里是不被允许的,实际工作中务必走内部流程确认。

1.3 一个必须先建立的认知:命令的“成功”不等于服务的“健康”

这是我最想强调的一点。Ping 通、端口通、HTTP 200,这三件事任何一件成立,都不能推导出“服务是健康的”。我遇到过太多次这样的情况:curl返回 200,但响应体里是一个业务错误码;或者 TCP 连接秒建,但服务端在拿到请求后卡死几十秒;或者 Ping 每秒都通,但实际业务流量丢包率高达 5%,因为 ICMP 和 TCP 走的队列优先级不一样。

所以要建立一个习惯:每次敲完命令,先问自己“这个结果能证伪什么、不能证伪什么”。Ping 不通,不能直接断定机器挂了,也可能是 ICMP 被策略拦了;Ping 通,不能断定端口开着;Telnet 连上,不能断定应用正常;Curl 200,不能断定业务逻辑对。这个思维习惯建立起来之后,排查效率会有质的变化,因为你不再会拿一个层次的结果去推断另一个层次的结论。

后面的章节我会分别把这三个命令拆开,重点讲“输出里的每一个字段在说什么”和“什么情况下它会骗你”。这两件事,恰恰是绝大多数教程不讲的。

2. Ping:最容易被误读的一个命令

Ping 是所有人学会的第一个网络命令,也是被误用最多的。它的输出看起来简单,实际上每一行都藏着信息,尤其是 TTL、mdev 和丢包模式这三处,读懂的人不多。

2.1 ICMP 报文里到底装了什么

先看一个典型输出,逐字拆:

$ ping -c 4 10.0.0.12 PING 10.0.0.12 (10.0.0.12) 56(84) bytes of data. 64 bytes from 10.0.0.12: icmp_seq=1 ttl=63 time=0.412 ms 64 bytes from 10.0.0.12: icmp_seq=2 ttl=63 time=0.398 ms 64 bytes from 10.0.0.12: icmp_seq=3 ttl=63 time=0.421 ms 64 bytes from 10.0.0.12: icmp_seq=4 ttl=63 time=0.405 ms --- 10.0.0.12 ping statistics --- 4 packets transmitted, 4 received, 0% packet loss, time 3004ms rtt min/avg/max/mdev = 0.398/0.409/0.421/0.009 ms

第一行56(84) bytes of data,这里有两个数字经常被忽略。56 是ICMP 载荷的字节数,也就是你实际发过去的数据长度,Linux 默认就是这个值;84 是加上 8 字节 ICMP 头和 20 字节 IP 头之后,在链路上实际传输的总字节数。这就是为什么下面每行显示的是64 bytes——64 等于 56 的载荷加上 8 字节 ICMP 头。搞清这个换算关系,后面做 MTU 探测的时候才知道该填多少。

icmp_seq是序号,从 1 开始递增。这个字段最大的用途是看连续性:如果输出里出现 seq 1、2、4、5,中间缺了 3,说明第 3 个包丢了或者回程丢了,这个信息比最后那个“丢包率 20%”要细得多。丢包是连续丢还是随机丢,指向的原因完全不同:连续丢往往是链路瞬断或者队列打满,随机丢更多是链路误码或者限速策略。

ttl=63是我认为最被低估的字段。TTL 是 IP 头里的生存时间,每经过一个路由设备就减 1,减到 0 就丢弃并回一个 ICMP 超时报文。所以 TTL 可以用来粗算跳数:不同系统的初始 TTL 不一样,Linux 常见 64,部分网络设备 255,历史上某些系统用 128。上面这个例子里收到 63,如果目标是 Linux,那跳数大约是 64 - 63 = 1,说明中间只隔了一台设备。这个技巧在判断“请求到底走到了哪一跳”时非常有用,尤其是在多机房、多层网关的环境里,你能立刻知道流量有没有跑偏。

time=0.412 ms是往返时延(RTT),注意是往返不是单向。同一机房内网这个值通常在 1ms 以内,同城跨机房 1~5ms,跨区域公网 20~50ms,跨洲际 150ms 以上。这些数字不是拍脑袋的,光在光纤里的传播速度大约是每毫秒 200 公里,所以物理距离基本决定了 RTT 的下限,任何明显低于这个物理极限的值都值得怀疑。

最后一行mdev = 0.009 ms是 Linux 特有字段,表示时延的平均偏差,你可以粗略理解成抖动。mdev 小说明网络稳定,mdev 大说明时延忽高忽低。上面这个 0.009ms 非常健康;如果看到 avg 是 5ms 但 mdev 是 4ms,那说明有些包 1ms 就回来了、有些要 9ms,这种抖动对实时性要求高的业务(比如音视频、游戏同步)是致命的,但对普通 HTTP 接口影响有限。

2.2 常用参数和跨平台差异

Linux 和 Windows 下的 ping 参数命名完全不同,这是新手最容易懵的地方。我把最常用的几个对照列出来:

用途Linux / macOSWindows
指定发包数量-c 10-n 10
指定载荷大小(字节)-s 1400-l 1400
指定发送间隔(秒)-i 0.5不支持,固定 1 秒
单包等待回复超时-W 2(秒)-w 2000(毫秒)
整个命令的总超时-w 10(秒)无直接对应
不允许分片-M do-f
只发不显示-q无
指定源地址-I eth0或-I 10.0.0.5-S 10.0.0.5

这里面-M do和-f值得单独说。它们的作用是禁止 IP 分片,配合不同的载荷大小,就能用来探测路径 MTU——也就是从你到目标这条路上,单个包最大能过多少字节。做法是从一个较大的值开始试,逐步往下降,直到不再出现“Packet too big”或者“需要分片但设置了 DF 标志”的错误。

具体怎么算:以太网的标准 MTU 是 1500 字节,这个 1500 包含了 20 字节 IP 头和 20 字节 TCP 头(如果是 TCP 流量),所以 TCP 的 MSS 最大是 1460。但如果你用 ping 测,ICMP 头只有 8 字节,那么 1500 - 20 - 8 = 1472,所以ping -M do -s 1472应该刚好能通,改成 1473 就会报错。这个 1472 是很多人记不住但很好用的一个常数。

# 边界探测,1472 通、1473 不通,说明路径 MTU 就是 1500 ping -M do -s 1472 -c 2 10.0.0.12 ping -M do -s 1473 -c 2 10.0.0.12

如果发现 1472 不通但 1400 通,说明链路上有设备把 MTU 调小了,常见于各种隧道封装场景。这类问题的典型症状是“小请求正常、大请求卡死”,因为 TCP 层虽然会做 MSS 协商,但协商只看两端,中间设备改小 MTU 后就可能悄悄丢大包,表现出来就是页面加载到一半卡住、接口上传大文件失败。这种问题用 telnet 和 curl 都很难定位,只有 ping 能直接测出来。

2.3 什么情况下 Ping 的结论完全不可信

这是关键部分。Ping 的结论在以下几种情况下会严重误导你,而这几种情况在生产环境里都不罕见。

第一种,ICMP 被策略拦截。很多环境出于各种考虑会限制 ICMP 流量,这时候 ping 100% 丢包,但目标服务完全正常。我吃过这个亏,看到 ping 不通就直接往网络故障方向查,折腾半天才发现只是探测方式被限制了,换成 telnet 端口立马就通。所以判断原则是:ping 不通时,一定要用端口探测交叉验证,不要单凭 ping 的结果下结论。

第二种,ICMP 被限速。有些设备对 ICMP 做速率限制,比如每秒只允许回 10 个。你ping -c 100或者ping -f打过去,就会看到大量丢包,看起来像网络很差,实际上业务流量毫无影响。判断方法是把发送频率降下来,ping -i 1 -c 20慢速跑,如果慢速不丢、快速丢,基本就是限速。

第三种,ICMP 和 TCP 走不同的队列或路径。在复杂的网络环境里,ICMP 可能走低优先级队列,或者被策略路由到另一条链路。这时候 ping 的时延和丢包率根本不能代表业务流量的真实情况。我见过 ping 平均 0.3ms 非常漂亮、但 TCP 连接经常超时的场景,原因就是两者走的路径不一样。

第四种,也是最有迷惑性的,Ping 通但端口全关。这其实是正常的,主机活着不代表服务跑着。但在排查时,很多人看到 ping 通就默认“网络没问题”,然后把注意力全部转向代码,结果绕了一大圈才发现是服务没启动。

所以我个人的习惯是:除非有明确理由,否则永远不要只看 ping 的结果。它只适合做最粗的第一层筛选,用来快速排除“目标主机完全失联”这种极端情况。一旦 ping 通过,立刻进入下一层,别在它身上浪费时间。

3. Telnet:真正在测的是 TCP 握手,不是 telnet

先说个容易让人误解的点:telnet 10.0.0.12 8080这条命令,在排查场景里和 telnet 协议本身一点关系都没有。我们只是借用它的客户端去发起一次 TCP 连接,验证目标端口上有没有进程在监听。真正干活的是操作系统的 TCP 协议栈,telnet 只是那个“敲门的人”。

3.1 三种返回结果对应的三种真相

这个命令的输出非常短,但三种典型结果的区分度极高,这是它最大的价值。

结果一:连上了。

$ telnet 10.0.0.12 8080 Trying 10.0.0.12... Connected to 10.0.0.12. Escape character is '^]'.

看到Connected to就说明 TCP 三次握手完成了。这证明了三件事:网络可达、目标主机在、目标端口有进程在监听。注意这里只是握手成功,不代表这个端口上的服务能正常处理请求。服务可能刚刚启动还没初始化完,可能线程池满了不再 accept,可能握手进入 backlog 队列但应用层根本没读——这些情况 telnet 都会显示 Connected,但实际业务是坏的。所以别把“端口通”等同于“服务好”。

结果二:被拒绝。

$ telnet 10.0.0.12 9999 Trying 10.0.0.12... telnet: connect to address 10.0.0.12: Connection refused

Connection refused是一个非常明确的信号,它意味着目标主机收到了你的 SYN 包,但目标端口上没有监听,于是协议栈回了一个 RST。这个结果说明网络完全通畅、主机完全正常,纯粹是服务没起来或者监听在了别的端口上。看到这个报错,排查方向立刻就该转向“进程在不在”“监听地址对不对”,不用再纠结网络。

顺带说一个特别常见的坑:服务监听了127.0.0.1:8080而不是0.0.0.0:8080。这时候你在本机 curl 完全正常,但从别的机器 telnet 就是 refused。解决方法是ss -lntp | grep 8080看监听地址,如果是127.0.0.1:8080就只能本机访问,改成0.0.0.0或者具体网卡地址才行。

结果三:干等着,什么都不返回。

$ telnet 10.0.0.12 8080 Trying 10.0.0.12...

一直卡在这里,几十秒后才超时退出。这种情况最麻烦,因为可能性最多:可能是防火墙把包 DROP 了(不回 RST,让你干等),可能是目标主机不可达,可能是回程路由有问题,也可能是中间设备的会话表满了。

和Connection refused对比着看就能明白差别:refused 是“对方明确告诉你没有”,超时是“对方根本不理你”。前者是应用层问题,后者是网络或策略问题。这个区分是排查里最有价值的一步,能直接决定你接下来往哪个方向走。

提示:telnet 卡住之后按 Ctrl+C 中止不了的,它捕获了信号。正确退出方式是先按Ctrl+]进入telnet>提示符,再输入quit回车。这个操作很多人第一次遇到会懵。

3.2 脚本化场景下 telnet 不好用,得换工具

telnet 是给人看的,不是给脚本用的。它的退出码不可靠,输出要靠字符串匹配,超时时间也不好控制。如果你要在脚本里做端口探测,我推荐下面三种替代方案,按可用性排序。

方案一:nc(netcat)

$ nc -zv -w 3 10.0.0.12 8080 Connection to 10.0.0.12 8080 port [tcp/*] succeeded!

-z表示只扫描不发送数据,-v输出详细信息,-w 3表示 3 秒超时。退出码 0 表示通、非 0 表示不通,脚本里可以直接判断。这是我最常用的方式,干净利落。注意不同发行版的 nc 实现(GNU netcat、OpenBSD netcat)参数略有差异,-w在部分版本上含义是“连接后多久无数据就断开”,用之前先nc -h确认一下。

方案二:bash 内置的 /dev/tcp

$ timeout 3 bash -c 'cat < /dev/null > /dev/tcp/10.0.0.12/8080' && echo "端口通" || echo "端口不通"

这个方案的最大好处是不需要安装任何东西,只要机器上有 bash 就能用。原理是 bash 把/dev/tcp/host/port当成一个特殊文件,重定向操作会触发真实的 TCP 连接。配合timeout防止卡死。缺点是不支持所有 shell,dash 和 sh 里没有这个特性,写脚本时 shebang 要明确写#!/bin/bash。

方案三:curl 的 telnet 模式

$ curl -s --connect-timeout 3 telnet://10.0.0.12:8080

这个用法比较冷门,但好处是只需要 curl 一个工具。--connect-timeout控制建连超时。退出码 0 表示连接成功,7 表示连接失败,28 表示超时,可以细分不同的失败原因。

至于 Windows 环境,如果是较新的版本(PowerShell 4.0 以上),用Test-NetConnection最方便:

Test-NetConnection -ComputerName 10.0.0.12 -Port 8080 -InformationLevel Detailed

输出里TcpTestSucceeded直接给 True 或 False,比 telnet 清晰得多,还顺带把 ping 的结果一并显示了。

3.3 端口通了但业务不通,Telnet 就到顶了

必须清楚 Telnet 的能力边界。它能验证的只有 TCP 握手这一个动作,握手成功之后的一切它都看不见。实际排查里经常出现“telnet 秒连、curl 卡死”的情况,这恰恰说明问题出在应用层,而不是网络层。

我遇到过一次很典型的情况:某个服务的端口 telnet 通,但 curl 请求一直挂起到超时。最后定位是连接数打满了 backlog 队列——内核会先完成三次握手把连接放进 accept 队列,但应用层根本没调 accept 去取,所以从客户端看握手是成功的,可请求永远得不到处理。这种问题用 telnet 看一万遍都是通的,只有 curl 加上超时和耗时统计才能暴露出来。

所以定位原则是:telnet 用来快速区分“网络层问题”还是“应用层问题”。它通了,就把网络因素基本排除,专心看应用;它不通,就别在应用层浪费时间,先解决网络。这个判断只需要几秒钟,性价比极高。

4. Curl:把整条链路的时间花在哪拆开看

如果说 ping 和 telnet 只能给出“通/不通”的二元结论,那 curl 提供的是连续的、可量化的观测数据。它能告诉你一个请求从发起 DNS 查询到收到最后一个字节,每个阶段各花了多久,这是定位“慢”类问题的核心武器。

4.1 从 -v 的输出读懂一次请求的完整生命线

先用-v看一个请求的详细过程:

$ curl -v --connect-timeout 5 -m 10 -o /dev/null http://10.0.0.12:8080/health * Trying 10.0.0.12:8080... * Connected to 10.0.0.12 (10.0.0.12) port 8080 > GET /health HTTP/1.1 > Host: 10.0.0.12:8080 > User-Agent: curl/8.4.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/json < Content-Length: 27 < { [27 bytes data] * Connection #0 to host 10.0.0.12 left intact

以*开头的是 curl 自己的信息,以>开头的是发出去的请求头,以<开头的是收到的响应头。这个分色规则记清楚,一眼就能分辨方向。

* Trying表示正在建立 TCP 连接,如果这一步卡住,问题就在网络或端口,和 telnet 的结论一致。* Connected表示握手完成。中间如果走 HTTPS,还会有几行 TLS 握手的信息,包括证书验证、加密套件协商。> GET之后是请求发出的时刻,< HTTP/1.1 200是收到响应头的时刻。

这里有几个参数是我几乎每次都会带上:

  • --connect-timeout 5只管建连阶段,5 秒还没握上手就放弃。这个必须设,否则遇到 DROP 的情况会默认等几十秒甚至更久。
  • -m 10是整个请求的总超时,包含 DNS、建连、等待响应、下载 body 全部过程。注意它和--connect-timeout是两个独立的上限,实际生效的是先到的那个。
  • -o /dev/null把响应体丢掉,只关心状态和耗时,不往终端刷一堆内容。
  • -s静默模式,不显示进度条。但一定要配-S(大写),否则连错误信息也被吞了,排查时会以为命令没跑。

还有两个排查专用参数值得记:--resolve可以绕过 DNS,直接把某个域名指向指定 IP,用来区分“DNS 解析错了”和“服务本身有问题”。用法是--resolve host:port:ip,比如--resolve api.internal:80:10.0.0.12,这样请求还是发到api.internal,但实际连的是 10.0.0.12,Host 头也不变。另一个是-4/-6,强制只用 IPv4 或 IPv6,用来排查双栈环境下的协议选择问题——有些环境 IPv6 配了但路由不通,客户端优先走 IPv6 就会卡住,强制-4立马正常,这个现象非常隐蔽。

4.2 用 -w 做分阶段计时,这才是 curl 的杀手锏

-w(write-out)是 curl 最被低估的功能。它可以在请求结束后,按你指定的格式输出一堆内部统计变量。这才是定位性能问题的正解。

先看完整的可用变量里最关键的几个:

变量含义累计还是独立
time_namelookupDNS 解析完成时刻从请求开始累计
time_connectTCP 连接建立完成时刻从请求开始累计
time_appconnectTLS/SSL 握手完成时刻从请求开始累计
time_pretransfer即将开始传输的时刻从请求开始累计
time_starttransfer收到第一个字节的时刻从请求开始累计
time_total整个请求结束的总耗时从请求开始累计
http_codeHTTP 状态码单一值
size_download下载的字节数单一值
speed_download平均下载速度(字节/秒)单一值

这里有个极其重要的坑:这些时间变量全部是从请求开始算起的累计值,不是各阶段独立的耗时。很多人直接把这些数字加起来,得出“总耗时 3 秒”的荒谬结论。正确做法是做减法。

我常用的一个格式串是这样的:

curl -o /dev/null -s -w " DNS解析: %{time_namelookup}s TCP建连: %{time_connect}s TLS握手完成: %{time_appconnect}s 首字节到达: %{time_starttransfer}s 请求总耗时: %{time_total}s HTTP状态码: %{http_code} 下载字节数: %{size_download} " http://10.0.0.12:8080/health

要得到各阶段的独立耗时,做减法:

  • DNS 耗时=time_namelookup
  • TCP 建连耗时=time_connect - time_namelookup
  • TLS 握手耗时=time_appconnect - time_connect(纯 HTTP 请求这项为 0)
  • 请求发送 + 服务端处理 + 首字节返回=time_starttransfer - time_appconnect(HTTP 请求则是time_starttransfer - time_connect)
  • 内容传输耗时=time_total - time_starttransfer

举个实际算例。假设某次请求的输出是:

DNS解析: 0.012000s TCP建连: 0.057000s TLS握手完成: 0.192000s 首字节到达: 0.835000s 请求总耗时: 0.841000s

那么拆出来是:DNS 12ms,TCP 建连 57 - 12 = 45ms,TLS 握手 192 - 57 = 135ms,服务端处理加首字节 835 - 192 = 643ms,内容传输 841 - 835 = 6ms。

这份拆解的价值立刻就体现出来了:大头在服务端处理,占了 643ms,占比 76%。DNS、建连、TLS 加起来才 192ms。这时候你的排查方向非常明确——去看服务端的处理逻辑、看数据库慢查询、看下游依赖调用,而不是纠结网络。

反过来,如果输出是这样的:

DNS解析: 0.480000s TCP建连: 0.495000s TLS握手完成: 0.510000s 首字节到达: 0.528000s 请求总耗时: 0.530000s

拆解一下:DNS 480ms(占了 90%),建连只有 15ms,TLS 15ms,服务端处理 18ms。这时候问题一目了然——DNS 解析慢,可能是 DNS 服务器响应慢、可能是配置了多个 DNS 服务器而第一个超时、也可能是用了本地缓存没命中。跟服务端代码毫无关系,改代码改到天亮也没用。

我给这些阶段定了几个参考阈值,用来快速判断哪些数字不正常(内网环境的标准,公网要放宽):

阶段健康范围(内网)异常信号
DNS 解析< 5ms(有缓存)或 < 50ms超过 100ms 说明 DNS 有问题
TCP 建连< 1ms(同机)或 < 5ms(同机房)超过 20ms 检查网络路径
TLS 握手< 30ms(内网)超过 100ms 检查证书链和会话复用
首字节< 100ms超过 500ms 说明服务端有瓶颈
内容传输取决于体积,看 speed_download速度明显低于带宽预期

这些阈值不是绝对的,要靠平时在正常环境下多跑几次建立基线。有了基线,异常就无处遁形,这比任何经验直觉都靠谱。

4.3 几个高频实战用法

除了计时,curl 还有几个用法在排查里特别顺手。

只看响应头,不下载内容。curl -I http://10.0.0.12:8080/health发的是 HEAD 请求,只拿头信息。速度快,适合批量检查服务健康状态。但要注意有些服务端不实现 HEAD,会返回 405,这时候就得用curl -s -o /dev/null -D -,效果一样但用的是 GET。

跟随重定向看最终落到哪。curl -sIL http://10.0.0.12:8080/api会把每一跳的重定向都打印出来,最后给出最终状态码。排查“接口莫名返回 301”这类问题时非常有用,能清楚看到跳转链路。

只看状态码,适合写循环。curl -s -o /dev/null -w "%{http_code}" http://10.0.0.12:8080/health只输出三个数字,脚本里直接判断。配合for循环可以做一个简易的持续探测:

for i in $(seq 1 20); do code=$(curl -s -o /dev/null -m 3 -w "%{http_code}" http://10.0.0.12:8080/health) echo "$(date +%H:%M:%S) -> $code" sleep 3 done

这段跑一分钟就能看出是持续失败还是间歇性失败。间歇性失败和持续失败的原因完全不同,前者更可能是负载不均、连接池耗尽或者某个实例异常。

POST 请求和超时验证。排查写接口时,curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' -m 5 http://10.0.0.12:8080/api是标配。我建议所有排查用的 POST 请求都带上-m,避免在某些异常情况下无限挂起,把排查窗口拖得很长。

注意:-m的值不要设得太短。我见过有人图快设成-m 1,结果把一个本来要 1.2 秒才能返回的正常慢接口误判成故障。设置超时前,先确认这个接口的合理响应时间上限是多少。

5. 常见问题速查与踩坑实录

前面讲的都是“正常情况下怎么看”,这一节讲的是“不正常的时候怎么想”。我把这些年遇到过的典型现象整理成了一张表,配合几个印象最深的踩坑经历,希望能帮你少走点弯路。

5.1 现象速查表

现象最可能的原因下一步动作
Ping 不通,Telnet 通ICMP 被策略限制忽略 ping 结果,用端口探测为准
Ping 通,Telnet 超时端口未监听 / 防火墙 DROP在目标机执行ss -lntp确认监听状态
Telnet 报 Connection refused端口无监听,主机正常检查服务进程和监听地址
Telnet 通,Curl 卡住不返回应用层无响应 / backlog 打满用 curl 分段计时,看卡在哪个阶段
Curl 返回 000连接失败或超时,未收到 HTTP 响应加-v重跑,看具体失败在哪一步
Curl 返回 502/504网关到后端的链路有问题查网关日志和后端实例健康状态
请求快但下载慢带宽限制或内容体积大看speed_download和size_download
小请求正常,大请求失败路径 MTU 不匹配用ping -M do逐步探测 MTU 边界
偶发超时,重试就好丢包 / 某实例异常 / 连接池不足循环探测定位规律,检查负载均衡配置
首次请求慢,后续快DNS 缓存未命中 / 连接未复用对比多次请求的time_namelookup

这张表我建议保存在手机备忘录里,半夜被叫醒的时候脑子是不清醒的,有个对照表能省掉大量试错时间。

5.2 几个印象深刻的踩坑经历

坑一:把 ping 的结果当成了最终结论。

有一次排查线上接口超时,第一步 ping 目标机,丢包率 30%,我立刻断定是网络抖动,然后花了两个小时去查链路监控、找网络团队协调,最后发现 ICMP 在那个网段被限速了,实际业务流量一切正常,真正的问题是某个下游依赖的连接池打满了。这次之后我给自己定了条规矩:ping 的结果永远只当参考,不当证据。至少要用 telnet 或 curl 交叉验证一次,才能形成判断。

坑二:Connection refused 被误判成网络不通。

新人经常把Connection refused和网络故障混为一谈,实际上这两个结论南辕北辙。refused 恰恰说明网络好得很,包能过去也能回来(不然你怎么收到 RST),问题 100% 在服务端。我现在带人的时候,第一课就是让他们记住这个区别:超时是网络问题,refused 是进程问题。这一句话能省掉很多无效排查。

坑三:curl 的累计时间被当成独立耗时。

我一开始用-w的时候,看到几个时间加起来跟总耗时对不上,还以为工具出问题了。后来才搞明白所有时间变量都是从请求起点的累计值。这个坑其实很隐蔽,因为如果你只关心“哪个阶段最慢”,看单个变量的绝对值也能看出一点端倪,很容易蒙混过关,但只要涉及到精确的阶段拆分,不做减法就会得出完全错误的结论。我在团队内部分享的时候专门强调过这一点,因为算错方向可能导致你去优化一个根本不是瓶颈的环节。

坑四:监听地址是 127.0.0.1,本机怎么测都通。

这个坑阴险在于它“看起来一切正常”。服务在本机 curl 完全没问题,日志也干干净净,但你从任何其他机器都连不上。我第一次遇到的时候排查了很久,最后ss -lntp一看就笑了——监听在127.0.0.1:8080,只有本机回环能访问。现在这成了我排查连通性问题的标准第二步:第一步 telnet 确认端口状态,第二步就是上目标机看监听地址。顺带说,IPv6 环境下还有个类似的问题:服务监听了::1而不是::,同样只有本机能访问。

坑五:加了 -m 但没加 --connect-timeout,仍然会长时间挂起。

这两个参数我之前也以为是包含关系,实测下来不是。-m管总时长没错,但在某些 TCP 建连卡在 SYN 重传的场景里,curl 对总超时的响应并不总是及时。加上--connect-timeout会强制在建连阶段就切断,行为更确定。现在我写任何探测脚本都是两个一起加,宁可多打几个字。

5.3 一个我常用的组合脚本

最后分享一个我日常排查时最常用的组合,思路就是三层串起来跑一遍,把结果一次性收集齐:

#!/bin/bash HOST="10.0.0.12" PORT="8080" URL="http://${HOST}:${PORT}/health" echo "===== 第 1 层:ICMP 探测 =====" ping -c 4 -W 2 "${HOST}" 2>&1 | tail -3 echo "" echo "===== 第 2 层:TCP 端口探测 =====" if timeout 3 bash -c "cat < /dev/null > /dev/tcp/${HOST}/${PORT}" 2>/dev/null; then echo "端口 ${PORT} 可连接" else echo "端口 ${PORT} 不可连接(超时或被拒绝)" fi echo "" echo "===== 第 3 层:应用层探测 =====" curl -s -o /dev/null --connect-timeout 3 -m 10 -w " DNS解析: %{time_namelookup}s TCP建连: %{time_connect}s 首字节到达: %{time_starttransfer}s 总耗时: %{time_total}s 状态码: %{http_code} " "${URL}"

这个脚本的价值在于一次执行就能拿到三层的信息,不用来回切命令。如果第 1 层就挂,直接找网络;第 2 层挂,找服务进程;第 3 层的时间拆分异常,就往应用方向查。我把它做成了一个 shell 函数放在自己的.bashrc里,参数化 host 和 port,半夜排查的时候一行命令就能出结论,比现敲快得多。

有个小细节值得说:脚本里用tail -3只保留 ping 的统计行。因为排查时你真正关心的往往是有没有丢包、平均时延多少,前面那一堆64 bytes from反而干扰视线。如果发现有丢包,再单独跑一次完整输出看丢包的分布规律。

还有一点,第 2 层我用的是 bash 的/dev/tcp而不是 nc,理由是兼容性最好,绝大多数 Linux 发行版默认都有 bash,不需要额外装包。如果你确定环境里有 nc,换成nc -zv -w 3也行,输出更友好一些。

这套东西我用了好几年,从单机服务到容器集群都能套用,唯一需要调整的就是端口和路径。真正把这三个命令的边界想清楚之后,你会发现排查这件事其实是有确定路径的,大部分时间浪费在“不知道该看哪一层”上,而不是“看了但看不懂”。先把分层的直觉建立起来,剩下的都是熟练度问题。

返回列表