当用户反馈页面打开变慢,监控图表上那条代表响应时间的曲线突然拔地而起时,很多人的第一反应是去看服务器负载。但往往CPU和内存占用率都很平静,这就让问题变得诡异起来。延迟突增通常不是单一故障,而是网络传输路径与后端处理链路中某个狭窄处发生了“血栓”。排查必须同时从数据包走过的路和代码执行的路两条线入手,缺一条都容易误判。
先确认延迟的性质:连通性慢还是响应慢动手之前,先花一分钟定性。在客户端执行一次完整的请求,观察浏览器开发者工具Network面板中的时间瀑布图。如果TTFB(首字节时间)很短,但Content Download很长,说明服务器处理快、网络传输慢,可能是带宽被挤占或大文件传输阻塞。如果TTFB本身就高达数秒,问题一定出在服务端处理或中间网络设备上。还有一种情况是DNS解析、SSL握手耗时异常,这属于建连慢,通常指向DNS服务器、CDN节点或证书链问题。
用curl命令可以精确分解耗时:
curl -o /dev/null -s -w "DNS解析: %{time_namelookup}s\nTCP连接: %{time_connect}s\nSSL握手: %{time_appconnect}s\n首字节: %{time_starttransfer}s\n总耗时: %{time_total}s\n" https://你的域名/接口路径
这几个数值一旦打印出来,问题出在哪个阶段就一目了然。DNS解析超过200毫秒要查域名服务器;TCP连接慢要查客户端到服务器的网络丢包;SSL握手慢往往是证书链过长或OCSP验证卡住;首字节慢则是后端应用或中间件的锅。
网络层排查:从第四层往下挖网络问题最怕“偶尔慢”,因为偶发性的丢包或重传很难抓现行。MTR(My Traceroute)是比单纯ping和traceroute更有效的工具,它把两者结合起来,持续向路径上每一跳发送探测包,统计每一跳的丢包率和延迟波动。
mtr -r -c 100 -n 目标IP
看结果时有条铁律:如果某一跳出现丢包,但后续所有跳丢包率均为0或更低,说明这一跳的路由器只是限制了ICMP响应速率,并非真正丢包。真正需要警惕的是,某一跳开始丢包,并且后续所有跳的丢包率都维持在同一水平甚至更高,这说明从这一跳起确实发生了数据包丢失。
丢包的直接后果是TCP重传。重传一旦发生,请求延迟会从几十毫秒飙升到几百甚至上千毫秒。在服务端可以用ss命令查看TCP重传统计:
ss -ti | grep -A 1 'ESTAB' | grep 'retrans'
如果retrans字段的数值在持续增长,说明服务端发出的包在网络上丢失了。这时需要检查服务端网卡是否丢包,用ifconfig或ip -s link查看网卡的errors、dropped、overruns计数。网卡Ring Buffer过小是高频丢包元凶,可以用ethtool -g eth0查看当前值,适当调大:
ethtool -G eth0 rx 4096 tx 4096
如果网卡层面没有异常,问题就出在上层交换设备或链路上。云服务器的话,检查一下实例的网络带宽是否触顶,突发带宽用尽后会被限速,表现为延迟剧烈抖动。查看云监控中的网络出/入带宽曲线,如果有一条平直的天花板线,那就是限速无疑。
后端链路排查:从负载均衡到应用线程请求到达服务器后,要经过负载均衡器、反向代理、应用容器、数据库等多层处理。每一层都可能成为瓶颈。排查时遵循“由外向内、逐层测量”的原则。
先在Nginx或Apache的访问日志中开启响应时间记录。Nginx在log_format中加上$request_time和$upstream_response_time:
log_format timed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct=$upstream_response_time';
$request_time是从收到客户端第一个字节到发送完最后一个字节的总时间,$upstream_response_time是后端应用处理耗时。两者差值过大,说明Nginx在读取客户端请求体或发送响应时阻塞了,可能是客户端网络慢导致Nginx写缓冲区满。如果$upstream_response_time本身就很大,问题在后端。
进入应用层,线程池耗尽是最常见的突发延迟原因。Java应用的话,线程池的活跃线程数达到最大值后,新请求会进入队列等待,等待时间就是额外延迟。Spring Boot应用可以通过Actuator的/actuator/metrics/tomcat.threads.current和tomcat.threads.busy端点实时查看。如果busy线程数持续等于maximum,说明请求处理不过来。临时解决是调大线程池上限,但根本方案是找出处理慢的原因。
数据库连接池同理。HikariCP或Druid的连接池如果活跃连接数打满,新的数据库请求会阻塞在获取连接这一步。监控连接池的ActiveConnections和PendingConnections指标,Pending大于0就是告警信号。慢SQL是连接池被耗尽的主要推手,一条原本50毫秒的查询,因为表锁或索引失效变成5秒,连接就迟迟无法释放,雪崩效应随之而来。
系统资源假象:别被平均CPU骗了很多延迟突增的案例中,服务器整体CPU使用率只有30%左右,看起来毫无压力。但CPU使用率是平均值,可能掩盖了单核打满的事实。一个多线程应用如果存在全局锁或单线程处理逻辑,其中一个CPU核心可能已经100%了,其他核心却在闲置。用top按1键展开每个核心的使用率,或者用mpstat -P ALL 1观察各核心的实时负载。如果发现某个核心持续100%,用perf top或async-profiler抓一下热点函数,通常能找到那把锁。
除了CPU,磁盘IO也是隐藏杀手。数据库服务器上,iostat -x 1查看磁盘的await和util指标。await是IO请求的平均等待时间,超过20毫秒就需要关注;util接近100%说明磁盘已经满负荷。但注意,SSD的并发能力远强于机械盘,util高不一定代表性能差,关键看await。如果await飙升,检查是否有大批量写入、日志刷盘策略过于激进,或者数据库的checkpoint操作在集中刷脏页。
内存与GC:延迟的隐形推手内存不足导致系统使用Swap,性能会断崖式下降。即使物理内存还有剩余,操作系统也可能因为内存回收机制主动换出一些匿名页。用vmstat 1观察si(swap in)和so(swap out)列,如果有持续的非零值,说明发生了换页,延迟增加几乎是必然的。Java应用还要关注GC暂停时间,一次Full GC暂停几秒,所有请求线程都被挂起,延迟曲线会呈现尖锐的毛刺。用jstat -gcutil PID 1000持续观察,FGC列增长且FGCT列耗时高,就要分析GC日志,调整堆大小或GC策略。
连接队列溢出:静默的请求丢弃高并发场景下,一个极易被忽视的瓶颈是TCP连接队列。服务端Socket有两个队列:SYN队列(半连接)和Accept队列(全连接)。当应用处理速度跟不上新连接到达速度时,Accept队列会满,新连接要么被丢弃,要么客户端收到SYN-ACK后服务端不响应ACK,表现为客户端建连超时。
用ss -lnt查看监听端口的Recv-Q和Send-Q。Recv-Q是当前Accept队列中的连接数,Send-Q是队列最大长度(backlog)。如果Recv-Q持续接近或等于Send-Q,说明队列已经溢出。调整应用启动时listen的backlog参数,并在系统层面调大net.core.somaxconn:
sysctl -w net.core.somaxconn=4096
同时检查Nginx的backlog配置项,确保与系统值匹配。SYN队列溢出则通过netstat -s | grep "SYNs to LISTEN"来查看计数是否增长。
云原生环境的特殊排查点如果应用部署在Kubernetes中,排查维度还要增加一层。Pod的CPU throttling是延迟突增的典型原因。即使Pod的CPU使用率不高,但如果设置了较低的limit,内核的CFS调度器会强制限流,导致进程被暂停。检查容器的nr_throttled指标:
cat /sys/fs/cgroup/cpu/cpu.stat | grep nr_throttled
如果这个值在持续增长,说明CPU limit设置过低,进程频繁被限流。适当提高limit或者使用Guaranteed QoS类避免限流。另外,Service的kube-proxy转发如果使用iptables模式,规则数量过多时转发延迟会线性增长,切换到IPVS模式能显著降低延迟。
服务网格如Istio的Sidecar代理也是延迟来源。每个请求都要经过两次Envoy代理,如果Sidecar资源不足,延迟会翻倍。检查Sidecar容器的CPU和内存使用,必要时增加资源配额。同时关注Envoy的cluster级别的upstream_rq_time指标,定位是哪个后端服务变慢。
构建端到端的观测能力临时抱佛脚式排查终究被动。要建立从客户端到服务端的全链路延迟监控。在代码中对关键外部调用(数据库、缓存、RPC)进行埋点,记录每次调用的耗时。当总延迟突增时,能迅速下钻到是哪个下游调用变慢了。OpenTelemetry的链路追踪可以自动串联这些信息,一条慢请求的完整调用链一目了然,哪个Span耗时最长,问题就出在哪个环节。
网络层面,持续对关键路径做TCP探测,记录RTT和丢包率。SmokePing这类工具可以画出网络延迟的长期趋势图,当延迟突增时,对比历史曲线能快速判断是周期性波动还是突发异常。结合应用层的QPS和延迟曲线,如果QPS没有明显变化但延迟飙升,基本可以排除容量问题,聚焦于某个组件性能退化;如果QPS和延迟同步上升,则是流量冲击导致,需要扩容或限流。
延迟突增的根因可能是一根松动的网线、一行未加索引的SQL、一个忘记释放的锁、一个过小的内核参数,也可能是这些因素的组合。排查时保持对数据的敬畏,每个假设都要用指标验证,不跳过任何一个环节。网络层的包、系统层的调用、应用层的日志,三股线索交汇之处,就是瓶颈所在。
