分布式 | 令人头疼的堆外内存泄露怎么排查?

news2025/1/23 10:21:42

作者:鲍凤其

爱可生 dble 团队开发成员,主要负责 dble 需求开发,故障排查和社区问题解答。少说废话,放码过来。

本文来源:原创投稿

*爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。


大家在使用 Java NIO 的过程中,是不是也遇到过堆外内存泄露的问题?是不是也苦恼过如何排查?

下面就给大家介绍一个在dble中排查堆外内存泄露的案例。

现象

有客户在使用dble之后,有一天dble对后端MySQL实例的心跳检测全部超时,导致业务中断,最后通过重启解决。

分析过程

dble 日志

首先当然是分析dble日志。从dble日志中可以发现:

  1. 故障时间点所有后端 MySQL 实例心跳都超时

  2. 日志中出现大量“You may need to turn up page size. The maximum size of the DirectByteBufferPool that can be allocated at one time is 2097152, and the size that you would like to allocate is 4194304”的日志

日志片段:

//心跳超时
2022-08-15 11:40:32.147  WARN [TimerScheduler-0] (com.actiontech.dble.backend.heartbeat.MySQLHeartbeat.setTimeout(MySQLHeartbeat.java:251)) - heartbeat to [xxxx:3306] setTimeout, previous status is 1
 
  
// 堆外内存可能泄露的可疑日志
2022-08-15 11:40:32.153  WARN [$_NIO_REACTOR_BACKEND-20-RW] (com.actiontech.dble.buffer.DirectByteBufferPool.allocate(DirectByteBufferPool.java:76)) - You may need to turn up page size. The maximum size of the DirectByteBufferPool that can be allocated at one time is 2097152, and the size that you would like to allocate is 4194304

通过上面的日志猜测:

  1. 所有MySQL 实例超时是很特殊的,可能是由于故障时间发生了长时间停顿的gc

  2. 而长时间停顿的gc可能是由于堆外内存不够,大量的业务流量涌进堆内存中,从而导致频繁的gc

验证猜想

为了验证上面的猜想,获取了dble机器的相关监控来看。

故障时 dble 机器的内存图:

可以看到确实存在短时间攀升。而 dble cpu 当时的使用率也很高。

再来看 dble 中 free buffer的监控图(这个指标是记录dble中Processor的堆外内存使用情况的):

从图中可以看到,从dble启动后堆外内存呈现递减的趋势。

通过上面的监控图,基本可以确认故障发生时的时序关系:

堆外内存长期呈现递减的趋势,堆外内存耗尽之后,在dble中会使用堆内存存储网络报文。

当业务流量比较大时,堆内存被迅速消耗,从而导致频繁的fullgc。这样dble来不及处理MySQL实例心跳的返回报文,就引发了生产上的一些列问题。

堆外内存泄露分析

从上面的分析来看,根因是堆外内存泄露,因此需要排查dble中堆外内存泄露的点。

考虑到dble中分配和释放堆外内存的操作比较集中,采用了btrace 对分配和释放的方法进行了采集。

btrace 脚本

该脚本主要记录分配和释放的对外内存的内存地址。

运行此脚本后,对程序的性能有 10% - 20% 的损耗,且日志量较大,由于堆外内存呈长期递减的趋势,因此只采集了2h的日志进行分析:

package com.actiontech.dble.btrace.script;
 
 
import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;
import sun.nio.ch.DirectBuffer;
 
import java.nio.ByteBuffer;
 
@BTrace(unsafe = true)
public class BTraceDirectByteBuffer {
 
    private BTraceDirectByteBuffer() {
    }
 
    @OnMethod(
            clazz = "com.actiontech.dble.buffer.DirectByteBufferPool",
            method = "recycle",
            location = @Location(Kind.RETURN)
    )
    public static void recycle(@ProbeClassName String pcn, @ProbeMethodName String pmn, ByteBuffer buf) {
        String threadName = BTraceUtils.currentThread().getName();
        // 排除一些线程的干扰
        if (!threadName.contains("writeTo")) {
            String js = BTraceUtils.jstackStr(15);
            if (!js.contains("heartbeat") && !js.contains("XAAnalysisHandler")) {
                BTraceUtils.println(threadName);
                if (buf.isDirect()) {
                    BTraceUtils.println("r:" + ((DirectBuffer) buf).address());
                }
                BTraceUtils.println(js);
            }
        }
    }
 
