[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
official account