Fix Dubbo ReferenceCount OOM (#12031)

Co-authored-by: earthchen <earthchen1996@gmail.com>
This commit is contained in:
Albumen Kevin 2023-04-11 18:58:22 +08:00 committed by GitHub
parent bc85a3f29a
commit d4032660d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 259 additions and 178 deletions

View File

@ -0,0 +1,27 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.dubbo.rpc.protocol.dubbo;
import org.apache.dubbo.remoting.exchange.ExchangeClient;
import java.util.List;
public interface ClientsProvider {
List<? extends ExchangeClient> getClients();
void close(int timeout);
}

View File

@ -37,6 +37,7 @@ import org.apache.dubbo.rpc.RpcInvocation;
import org.apache.dubbo.rpc.protocol.AbstractInvoker;
import org.apache.dubbo.rpc.support.RpcUtils;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
@ -46,10 +47,9 @@ import static org.apache.dubbo.common.constants.CommonConstants.DEFAULT_TIMEOUT;
import static org.apache.dubbo.common.constants.CommonConstants.GROUP_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.INTERFACE_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.PATH_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.PAYLOAD;
import static org.apache.dubbo.common.constants.CommonConstants.TIMEOUT_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.VERSION_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.PAYLOAD;
import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_ERROR_CLOSE_CLIENT;
import static org.apache.dubbo.rpc.Constants.TOKEN_KEY;
/**
@ -57,7 +57,7 @@ import static org.apache.dubbo.rpc.Constants.TOKEN_KEY;
*/
public class DubboInvoker<T> extends AbstractInvoker<T> {
private final ExchangeClient[] clients;
private final ClientsProvider clientsProvider;
private final AtomicPositiveInteger index = new AtomicPositiveInteger();
@ -68,13 +68,13 @@ public class DubboInvoker<T> extends AbstractInvoker<T> {
private final int serverShutdownTimeout;
public DubboInvoker(Class<T> serviceType, URL url, ExchangeClient[] clients) {
this(serviceType, url, clients, null);
public DubboInvoker(Class<T> serviceType, URL url, ClientsProvider clientsProvider) {
this(serviceType, url, clientsProvider, null);
}
public DubboInvoker(Class<T> serviceType, URL url, ExchangeClient[] clients, Set<Invoker<?>> invokers) {
public DubboInvoker(Class<T> serviceType, URL url, ClientsProvider clientsProvider, Set<Invoker<?>> invokers) {
super(serviceType, url, new String[]{INTERFACE_KEY, GROUP_KEY, TOKEN_KEY});
this.clients = clients;
this.clientsProvider = clientsProvider;
this.invokers = invokers;
this.serverShutdownTimeout = ConfigurationUtils.getServerShutdownTimeout(getUrl().getScopeModel());
}
@ -87,10 +87,11 @@ public class DubboInvoker<T> extends AbstractInvoker<T> {
inv.setAttachment(VERSION_KEY, version);
ExchangeClient currentClient;
if (clients.length == 1) {
currentClient = clients[0];
List<? extends ExchangeClient> exchangeClients = clientsProvider.getClients();
if (exchangeClients.size() == 1) {
currentClient = exchangeClients.get(0);
} else {
currentClient = clients[index.getAndIncrement() % clients.length];
currentClient = exchangeClients.get(index.getAndIncrement() % exchangeClients.size());
}
try {
boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
@ -143,7 +144,7 @@ public class DubboInvoker<T> extends AbstractInvoker<T> {
if (!super.isAvailable()) {
return false;
}
for (ExchangeClient client : clients) {
for (ExchangeClient client : clientsProvider.getClients()) {
if (client.isConnected() && !client.hasAttribute(Constants.CHANNEL_ATTRIBUTE_READONLY_KEY)) {
//cannot write == not Available ?
return true;
@ -168,14 +169,7 @@ public class DubboInvoker<T> extends AbstractInvoker<T> {
if (invokers != null) {
invokers.remove(this);
}
for (ExchangeClient client : clients) {
try {
client.close(serverShutdownTimeout);
} catch (Throwable t) {
logger.warn(PROTOCOL_ERROR_CLOSE_CLIENT, "", "", t.getMessage(), t);
}
}
clientsProvider.close(serverShutdownTimeout);
} finally {
destroyLock.unlock();
}

View File

@ -19,8 +19,8 @@ package org.apache.dubbo.rpc.protocol.dubbo;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.URLBuilder;
import org.apache.dubbo.common.config.ConfigurationUtils;
import org.apache.dubbo.common.threadpool.manager.FrameworkExecutorRepository;
import org.apache.dubbo.common.url.component.ServiceConfigURL;
import org.apache.dubbo.common.utils.CollectionUtils;
import org.apache.dubbo.common.utils.NetUtils;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.remoting.Channel;
@ -49,16 +49,16 @@ import org.apache.dubbo.rpc.protocol.AbstractProtocol;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.apache.dubbo.common.constants.CommonConstants.GROUP_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.INTERFACE_KEY;
@ -66,7 +66,6 @@ import static org.apache.dubbo.common.constants.CommonConstants.LAZY_CONNECT_KEY
import static org.apache.dubbo.common.constants.CommonConstants.PATH_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.STUB_EVENT_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.VERSION_KEY;
import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_ERROR_CLOSE_CLIENT;
import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_ERROR_CLOSE_SERVER;
import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_FAILED_REFER_INVOKER;
import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_UNSUPPORTED;
@ -105,9 +104,7 @@ public class DubboProtocol extends AbstractProtocol {
* <host:port,Exchanger>
* Map<String, List<ReferenceCountExchangeClient>
*/
private final Map<String, Object> referenceClientMap = new ConcurrentHashMap<>();
private static final Object PENDING_OBJECT = new Object();
private final Map<String, SharedClientsProvider> referenceClientMap = new ConcurrentHashMap<>();
private final AtomicBoolean destroyed = new AtomicBoolean();
@ -237,6 +234,7 @@ public class DubboProtocol extends AbstractProtocol {
return invocation;
}
};
this.frameworkModel = frameworkModel;
}
/**
@ -415,7 +413,7 @@ public class DubboProtocol extends AbstractProtocol {
return invoker;
}
private ExchangeClient[] getClients(URL url) {
private ClientsProvider getClients(URL url) {
int connections = url.getParameter(CONNECTIONS_KEY, 0);
// whether to share connection
// if not configured, connection is shared, otherwise, one connection for one service
@ -428,17 +426,13 @@ public class DubboProtocol extends AbstractProtocol {
: url.getParameter(SHARE_CONNECTIONS_KEY, (String) null);
connections = Integer.parseInt(shareConnectionsStr);
List<ReferenceCountExchangeClient> shareClients = getSharedClient(url, connections);
ExchangeClient[] clients = new ExchangeClient[connections];
Arrays.setAll(clients, shareClients::get);
return clients;
return getSharedClient(url, connections);
}
ExchangeClient[] clients = new ExchangeClient[connections];
for (int i = 0; i < clients.length; i++) {
clients[i] = initClient(url);
}
return clients;
List<ExchangeClient> clients = IntStream.range(0, connections)
.mapToObj((i) -> initClient(url))
.collect(Collectors.toList());
return new ExclusiveClientsProvider(clients);
}
/**
@ -448,106 +442,24 @@ public class DubboProtocol extends AbstractProtocol {
* @param connectNum connectNum must be greater than or equal to 1
*/
@SuppressWarnings("unchecked")
private List<ReferenceCountExchangeClient> getSharedClient(URL url, int connectNum) {
private SharedClientsProvider getSharedClient(URL url, int connectNum) {
String key = url.getAddress();
Object clients = referenceClientMap.get(key);
if (clients instanceof List) {
List<ReferenceCountExchangeClient> typedClients = (List<ReferenceCountExchangeClient>) clients;
if (checkClientCanUse(typedClients)) {
batchClientRefIncr(typedClients);
return typedClients;
}
}
List<ReferenceCountExchangeClient> typedClients = null;
synchronized (referenceClientMap) {
for (; ; ) {
// guarantee just one thread in loading condition. And Other is waiting It had finished.
clients = referenceClientMap.get(key);
if (clients instanceof List) {
typedClients = (List<ReferenceCountExchangeClient>) clients;
if (checkClientCanUse(typedClients)) {
batchClientRefIncr(typedClients);
return typedClients;
} else {
referenceClientMap.put(key, PENDING_OBJECT);
break;
}
} else if (clients == PENDING_OBJECT) {
try {
referenceClientMap.wait();
} catch (InterruptedException ignored) {
}
} else {
referenceClientMap.put(key, PENDING_OBJECT);
break;
}
}
}
try {
// connectNum must be greater than or equal to 1
connectNum = Math.max(connectNum, 1);
// If the clients is empty, then the first initialization is
if (CollectionUtils.isEmpty(typedClients)) {
typedClients = buildReferenceCountExchangeClientList(url, connectNum);
// connectNum must be greater than or equal to 1
int expectedConnectNum = Math.max(connectNum, 1);
return referenceClientMap.compute(key, (originKey, originValue) -> {
if (originValue != null && originValue.increaseCount()) {
return originValue;
} else {
for (int i = 0; i < typedClients.size(); i++) {
ReferenceCountExchangeClient referenceCountExchangeClient = typedClients.get(i);
// If there is a client in the list that is no longer available, create a new one to replace him.
if (referenceCountExchangeClient == null || referenceCountExchangeClient.isClosed()) {
typedClients.set(i, buildReferenceCountExchangeClient(url));
continue;
}
referenceCountExchangeClient.incrementAndGetCount();
}
return new SharedClientsProvider(this, originKey, buildReferenceCountExchangeClientList(url, expectedConnectNum));
}
} finally {
synchronized (referenceClientMap) {
if (typedClients == null) {
referenceClientMap.remove(key);
} else {
referenceClientMap.put(key, typedClients);
}
referenceClientMap.notifyAll();
}
}
return typedClients;
});
}
/**
* Check if the client list is all available
*
* @param referenceCountExchangeClients
* @return true-availablefalse-unavailable
*/
private boolean checkClientCanUse(List<ReferenceCountExchangeClient> referenceCountExchangeClients) {
if (CollectionUtils.isEmpty(referenceCountExchangeClients)) {
return false;
}
// As long as one client is not available, you need to replace the unavailable client with the available one.
return referenceCountExchangeClients.stream()
.noneMatch(referenceCountExchangeClient -> referenceCountExchangeClient == null
|| referenceCountExchangeClient.getCount() <= 0 || referenceCountExchangeClient.isClosed());
}
/**
* Increase the reference Count if we create new invoker shares same connection, the connection will be closed without any reference.
*
* @param referenceCountExchangeClients
*/
private void batchClientRefIncr(List<ReferenceCountExchangeClient> referenceCountExchangeClients) {
if (CollectionUtils.isEmpty(referenceCountExchangeClients)) {
return;
}
referenceCountExchangeClients.stream()
.filter(Objects::nonNull)
.forEach(ReferenceCountExchangeClient::incrementAndGetCount);
protected void scheduleRemoveSharedClient(String key, SharedClientsProvider sharedClient) {
this.frameworkModel.getBeanFactory().getBean(FrameworkExecutorRepository.class)
.getSharedExecutor()
.submit(() -> referenceClientMap.remove(key, sharedClient));
}
/**
@ -648,53 +560,16 @@ public class DubboProtocol extends AbstractProtocol {
serverMap.clear();
for (String key : new ArrayList<>(referenceClientMap.keySet())) {
Object clients = referenceClientMap.remove(key);
if (clients instanceof List) {
List<ReferenceCountExchangeClient> typedClients = (List<ReferenceCountExchangeClient>) clients;
if (CollectionUtils.isEmpty(typedClients)) {
continue;
}
for (ReferenceCountExchangeClient client : typedClients) {
closeReferenceCountExchangeClient(client);
}
}
SharedClientsProvider clients = referenceClientMap.remove(key);
clients.forceClose();
}
PortUnificationExchanger.close();
referenceClientMap.clear();
super.destroy();
}
/**
* close ReferenceCountExchangeClient
*
* @param client
*/
private void closeReferenceCountExchangeClient(ReferenceCountExchangeClient client) {
if (client == null) {
return;
}
try {
if (logger.isInfoEnabled()) {
logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
}
client.close(client.getShutdownWaitTime());
// TODO
/*
* At this time, ReferenceCountExchangeClient#client has been replaced with LazyConnectExchangeClient.
* Do you need to call client.close again to ensure that LazyConnectExchangeClient is also closed?
*/
} catch (Throwable t) {
logger.warn(PROTOCOL_ERROR_CLOSE_CLIENT, "", "", t.getMessage(), t);
}
}
/**
* only log body in debugger mode for size & security consideration.
*

View File

@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.dubbo.rpc.protocol.dubbo;
import org.apache.dubbo.common.logger.ErrorTypeAwareLogger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.remoting.exchange.ExchangeClient;
import java.util.List;
import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_ERROR_CLOSE_CLIENT;
public class ExclusiveClientsProvider implements ClientsProvider {
private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(ExclusiveClientsProvider.class);
private final List<ExchangeClient> clients;
public ExclusiveClientsProvider(List<ExchangeClient> clients) {
this.clients = clients;
}
@Override
public List<ExchangeClient> getClients() {
return clients;
}
@Override
public void close(int timeout) {
for (ExchangeClient client : clients) {
try {
client.close(timeout);
} catch (Throwable t) {
logger.warn(PROTOCOL_ERROR_CLOSE_CLIENT, "", "", t.getMessage(), t);
}
}
}
}

View File

@ -0,0 +1,132 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.dubbo.rpc.protocol.dubbo;
import org.apache.dubbo.common.logger.ErrorTypeAwareLogger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.CollectionUtils;
import java.util.List;
import java.util.Objects;
import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_ERROR_CLOSE_CLIENT;
public class SharedClientsProvider implements ClientsProvider {
private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(SharedClientsProvider.class);
private final DubboProtocol dubboProtocol;
private final String addressKey;
private final List<ReferenceCountExchangeClient> clients;
public SharedClientsProvider(DubboProtocol dubboProtocol, String addressKey, List<ReferenceCountExchangeClient> clients) {
this.dubboProtocol = dubboProtocol;
this.addressKey = addressKey;
this.clients = clients;
}
@Override
public List<ReferenceCountExchangeClient> getClients() {
return clients;
}
public synchronized boolean increaseCount() {
if (checkClientCanUse(clients)) {
batchClientRefIncr(clients);
return true;
}
return false;
}
@Override
public synchronized void close(int timeout) {
for (ReferenceCountExchangeClient client : clients) {
try {
client.close(timeout);
} catch (Throwable t) {
logger.warn(PROTOCOL_ERROR_CLOSE_CLIENT, "", "", t.getMessage(), t);
}
}
if (!checkClientCanUse(clients)) {
dubboProtocol.scheduleRemoveSharedClient(addressKey, this);
}
}
public synchronized void forceClose() {
for (ReferenceCountExchangeClient client : clients) {
closeReferenceCountExchangeClient(client);
}
}
/**
* Check if the client list is all available
*
* @param referenceCountExchangeClients
* @return true-availablefalse-unavailable
*/
private boolean checkClientCanUse(List<ReferenceCountExchangeClient> referenceCountExchangeClients) {
if (CollectionUtils.isEmpty(referenceCountExchangeClients)) {
return false;
}
// As long as one client is not available, you need to replace the unavailable client with the available one.
return referenceCountExchangeClients.stream()
.noneMatch(referenceCountExchangeClient -> referenceCountExchangeClient == null
|| referenceCountExchangeClient.getCount() <= 0 || referenceCountExchangeClient.isClosed());
}
/**
* Increase the reference Count if we create new invoker shares same connection, the connection will be closed without any reference.
*
* @param referenceCountExchangeClients
*/
private void batchClientRefIncr(List<ReferenceCountExchangeClient> referenceCountExchangeClients) {
if (CollectionUtils.isEmpty(referenceCountExchangeClients)) {
return;
}
referenceCountExchangeClients.stream()
.filter(Objects::nonNull)
.forEach(ReferenceCountExchangeClient::incrementAndGetCount);
}
/**
* close ReferenceCountExchangeClient
*
* @param client
*/
private void closeReferenceCountExchangeClient(ReferenceCountExchangeClient client) {
if (client == null) {
return;
}
try {
if (logger.isInfoEnabled()) {
logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
}
client.close(client.getShutdownWaitTime());
// TODO
/*
* At this time, ReferenceCountExchangeClient#client has been replaced with LazyConnectExchangeClient.
* Do you need to call client.close again to ensure that LazyConnectExchangeClient is also closed?
*/
} catch (Throwable t) {
logger.warn(PROTOCOL_ERROR_CLOSE_CLIENT, "", "", t.getMessage(), t);
}
}
}

View File

@ -36,6 +36,7 @@ import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.List;
import static org.apache.dubbo.common.constants.CommonConstants.SHUTDOWN_WAIT_KEY;
@ -172,11 +173,12 @@ class DubboInvokerAvailableTest {
}
private ExchangeClient[] getClients(DubboInvoker<?> invoker) throws Exception {
Field field = DubboInvoker.class.getDeclaredField("clients");
Field field = DubboInvoker.class.getDeclaredField("clientsProvider");
field.setAccessible(true);
ExchangeClient[] clients = (ExchangeClient[]) field.get(invoker);
Assertions.assertEquals(1, clients.length);
return clients;
ClientsProvider clientsProvider = (ClientsProvider) field.get(invoker);
List<? extends ExchangeClient> clients = clientsProvider.getClients();
Assertions.assertEquals(1, clients.size());
return clients.toArray(new ExchangeClient[0]);
}
public class DemoServiceImpl implements IDemoService {

View File

@ -205,7 +205,7 @@ class ReferenceCountExchangeClientTest {
// invoker status is available because the default value of associated lazy client's initial state is true.
Assertions.assertTrue(helloServiceInvoker.isAvailable(), "invoker status unavailable");
// revive: initial the lazy client's exchange client again.
// revive: initial the lazy client's exchange client again.
Assertions.assertEquals("hello", helloService.hello());
destroy();
@ -285,11 +285,12 @@ class ReferenceCountExchangeClientTest {
private List<ExchangeClient> getInvokerClientList(Invoker<?> invoker) {
@SuppressWarnings("rawtypes") DubboInvoker dInvoker = (DubboInvoker) invoker;
try {
Field clientField = DubboInvoker.class.getDeclaredField("clients");
Field clientField = DubboInvoker.class.getDeclaredField("clientsProvider");
clientField.setAccessible(true);
ExchangeClient[] clients = (ExchangeClient[]) clientField.get(dInvoker);
ClientsProvider clientsProvider = (ClientsProvider) clientField.get(dInvoker);
List<? extends ExchangeClient> clients = clientsProvider.getClients();
List<ExchangeClient> clientList = new ArrayList<ExchangeClient>(clients.length);
List<ExchangeClient> clientList = new ArrayList<ExchangeClient>(clients.size());
for (ExchangeClient client : clients) {
clientList.add(client);
}