    @OnMethod(
            clazz = "com.actiontech.dble.buffer.DirectByteBufferPool",
            method = "allocate",
            location = @Location(Kind.RETURN)
    )
    public static void allocate(@Return ByteBuffer buf) {
        String threadName = BTraceUtils.currentThread().getName();
        // 排除一些线程的干扰
        if (!threadName.contains("writeTo")) {
            String js = BTraceUtils.jstackStr(15);
            // 排除心跳等功能的干扰
            if (!js.contains("heartbeat") && !js.contains("XAAnalysisHandler")) {
                BTraceUtils.println(threadName);
                if (buf.isDirect()) {
                    BTraceUtils.println("a:" + ((DirectBuffer) buf).address());
                }
                BTraceUtils.println(js);
            }
        }
    }
 
}

分析采集的btrace日志

采集命令:

$ btrace -o 日志的路径 -u 11735 /path/to/BTraceDirectByteBuffer.java

过滤出分配的堆外内存的地址:

$ grep '^a:' /tmp/142-20-dble-btrace.log > allocat.txt
$ sed 's/..//' allocat.txt > allocat_addr.txt # 删除前两个字符

过滤出释放的堆外内存的地址:

$ grep '^r:' /tmp/142-20-dble-btrace.log > release.txt
$ sed 's/..//' release.txt > release_addr.txt # 删除前两个字符

此时取两个文件的差集:

$ sort allocat_addr.txt release_addr.txt | uniq -u > res.txt

这样 res.txt 得到的是仅仅分配而没有释放的堆外内存(可能会有不准确)

从中任选几个堆外内存的 address,查看堆栈。排除掉误记录的堆栈后,出现最多的堆栈如下:

complexQueryExecutor176019
a:139999811142058
com.actiontech.dble.buffer.DirectByteBufferPool.allocate(DirectByteBufferPool.java:82)
com.actiontech.dble.net.connection.AbstractConnection.allocate(AbstractConnection.java:395)
com.actiontech.dble.backend.mysql.nio.handler.query.impl.OutputHandler.<init>(OutputHandler.java:51)
com.actiontech.dble.services.factorys.FinalHandlerFactory.createFinalHandler(FinalHandlerFactory.java:28)
com.actiontech.dble.backend.mysql.nio.handler.builder.HandlerBuilder.build(HandlerBuilder.java:90)
com.actiontech.dble.server.NonBlockingSession.executeMultiResultSet(NonBlockingSession.java:608)
com.actiontech.dble.server.NonBlockingSession.lambda$executeMultiSelect$55(NonBlockingSession.java:670)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
java.lang.Thread.run(Thread.java:748)

review 代码

在通过btrace知道了dble中的泄露点之后,下面就回到dble的代码中 review 代码。

首先通过上面的堆栈定位到下面的代码:

// com/actiontech/dble/backend/mysql/nio/handler/builder/HandlerBuilder.java
public RouteResultsetNode build(boolean isHaveHintPlan2Inner) throws Exception {
    TraceManager.TraceObject traceObject = TraceManager.serviceTrace(session.getShardingService(), "build&execute-complex-sql");
    try {
        final long startTime = System.nanoTime();
        BaseHandlerBuilder builder = getBuilder(session, node, false);
        DMLResponseHandler endHandler = builder.getEndHandler();
        // 泄露点在这,dble 会创建 OutputHandler实例,OutputHandler会分配堆外内存
        DMLResponseHandler fh = FinalHandlerFactory.createFinalHandler(session);
        endHandler.setNextHandler(fh);
         
        ...
 
        RouteResultsetNode routeSingleNode = getTryRouteSingleNode(builder, isHaveHintPlan2Inner);
        if (routeSingleNode != null)
            return routeSingleNode;
 
 
        HandlerBuilder.startHandler(fh);
        session.endComplexExecute();
        long endTime = System.nanoTime();
        LOGGER.debug("HandlerBuilder.build cost:" + (endTime - startTime));
        session.setTraceBuilder(builder);
    } finally {
        TraceManager.finishSpan(session.getShardingService(), traceObject);
    }
    return null;
}
  
  
// com/actiontech/dble/backend/mysql/nio/handler/query/impl/OutputHandler.java
public OutputHandler(long id, NonBlockingSession session) {
    super(id, session);
    session.setOutputHandler(this);
    this.lock = new ReentrantLock();
    this.packetId = (byte) session.getPacketId().get();
    this.isBinary = session.isPrepared();
    // 分配堆外内存
    this.buffer = session.getSource().allocate();
}

