【响应式编程】Schedulers工程实践之Hook&Decorator装饰器

488 阅读6分钟

原创:临虹路365号(微信公众号ID:codegod365),欢迎分享,转载请保留出处。

不同于以往的java库,Reactor包是一个相对更关注工程实用性的,除了核心的功能外,通常会暴漏一些可扩展的能力,通常以hook的形式存在。因为响应式编程是异步非阻塞的,为了方便编程,Reactor提供了方便debug、打印日志等hook(钩子)。Schedulers也不例外,暴露了两种扩展方式,一个是onScheduleHook,另一个是addExecutorServiceDecorator方法。对于decorator方式,已经内置了用于获取metrics的SchedulerMetricDecorator类。

onScheduleHook

如名字所言,比较好理解,就是对Scheduler增加hook的方法,我们先看看怎么增加hook。

public static void onScheduleHook(String key, Function<Runnable, Runnable> decorator) {
    synchronized (onScheduleHooks) {
        onScheduleHooks.put(key, decorator);
        Function<Runnable, Runnable> newHook = null;
        for (Function<Runnable, Runnable> function : onScheduleHooks.values()) {
            if (newHook == null) {
                newHook = function;
            }
            else {
                newHook = newHook.andThen(function);
            }
        }
        onScheduleHook = newHook;
    }
}

通过调用静态方法onScheduleHook,即可新增hook,每次新增,都会重新遍历所有的hook生成一遍新的hook,即onScheduleHook = newHook。至于需要传入一个key,主要是为了后续删除hook方便,可以指定key来删除特定的hook,具体可以参见resetOnScheduleHook(String key)方法。

onScheduleHook这个静态变量在什么时候会被使用呢?

通过源码分析,所有的Scheduler类最终都会调用Schedulers类的schdule相关的静态方法来最终执行,例如directSchedule方法:

static Disposable directSchedule(ScheduledExecutorService exec,
        Runnable task,
        @Nullable Disposable parent,
        long delay,
        TimeUnit unit) {
    task = onSchedule(task); //进行hook apply
    SchedulerTask sr = new SchedulerTask(task, parent);
    Future<?> f;
    if (delay <= 0L) {
        f = exec.submit((Callable<?>) sr);
    }
    else {
        f = exec.schedule((Callable<?>) sr, delay, unit);
    }
    sr.setFuture(f);

    return sr;
}

每一个schedule的方法内,第一个调用的就是onSchedule方法,其内部就是对即将执行的runnable加入hook,即:

public static Runnable onSchedule(Runnable runnable) {
    Function<Runnable, Runnable> hook = onScheduleHook;
    if (hook != null) {
        return hook.apply(runnable);
    }
    else {
        return runnable;
    }
}

hook使用举例

那hook怎么使用呢? 这里,我们以一个简单的例子来演示一下。我们知道reactor因为是异步非阻塞的,同时 也是声明式编程,很多时候要理清楚调用关系,以及是哪个线程(池)在执行,都会有困难。因为reactor用的线程池都是Scheduler,所以可以通过增加hook来获取每次执行的异步方法以及是哪个线程池在执行。

Schedulers.onScheduleHook("time cost", runnable -> {
  return () -> {
    long start = System.currentTimeMillis();
    runnable.run();
    System.out.println("runnable name is " + runnable.getClass().getName() +
        ", thread name is " + Thread.currentThread().getName() +
        ", time cost is " + (System.currentTimeMillis() - start));
  };
});

