ARTICLE DETAIL

资讯详情

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

JCSprout 实战:Disruptor 环形队列引发线上 OOM 的排查、定位与根治

JCSprout 实战:Disruptor 环形队列引发线上 OOM 的排查、定位与根治 文档教程后端【免费下载链接】JCSprout‍ Java Core Sprout : basic, concurrent, algorithm项目地址https://gitcode.com/gh_mirrors/jc/JCSprout点击查看免费下载线上应用反复抛出OutOfMemoryError是每位后端开发者都可能遇到的难题——相比数组越界、空指针等业务异常内存溢出往往表现为日志偶发、重启即好、却愈演愈烈让人无从下手。本文以 JCSprout 仓库记录的Kafka 消费 批量持久化应用发生内存溢出的真实排查案例为主线完整还原「表象 → 排查 → 定位 → 解决」四个步骤并深入到com.lmax.disruptor.RingBuffer的环形队列原理与仓库源码帮助你掌握一套可复用的 JVM OOM 实战排查方法从 GC 日志与 jstat 观察堆变化到 VisualVM 本地复现与 HeapDump 定位对象最终定位并修复配置层面的根因。问题表象Kafka 消费程序频繁 OOM生产上一个应用的业务逻辑非常简单从 Kafka 中消费消息然后批量做持久化操作。但该应用不断爆出内存溢出并且随着业务量的增长出现异常的频次越来越高——Kafka 消息越多OOM 就越快。由于当时还有其他工作只能让运维先通过重启临时恢复同时监控好堆内存以及 GC 情况。重启大法虽好可是依然不能从根本上解决问题。这个表象背后隐藏着一个关键线索异常频次与消息量正相关。它暗示内存占用与数据量相关而非偶发的单点问题为后续排查指明了方向。排查GC 日志与 jstat 揭示老年代异常运维收集到内存数据与 GC 日志后首先观察堆内存的整体走势。分析发现老年代的内存使用即使在发生 GC 后也一直居高不下且随着时间推移越来越高结合jstat日志即使发生了 Full GCFGC老年代也已经回收不了内存直接到顶甚至有几台应用的 FGC 达到了上百次单次 FGC 时间也高得可怕。这说明应用的内存使用一定存在问题——有大量赖皮对象始终回收不掉。这些对象一定与GC Roots存在引用链导致可达性分析判定它们存活。关于可达性分析的原理可以参见仓库中的 GarbageCollection.mdJVM 从GC Roots方法区静态变量引用的对象、虚拟机栈引用的对象等向下搜索形成引用链当一个对象到GC Roots没有任何引用链时才会被判定为可回收。老年代内存到顶、FGC 也回收不掉正是这些对象仍然挂在使用中的引用链上。定位本地复现与 HeapDump 找出元凶生产 dump 文件太大转战本地复现生产上的内存 dump 文件非常大达到了几十 G与内存设置过大有关直接用 MAT 分析需要花费大量时间。于是转换思路在本地复现问题这样定位起来会容易得多。复现的第一步是尽量还原生产环境将本地应用最大堆内存设置为150M调小堆可以让问题更快暴露在消费 Kafka 处 Mock 成一个while循环不断生成数据应用启动后用VisualVM连接应用实时监控内存与 GC 情况。结果跑了 10 几分钟内存使用并没有什么问题每产生一次 GC内存都能被有效回收。第一次复现失败。复盘差异从一条到几百条复现失败后回头 review 代码发现生产的逻辑和用 while 循环 Mock 数据并不一样查看生产日志发现每次从 Kafka 取出的都是几百条数据而 Mock 时每次只能产生一条数据。数据量差了几百倍问题自然难以暴露。于是改为在服务器上跑一个生产者程序源源不断地向 Kafka 中发送数据以尽可能模拟生产情况。果然不出意外只跑了一分多钟内存就顶不住了——GC 频次非常高但内存回收却相形见拙后台也开始打印内存溢出。问题成功复现。HeapDumpRingBuffer 占了近 50% 内存从现象看内存中有许多对象一直存在强引用关系导致得不到回收。为了确认到底是什么对象占用了这么多内存利用VisualVM 的 HeapDump 功能立即 dump 出当前应用的内存情况。结果发现com.lmax.disruptor.RingBuffer类型的对象占用了将近 50% 的内存。看到这个包名自然就想到了Disruptor环形队列。再次 review 代码确认从 Kafka 里取出的 700 条数据是直接往 Disruptor 里丢的。这也解释了为什么第一次模拟数据没能复现问题模拟时是一个对象放进队列而生产是 700 条数据放进队列数据量存在 700 倍的差距。根因RingBuffer 环形队列与对象长期存活环形队列的覆盖写机制Disruptor 是一个环形队列RingBuffer核心机制是数组复用、环形覆盖在对象没有被覆盖之前slot 上的对象会一直存在。仓库作者做了一个小实验验证这一点设置队列大小为 8从 0~9 往里面写 10 条数据当写到第 8 条时就会把之前 0 的位置覆盖掉后面的以此类推类似于 HashMap 的取模定位。队列大小 8依次写入 0,1,2,...,9 写入 0~7 → 占满 8 个 slot 写入 8 → 覆盖 slot 0原数据 0 被替换 写入 9 → 覆盖 slot 1原数据 1 被替换这意味着队列中对象的生命周期取决于队列容量与写入速率只要写入持续进行slot 上的对象就会不断被新对象替换但如果消费端处理不过来或容量配置过大大量已入队对象就会长期驻留内存。700 × 1024 × 1024 的恐怖数量级推演生产场景假设队列大小是 1024随着系统持续运行最终 1024 个位置上都会装满对象而且每个位置装载的是 700 条 Kafka 消息——也就是一次性往队列里丢了 700 条数据。实际查看生产上 Disruptor 的 RingBuffer 配置后结果令人吃惊1024 * 1024约 105 万。这个数量级下即使每个事件只占少量字节105 万个 slot 一旦被填满内存也会瞬间失控。这也与前面观察到的老年代到顶、FGC 回收不掉完全吻合RingBuffer 内的对象始终与生产者/消费者游标保持着引用关系属于 GC Roots 可达对象。仓库源码佐证Disruptor 的完整使用链路JCSprout 仓库中提供了完整的 Disruptor 演示代码位于 src/main/java/com/crossoverjie/disruptor依赖版本为com.lmax:disruptor:3.3.7见 pom.xml。这套代码与本案例的 RingBuffer 行为一一对应。入口LongEventMain 构造并启动 DisruptorLongEventMain.java 展示了 Disruptor 的完整初始化流程// 指定 RingBuffer 的大小必须是 2 的幂 int bufferSize 8; // 构造 Disruptor事件工厂、队列大小、消费者线程池、 // 生产者类型单生产者、等待策略YieldingWaitStrategy DisruptorLongEvent disruptor new Disruptor(factory, bufferSize, executor, ProducerType.SINGLE, new YieldingWaitStrategy()); // 连接事件处理器 disruptor.handleEventsWith(new LongEventHandler()); // 启动 Disruptor启动所有消费者线程 disruptor.start(); // 从 Disruptor 获取 RingBuffer 用于发布事件 RingBufferLongEvent ringBuffer disruptor.getRingBuffer(); LongEventProducer producer new LongEventProducer(ringBuffer); // 循环发布 100 万条数据 for (long l 0; l 1000000; l) { productExecutor.execute(new Work(producer, l)); }代码中有两个与本案例强相关的细节bufferSize 8即环形队列容量。正是本文案例中队列大小 8、写入 10 条覆盖前 2 条实验的代码出处批量发布Work任务通过productExecutor2 线程并发执行producer.onData(bb)模拟一次大量数据涌入队列的生产形态与线上700 条数据直接丢进队列的场景一致。发布链路next → get → publishLongEventProducer.java 展示了向 RingBuffer 发布事件的三个关键步骤public void onData(long bb) { long sequence ringBuffer.next(); // 1. 获取下一个可用的序号 try { LongEvent event ringBuffer.get(sequence); // 2. 获取该序号对应的事件槽位 event.set(bb); // 填充数据 } finally { ringBuffer.publish(sequence); // 3. 发布通知消费者 } }关键点在于第 2 步ringBuffer.get(sequence)返回的是预先分配好的、可复用的对象槽位生产者只是改写其内容。RingBuffer 在启动时通过 LongEventFactory.java 的newInstance()一次性创建所有事件对象LongEvent.java这些对象从创建起就常驻内存生命周期完全由环形队列的槽位覆盖逻辑决定——容量越大、消费越慢驻留对象就越多这正是本次 OOM 的直接机理。消费端LongEventHandlerLongEventHandler.java 实现EventHandlerLongEvent在onEvent中消费事件。若消费速度跟不上生产速度队列就会被快速填满进一步加剧对象驻留。解决调小 RingBuffer 容量并回归验证根因确认后验证方案也很直接将 RingBuffer 容量调小。作者在本地将该值换为2一个最小值做实验同样的 128M 内存同样通过 Kafka 源源不断地取出数据用监控工具持续观察。结果跑了20 几分钟系统一切正常每当一次 GC 都能回收大部分内存最终内存走势呈现锯齿状——说明对象不再长期驻留GC 恢复有效。问题被证实并解决。最终生产上的修复只是修改配置将 RingBuffer 大小从1024 * 1024调小一行业务代码都没有改。需要说明的是RingBuffer 容量并非越小越好。过小的队列会降低吞吐、增加背压与丢失风险生产上这个值具体设置多少必须根据业务的实际消费速率、单条消息大小、可接受的延迟和内存预算来测试确定。但原来的1024 * 1024是绝对不能再使用了。延伸堆内存溢出的通用分析手段本案例本质是一次典型的堆内存溢出。仓库中的 OOM-analysis.md 总结了堆 OOM 的通用分析思路只要不断创建对象且GC-Roots到对象之间存在引用链JVM 就不会回收对象可将-Xms最小堆与-Xmx最大堆设为相同值禁止自动扩展堆内存用while(true)循环不断创建对象即可复现堆 OOM参考 HeapOOM.java可配置-XX:HeapDumpOnOutOfMemoryError在发生 OOM 时自动 dump 堆栈到文件出现 OOM 后通过工具分析GC-Roots引用链查看对象与GC Roots如何关联判断是对象生命周期过长还是对象确实该存在后者则需要调大堆内存。本案例正是沿着对象与 GC Roots 存在引用链 → 找出引用源头RingBuffer→ 从源头控制对象数量的路径完成的定位。总结一套可复用的 OOM 排查方法论回看整个排查过程虽然最终只改了一行配置但方法论价值远超修复本身表象收集记录 OOM 频次与业务量的相关性、堆内存与 GC 状态判断问题是否与数据量正相关日志排查利用jstat、GC 日志观察老年代走势与 FGC 情况确认是否存在无法回收的对象本地复现生产 dump 过大时优先本地复现——但必须精确还原生产的数据特征单条 vs 批量、队列容量、消息量否则复现必然失败HeapDump 定位用 VisualVM 的 HeapDump 找出占用内存最大的对象类型再回查代码确认引用来源原理分析结合组件底层机制如 Disruptor 环形队列的槽位复用、覆盖写解释根因最小验证将可疑参数调到极端值做对照实验用锯齿状 GC 曲线验证修复有效。本案例也再次印证了一个朴素的道理Disruptor 东西虽好也不能乱用——高性能组件的容量参数如果设置不当反而会成为内存黑洞。完整的演示代码与排查案例均可通过 src/main/java/com/crossoverjie/disruptor 与 docs/jvm/OOM-Disruptor.md 在本仓库中查阅。赞分享文档教程后端【免费下载链接】JCSprout‍ Java Core Sprout : basic, concurrent, algorithm项目地址https://gitcode.com/gh_mirrors/jc/JCSprout点击查看免费下载相关推荐JCSprout 实战一次由 Disruptor RingBuffer 引发的线上 OOM 排查与解决全记录JCSprout 实战一次由 Disruptor RingBuffer 引发的线上 OOM 排查与解决全记录 导读 本文是 JCSprout 项目对一次真实线文档教程后端老款Mac升级macOS SonomaOpenCore Legacy Patcher完整实操指南老款Mac升级macOS SonomaOpenCore Legacy Patcher完整实操指南 一台 2015 款 13 英寸 MacBook Pro16文档教程后端JCSprout 生产事故复盘JDK 1.7 下并发写入 HashSet 引发 HashMap 环形链表死循环与线程池任务堆积排查JCSprout 生产事故复盘JDK 1.7 下并发写入 HashSet 引发 HashMap 环形链表死循环与线程池任务堆积排查 导读 本文完整复盘了 JC文档教程后端上一篇【免费下载】 VRoidStudio汉化插件使用教程下一篇探索开源之美OpenLauncher项目介绍创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表