Dubbo has a memory leak

Posted by Renich on Sun, 02 Jan 2022 08:18:22 +0100

[premise]

The content described in this article is based on Dubbo version 2.7.3

[text]


As shown above, in the previous article com. alibaba. There is a memory leak in fastjson In this article, we explained that there is a memory leak in threadLocals of threads. Look closely at the figure above. There is another place where 662.19KB of memory also exists in threadLocalMap attribute, which is also abnormal

View the internal properties of threadLocalMap

At org apache. dubbo. rpc. The result attribute inside FutureContext holds 651.93KB of memory. The content of this result is actually the return value of the Dubbo interface The FutureContext object is also created when calling the external Dubbo interface

We briefly analyze the process of the next business thread calling the Dubbo interface

When the business thread needs to call the external Dubbo interface, it will create a DefaultFuture. Each DefaultFuture object will have a unique Id corresponding to it, and put this relationship in the Map

private DefaultFuture(Channel channel, Request request, int timeout) {
    this.channel = channel;
    this.request = request;
    this.id = request.getId();
    this.timeout = timeout > 0 ? timeout : channel.getUrl().getPositiveParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
    // Storage ID < - > defaultfuture relationship
    FUTURES.put(id, this);
    CHANNELS.put(id, channel);
}

Since interface calls will have timeout, how to implement this timeout mechanism?

Put a timeout task on the time wheel

// org.apache.dubbo.remoting.exchange.support.DefaultFuture#newFuture
public static DefaultFuture newFuture(Channel channel, Request request, int timeout) {
    final DefaultFuture future = new DefaultFuture(channel, request, timeout);
    // Timeout check
    timeoutCheck(future);
    return future;
}

// org.apache.dubbo.remoting.exchange.support.DefaultFuture#timeoutCheck
private static void timeoutCheck(DefaultFuture future) {
    TimeoutCheckTask task = new TimeoutCheckTask(future.getId());
    future.timeoutCheckTask = TIME_OUT_TIMER.newTimeout(task, future.getTimeout(), TimeUnit.MILLISECONDS);
}

Before me Time wheel in Netty (v3.10.7) This paper introduces the time wheel to detect whether the task has expired or not

The next step is to assemble DefaultFuture and other information into a FutureContext and put it into the ThreadLocalMap of the thread

// org.apache.dubbo.rpc.protocol.dubbo.DubboInvoker#doInvoke
protected Result doInvoke(final Invocation invocation) throws Throwable {
    try {
        boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
        int timeout = getUrl().getMethodPositiveParameter(methodName, TIMEOUT_KEY, DEFAULT_TIMEOUT);
        if (isOneway) {
            boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
            currentClient.send(inv, isSent);
            return AsyncRpcResult.newDefaultAsyncResult(invocation);
        } else {
            AsyncRpcResult asyncRpcResult = new AsyncRpcResult(inv);
            CompletableFuture<Object> responseFuture = currentClient.request(inv, timeout);
            asyncRpcResult.subscribeTo(responseFuture);
            // 
            FutureContext.getContext().setCompatibleFuture(responseFuture);
            return asyncRpcResult;
        }
    }
}

// org.apache.dubbo.rpc.FutureContext
public class FutureContext {
    private static InternalThreadLocal<FutureContext> futureTL = new InternalThreadLocal<FutureContext>() {
        @Override
        protected FutureContext initialValue() {
            return new FutureContext();
        }
    };

    public static FutureContext getContext() {
        return futureTL.get();
    }
}

To sum up, save the id and DefaultFuture into the Map, set the scheduled task, and put the DefaultFuture into the thread ThreadLocalMap, and the thread can be blocked


The get method has been called by the thread

When the Dubbo provider returns data, the Dubbo caller's thread can process the response

As shown in the figure above, the Dubbo thread of the Dubbo caller starts processing the response

// org.apache.dubbo.remoting.exchange.support.DefaultFuture#received(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.exchange.Response, boolean)
public static void received(Channel channel, Response response, boolean timeout) {
    try {
    	// Remove the relationship with ID < - > defaultfuture from the Map
        DefaultFuture future = FUTURES.remove(response.getId());
        if (future != null) {
            Timeout t = future.timeoutCheckTask;
            if (!timeout) {
                // Cancel the timeout task on the time wheel
                t.cancel();
            }
            // 
            future.doReceived(response);
        } else {
            logger.warn("The timeout response finally returned at "
                    + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()))
                    + ", response " + response
                    + (channel == null ? "" : ", channel: " + channel.getLocalAddress()
                    + " -> " + channel.getRemoteAddress()));
        }
    } finally {
        CHANNELS.remove(response.getId());
    }
}

First, remove the relationship of ID < - > defaultfuture from the Map and cancel the timeout task on the time wheel

The next step is to set the response data to DefaultFuture and wake up the previously blocked thread

// org.apache.dubbo.remoting.exchange.support.DefaultFuture#doReceived
private void doReceived(Response res) {
    if (res.getStatus() == Response.OK) {
        this.complete(res.getResult());
    }
}

// java.util.concurrent.CompletableFuture#complete
public boolean complete(T value) {
    // Set the result to DefaultFuture
    boolean triggered = completeValue(value);
    // Wake up blocked thread
    postComplete();
    return triggered;
}

Asynchronous programming is used

The awakened blocking thread can get the set data from DefaultFuture and continue the subsequent business processing


As shown in the figure above, the result value in the FutureContext in the thread ThreadLocalMap of the consumer remains in the thread ThreadLocalMap and will not be released, resulting in memory leakage

Personal site
Language bird

official account

Topics: Java Zookeeper rpc