Flux.interval(Duration.ofMillis(100))
    .publishOn(Schedulers.boundedElastic())
    .subscribe(v -> {
          long sleeptime = (long) (Math.random() * 100);
          try {
            Thread.sleep(sleeptime);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println("value is " + v + ", sleep time is " + sleeptime);
    });

输出日志如下:

runnable name is reactor.core.publisher.FluxPublishOn$PublishOnSubscriber, thread name is boundedElastic-1, time cost is 0
runnable name is reactor.core.publisher.FluxInterval$IntervalRunnable, thread name is parallel-1, time cost is 0
value is 0, sleep time is 59
runnable name is reactor.core.publisher.FluxPublishOn$PublishOnSubscriber, thread name is boundedElastic-1, time cost is 63
runnable name is reactor.core.publisher.FluxInterval$IntervalRunnable, thread name is parallel-1, time cost is 0
value is 1, sleep time is 16
runnable name is reactor.core.publisher.FluxPublishOn$PublishOnSubscriber, thread name is boundedElastic-1, time cost is 18
runnable name is reactor.core.publisher.FluxInterval$IntervalRunnable, thread name is parallel-1, time cost is 0
value is 2, sleep time is 27
runnable name is reactor.core.publisher.FluxPublishOn$PublishOnSubscriber, thread name is boundedElastic-1, time cost is 32
runnable name is reactor.core.publisher.FluxInterval$IntervalRunnable, thread name is parallel-1, time cost is 0
value is 3, sleep time is 2
runnable name is reactor.core.publisher.FluxPublishOn$PublishOnSubscriber, thread name is boundedElastic-1, time cost is 3

从日志中可以看到,整个Flux流,先通过FluxInterval按每100ms定时生成value,然后放入queue中,然后每次onNext中由PublishOnSubscriber从队列中拿去,并继续执行往下执行后续的onNext方法。

MDC的异步传递

下面来举个有实际效果的例子。我们知道,在以前命令式编程中,传递Context通常是利用ThreadLocal来传递,即与Thread线程进行绑定,例如log日志中的MDC。遗憾的事,在响应式编程中,这种方式就行不通了,因为经常会出现异步调用的情况,或者说数据流是在不同线程池的不断执行的,例如通过publishOn或者subscribeOn来切换线程池等。为了解决异步context的传递,我们可以用上面类似的hook方式来解决,我们以MDC来举例。

Schedulers.onScheduleHook("mdc", runnable -> {
  //外部线程中的MDC,在执行task前,会在onSchedule方法中执行
  Map<String, String> map = MDC.getCopyOfContextMap();
  return () -> {
      //线程池中执行,在执行runnable前,先对MDC进行赋值
      if (map != null) {
        MDC.setContextMap(map);
      }
      try {
        runnable.run();
      } finally {
        //注意一定要在finally中清除MDC,防止MDC被污染
        MDC.clear();
      }
  };
});

decorator方式

Schedulers除了提供利用hook的方式来暴露扩展点外,还提供了利用decorator的方式,即对每一个“工作线程”进行对应方法的装饰器封装,从而实现类似aspect切面的效果。
我们先看下Scheduler类是怎么添加一个decorator实例。
首先是添加decorator:

public static boolean addExecutorServiceDecorator(String key, BiFunction<Scheduler, ScheduledExecutorService, ScheduledExecutorService> decorator) {
    synchronized (DECORATORS) {
        return DECORATORS.putIfAbsent(key, decorator) == null;
    }
}

通过指定key的方式,添加一个指定decorator封装的方法,传入key的作用则是方便remove时候的定位。

那decorator方法是如何被调用的呢?

public static ScheduledExecutorService decorateExecutorService(Scheduler owner, ScheduledExecutorService original) {
    synchronized (DECORATORS) {
        for (BiFunction<Scheduler, ScheduledExecutorService, ScheduledExecutorService> decorator : DECORATORS.values()) {
            original = decorator.apply(owner, original);
        }
    }

    return original;
}

是通过对Scheduler对象以及其本身的"工作线程"original进行了apply调用,进行生成新的decorator后的"工作线程"ScheduledExecutorService。
所以也就很自然的联想到decorateExecutorService会在Scheduler对象创建“工作线程”时被调用。例如在ParellelScheduler中,在初始化线程池时,会对每一个新生成的“工作线程”进行decorator的封装调用。

void init(int n) {
    ScheduledExecutorService[] a = new ScheduledExecutorService[n];
    for (int i = 0; i < n; i++) {
        //this.get() new 一个新的"工作线程"即ScheduledThreadPoolExecutor
        a[i] = Schedulers.decorateExecutorService(this, this.get());
    }
    EXECUTORS.lazySet(this, a);
}
public ScheduledExecutorService get() {
    ScheduledThreadPoolExecutor poolExecutor = new ScheduledThreadPoolExecutor(1, factory);
    poolExecutor.setMaximumPoolSize(1);
    poolExecutor.setRemoveOnCancelPolicy(true);
    return poolExecutor;
}

所以,从上面可以看出,decorator方式封装的是整个线程池,是对每一个工作线程ScheduledExecutorService的切面封装,其效果是可以封装所有的方法,除了执行任务的schedule外,还包括了shutdown、execute、submit等等所有的方法,进而可以扩展或干预整个线程池的生命周期与执行过程。

Metrics监控举例

我们知道一个程序如果需要具备工程能力,就需要有对应的监控指标能力,线程池作为程序中很重要的一个组件自然也需要进行监控,这样才具备工程能力,这也是Java原生线程池所不具备的。
前面也说过,Reactor从一开始就定位是用于工程的,所以在Reactor包里已经默认实现了关于Scheduler的metric的decarator类 —— SchedulerMetricDecorator

public static void enableMetrics() {
    if (Metrics.isInstrumentationAvailable()) {
        addExecutorServiceDecorator(SchedulerMetricDecorator.METRICS_DECORATOR_KEY, new SchedulerMetricDecorator());
    }
}

通过调用Schedulers的静态方法enableMetrics()即会启动SchedulerMetricDecorator的装饰器。但由于metric的体系是基于OpenMetrics的,所以会通过Metrics.isInstrumentationAvailable()检测是否引入了OpenMetrics,如果没有引入,则仍然不会启动SchedulerMetricDecorator。

下面来看下SchedulerMetricDecorator:

public synchronized ScheduledExecutorService apply(Scheduler scheduler, ScheduledExecutorService service) {
    //this is equivalent to `toString`, a detailed name like `parallel("foo", 3)`
    String schedulerName = Scannable
            .from(scheduler)
            .scanOrDefault(Attr.NAME, scheduler.getClass().getName());

    //we hope that each NAME is unique enough, but we'll differentiate by Scheduler
    String schedulerId =
            seenSchedulers.computeIfAbsent(scheduler, s -> {
                int schedulerDifferentiator = this.schedulerDifferentiator
                        .computeIfAbsent(schedulerName, k -> new AtomicInteger(0))
                        .getAndIncrement();

                return (schedulerDifferentiator == 0) ? schedulerName
                        : schedulerName + "#" + schedulerDifferentiator;
            });

    //we now want an executorId unique to a given scheduler
    String executorId = schedulerId + "-" +
            executorDifferentiator.computeIfAbsent(scheduler, key -> new AtomicInteger(0))
                                  .getAndIncrement();

    Tags tags = Tags.of(TAG_SCHEDULER_ID, schedulerId);

    class MetricsRemovingScheduledExecutorService extends DelegatingScheduledExecutorService {

        MetricsRemovingScheduledExecutorService() {
            super(ExecutorServiceMetrics.monitor(globalRegistry, service, executorId, tags));
        }

        @Override
        public List<Runnable> shutdownNow() {
            removeMetrics();
            return super.shutdownNow();
        }

        @Override
        public void shutdown() {
            removeMetrics();
            super.shutdown();
        }

        void removeMetrics() {
            Search.in(globalRegistry)
                  .tag("name", executorId)
                  .meters()
                  .forEach(globalRegistry::remove);
        }
    }
    return new MetricsRemovingScheduledExecutorService();
}

代码有点长,主要是分为两段,第一段是行数1-25行,是对一个scheduler实例生成一个默认编号并写入tag,其编号默认格式为:scheduler类型 # 类计数 - 实例计数。 然后是第二段,返回生成一个对ScheduledExecutorService的封装MetricsRemovingScheduledExecutorService,其重点是构造函数中的ExecutorServiceMetrics#monitor方法,该类属于OpenMetrics中,其方法是真正的对ScheduledExecutorService进行metric相关的监控,返回TimedScheduledExecutorService类,可以从构造函数中获知大致监控项有:

public TimedScheduledExecutorService(MeterRegistry registry, ScheduledExecutorService delegate,
                                     String executorServiceName, String metricPrefix,
                                     Iterable<Tag> tags) {
    this.registry = registry;
    this.delegate = delegate;
    Tags finalTags = Tags.concat(tags, "name", executorServiceName);
    this.executionTimer = registry.timer(metricPrefix + "executor", finalTags);
    this.idleTimer = registry.timer(metricPrefix + "executor.idle", finalTags);
    this.scheduledOnce = registry.counter(metricPrefix + "executor.scheduled.once", finalTags);
    this.scheduledRepetitively = registry.counter(metricPrefix + "executor.scheduled.repetitively", finalTags);
}
  • executionTimer —— 一个任务的执行花费的时间计时。
  • idleTimer —— 一个任务从创造出来(new)开始后在真正被执行前的在队列等待时间。
  • scheduledOnce —— 记录一次性任务的执行次数
  • scheduledRepetitively —— 记录循环任务的执行次数

关于OpenMetrics的知识与使用方式,可以参见前面的文章:如何在SpringBoot Actuator中自定义Metrics

总结

本文介绍了Schedulers下的原生工程能力,即原生地增加了hook与decarator两种扩展方式。然后以实际工程中的例子 —— 日志中的MDC跨线程传递问题以及线程池的监控指标的工程解决方案进行了举例说明。


原创不易,需要一点正反馈,点赞+收藏+关注,三连走一波~ ❤

如果这篇文章对您有所帮助,或者有所启发的话,请关注公众号【临虹路365号】(微信公众号ID:codegod365),您的支持是我们坚持写作最大的动力。