通过上面的代码可以判断在构造复杂查询执行链的时候会分配堆外内存。

问题到这其实还是没有解决,上述代码仅仅找到了堆外内存分配的地方,堆外内存没有释放仍然有以下几种可能:

  1. 程序bug导致复杂查询未下发,从而执行链被丢弃而没有回收buffer
  2. 程序下发了,由于未知bug导致没有释放buffer

dble 中复杂查询的下发和执行都是异步调用并且逻辑链比较复杂,因此很难通过review代码的方式确认是哪种情况导致。

那如何进一步缩小范围呢?

堆内存dump

既然堆外内存泄露的比较快,平常状态下的dump 文件中应该可以找到异常的没有被回收的OutputHandler实例。

在dble 复杂查询的执行链中,OutputHandler 实例的生命周期往往伴随着BaseSelectHandler,因此是否可以通过异常OutputHandler的BaseSelectHandler来确定复杂查询有没有下发来进一步缩小范围。

通过现场收集到的异常OutputHandler中buffer的状态是:

正常写出的OutputHandler中buffer的状态是:

找到的异常的OutputHandler的BaseSelectHandler中状态值:

可以看出其中的状态值都是初始值,可以认为,异常的OutputHandler执行链没有被执行就被丢弃了。

这样范围被进一步缩小,此次堆外内存泄露是由于程序bug导致复杂查询的执行链被丢弃而导致的。

重新回到代码中,review 下发复杂查询之前和构造之后的代码:

// com/actiontech/dble/backend/mysql/nio/handler/builder/HandlerBuilder.java
public RouteResultsetNode build(boolean isHaveHintPlan2Inner) throws Exception {
    TraceManager.TraceObject traceObject = TraceManager.serviceTrace(session.getShardingService(), "build&execute-complex-sql");
    try {
        final long startTime = System.nanoTime();
        BaseHandlerBuilder builder = getBuilder(session, node, false);
        DMLResponseHandler endHandler = builder.getEndHandler();
        // 泄露点在这,dble 会创建 OutputHandler,OutputHandler会分配堆外内存
        DMLResponseHandler fh = FinalHandlerFactory.createFinalHandler(session);
        endHandler.setNextHandler(fh);
         
        ...
 
        RouteResultsetNode routeSingleNode = getTryRouteSingleNode(builder, isHaveHintPlan2Inner);
        if (routeSingleNode != null)
            return routeSingleNode;
 
        // 下发复杂查询,review 之前的代码
        HandlerBuilder.startHandler(fh);
        session.endComplexExecute();
        long endTime = System.nanoTime();
        LOGGER.debug("HandlerBuilder.build cost:" + (endTime - startTime));
        session.setTraceBuilder(builder);
    } finally {
        TraceManager.finishSpan(session.getShardingService(), traceObject);
    }
    return null;
}

review 到 startHandler 的时候,上一个语句 return routeSingleNode 引起了我的注意。

按照逻辑,岂不是如果符合条件 routeSingleNode != null ,就不会执行 startHandler,而直接返回了。而且执行链的作用域在本方法内,不存在方法外的回收操作,这不就满足了未下发而直接返回的条件了。

至此,泄露的原因找到了。

修复

修复的话,在OutputHandler中,不采取预分配buffer,而是使用到的时候才会进行分配。

总结

