ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

JCSprout 实战:一次生产 CPU 100% 排查优化与 Disruptor 等待策略调优

JCSprout 实战:一次生产 CPU 100% 排查优化与 Disruptor 等待策略调优 文档教程后端【免费下载链接】JCSprout‍ Java Core Sprout : basic, concurrent, algorithm项目地址https://gitcode.com/gh_mirrors/jc/JCSprout点击查看免费下载本篇基于 JCSprout 仓库的 docs/jvm/cpu-percent-100.md 实战记录展开还原一次真实的生产服务器 CPU 负载飙高故障从ps、top -Hp、jstack等常规排查手段切入逐步定位到 Disruptor 环形队列的YieldingWaitStrategy等待策略在消费线程数远超 CPU 核心数时引发的自旋 yield风暴并通过本地模拟复现与对照实验给出两种优化路径。读完本文你将掌握一套完整的高 CPU 问题定位 → 线程栈分析 → 本地复现 → 策略调优 → 架构拆分排查方法论。问题背景年底的运维报警项目上线后不久运维突然报警部分服务器负载非常高。排查现场只运行着一个 Java 应用没有其他可疑进程因此问题几乎可以锁定在该 Java 进程内部。值得一提的是作者此前还刻意提高过某些服务器的负载用于内存分配实验详见 JVM 内存分配好在两套环境互不影响本次报警是真实的生产问题。这种Java 进程占满 CPU的故障在生产环境中非常典型定位的关键在于把 CPU 使用率从进程粒度下钻到线程粒度再把线程栈快照与代码逻辑对应起来。定位问题一条完整的线程级排查链路第一步ps拿到 Java 进程 PID先确认负载来源并拿到目标进程号ps -ef | grep java记录下 Java 应用的PID后续所有操作都围绕这个进程展开。第二步top -Hp按 CPU 排序查看线程top默认显示进程级汇总必须进入线程视图才能看到到底是谁在烧 CPUtop -Hp pid进入交互界面后按大写P可以将线程按照CPU 使用比例排序。此时可以看到若干个线程的 CPU 使用率高达 100% 左右热点线程立刻浮出水面。第三步jstackdump 线程栈快照为了看清这些热点线程正在执行什么代码把线程栈快照落盘jstack pid pid.logjstack输出的每个线程栈中都包含线程的nidnative thread id这是和top中线程 ID 对应的关键索引。第四步线程 ID 转 16 进制在快照中精确定位在top的热点线程中随机挑选一个例如pid194283将其转换为 16 进制得到2f6eb因为线程快照中线程 ID 是以 16 进制存放的形如nid0x2f6eb必须做进制转换才能在日志中精确搜索。grep 0x2f6eb -A 20 pid.log搜索结果显示该线程正卡在Disruptor的堆栈上。作者此前就遇到过一起由 Disruptor 队列引发的内存溢出详见 强如 Disruptor 也发生内存溢出因此对这条堆栈非常敏感——没想到同一框架又惹出了新麻烦。第五步借助线程分析平台批量归类手工 grep 只能看单线程为了直观查看全部线程的状态分布把快照上传到专门的线程分析平台如 fastthread.io 这类在线分析工具。平台会列出所有消耗 CPU 的线程结果发现几乎全部命中同一条堆栈都是Disruptor 队列的消费堆栈都在执行java.lang.Thread.yield函数线程状态均为RUNNABLE处于该状态的线程约有30 多个。根因初判大量线程自旋 yield 互相竞争Thread.yield的语义是让出当前 CPU 时间片重新参与调度竞争。如果大量线程同时处于自旋等待 → yield 让出 → 又抢回 CPU的循环中调度器会频繁切换上下文宏观表现就是 CPU 使用率居高不下。结合30 多个线程都在执行 yield和堆栈全部指向 Disruptor这两个事实初步判断大量 Disruptor 消费线程执行yield后互相竞争导致 CPU 使用率升高。源码与配置层面的印证等待策略是关键业务使用方式一个业务两个队列队列数量爆炸Review 代码后发现系统按业务场景解耦每一个业务场景内部都会使用 2 个 Disruptor 队列。假设当前有 7 个业务类型2 个队列 × 7 个业务 14 个 Disruptor 队列每个队列又有一个消费者线程仅此一项就创建了 14 个消费线程生产环境数量更多。而它们都跑在同一台服务器的同一进程里共享 CPU 资源。YieldingWaitStrategy压榨 CPU 的自旋 yield 策略进一步查看配置发现消费等待策略被设置为YieldingWaitStrategy。查询 Disruptor 官方文档可知YieldingWaitStrategy是一种充分压榨 CPU 的策略使用自旋 yield的方式来提高性能当消费线程Event Handler threads的数量小于 CPU 核心数时推荐使用该策略。也就是说该策略的设计初衷是用高 CPU 占用换取低延迟消费线程没有新事件时不会休眠而是自旋探测序号并周期性让出 CPU。它成立的前提是消费线程数 CPU 核心数——消费线程可以各占一个核并行自旋一旦消费线程数远超 CPU 核心数大量线程在极少的核上反复自旋、让出、再竞争CPU 使用率自然飙升。仓库源码佐证演示工程就是同款姿势JCSprout 仓库中的演示代码 LongEventMain.java 完整复刻了这种高风险用法// 消费线程池固定 15 个线程 ThreadPoolExecutor executor new ThreadPoolExecutor(15, 15, 1, TimeUnit.MILLISECONDS, queue, namedThreadFactory); // 环形缓冲区大小必须为 2 的幂 int bufferSize 8; // 构造 Disruptor显式指定 YieldingWaitStrategy DisruptorLongEvent disruptor new Disruptor(factory, bufferSize, executor, ProducerType.SINGLE, new YieldingWaitStrategy()); // 每个队列挂一个消费者 disruptor.handleEventsWith(new LongEventHandler());这段代码对应了生产事故的两个特征线程池固定 15 个线程LongEventMain.java每个 Disruptor 一个消费者多个队列叠加后消费线程数远超核心数构造 Disruptor 时显式传入YieldingWaitStrategyLongEventMain.java复现了生产上的等待策略配置。项目通过 pom.xml 引入com.lmax:disruptor:3.3.7依赖上述 API 均基于该版本。生产者侧的数据流也值得留意LongEventProducer.java 展示了标准的发布流程——ringBuffer.next()申请序号 →ringBuffer.get(sequence)获取事件槽位 → 填充数据 →ringBuffer.publish(sequence)发布而 LongEventHandler.java 的消费者仅打印一条日志。这个极简消费者恰好是YieldingWaitStrategy暴露问题的放大器消费者任务瞬间完成其余时间全部消耗在自旋等待上。本地模拟复现15 个队列验证 CPU 飙升纸上谈兵不算数作者在本地复刻了生产环境验证判断是否成立。模拟环境搭建创建15 个 Disruptor 队列对应生产的多业务多队列每个队列用一个线程池持续往队列里发送100 万条数据消费程序仅仅打印日志不处理任何业务。该模拟与仓库中的 LongEventMain.java 结构完全一致15 个固定线程的消费线程池new ThreadPoolExecutor(15, 15, ...)、每队列一个LongEventHandler、生产者通过productExecutor.execute(new Work(producer, l))提交 100 万次发布任务LongEventMain.java。复现现象跑了一段时间后观察CPU 使用率确实很高jstackdump 线程后发现与生产现象完全一致消费线程全部处于RUNNABLE状态同时都在执行yield。问题在本地成功复现说明判断方向正确。对照实验两种优化方案的量化验证方案一等待策略切换为BlockingWaitStrategyDisruptor 官方文档同样指出BlockingWaitStrategy是默认的等待策略它基于锁LockCondition机制消费线程没有新事件时会被真正阻塞挂起而不是自旋因此对 CPU 使用率不友好程度极低。在保持其它条件完全一致的情况下仅将等待策略换为BlockingWaitStrategy重新跑一遍模拟CPU 使用率明显下降再次 dump 线程发现大部分线程已经处于waiting阻塞等待状态而非RUNNABLE自旋。这从线程状态层面解释了 CPU 降低的原因waiting线程不会参与调度竞争自然不再消耗 CPU 时间片。方案二削减队列数量回归策略适用前提BlockingWaitStrategy只是减缓了症状作者注意到YieldingWaitStrategy的官方适用前提是消费线程数 CPU 核心数而当前的用法显然违背了这一前提一个队列一个消费者队列一多线程数就爆了。于是做了第二个对照实验只保留 1 个 Disruptor 队列等待策略依然是YieldingWaitStrategy。结果跑了一分钟CPU 使用率一直比较平稳且不高。说明问题根源不只是等待策略本身更是队列消费线程数量与 CPU 核心数严重不匹配。策略没有好坏只有用对场景和用错场景。线上优化与根治方案综合两次对照实验作者给出了由浅入深的两步走方案快速止血先将线上等待策略从YieldingWaitStrategy换为BlockingWaitStrategy立刻把 CPU 使用率压下来。该策略带来的延迟代价在现有业务上可以接受适合作为临时缓解手段根本解决拆分应用。现状是一个应用同时处理 N 个业务、每个业务使用好几个 Disruptor 队列所有队列共享一台服务器的 CPU。应将应用按业务拆分为一个应用处理一种业务类型分别独立部署——这样既能让 Disruptor 消费线程数回归小于 CPU 核心数的适用区间又实现了故障隔离、互不影响。顺带发现线程池配置也存在资源浪费这次 dump 线程时还发现老系统竟然创建了800 个线程。原因是创建线程池时核心线程数与最大线程数设置成了相同值导致空闲线程永远得不到回收白白占用线程栈内存和调度开销。因此在拆分业务的同时也应当结合业务量调整线程池参数把线程数降下来物尽其用。总结一次 CPU 100% 排查的完整方法论回顾整个排查过程可以提炼出一条可复用的实战链路阶段手段产出定位热点ps→top -Hp→ 按P排序拿到高 CPU 线程 ID抓取现场jstack pid pid.log线程栈快照精确命中线程 ID 十进制转 16 进制后 grepnid定位到 Disruptor 堆栈批量归类线程分析平台 / 再次 dump确认 30 线程都在yield自旋根因确认Review 配置YieldingWaitStrategy 多队列多消费者消费线程数远超 CPU 核心数本地复现15 个队列模拟压测dump 对比现象与生产一致对照验证换BlockingWaitStrategy减为单队列CPU 均显著下降落地优化换策略止血 → 按业务拆分应用 → 收缩线程池根治 CPU 100%几个关键结论值得记住jstack中的线程 ID 是 16 进制的与top输出的十进制线程号必须做进制转换才能对应Thread.yield本身不是原罪大量线程同时自旋 yield 互相竞争才是 CPU 飙升的机制YieldingWaitStrategy只适合消费线程数小于 CPU 核心数的低延迟场景默认的BlockingWaitStrategy用阻塞换 CPU是求稳场景的更优解中间件用得越多越要关注资源总量队列、消费者、线程池的个数乘以业务类型数之后可能远超预期这类数量放大是隐形的性能炸弹。如果你想亲手复现文中的实验可直接运行 JCSprout 仓库中的演示代码 LongEventMain.java需引入 pom.xml 中的com.lmax:disruptor:3.3.7依赖配合top -Hp与jstack观察 CPU 和线程状态的变化即可直观感受两种等待策略的巨大差异。同主题的姊妹篇 一次内存溢出排查优化实战 记录了 Disruptor 环形缓冲区引发的另一类线上故障OOM两篇文章相互印证共同构成Disruptor 使用避坑的完整案例集。赞分享文档教程后端【免费下载链接】JCSprout‍ Java Core Sprout : basic, concurrent, algorithm项目地址https://gitcode.com/gh_mirrors/jc/JCSprout点击查看免费下载相关推荐JCSprout 实战一次由 Disruptor RingBuffer 引发的线上 OOM 排查与解决全记录JCSprout 实战一次由 Disruptor RingBuffer 引发的线上 OOM 排查与解决全记录 导读 本文是 JCSprout 项目对一次真实线文档教程后端JCSprout 实战Disruptor 环形队列引发线上 OOM 的排查、定位与根治JCSprout 实战Disruptor 环形队列引发线上 OOM 的排查、定位与根治 线上应用反复抛出 OutOfMemoryError 是每位后端开发者都文档教程后端LMAX Disruptor等待策略终极指南Blocking、Yielding和BusySpin性能对比与选择技巧LMAX Disruptor等待策略终极指南Blocking、Yielding和BusySpin性能对比与选择技巧 LMAX Disruptor是一款高性能的并发编程上一篇零停机升级JuiceFS版本无缝过渡实战指南下一篇SyncTV部署完全教程Docker、Helm与二进制安装对比创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表