Backend Development 11 min read

Investigating Off‑Heap Memory Leak in Dble Using Java NIO and BTrace

This article walks through a real‑world case of off‑heap memory leakage in the Dble proxy, detailing symptom observation, log analysis, monitoring, BTrace instrumentation, code review, and the eventual fix that delays buffer allocation until needed.

Aikesheng Open Source Community
Aikesheng Open Source Community
Aikesheng Open Source Community
Investigating Off‑Heap Memory Leak in Dble Using Java NIO and BTrace

When using Java NIO with Dble, a client experienced a complete timeout of MySQL heartbeat checks, which was temporarily resolved by restarting the service. The root cause was identified as an off‑heap memory leak.

Phenomenon

All backend MySQL instances reported heartbeat timeouts, and the logs contained repeated warnings about exceeding the DirectByteBufferPool size.

Analysis Process

Log inspection revealed messages such as //心跳超时 and 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 . These indicated possible long GC pauses due to insufficient off‑heap memory.

Verification

Monitoring graphs showed a short‑term spike in memory usage and high CPU load on the Dble host, while the free‑buffer metric demonstrated a gradual decline of off‑heap memory after startup, confirming that the leak exhausted the off‑heap pool and forced Dble to allocate on‑heap buffers for network packets.

Off‑Heap Memory Leak Analysis

BTrace was employed to trace allocation and release of off‑heap buffers. The following BTrace script records the addresses of allocated and recycled DirectByteBuffers:

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 {
    @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);
            }
        }
    }
}

Running the script and filtering the logs:

$ btrace -o /path/to/log -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

The resulting res.txt lists addresses that were allocated but never released, pointing to the leak.

Code Review

Stack traces from the leaked addresses highlighted the allocation path:

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.
(OutputHandler.java:51)
...

Reviewing the relevant Dble source revealed that OutputHandler creates a DirectByteBuffer during construction:

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();
    // allocate off‑heap memory
    this.buffer = session.getSource().allocate();
}

The allocation occurs even when the complex query execution chain is later abandoned (e.g., when routeSingleNode != null causes an early return), leaving the buffer unreleased.

Fix

The resolution is to defer buffer allocation until it is actually needed, removing the eager allocation in the OutputHandler constructor.

Conclusion

By combining log inspection, monitoring, BTrace instrumentation, and targeted code review, the off‑heap memory leak in Dble was traced to premature buffer allocation in OutputHandler , and the fix prevents unnecessary off‑heap consumption.

backenddebuggingMemory LeakOff-heap memoryJava NIOBTraceDBLE
Aikesheng Open Source Community
Written by

Aikesheng Open Source Community

The Aikesheng Open Source Community provides stable, enterprise‑grade MySQL open‑source tools and services, releases a premium open‑source component each year (1024), and continuously operates and maintains them.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.