
2. 用curl-W参数拆解响应时间的每个环节接手接口性能排查的时候我最常用的工具不是那些重型压测平台反而是curl。原因很简单curl几乎无处不在Linux服务器自带macOS自带Windows 10以上也自带不需要额外装JDK、不需要启动Agent、不需要申请压测资源。你要做的只是写一个循环把curl扔进去跑就能得到一条时间曲线。这篇文章我就把我实际用的方法、脚本和踩过的坑完整写出来从最简单的单次测量讲到并发探测再到日志统计和问题定位。先说清楚这个方案能解决什么问题。它适合这几类场景接口偶发变慢但你抓不到规律、上线后需要观察一段时间确认服务质量、压测工具太重懒得搭、或者你只想在命令行里快速确认某个接口的耗时是否正常。它不适合严格意义上的高并发压测因为curl本身是单连接工具后面我会讲怎么用xargs做轻量并发但它的定位始终是轻量探测不是替代专业压测工具。1. curl测接口响应时间的基本姿势别再只会用time命令了很多人测接口耗时第一反应是time curl http://example.com/api这个做法能看个大概但问题在于time输出的real、user、sys是进程级别的数据你无法区分网络连接耗时、TLS握手耗时、服务端处理耗时和响应体传输耗时。而接口变慢的时候恰恰需要这些分段数据才能定位瓶颈到底在哪一段。curl自身提供了一套非常有用的统计字段通过-w参数输出。我在实际项目里最常用的字段组合是这些字段含义典型用途time_namelookupDNS解析耗时判断域名解析是否变慢time_connectTCP三次握手耗时判断网络连通性和RTTtime_appconnectTLS/SSL握手完成耗时判断HTTPS握手开销time_pretransferHTTP请求发出前总耗时粗略看建立连接全过程time_starttransfer从开始到收到首字节耗时服务端处理速度的关键指标time_total整个过程总耗时最常用的总览指标有几个细节值得注意。第一这些时间字段的值单位是秒curl会输出浮点数。第二如果你请求的是HTTP而不是HTTPStime_appconnect会是0因为根本没走TLS。第三time_starttransfer包含了连接建立的时间所以它比time_total小不了太多真正要看服务端处理能力需要把time_starttransfer减去time_pretransfer差值才是服务端从收到请求到开始返回数据的处理时间。实际使用的命令长这样curl -s -o /dev/null -w \ dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n \ https://api.example.com/health-s是静默模式-o /dev/null把响应体丢掉因为我们只关心时间不关心内容。输出类似这样dns:0.021 connect:0.052 tls:0.089 ttfb:0.342 total:0.358从这一行数据就能看出服务端处理时间约等于0.342减0.089大概0.25秒。如果ttfb和total差值很大说明响应体传输很慢可能是带宽问题或者响应体过大如果ttfb本身很大而connect很小那问题大概率在服务端逻辑或者网关层。一个小坑-w后面的格式化字符串\n在双引号里会被bash转义成换行符但在某些脚本场景下你想把多字段拼在一行就把\n改成空格或者干脆不加。这个后面写循环脚本时还会细说。2. 持续测试的核心脚本循环、间隔与日志记录单次测量只能看到某一瞬间的快照。接口变慢往往是间歇性的可能每半小时出现一次超时也可能每天凌晨来一波尖峰。这时候就需要把curl放进循环里持续跑并记录带时间戳的日志。我先给出一个我实际在用的基线脚本然后逐行拆解为什么这么写。#!/bin/bash URLhttps://api.example.com/v1/order/status?orderId123456 LOG_FILE/var/log/api_resp.log INTERVAL5 # 每次请求间隔秒数 THRESHOLD2.0 # 超过2秒标记为SLOW MAX_RUNS1000 # 最多跑1000次 run0 while [ $run -lt $MAX_RUNS ]; do run$((run 1)) TS$(date %Y-%m-%d %H:%M:%S) RESP$(curl -s -o /dev/null -w %{http_code} %{time_total} \ --connect-timeout 5 --max-time 30 \ -H Content-Type: application/json \ $URL) HTTP_CODE$(echo $RESP | awk {print $1}) TOTAL_TIME$(echo $RESP | awk {print $2}) FLAGOK if [ $HTTP_CODE ! 200 ]; then FLAGERROR elif (( $(echo $TOTAL_TIME $THRESHOLD | bc -l) )); then FLAGSLOW fi echo $TS code$HTTP_CODE time${TOTAL_TIME}s flag$FLAG $LOG_FILE sleep $INTERVAL done几个关键设计点为什么用while而不是for循环虽然for i in $(seq 1 1000)也能实现循环但while配合MAX_RUNS更灵活你可以随时改成死循环再用nohup放后台跑或者用CtrlC停掉后查看日志。while结构加一个run计数器也能方便实现每N次重启一次的逻辑。为什么要分开设置--connect-timeout和--max-time这是接口测试最容易踩的坑。--connect-timeout只限制建立连接的时间默认是无限等待。如果你的脚本不设置这个参数当服务端IP不可达时TCP SYN包会一直重试curl会卡住几分钟甚至更久整个循环就被拖死了。--max-time限制整个HTTP请求过程的总时间。我一般设5秒的连接超时和30秒的总超时这个组合能保证单次请求最坏情况下35秒内结束不会让监控脚本失去响应。bc是什么为什么用它比较浮点数bash自带的[ $TOTAL_TIME -gt 2 ]只支持整数比较而curl输出的时间都是浮点数。Linux下最简单可靠的浮点比较方案就是bc。如果你不想依赖bc也可以用awk BEGIN{exit !($TOTAL_TIME2.0)}替代但bc在大多数发行版都默认安装直接用就行。关于间隔的取舍。INTERVAL设成多少取决于你的场景。如果只是为了观察接口是否稳定5秒一次足够了如果想做短时间内的连续探测找出偶发超时可以设1秒如果只是挂在服务器上做长期运行监控建议30秒到60秒一次避免给服务端造成无谓压力。我见过有人把间隔设成0秒跑死循环结果自己的出口IP被WAF封了这个教训值得记住。3. 输出格式与数据落盘从日志裸奔到结构化统计上面脚本直接输出code200 time0.358s flagOK这种人类可读文本好处是拿到就能看懂坏处是不方便做统计分析。如果你想对大量数据进行P95、P99计算或者画响应时间趋势图这种格式会很难处理。我在实际项目中用的是两套输出方案。方案一日志文件用文本行适合快速人工排查。保持上面脚本的模式每行包含时间戳、状态码、耗时、标记。这种格式配合grep就能完成大多数排查。比如查最慢的请求grep flagSLOW /var/log/api_resp.log | tail -20查错误请求grep code503\|code500 /var/log/api_resp.log方案二结构化输出CSV适合写脚本统计。把输出改成这样RESULT$(curl -s -o /dev/null -w %{http_code},%{time_namelookup},%{time_connect},%{time_appconnect},%{time_starttransfer},%{time_total} $URL) echo $TS,$RESULT /var/log/api_resp.csv每行就是一个完整的时间戳加六个字段的CSV。配合awk做统计非常方便比如统计所有请求的耗时分布awk -F, {sum$8; count} END {print avg:, sum/count} /var/log/api_resp.csv如果要算P95需要先排序再取分位。我的做法是先把time_total列提取出来排序然后用awk算分位点tail -n 1 /var/log/api_resp.csv | awk -F, {print $8} | sort -n /tmp/times.txt total$(wc -l /tmp/times.txt) p95_line$(echo scale0; $total * 95 / 100 | bc) sed -n ${p95_line}p /tmp/times.txt这一套组合命令能让你对接口性能有一个相对宏观的认识平均耗时不重要P95和P99才是用户体验的真实反映。如果P95接近2秒而平均值只有0.5秒说明有少数请求非常慢拖累了整体体验这种情况在平均值上看不出来但分位点立刻暴露。关于文件轮转的一个忠告。长时间运行监控脚本日志文件会越来越大。我的习惯是配合logrotate做轮转或者直接在脚本里判断文件大小超过50MB就mv成带日期后缀的备份文件。别等到磁盘写满才发现那几天你跑的所有监控数据可能都已经丢了。4. 并发探测一个curl不够用xargs做轻量并发前面说的循环脚本是串行的一次只发一个请求。但很多接口的性能问题是在并发情况下才暴露的——单个请求一切正常并发一上来就超时、报错、响应时间飙升。想测试这种场景不需要上JMeterxargs配合curl就能实现轻量并发。先看基础用法seq 1 30 | xargs -P 10 -I {} \ curl -s -o /dev/null -w req:{} time:%{time_total} code:%{http_code}\n \ https://api.example.com/v1/ping-P 10表示同时跑10个进程seq 1 30生成30个任务编号-I {}把编号传给curl。这条命令的效果就是1秒内发出30个并发请求每个请求独立计时输出各自的耗时和状态码。这里有个容易出错的地方-I {}指定了替换符后花括号{}在命令里被xargs替换成任务编号。如果你用的是-I 那命令里就要写。很多人习惯用{}但偶尔会和一些工具的占位符冲突我用xargs时更推荐用或者{}本身没有歧义就行。进一步升级给并发探测加上持续时间控制。如果你想让并发持续30秒可以先算出总请求数或者用一个while循环配合xargsend$((SECONDS 30)) while [ $SECONDS -lt $end ]; do seq 1 10 | xargs -P 10 -I {} \ curl -s -o /dev/null -w %{http_code} %{time_total}\n \ https://api.example.com/v1/ping done这个方法有一个隐含问题不同curl进程之间是独立的没有共享连接池所以每个请求都要重新走一遍DNS解析和TCP建连。这既是缺点也是优点。缺点是它测出来的耗时比实际用户使用场景偏高——真实用户在客户端会有keepalive连接复用不需要每次都重新握手。优点是它模拟了最坏情况能测试服务端在大规模新建连接下的表现。明确这一点后你就能根据结果判断如果并发测试下time_total飙升但time_connect也不低那瓶颈可能在网络层或者入口网关的连接处理能力上如果time_connect正常而time_starttransfer很高那瓶颈在服务端业务逻辑。有一点必须提醒并发探测比串行探测更容易触发服务端的限流、熔断或者WAF拦截。我遇到过几次并发脚本跑着跑着服务端开始大量返回429然后我把并发数从10调到50直接触发了IP封禁。所以做并发测试之前先确认测试环境或者确认下游服务能承受这个并发量别把生产环境整挂了。5. 响应时间异常时的排查链路从一个超时案例说起持续监控的价值在于发现问题但真正让你头疼的是发现问题之后怎么定位。我以一个实际遇到的案例来讲完整的排查思路某个接口在凌晨2点左右持续出现超时curl输出curl: (56) Recv failure: Connection reset by peer伴随大量5xx错误。第一步确认超时发生的范围。把日志按时间切片看超时是从几点开始、几点结束的持续时间多久。如果是固定时间点出现的优先怀疑定时任务——比如凌晨的日志清理、数据备份、批处理任务抢占了CPU或数据库连接。当时我们排查后发现是凌晨的报表统计任务和接口服务共享同一个数据库实例大量聚合查询把数据库连接池占满了接口请求拿不到连接就只能等超时。第二步拆分耗时字段。用time_starttransfer减去time_pretransfer算出服务端处理时间。如果服务端处理时间正常说明是网络问题或者网关层问题如果服务端处理时间本身很大那就要进去看慢查询、锁等待、GC之类的JVM内部状态。第三步抓现场。curl的-v参数会打印完整的HTTP请求和响应头包括TLS握手细节。当出现连接重置时-v能看到是哪一个环节断掉的。SSL connection using TLSv1.3后面跟着Recv failure那基本是服务端主动断开了连接可能是超时配置太短也可能是服务端负载过高主动拒绝。这里补充一个排查连接问题的实用命令——curl -v telnet://host:port可以精确测试TCP端口连通性。虽然curl本身就能测但用telnet协议可以排除HTTP层干扰更干净地看TCP层是否正常。curl -v telnet://api.example.com:443如果卡住了多半是网络不通或者防火墙拦截。第四步验证修复效果。改完配置或者重启服务后不要急着下结论。让监控脚本继续跑一段时间比如至少覆盖一个业务低峰期和一个高峰期对比修复前后的耗时分布。如果P95从之前的2.5秒降到0.5秒并且不再有超时记录才算真正解决。还有一类坑值得单独说DNS缓存导致的假象。如果你在脚本里固定写域名访问而DNS解析出了问题比如某个DNS服务器性能很烂或者配合了本地hostscurl的time_namelookup会飙高。这种问题的特点是所有请求的time_namelookup都很大但time_connect正常。排查方式是先用dig或者nslookup单独测DNS解析耗时然后对比curl输出中的time_namelookup。如果只要手头多测几次curl -w就能发现的异常就不需要引入复杂的监控平台。6. 脚本健壮性细节超时、重试与错误处理持续测试脚本跑在服务器上需要考虑网络抖动、进程被杀、磁盘满等异常情况。我踩过的坑不少这里把能想到的健壮性细节过一遍。sleep的坑。sleep $INTERVAL这个命令本身有返回码如果脚本在执行sleep时被信号打断while循环会直接退出。想避免这个问题可以在循环里加一个判断if ! sleep $INTERVAL; then echo $(date) sleep interrupted, exiting $LOG_FILE break ficurl本身的退出码。curl很多非200状态码也是正常退出收尾逻辑看不出问题。如果你想区分服务端返回了500和根本没连通这两种情况需要检查curl的退出码。curl退出码7代表连接失败28代表超时35代表TLS握手失败60代表证书验证失败。建议在脚本里加上退出码记录if [ $? -ne 0 ]; then echo curl exit code: $? $LOG_FILE fi重试机制。既然目标是持续监控偶尔一两次网络抖动不应该直接算作接口故障。我给脚本增加一个重试逻辑连续两次失败才记为一次故障单次失败先重试一次。重试时要把间隔拉开比如第一次失败后等1秒重试第二次失败后等5秒重试避免重试风暴。这个逻辑看似简单但能有效减少误报。日志写入的原子性问题。多个curl测试脚本写同一个日志文件时要用追加并且确保每次写一行是完整的一行。当并发测试场景下多个进程同时向同一个文件追加内容有可能出现一行被截断的情况。解决办法是每个进程写自己的临时文件结束后再合并或者用flock加锁。我实际测试时发现Linux下echo file对单行小内容一般不会交错但两个进程同时写多行就会出问题。最保险的方案还是每个进程独立日志文件最后用cat合并。7. 实战中的告警推送不盯日志让脚本替你盯脚本跑起来之后你不大可能24小时盯着终端看输出。如果问题只出现在凌晨2点你睡醒之后再去翻日志就失去了实时处理的价值。所以我每次搭监控脚本都会顺手加一个告警推送方法有很多我列几个实际用过的最省事的方案邮件告警。服务器上配置好mailx脚本检测到SLOW或者ERROR时发一封标题带时间戳的邮件。比如if [ $FLAG ! OK ]; then echo $TS $URL $HTTP_CODE $TOTAL_TIME | mail -s API monitor alert at $TS youexample.com fi稍微现代一点的方案Webhook。很多协作工具都有Webhook机器人直接用curl调Webhook就能把消息推到手机端。比如企业微信、钉钉、Slack都有这个功能。一条消息就是一次POSTcurl -s -X POST https://webhook.example.com/hook/xxx \ -H Content-Type: application/json \ -d {msg_type:text,content:API slow: 2.3s 2.0s threshold}可自定义的进阶方案直接调监控系统API。如果你公司内部有Prometheus、Zabbix或者云厂商的监控服务可以在脚本里调用对应的Push API把数据点上报上去用现成的图表和告警规则来管理。这个方案的前期成本高但一旦配好后续维护轻松很多。告警推送还有一个细节要防止告警风暴。凌晨服务故障持续了10分钟如果你每5秒触发一次告警手机能被打爆。我通常的做法是加一个告警抑制逻辑同一类型的异常10分钟内最多推送一次。实现方式很简单记录上次推送时间如果当前时间减上次推送时间小于600秒就不再推送。8. 我给接口做持续响应时间测试的最终建议回到开头的场景如果你只是想快速验证一个接口今天的响应时间正常一行curl加-w参数就够了。如果你想长期观察某些核心接口建议不要临时抓几条命令跑而是把监控脚本固化下来配合crontab定期执行。比如每小时跑一次持续测试每次跑5分钟把结果追加到当天的日志文件。我做这类事情的习惯是把日志按天分文件文件名带上日期比如api_resp_20250601.log这样复盘时直接找到对应日期的文件就能看到全天的时间曲线。为了配合这个习惯我在脚本里加了按日期滚动日志文件的逻辑LOG_FILE/var/log/api_resp_$(date %Y%m%d).log这个做法踩过几次坑之后成了我的标准操作。早期我把所有日志写同一个文件跑了三个月之后文件有几百MB用grep查一条记录都要等好几秒。按天分文件之后单个文件一般也就几MB查询和归档都方便配合logrotate还能自动清理90天前的旧文件。还有一个建议是着手做任何持续监控之前先确认要监控的URL是稳定的、可重放的。如果你监控的接口要求每次请求带不同的签名或者token你的监控脚本需要先实现签名生成逻辑这会让脚本复杂很多也就偏离了轻量定位。我一般挑那些无状态、无副作用的接口作为监控目标比如健康检查、版本信息、或者公开查询接口。如果必须监控带鉴权的接口就把token定期换到配置文件里脚本从配置文件读取避免每次改动都要改脚本本身。curl本身不复杂但把curl放进一个循环、加上超时控制、输出统计分析、配合告警就能变成一套轻量但完整的接口监控方案。这套方案的优点是零依赖、部署快、排查链路直白缺点是你需要自己处理脚本的健壮性和告警逻辑但作为第一层监控武器它比任何重型工具都更敏捷。