Java技巧:创建监视友好的ExecutorService

在本文中,我们将扩展具有监视功能的ExecutorService实现。 这种监视功能将帮助我们在实时生产环境中测量多个池参数,即活动线程,工作队列大小等。 它还将使我们能够衡量任务执行时间,成功任务计数和失败任务计数。

监控库

至于监控库,我们将使用Metrics 。 为了简单起见,我们将使用ConsoleReporter ,它将向控制台报告指标。 对于生产级应用程序,我们应该使用高级报告器(即Graphite报告器)。 如果您不熟悉指标,那么建议您阅读入门指南

让我们开始吧。

扩展ThreadPoolExecutor

我们将使用ThreadPoolExecutor作为新类型的基类。 我们将其称为MonitoredThreadPoolExecutor 。 此类将接受MetricRegistry作为其构造函数参数之一–

public class MonitoredThreadPoolExecutor extends ThreadPoolExecutor {
  private final MetricRegistry metricRegistry;

  public MonitoredThreadPoolExecutor(
      int corePoolSize,
      int maximumPoolSize,
      long keepAliveTime,
      TimeUnit unit,
      BlockingQueue<Runnable> workQueue,
      MetricRegistry metricRegistry
  ) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    this.metricRegistry = metricRegistry;
  }

  public MonitoredThreadPoolExecutor(
      int corePoolSize,
      int maximumPoolSize,
      long keepAliveTime,
      TimeUnit unit,
      BlockingQueue<Runnable> workQueue,
      ThreadFactory threadFactory,
      MetricRegistry metricRegistry
  ) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    this.metricRegistry = metricRegistry;
  }

  public MonitoredThreadPoolExecutor(
      int corePoolSize,
      int maximumPoolSize,
      long keepAliveTime,
      TimeUnit unit,
      BlockingQueue<Runnable> workQueue,
      RejectedExecutionHandler handler,
      MetricRegistry metricRegistry
  ) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    this.metricRegistry = metricRegistry;
  }

  public MonitoredThreadPoolExecutor(
      int corePoolSize,
      int maximumPoolSize,
      long keepAliveTime,
      TimeUnit unit,
      BlockingQueue<Runnable> workQueue,
      ThreadFactory threadFactory,
      RejectedExecutionHandler handler,
      MetricRegistry metricRegistry
  ) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    this.metricRegistry = metricRegistry;
  }
}

注册仪表以测量特定于池的参数

量表是一个值的瞬时量度。 我们将使用它来测量不同的池参数,例如活动线程数,任务队列大小等。

在注册仪表之前,我们需要确定如何为线程池计算指标名称。 每个度量标准,无论是仪表,计时器还是仪表,都有一个唯一的名称。 此名称用于标识度量标准来源。 此处的约定是使用点分字符串,该点分字符串通常由要监视的类的完全限定名称构成。

对于我们的线程池,我们将使用其完全限定名称作为指标名称的前缀。 另外,我们将添加另一个名为
poolName,客户端将使用它来指定特定于实例的标识符。

实施这些更改后,该类如下所示–

public class MonitoredThreadPoolExecutor extends ThreadPoolExecutor {
  private final MetricRegistry metricRegistry;
  private final String metricsPrefix;

  public MonitoredThreadPoolExecutor(
      int corePoolSize,
      int maximumPoolSize,
      long keepAliveTime,
      TimeUnit unit,
      BlockingQueue<Runnable> workQueue,
      MetricRegistry metricRegistry,
      String poolName
  ) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    this.metricRegistry = metricRegistry;
    this.metricsPrefix = MetricRegistry.name(getClass(), poolName);
  }

  // Rest of the constructors
}

现在我们准备注册我们的仪表。 为此,我们将定义一个私有方法–

private void registerGauges() {
  metricRegistry.register(MetricRegistry.name(metricsPrefix, "corePoolSize"), (Gauge<Integer>) this::getCorePoolSize);
  metricRegistry.register(MetricRegistry.name(metricsPrefix, "activeThreads"), (Gauge<Integer>) this::getActiveCount);
  metricRegistry.register(MetricRegistry.name(metricsPrefix, "maxPoolSize"), (Gauge<Integer>) this::getMaximumPoolSize);
  metricRegistry.register(MetricRegistry.name(metricsPrefix, "queueSize"), (Gauge<Integer>) () -> getQueue().size());
}

对于我们的示例,我们正在测量核心池大小,活动线程数,最大池大小和任务队列大小。 根据监视要求,我们可以注册更多/更少的量规来测量不同的属性。

现在,所有构造函数都将调用此私有方法–

public MonitoredThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    MetricRegistry metricRegistry,
    String poolName
) {
  super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
  this.metricRegistry = metricRegistry;
  this.metricsPrefix = MetricRegistry.name(getClass(), poolName);
  registerGauges();
}

测量任务执行时间

为了衡量任务执行时间,我们将覆盖ThreadPoolExecutor提供的两个生命周期方法– beforeExecuteafterExecute

顾名思义,执行任务之前,将由执行任务的线程调用beforeExecute回调。 此回调的默认实现不执行任何操作。

同样,在执行每个任务之后,执行任务的线程将调用afterExecute回调。 此回调的默认实现也不执行任何操作。 即使任务抛出未捕获的RuntimeExceptionError ,也会调用此回调。

我们将在beforeExecute覆盖中启动一个Timer ,然后将其用于afterExecute覆盖中以获取总的任务执行时间。 为了存储对Timer的引用,我们将在类中引入一个新的ThreadLocal字段。

回调的实现如下:

public class MonitoredThreadPoolExecutor extends ThreadPoolExecutor {
  private final MetricRegistry metricRegistry;
  private final String metricsPrefix;
  private ThreadLocal<Timer.Context> taskExecutionTimer = new ThreadLocal<>();

  // Constructors

  @Override
  protected void beforeExecute(Thread thread, Runnable task) {
    super.beforeExecute(thread, task);
    Timer timer = metricRegistry.timer(MetricRegistry.name(metricsPrefix, "task-execution"));
    taskExecutionTimer.set(timer.time());
  }

  @Override
  protected void afterExecute(Runnable task, Throwable throwable) {
    Timer.Context context = taskExecutionTimer.get();
    context.stop();
    super.afterExecute(task, throwable);
  }
}

记录由于未捕获的异常而导致的失败任务数

afterExecute回调的第二个参数是Throwable 。 如果非null,则此Throwable引用导致执行终止的未捕获RuntimeExceptionError 。 我们可以使用此信息来部分计算由于未捕获的异常而突然终止的任务总数。

要获得失败任务的总数,我们必须考虑另一种情况。 使用execute方法提交的任务将抛出任何未捕获的异常,并且它将用作afterExecute回调的第二个参数。 但是,执行者服务会吞下使用Submit方法提交的任务。 JavaDoc (重点是我的话)中对此做了清楚的解释-


注意:如果将动作显式地或通过诸如Submit之类的方法包含在任务(例如FutureTask)中,则这些任务对象会捕获并维护计算异常,因此它们不会导致突然终止,并且内部异常不会传递给此方法 如果您想使用此方法捕获两种类型的失败,则可以进一步探查此类情况,例如,在此示例子类中,如果任务被中止,则打印直接原因或潜在异常。 幸运的是,同一文档还为此提供了一种解决方案,即检查可运行对象以查看其是否为Future ,然后获取基础异常。

结合这些方法,我们可以如下修改afterExecute方法–

@Override
protected void afterExecute(Runnable runnable, Throwable throwable) {
  Timer.Context context = taskExecutionTimer.get();
  context.stop();

  super.afterExecute(runnable, throwable);
  if (throwable == null && runnable instanceof Future && ((Future) runnable).isDone()) {
    try {
      ((Future) runnable).get();
    } catch (CancellationException ce) {
      throwable = ce;
    } catch (ExecutionException ee) {
      throwable = ee.getCause();
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt();
    }
  }
  if (throwable != null) {
    Counter failedTasksCounter = metricRegistry.counter(MetricRegistry.name(metricsPrefix, "failed-tasks"));
    failedTasksCounter.inc();
  }
}

计算成功任务的总数

先前的方法也可以用于计算成功任务的总数:完成的任务不会抛出任何异常或错误–

@Override
protected void afterExecute(Runnable runnable, Throwable throwable) {
  // Rest of the method body .....

  if (throwable != null) {
    Counter failedTasksCounter = metricRegistry.counter(MetricRegistry.name(metricsPrefix, "failed-tasks"));
    failedTasksCounter.inc();
  } else {
    Counter successfulTasksCounter = metricRegistry.counter(MetricRegistry.name(metricsPrefix, "successful-tasks"));
    successfulTasksCounter.inc();
  }
}

结论

在本文中,我们研究了对ExecutorService实现的一些监视友好的自定义。 像往常一样,任何建议/改进/错误修复将不胜感激。 至于示例源代码,它已上传到
Github

翻译自: https://www.javacodegeeks.com/2018/05/java-tips-creating-a-monitoring-friendly-executorservice.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值