最近有几个业务方在Provider端经常抛出线程池溢出异常,因此有人提出了一个Dubbo线程池监控需求。实现起来还是比较简单:在已有监控系统上,打点统计到对应的Metric
上就好了。
Dubbo线程池在新版本做了一次改改造(具体是哪个版本不是很清楚,目前我的版本是2.7.8,在本文中就以这个为分界线吧),在2.7.8之前,对应的线程池会存到DataStore
中,DataStore
是一个SPI
接口,对应的默认实现类为SimpleDataStore
;从2.7.8开始,线程池相关信息已经不放在DataStore
中了,对应的接口是ExecutorRepository
,ExecutorRepository
也是一个SPI
接口,对应的默认实现类DefaultExecutorRepository
DataStore
public class SimpleDataStore implements DataStore {
// consuemr/provider -> port -> ExecutorService
private ConcurrentMap<String, ConcurrentMap<String, Object>> data = new ConcurrentHashMap();
}
DataStore
比较简单,就不过多说明了。因为我看的是最新的代码(2.7.8),有关于在哪里将对应的ExecutorService
缓存到DataStore
已经找不到了,但目前还可以找到哪些地方用到了它: ThreadPoolStatusChecker
在介绍ThreadPoolStatusChecker
之前,简单提以下Dubbo的telent
服务治理命令,telent
到对应的Dubbo端口后,可以执行一些服务治理的命令,如: ls 、 invoke 、 status,其中 status 命令就是简单Dubbo服务的一些健康状态,检查项包括:
public class DubboStatusChecker implements InitializingBean {
private StatusChecker memoryStatusChecker;
private StatusChecker serverStatusChecker;
private StatusChecker threadPoolStatusChecker;
private StatusChecker registryStatusChecker;
private StatusChecker springStatusChecker;
private StatusChecker dataSourceStatusChecker;
}
看名字大概就知道了什么意思了,其中就有ThreadPoolStatusChecker
,就是检查线程池的状态,简单看看它的实现
@Activate
public class ThreadPoolStatusChecker implements StatusChecker {
@Override
public Status check() {
DataStore dataStore = ExtensionLoader.getExtensionLoader(DataStore.class).getDefaultExtension();
Map<String, Object> executors = dataStore.get(CommonConstants.EXECUTOR_SERVICE_COMPONENT_KEY);
StringBuilder msg = new StringBuilder();
Status.Level level = Status.Level.OK;
for (Map.Entry<String, Object> entry : executors.entrySet()) {
String port = entry.getKey();
ExecutorService executor = (ExecutorService) entry.getValue();
if (executor instanceof ThreadPoolExecutor) {
ThreadPoolExecutor tp = (ThreadPoolExecutor) executor;
// 只要活跃线程数小于最大线程数,就算OK
boolean ok = tp.getActiveCount() < tp.getMaximumPoolSize() - 1;
Status.Level lvl = Status.Level.OK;
if (!ok) {
level = Status.Level.WARN;
lvl = Status.Level.WARN;
}
if (msg.length() > 0) {
msg.append(";");
}
msg.append("Pool status:").append(lvl).append(", max:").append(tp.getMaximumPoolSize()).append(", core:")
.append(tp.getCorePoolSize()).append(", largest:").append(tp.getLargestPoolSize()).append(", active:")
.append(tp.getActiveCount()).append(", task:").append(tp.getTaskCount()).append(", service port: ").append(port);
}
}
return msg.length() == 0 ? new Status(Status.Level.UNKNOWN) : new Status(level, msg.toString());
}
}
可以看到这里的检验逻辑也比较简单,只要活跃线程数小于最大线程数就算OK
基于DataStore做线程池监控
- 通过SPI拿到DataStore,因为DataStore里面缓存了所有线程池相关的信息
- 开一个定时任务,定时采集线程池信息上报的监控系统
ExecutorRepository
前面已经说过,在2.7.8中DataStore
已经不用了,取而代之的是ExecutorRepository
,简单看看它的接口吧
@SPI("default")
public interface ExecutorRepository {
// 在服务暴露/服务引用 即 AbstractServer#构造函数/AbstractClient#initExecutor 中会用到
ExecutorService createExecutorIfAbsent(URL url);
// 通过URL来获取对应的线程池
ExecutorService getExecutor(URL url);
void updateThreadpool(URL url, ExecutorService executor);
ScheduledExecutorService nextScheduledExecutor();
ScheduledExecutorService getServiceExporterExecutor();
ExecutorService getSharedExecutor();
}
- 主要关注的就是
createExecutorIfAbsent
和getExecutor
方法,即什么时候存
和什么时候取
- 在
存 取
对应的线程池的时候,基于参数URL
,URL
上有有几个核心的参数: side、port、线程池类型、线程数相关的参数
// provider/consumer -> port -> ExecutorService
private ConcurrentMap<String, ConcurrentMap<Integer, ExecutorService>> data = new ConcurrentHashMap<>();
public synchronized ExecutorService createExecutorIfAbsent(URL url) {
// EXECUTOR_SERVICE_COMPONENT_KEY 即为 ExecutorService.class.getName(), 这里代表 提供端
String componentKey = EXECUTOR_SERVICE_COMPONENT_KEY;
if (CONSUMER_SIDE.equalsIgnoreCase(url.getParameter(SIDE_KEY))) {
componentKey = CONSUMER_SIDE;
}
Map<Integer, ExecutorService> executors = data.computeIfAbsent(componentKey, k -> new ConcurrentHashMap<>());
// 端口, 如果应用中只使用了一种协议,一般就只有一个端口
Integer portKey = url.getPort();
ExecutorService executor = executors.computeIfAbsent(portKey, k -> createExecutor(url));
return executor;
}
public ExecutorService getExecutor(URL url) {
String componentKey = EXECUTOR_SERVICE_COMPONENT_KEY;
if (CONSUMER_SIDE.equalsIgnoreCase(url.getParameter(SIDE_KEY))) {
componentKey = CONSUMER_SIDE;
}
Map<Integer, ExecutorService> executors = data.get(componentKey);
if (executors == null) {
return null;
}
Integer portKey = url.getPort();
ExecutorService executor = executors.get(portKey);
return executor;
}
代码还是比较简单,其中有几点需要注意的:
- 在2.7.8中,消费端线程池模型做了改造,经过我测试下来,消费端的线程池虽然会初始化,但在后续已经用不上了。具体请参考官网的:消费端线程池模型改造
存 取
对应的线程池的时候,都需要URL
参数- ExecutorRepository没有暴露对应的接口获取线程池缓存
基于ExecutorRepository做线程池监控
因为我们在存 取
对应的线程池的时候,都需要URL
参数,那我们该如何为获取这个URL
参数?这里分两块来考虑,消费端和提供端:
1、消费端:对消费端的线程池监控没什么意义,特别是经过Dubbo新版本对消费端线程池模型改造之后,目前还不清楚什么情况下会用到消费端初始化的那个线程池。在每次RPC调用的时候,其实都会调用getExecutor
方法,对用的代码在DubboInvoker#doInvoke -> AbstractInvoker#getCallbackExecutor
。所以是不是可以在Filter层做一些处理?其实这个URL
主要涉及到的参数就是:side、port、线程池类型、线程数相关的参数。但没必要这样做。
2、提供端:在服务暴露的时候,会涉及到线程池的初始化,对应的代码在 DubboProtocol#export -> DubboProtocol#openServer
中。所以拿到对应的URL是不是反过来就可以?可以通过DubboProtocol来获取
Collection<Exporter<?>> exporters = DubboProtocol.getDubboProtocol().getExporters()
然后通过 exporters 拿到对应的 url
但这样有个问题,如果此时应用中如果暴露了多个协议,此时只能获取Dubbo协议(PORT)对应的线程池,其它协议的线程池就没有统计到
还有一种思路是: 先获取应用中的所有协议,然后拼接URL,再通过 ExecutorRepository 获取对应的线程池