到这里,整个堆外内存泄露的排查就结束了。希望对大家有帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/91573.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenFace Win10 运行和抽离部分代码

需求&#xff1a;提取出OpenFace中的GazeAnaLyser 中的部分代码往一个写好的接口里面放&#xff0c;主要实现提取面部的所有关键点&#xff0c;估计出视线的功能&#xff1b; 一&#xff0c;openface的安装与使用 在win10上把openface跑起来这个链接够用了&#xff0c;这里主要…

非零基础自学Golang 第5章 流程控制 5.2 循环控制

非零基础自学Golang 文章目录非零基础自学Golang第5章 流程控制5.2 循环控制5.2.1 for循环5.2.2 break 跳出循环5.2.3 continue 继续循环第5章 流程控制 5.2 循环控制 5.2.1 for循环 Go语言中的循环逻辑通过for关键字实现。不同于其他编程语言&#xff0c;Go语言没有while关…

NeurIPS2022 | OmniVL: 用于Image-Language和Video-Language任务的通用模型

原文标题&#xff1a;OmniVL: One Foundation Model for Image-Language and Video-Language Tasks 论文链接&#xff1a;OmniVL: One Foundation Model for Image-Language and Video-Language Tasks | OpenReview 三模态统一的工作。 一、问题提出 旨在设计一个全视觉语言…

正点原子基于库和寄存器建立keil的工程文件简单理解(不是具体步骤)

下载mdk 个人上传免费的pdf:https://download.csdn.net/download/weixin_43794311/87232741&#xff0c;或直接到正点原子官网下载 https://www.keil.com/download/ 两种建立方式的本质理解 库函数是有人基于寄存器已经完成对寄存器的设置&#xff0c;只要根据函数参数意义&…

【Uni-App】vscode 开发uni-app 配置eslint、prettier 实现代码检查和代码自动格式化

目录一&#xff1a;前言二、利用HBuilderX创建uni-app项目三、配置代码检查和代码自动格式化1. 在vscode中打开项目2. 创建package.json3. 添加eslint、prettier相关依赖4. 配置.eslintrc.js5. 配置.prettierrc.json6. 配置.editorconfig7. 配置.eslintignore如果以上还不生效&…

USB——域,包,事务及传输

USB 域&#xff0c;包&#xff0c;事务及传输 域组成包包组成事务事务组成传输 字节序 LSB 概览 域通常来说有八个 SYNCPIDADDRENDP&#xff1a;FRAMEDATACRCEOP 多个域组成包&#xff0c;USB 的包分为四大类 token&#xff1a;令牌包data&#xff1a;数据包handshake&…

centos7安装samba

关闭防火墙 [rootlocalhost ~]# systemctl stop firewalld.service [rootlocalhost ~]# systemctl disable firewalld.service Removed symlink /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service. Removed symlink /etc/systemd/system/basic.target.wants/f…

单目测距(yolo目标检测+标定+测距代码)

单目测距&#xff08;目标检测标定测距&#xff09;** 实时感知本车周围物体的距离对高级驾驶辅助系统具有重要意义&#xff0c;当判定物体与本车距离小于安全距离时便采取主动刹车等安全辅助功能&#xff0c;这将进一步提升汽车的安全性能并减少碰撞的发生。上一章本文完成了目…

Shell 标准输入和输出

无论是要交给程序处理的数据&#xff0c;还是控制脚本的简单命令&#xff0c;都少不了输入和输出。程序要做的第一件事就是处理如同一阴一阳的“输入与输出”。 1 、从文件获取输入 当我们希望向文件输出内容时&#xff0c;我们可以通过符号 > 或 >> 实现。而用代表…

[ Linux ] 线程独立栈,线程分离,Linux线程互斥

目录 1.线程栈 1.1pthread_t 1.2用户级的线程id与内核LWP的对应关系 2.分离线程 2.1 pthread_detch 3.线程互斥 3.1互斥相关概念 3.2 互斥量mutex 3.3 售票系统案例验证共享变量会有问题 3.4 解决抢票问题 3.5互斥量的接口 3.5.1初始化互斥量 3.5.2 销毁互斥量 3…

