拓冰建站拓冰建站
首页 / 资讯中心 / 正文

CPU仅15%接口延迟却飙到3s?从线程到容器全面排查等待瓶颈

线上反馈接口延迟飙升P99 从 200ms 涨到 3s登录服务器先看 CPU好家伙使用率才 15%。第一反应是“机器配置不够加节点”但如果每次都靠加机器解决问题迟早要在扩容上栽跟头。CPU 不高不代表系统不忙。更准确的判断是请求没有在“算”而是卡在了某个“等”上——等锁、等数据库连接、等网络包、等磁盘 IO甚至是在等 CPU 调度。这篇文章不绕弯子直接按线上排障顺序走一遍从 CPU 指标拆解、线程栈分析、数据库慢查询、网络重传、磁盘 IO一直查到虚拟机和容器的 CPU 调度限制最后给出一张排查对照速查表。1. 现象拆解CPU 不高延迟高说明请求卡住了先建立一个基本认知CPU 利用率低只能说明 CPU 没有被计算密集型任务占满不能说明系统没有瓶颈。接口延迟高本质上是请求处理链路里某个环节产生了排队或等待。最常见的几种等待场景场景典型表现为什么 CPU 不高线程池耗尽请求排队线程全部阻塞线程在等锁/等IO不是算锁竞争大量线程 BLOCKED只有少数线程执行大部分线程在挂起状态数据库慢查询连接池打满SQL长时间执行应用CPU闲置DB端可能也不高连接池耗尽获取数据库/Redis连接超时客户端在等空闲连接网络丢包重传TCP 重传率高响应延迟抖动CPU 处理包开销不大但应用等包磁盘 IO 等待iowait 高线程 D 状态磁盘是瓶颈CPU 在等磁盘GC 停顿Full GC 频率高STW 时间长GC 线程占用有限但业务全停容器/虚拟机 CPU 限制CPU steal 高线程得不到调度业务进程想跑但 CPU 时间被分走伪共享多线程修改同一缓存行性能骤降缓存一致性流量巨大但 CPU 占比不高排障的基本思路就是延迟高 CPU 低不要先想着扩容先把请求链路卡在哪一环节找出来。谁在等、等什么、等了多久三个问题问完问题往往就浮出来了。2. 排查前置先确认 CPU 统计没有被“骗”很多排障人员第一步就被指标误导了。线上环境里CPU 的统计口径直接影响判断结果。第一确认你看的是容器还是宿主机的 CPU。容器里执行top看到的通常是整个宿主机的 CPU 使用情况。如果宿主机上其他容器在跑任务当前容器看到的 CPU 可能被“平均”了也可能显示的是多个 CPU 核心的累计值。更稳妥的方式是看 cgroup 的 CPU 统计或者通过docker stats精确查看当前容器的 CPU 占用。Kubernetes 环境里可以直接看 kubelet 暴露的 container_cpu_usage_seconds_total 指标避免被宿主机的整体负载干扰。第二看 CPU 使用率的粒度。平均 15% 可能掩盖了一个事实某个 CPU 核心已经被软中断打满或者是某一个业务线程在疯狂自旋。top默认显示的是整体 CPU要按核心拆开看用mpstat -P ALL 1观察每个核心的使用率。如果某个核心跑满而其他核心空闲问题往往出在中断绑定或单个热点线程上。# 每隔 1 秒输出每个 CPU 核心的使用情况 mpstat -P ALL 1 # 按进程维度看 CPU定位热点进程 pidstat -u 1 # 查看系统整体负载对比 CPU 利用率判断是否有阻塞 uptime负载load average和 CPU 利用率是两个不同的概念。负载高但 CPU 利用率低几乎可以断定有进程阻塞在不可中断状态或等待 IO。比如uptime显示 load 超过核心数好几倍top里 CPU 却只有 15%这时候问题大概率不在计算而在 IO 或锁。第三确认 CPU 使用率的构成。top的%Cpu(s)一行里us是用户态、sy是内核态、wa是 IO 等待、st是被虚拟机偷走的时间。只看总和 15% 远远不够。如果sy占了 10% 以上说明系统调用和内核锁竞争很重如果wa高说明磁盘 IO 拖后腿如果st高说明宿主机在抢 CPU。3. 线程维度锁竞争、阻塞与线程池排队CPU 利用率低但接口延迟高最常见的原因是业务线程没有在跑而是被挂起了。这种情况下线程栈能直接告诉你答案。3.1 用 jstack 统计线程状态找到应用进程 PID 后执行jstack导出线程快照然后统计线程状态分布# 导出线程快照间隔 5 秒抓 3 次避免只抓到瞬时状态 jstack 12345 thread_dump_1.txt sleep 5 jstack 12345 thread_dump_2.txt sleep 5 jstack 12345 thread_dump_3.txt # 统计线程状态 grep java.lang.Thread.State thread_dump_1.txt | sort | uniq -c重点关注三类状态BLOCKED线程在等待进入同步块/方法通常是锁竞争的直接证据。如果大量线程被同一个锁阻塞说明存在锁热点。WAITING线程在等待另一个线程的通知比如Object.wait()、LockSupport.park()。线程池中的空闲线程也是这个状态但如果活跃线程数量没到上限请求却超时就要怀疑连接池获取或队列提交环节。TIMED_WAITING带超时时间的等待可能是在 sleep、等待网络响应也可能是拿锁超时。这个状态需要结合线程栈内容看具体在等什么。线程快照里还能看到具体的业务代码行。比如线程栈停在java.net.SocketInputStream.socketRead0说明线程在等网络响应停在DruidDataSource.getConnection或HikariPool.getConnection说明在等数据库连接停在ReentrantLock.lock说明在抢锁。3.2 用 Arthas 快速定位问题线程如果生产环境允许接入诊断工具Arthas 比 jstack 更直观。启动后执行thread -n 3可以列出 CPU 占用最高的三个线程执行thread -b可以找出当前阻塞其他线程的锁。# 查看当前最忙的 3 个线程 thread -n 3 # 找出阻塞其他线程的锁 thread -b # 查看某个线程的完整栈 thread 42这套组合拳之后大多数“CPU 低但接口慢”的场景已经有眉目了。如果线程栈正常没有明显锁等待就要往下一层查。3.3 线程池队列堆积线程池配置不当引起的延迟飙升CPU 利用率通常也不高。例如 Tomcat 默认线程池配置下如果所有线程都阻塞在下游服务超时上新请求只能在队列里排队。表现就是接口延迟不断上涨但 CPU 和数据库都没明显压力。排查方法看线程池监控指标。Spring Boot 场景下Tomcat 线程池可以通过 Actuator 暴露tomcat.threads.busy和tomcat.threads.current当 busy 线程数接近最大线程数时说明线程池接近打满。同时配合下游依赖的超时配置检查确认是否有大量请求卡在下游。3.4 GC 停顿也需要先排除GC 导致的服务停顿也表现为 CPU 利用率不高但接口延迟飙升。Full GC 期间业务线程全部暂停CPU 会有短暂升高但如果你观察的时间点不对看到的 CPU 可能并不高。# 查看 GC 频率和耗时每 1 秒输出一次 jstat -gcutil 12345 1000重点看FGCFull GC 次数和FGCTFull GC 累计耗时。如果FGCT在快速上涨说明 GC 是延迟元凶之一。GC 问题可以和线程问题并行排查优先确认一次 Full GC 停顿时长是否超过接口超时阈值。4. 数据库与中间件慢查询、连接池耗尽线程状态正常但大量线程都停在获取数据库连接或执行 SQL 的位置接下来就要查数据库。4.1 先看连接池数据库连接池耗尽几乎是“CPU 低 延迟高”的标配原因。应用进程的线程在等待空闲连接CPU 自然不高但接口已经卡死。连接池大小的配置、慢 SQL 持有连接时间过长、大事务迟迟不提交都会把连接池吃满。排查顺序先看应用日志里有没有获取连接超时的报错再看监控面板里的活跃连接数。如果是 Druid可以通过 Druid 的监控页面或者druid.sql相关指标查看活跃连接和 SQL 执行时间。如果是 HikariCP配置好 Actuator 后可以从hikaricp.connections.active指标观察。4.2 再查慢查询和大事务连接池耗尽通常是结果不是原因。真正要查的是为什么连接持有时间过长。-- 查看当前正在执行的 SQL观察执行时间和事务状态 SHOW FULL PROCESSLIST;如果存在State为Sending data且Time很大的查询说明慢 SQL 持有连接不放。更隐蔽的是大事务事务内有多条 SQL其中一条没走索引整条事务就卡住了其他线程只能排队等锁。还需要关注锁等待。SHOW ENGINE INNODB STATUS可以输出当前 InnoDB 的事务和锁等待信息。排障时要重点看LATEST DETECTED DEADLOCK部分以及等待锁的线程持有事务的时间。一般来说这个环节最容易定位到两种问题一条 SQL 没走索引导致的慢查询拖垮所有请求或者一个长时间事务持锁导致其他请求全部堵在锁等待上。4.3 Redis 也要测一下延迟如果业务链路依赖 RedisRedis 变慢同样会让应用线程大面积等待。用redis-cli --latency直接测本机到 Redis 的延迟redis-cli -h your-redis-host -p 6379 --latency如果延迟超过几十毫秒甚至上百毫秒要重点检查 Redis 是否在执行阻塞命令、是否在做 AOF 持久化刷盘、是否触发了大 key 操作或者内存是否满了导致淘汰策略频繁触发。5. 网络维度重传、丢包和软中断网络层的问题经常被忽略。接口延迟高可能是因为请求包在网络上丢了TCP 协议栈在反复重传或者网卡软中断处理的 CPU 核心被铺满。5.1 看 TCP 重传和丢包# 查看 TCP 重传统计 netstat -s | grep -i retrans # 或者用 nstat 查看增量 nstat -az | grep -i retrans重传率高会直接拉高接口延迟但 CPU 几乎看不出明显变化。还需要确认接入了哪些网络设备、链路是否稳定最简单的方式是检查服务端和客户端之间的丢包率。5.2 看带宽和网卡错误# 每隔 1 秒查看网络设备吞吐和错误包 sar -n DEV 1关注rxerrs、txerrs、rxdrop、txdrop这几列。如果错误包或者丢包不断增长说明物理网卡或者虚拟交换机存在问题。如果rxkB/s接近带宽上限说明带宽已经打满应用在争抢带宽资源。5.3 看软中断和连接队列网卡收包后软中断处理如果集中在一个 CPU 核心上也会造成“整体 CPU 低、单核跑满”的现象。用mpstat -P ALL 1观察某个核心的软中断占比softirq列是否过高。连接队列溢出也是延迟飙升的常见原因。用ss -lnt查看 Listen 队列ss -lnt对于 LISTEN 状态的端口Recv-Q表示当前已经建立但未被应用 accept 的连接数Send-Q表示最大队列长度。如果Recv-Q长期接近Send-Q说明应用处理不过来新连接请求会在内核队列里耗时长但 CPU 不一定高。6. 磁盘与 SwapIO 拖垮接口延时磁盘 IO 是“CPU 低 接口慢”里最容易被低估的因素。尤其在高并发写入场景下日志同步刷盘、数据库 WAL 写入、临时文件读写都可能成为延迟瓶颈。6.1 用 iostat 看真实 IO 压力# 查看磁盘利用率和 IO 等待时间每 1 秒刷新 iostat -x 1重点看三列%util表示磁盘繁忙程度await表示 IO 请求平均处理时间svctm是真实磁盘响应时间。如果await远高于svctm说明大量请求在排队。%util接近 100% 时基本可以确定磁盘是瓶颈。6.2 用 vmstat 看阻塞进程# 每隔 1 秒刷新系统状态 vmstat 1b列表示处于不可中断睡眠状态的进程数wa列表示 CPU 等待 IO 的时间占比。如果b长期大于 0 且wa偏高说明已经有进程阻塞在磁盘 IO 上了。6.3 观察 Swap 交换内存紧张触发 Swap 后任何一次内存访问都可能触发磁盘读写接口延迟会呈数量级上升。用free -h或cat /proc/meminfo查看SwapCached和 Swap 使用量。vmstat 1里的si和so两列如果持续非零说明系统正在频繁换入换出此时接口延迟高不是 CPU 的锅是内存和磁盘共同拖累。容器环境下还需要检查磁盘 IO 是否有 cgroup 限制。如果容器所在宿主机的磁盘本身繁忙其他容器的 IO 也会影响你。7. 虚拟化与容器调度CPU steal 和 CPU Limit很多线上服务跑在虚拟机和容器环境里。这种环境下排查 CPU必须额外关注两个指标ststeal time和 cgroup 的 CPU 限流。7.1 CPU steal 高说明宿主机在抢 CPUvmstat 1输出里的st列表示虚拟机等待宿主机分配 CPU 的时间占比。如果st持续高于 5%说明宿主机 CPU 超卖严重你的虚拟机在“排队等 CPU 时间片”。这种情况下业务进程的 CPU 利用率可能只有 15%但每个线程都经常被暂停接口延迟自然飙升。这也解释了为什么某些虚拟化环境里会出现“业务进程 CPU 不高但应用特别卡”的现象。排障时可以运行vmstat 1观察st列如果数值偏高需要联系宿主机管理员确认资源分配。虚拟机监控软件本身消耗高 CPU 的情况也需要重点关注是否是宿主机资源分配异常导致的。7.2 容器 CPU Limit 导致限流容器场景下如果设置了 CPU Limit即使宿主机资源充足容器内进程超过限制后也会被节流。这时候容器内看到的 CPU 使用率被限制在一个低水平但请求处理速度上不去。Kubernetes 环境下可以进入容器查看 cgroup 的 CPU 统计# cgroup v1 路径示例实际路径以系统挂载为准 cat /sys/fs/cgroup/cpu/cpu.stat # cgroup v2 路径示例 cat /sys/fs/cgroup/cpu.stat重点看nr_throttled被限流的次数和throttled_time被限流的总时长。如果throttled_time在持续增长说明容器频繁被 CPU Limit 限制。这类问题在外表上就是“CPU 不高但服务变慢”而且往往在流量高峰时更明显。7.3 资源调度配置不合理大数据或批处理场景里资源调度分配不当也会出现类似现象。比如 Spark on YARN 作业分配的核数过少任务并行度不足单个作业执行时间被拉长但整体 CPU 利用率并不高。这种场景在线上排障时要区分是实时接口延迟问题还是批处理任务执行变慢。后者直接查看资源调度器的任务分配日志即可定位。8. 延迟飙升 CPU 偏低的常见原因对照速查表分类标志性表现关键命令/指标处理方向锁竞争大量线程 BLOCKED同一锁jstack统计线程状态定位热点锁减少锁粒度连接池耗尽获取连接超时活跃连接满HikariCP/Druid 监控指标查慢 SQL、调大连接池、优化事务数据库慢查询SQL 执行时间长SHOW FULL PROCESSLIST加索引、拆分事务、优化 SQLTCP 重传重传率高netstat -s、ss检查链路质量、调整超时网络软中断单核 softirq 高mpstat -P ALL 1开启 RPS、调整中断绑定磁盘 IO%util接近 100%iostat -x 1换 SSD、加缓存、减少同步写Swap 交换si/so持续非零vmstat 1调整内存参数、扩容内存CPU stealst列持续偏高vmstat 1联系宿主机管理员避免超卖容器 CPU 限流throttled_time增长cgroupcpu.stat调大 CPU LimitGC 停顿FGC 频繁、FGCT 增长jstat -gcutil调整堆参数、优化对象分配伪共享多线程性能骤降cache-misses 高perf stat -e cache-misses对齐填充、拆分变量线程池队列堆积请求排队、线程全忙ThreadPoolExecutor 指标调整队列策略、优化下游超时这张表的核心价值不在“每个原因怎么解决”而在于“遇到这类现象先往哪个方向查”。实际排障时按照线程栈 - 数据库 - 网络 - 磁盘 - 虚拟化 的顺序走一遍多数问题都能定位。9. 从被动排障到主动预防压测与监控建设线上出问题再排查成本已经很高了。更好的做法是把 CPU 的“低利用率高延迟”场景在日常压测中提前暴露。压测时不要只看平均 CPU要同时记录以下几类指标CPU 拆解指标us、sy、wa、st四类占比按核心维度记录。线程池指标活跃线程数、队列深度、拒绝次数。连接池指标活跃连接数、等待获取连接时间。GC 指标Young GC 和 Full GC 频率、单次停顿时间。延迟指标P50、P95、P99而不是只看平均值。网络指标重传数、带宽利用率、连接队列溢出数。磁盘指标%util、await、svctm。压测时构建“锁竞争 慢查询 偶发 GC”的混合场景往往比单一高并发场景更能暴露这类问题。Linux 下stress工具可以对 CPU、内存、IO 做定向施压但更完整的方式还是通过压测平台构造真实流量。监控方面如果一个系统之前没有 CPUus/sy/wa/st的拆分监控建议尽快补上。很多线上 CPU 问题没有第一时间发现就是因为只采集了 CPU 使用率总和缺少四个关键维度的历史数据。有了历史曲线再遇到“CPU 15% 延迟飙升”的反馈可以快速定位是哪一类等待加剧了而不是从零开始抓现场。另外线上排障操作要严格遵守变更规范。抓线程栈、慢查询日志这类操作通常在低峰期执行避免对生产造成额外压力。涉及连接池、线程池参数调整要有回滚方案先小范围验证再全面推广。10. 排障 SOP 总结与建议遇到“CPU 才 15%接口延迟却飙升”的反馈建议按这个顺序执行不要跳步确认 CPU 统计口径看的是容器还是宿主机平均值还是按核拆分us/sy/wa/st各占多少。抓线程栈连续抓三次统计线程状态定位是锁竞争、连接池等待还是网络等待。确认是否 GC 停顿用jstat -gcutil排除 Full GC 影响。查数据库和中间件连接池活跃数、慢查询、大事务、Redis 延迟。查网络层TCP 重传、带宽、软中断、连接队列溢出。查磁盘和内存iostat、vmstat、Swap。查虚拟化层CPU steal、容器 CPU Limit、宿主机资源状态。这七个步骤做完99% 的场景能把根因定位出来。剩下的 1%往往需要重新审视业务代码层面的特殊问题比如死循环自旋、伪共享导致的缓存一致性风暴。这类问题通常伴随系统调用占比异常升高可以用perf进一步采样。把 CPU 的us/sy/wa/st四个维度记在心里排障时先拆指标再动手。看得懂 CPU才不会被 CPU 骗。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门