Dubbo线程池监控

2,478 阅读4分钟

最近有几个业务方在Provider端经常抛出线程池溢出异常,因此有人提出了一个Dubbo线程池监控需求。实现起来还是比较简单:在已有监控系统上,打点统计到对应的Metric上就好了。

image.png

Dubbo线程池在新版本做了一次改改造(具体是哪个版本不是很清楚,目前我的版本是2.7.8,在本文中就以这个为分界线吧),在2.7.8之前,对应的线程池会存到DataStore中,DataStore是一个SPI接口,对应的默认实现类为SimpleDataStore;从2.7.8开始,线程池相关信息已经不放在DataStore中了,对应的接口是ExecutorRepositoryExecutorRepository也是一个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做线程池监控

  1. 通过SPI拿到DataStore,因为DataStore里面缓存了所有线程池相关的信息
  2. 开一个定时任务,定时采集线程池信息上报的监控系统

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();
}
  1. 主要关注的就是createExecutorIfAbsentgetExecutor方法,即 什么时候存什么时候取
  2. 存 取对应的线程池的时候,基于参数URLURL上有有几个核心的参数: 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;
}

代码还是比较简单,其中有几点需要注意的:

  1. 在2.7.8中,消费端线程池模型做了改造,经过我测试下来,消费端的线程池虽然会初始化,但在后续已经用不上了。具体请参考官网的:消费端线程池模型改造
  2. 存 取对应的线程池的时候,都需要URL参数
  3. 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 获取对应的线程池