MSF之ssh_login漏洞

ssh_login准备实操准备 目标机&#xff1a;windows xp 攻击机&#xff1a;kali 工具&#xff1a;metasploit framework 实操 先查看两机器的ip kali的ip为172.17.0.1 xp的ip为192.168.17.130 互相ping一下 没问题。 打开msf search ssh_login 爆出模块 use 0 show o…

vue项目打包流程与反向代理Nginx的使用

目录 前言 参考文章 正文 1.打包前的配置工作 做反向代理的原因&#xff08;Vue项目打包后Proxy失效的问题&#xff09;&#xff1a; 2.Nginx使用 前言 突发灵感想学习下打包&#xff0c;第一反应是学习webpack&#xff0c;翻找一通后发现用不着webpack&#xff0c;因为…

ORB-SLAM2 --- Tracking::UpdateLocalPoints函数

目录 1.函数作用 2.函数流程 3.code 4.函数解析 1.函数作用 更新局部关键点。先把局部地图清空&#xff0c;然后将局部关键帧的有效地图点添加到局部地图中。 2.函数流程 这是更新局部地图中的一个小函数&#xff0c;我们在Tracking::UpdateLocalKeyFrames更新了局部关键…

C++ Reference: Standard C++ Library reference: Containers: map: map: value_comp

C官网参考链接&#xff1a;https://cplusplus.com/reference/map/map/value_comp/ 公有成员函数 <map> std::map::value_comp value_compare value_comp() const;返回值比较对象 返回一个比较对象&#xff0c;该对象可用于比较两个元素&#xff0c;以获得第一个元素的键…

Python 基础| Python 直接赋值、深拷贝和浅拷贝

先看这三个词的意思我觉得菜鸟的总结就很好 Python 直接赋值、浅拷贝和深度拷贝 | 菜鸟教程 直接赋值&#xff1a;其实就是对象的引用&#xff08;别名&#xff09;。 浅拷贝(copy)&#xff1a;拷贝父对象&#xff0c;不会拷贝对象的内部的子对象。 深拷贝(deepcopy)&#xf…

采购过程中会遇到的四种风险!如何管理和控制?

采购风险通常是指采购过程可能出现的一些意外情况&#xff0c;这些情况都会影响采购预期目标的实现。采购风险通常是由管理不善引起的&#xff0c;本文解释了采购过程中会遇到的四种风险&#xff0c;并介绍通过正确实施8Manage SRM采购管理系统&#xff0c;可以有效管理和控制它…

说说Python程序的执行过程

1. Python是一门解释型语言&#xff1f; 我初学Python时&#xff0c;听到的关于Python的第一句话就是&#xff0c;Python是一门解释性语言&#xff0c;我就这样一直相信下去&#xff0c;直到发现了*.pyc文件的存在。如果是解释型语言&#xff0c;那么生成的*.pyc文件是什么呢&…

工程项目管理的特点

工程项目管理是一种只关注工程项目的项目管理。它使用与任何其他类型的项目管理相同的标准方法和流程。这种专业化可能会吸引任何想要进入项目管理领域的具有工程背景的人。 工程项目管理与工程管理 工程管理侧重于对具有以下特点的工程师和工程任务的管理&#xff1a; 1、…

java UDP通信程序DatagramSocket数据接收

在查看本文前 您可以先看看我的文章 java UDP通信程序DatagramSocket数据发送 对UDP有一个基本的了解 然后这里我们就直接看代码了 我们先创建一个包 包下创建两个类 分别是 sendOut 发送类 参考代码如下 import java.io.IOException; import java.net.DatagramPacket; impo…

【Flutter 笔记系列 第 3 篇】如何正确对待Name source files using `lowercase with underscores`

相信很多安装了一些提示插件的小伙伴都遇见过 Name source files using lowercase with underscores flutter 提示 如下图 此时会有两种选择 1.能跑就行&#xff0c;无视它 2.好烦&#xff0c;我也没干什么怎么就提示不规范了。 3.一定是哪里出了问题&#xff0c;我要找到…