Reactor背压

在响应式的系统中,数据的生产和消费的速度往往是不一致的,当数据的消费速度大于生产速度时,每生产一个数据都能被及时消费,这时候系统可以良好地运行。但当数据的生产数据大于消费速度时,来不及被消费的这部分数据就有可能将系统压垮。因此产生了背压这个概念,用于处理在异步场景下,生产速度大于消费速度的问题。

一、什么是背压

背压是流体力学中借鉴过来的一个概念,它的原义是抵抗所需流体通过管道的阻力或力,引申到软件领域,即是响应式数据流的下游向它的上游反馈其处理能力的一种机制。

背压的出现是为了解决上游组件的过量消息导致下游组件无法及时处理,从而导致系统崩溃的问题。当下游的负载过重时,它应当通过某种机制向上游反馈目前正在遭受过重的负载这个事实,以期上游能够减缓消息的下发,减轻自身的负载。

背压是一种重要的反馈机制,使得系统在高负载的情况下能够良好地运行。反之当下游比较空闲时,可以向上游请求更多的消息进行处理。背压本质上也是一种流量控制机制,上游根据下游反馈的处理能力来决定消息下发的速度。

二、Reactive Streams规范中的背压

Reactive Streams是一个异步流处理的API规范,它的主要目标有2个:

  1. 管理跨异步边界的流数据交换 - 即将元素传递到另一个线程或线程池
  2. 确保接收方不会强制缓冲任意数量的数据,为了使线程之间的队列有界,引入了背压。

Reactive Streams中提供了4种组件:

  • Publisher:产生消息的发布者
  • Subscriber:处理消息的订阅者
  • Subscription:用于绑定Publisher和Subscriber的关系
  • Processor:中间处理组件,既是Publisher,也是Subscriber

Reactive Stream规范指出,Subscriber必须通过Subscription.request(long n)方法请求所需的信号,以接受来自onNext的信号,即Subscriber有责任决定它可以接受多少个元素并将其传达给Publisher。从中可以看出Subscription的request方法是背压实现的关键。

Reactive Stream虽然约定了背压的传递机制,但是并没有限制如何处理从上游发出,来不及被下游处理的元素,这部分内容由规范的实现来决定。

三、Reactor中的背压策略

Reactor框架是Reactive Streams的一个实现,它提供了以下5种背压策略:

  • BUFFER:(默认值)以在下游无法跟上时缓冲所有信号。(这会实现无限缓冲,并可能导致OutOfMemoryError)
  • DROP:如果下游尚未准备好接收信号,则丢弃该信号
  • LATEST:让下游只从上游获取最新信号
  • ERROR:在下游无法跟上时发出错误信号IllegalStateException
  • IGNORE:完全忽略下游背压请求,当下游队列充满时会导致IllegalStateException

下面通过一个简单的案例来试验下上述5种背压策略。

(1)首先定义一个事件的结构Event、事件源EventSource以及一个事件监听器接口EventListener:

复制代码
public class EventSource {
 private List<EventListener> listeners = new ArrayList<>();

    public void registry(EventListener listener) {
 listeners.add(listener);
 }

 public void next(Event event) {
        System.out.println("<<< send event: " + event.getMsg());
 listeners.forEach(l -> l.onNext(event));
 }

 public void complete() {
 listeners.forEach(EventListener::onComplete);
 }

 @Data
    @AllArgsConstructor
 public static class Event {
 private String msg;
 }
}
interface  EventListener {
    void onNext(EventSource.Event event);

    void onComplete();
}

(2)然后定义一个慢消费者SlowSubscriber,消费者中维护了一个线程池,当接受到一个事件后,将该事件放到线程池中执行,实现异步消费:

复制代码
public class SlowSubscriber extends BaseSubscriber<EventSource.Event> {
 private int capacity;
    private int processTime;

    private ThreadPoolExecutor pool;

    public SlowSubscriber(int capacity, int processTime) {
 this.capacity = capacity;
        this.processTime = processTime;
        this.pool = new ThreadPoolExecutor(1, 1, 1,
 TimeUnit.SECONDS, new ArrayBlockingQueue<>(capacity));
 }

 @Override
 protected void hookOnSubscribe(Subscription subscription) {
        System.out.println("========== request " + capacity + " ==========");
 request(capacity);
 }

 @Override
 protected void hookOnNext(EventSource.Event event) {
 pool.submit(() -> {
            System.out.println(">>> receive event: " + event.getMsg());
            try {
                TimeUnit.MILLISECONDS.sleep(processTime);
 } catch (InterruptedException e) {
                e.printStackTrace();
 }
            System.out.println("========== request 1 ==========");
 request(1);
 });
 }

 @Override
 protected void hookOnComplete() {
        System.out.println("Complete");
 }

 @Override
 protected void hookOnError(Throwable throwable) {
        throwable.printStackTrace();
 }

 @Override
 protected void hookOnCancel() {
        System.out.println("Cancel");
 }
}

(3)定义一个快生产者FastPublisher:

复制代码
public class FastPublisher {
 private EventSource source;
    private int processTime;

    public FastPublisher(EventSource source, int processTime) {
 this.source = source;
        this.processTime = processTime;
 }

 public void send() {
 for (int i = 0; i < 10; i++) {
 source.next(new EventSource.Event("event " + i));
            try {
                TimeUnit.MILLISECONDS.sleep(processTime);
 } catch (InterruptedException e) {
                e.printStackTrace();
 }
        }
 source.complete();
 }
}

(4)最后是测试类,分别定义测试以上5种背压策略的方法:

复制代码
public class Test {
 private static int SUBSCRIBER_CAPACITY = 5;
    private static int PUBLISH_TIME = 200;
    private static int CONSUME_TIME = 1000;

    private EventSource source = new EventSource();

    public Flux<EventSource.Event> createFlux(FluxSink.OverflowStrategy backpressure) {
 return Flux.create(sink -> {
 source.registry(new EventListener() {
 @Override
 public void onNext(EventSource.Event event) {
 sink.next(event);
 }

 @Override
 public void onComplete() {
 sink.complete();
 }
            });
 }, backpressure);
 }

 public void testBuffer() {
        createFlux(FluxSink.OverflowStrategy.BUFFER).subscribe(new SlowSubscriber(SUBSCRIBER_CAPACITY, CONSUME_TIME));
        new FastPublisher(source, PUBLISH_TIME).send();

 }

 public void testDrop() {
        createFlux(FluxSink.OverflowStrategy.DROP).subscribe(new SlowSubscriber(SUBSCRIBER_CAPACITY, CONSUME_TIME));
        new FastPublisher(source, PUBLISH_TIME).send();
 }

 public void testError() {
        createFlux(FluxSink.OverflowStrategy.ERROR).subscribe(new SlowSubscriber(SUBSCRIBER_CAPACITY, CONSUME_TIME));
        new FastPublisher(source, PUBLISH_TIME).send();
 }

 public void testLatest() {
        createFlux(FluxSink.OverflowStrategy.LATEST).subscribe(new SlowSubscriber(SUBSCRIBER_CAPACITY, CONSUME_TIME));
        new FastPublisher(source, PUBLISH_TIME).send();
 }

 public void testIgnore() {
        createFlux(FluxSink.OverflowStrategy.IGNORE).subscribe(new SlowSubscriber(SUBSCRIBER_CAPACITY, CONSUME_TIME));
        new FastPublisher(source, PUBLISH_TIME).send();
 }

 public static void main(String[] args) {
 new Test().testBuffer();
        new Test().testDrop();
        new Test().testError();
        new Test().testLatest();
        new Test().testIgnore();
 }
}

接下来看下具体的运行效果:

(1)BUFFER策略的运行结果如下,可以看到所有的事件都能被正常消费,来不及处理的事件会被缓存起来,等待消费者将之前的事件处理完后继续处理:

(2)DROP策略的运行结果如下,可以看到只消费了6个事件,剩下4个事件由于消费者没有能力继续处理而被丢弃了:

其它背压策略的执行结果这里不再一一展示,感兴趣的读者可以自己执行代码看下输出的结果。

四、Reactor如何实现背压

接下来通过分析Reactor的源码来看下它是如何实现背压的。首先看下响应式流的创建方法create,该方法提供了2个入参,一个是Consumer对象,通过FluxSink向下游发送消息,另一个则是背压策略。这部分代码没有复杂的逻辑,只是实例化了FluxCreate类,并给其相关属性进行赋值:

复制代码
public static <T> Flux<T> create(Consumer<? super FluxSink<T>> emitter, OverflowStrategy backpressure) {
 return onAssembly(new FluxCreate<>(emitter, backpressure, FluxCreate.CreateMode.PUSH_PULL));
}
final class FluxCreate<T> extends Flux<T> implements SourceProducer<T> {
 ......
   FluxCreate(Consumer<? super FluxSink<T>> source,
         FluxSink.OverflowStrategy backpressure,
         CreateMode createMode) {
      this.source = Objects.requireNonNull(source, "source");
      this.backpressure = Objects.requireNonNull(backpressure, "backpressure");
      this.createMode = createMode;
   }
   ......
}

然后看下FluxCreate的订阅方法subscribe,该方法需要传入一个Subscriber对象,方法具体实现如下:

复制代码
public void subscribe(CoreSubscriber<? super T> actual) {
   BaseSink<T> sink = createSink(actual, backpressure);

 actual.onSubscribe(sink);
   try {
 source.accept(
 createMode == CreateMode.PUSH_PULL ? new SerializedSink<>(sink) :
                  sink);
 }
 catch (Throwable ex) {
      Exceptions.throwIfFatal(ex);
 sink.error(Operators.onOperatorError(ex, actual.currentContext()));
 }
}

subscribe方法首先调用createSink方法实创建了一个BaseSink对象,通过BaseSink的继承关系可以知道这是一个Subscirption,createSink方法如下,根据背压策略选择具体的Sink进行实例化:

复制代码
static <T> BaseSink<T> createSink(CoreSubscriber<? super T> t,
 OverflowStrategy backpressure) {
 switch (backpressure) {
 case IGNORE: {
 return new IgnoreSink<>(t);
 }
 case ERROR: {
 return new ErrorAsyncSink<>(t);
 }
 case DROP: {
 return new DropAsyncSink<>(t);
 }
 case LATEST: {
 return new LatestAsyncSink<>(t);
 }
 default: {
 return new BufferAsyncSink<>(t, Queues.SMALL_BUFFER_SIZE);
 }
   }
}

这里选择BUFFER策略进行分析,可以看到BufferAsyncSink内部维护了一个无界队列,用于存放来不及消费的消息。其父类BaseSink维护了一个requested属性,该属性便是用来记录下游目前已经请求的消息数量,用于实现背压控制:

复制代码
static final class BufferAsyncSink<T> extends BaseSink<T> {

 final Queue<T> queue;

 Throwable error;
   volatile boolean done; //done is still useful to be able to drain before the terminated handler is executed

 volatile int wip;
 @SuppressWarnings("rawtypes")
 static final AtomicIntegerFieldUpdater<BufferAsyncSink> WIP =
         AtomicIntegerFieldUpdater.newUpdater(BufferAsyncSink.class, "wip");

 BufferAsyncSink(CoreSubscriber<? super T> actual, int capacityHint) {
 super(actual);
      this.queue = Queues.<T>unbounded(capacityHint).get();
 }
   ......
}
static abstract class BaseSink<T> extends AtomicBoolean
 implements FluxSink<T>, InnerProducer<T> {
 ......
   volatile long requested;
 @SuppressWarnings("rawtypes")
 static final AtomicLongFieldUpdater<BaseSink> REQUESTED =
         AtomicLongFieldUpdater.newUpdater(BaseSink.class, "requested");
 ......
 BaseSink(CoreSubscriber<? super T> actual) {
 this.actual = actual;
      this.ctx = actual.currentContext();
 }
}

再回到subscribe方法里,接着便是调用Subscirber的toSubscribe方法启动消费,在上述的示例中传入的Subscirber是自定义的SlowSubscriber,其toSubscribe方法的逻辑很简单,只是通过BufferAsyncSink的request方法告诉上游我可以处理多少消息。subscribe方法的最后则是调用FluxCreate初始化时传入的Consumer开启消息生产的过程。

接着再来看看BufferAsyncSink的request方法是如何实现背压控制的。该方法在其父类BaseSink中实现,具体如下:

复制代码
public final void request(long n) {
 if (Operators.validate(n)) {
      Operators.addCap(REQUESTED, this, n);
 ......
      onRequestedFromDownstream();
 }
}

方法的逻辑比较简单,首先是更新requested属性,将下游请求的消息数量n加到requested中,然后通过抽象方法onRequestedFromDownstream实现具体的背压控制。BufferAsyncSink中的onRequestedFromDownstream方法如下,只是简单地调用了drain方法:

复制代码
void onRequestedFromDownstream() {
   drain();
}

再来看下BufferAsyncSink的next方法,除了将接受到的消息放入队列中外,也是调用drain方法来将消息发送给下游:

复制代码
public FluxSink<T> next(T t) {
   queue.offer(t);
   drain();
   return this;
}

由此可见drain是实现背压策略的核心方法,其方法实现如下:

复制代码
void drain() {
 if (WIP.getAndIncrement(this) != 0) {
 return;
 }

 final Subscriber<? super T> a = actual;
   final Queue<T> q = queue;

   for (; ; ) {
 long r = requested;
      long e = 0L;

      while (e != r) {
 if (isCancelled()) {
            Operators.onDiscardQueueWithClear(q, ctx, null);
            if (WIP.decrementAndGet(this) != 0) {
 continue;
 }
 else {
 return;
 }
         }

 boolean d = done;

 T o = q.poll();

         boolean empty = o == null;

         if (d && empty) {
            Throwable ex = error;
            if (ex != null) {
 super.error(ex);
 }
 else {
 super.complete();
 }
 return;
 }

 if (empty) {
 break;
 }

         a.onNext(o);

 e++;
 }

 if (e == r) {
 if (isCancelled()) {
            Operators.onDiscardQueueWithClear(q, ctx, null);
            if (WIP.decrementAndGet(this) != 0) {
 continue;
 }
 else {
 return;
 }
         }

 boolean d = done;

         boolean empty = q.isEmpty();

         if (d && empty) {
            Throwable ex = error;
            if (ex != null) {
 super.error(ex);
 }
 else {
 super.complete();
 }
 return;
 }
      }

 if (e != 0) {
         Operators.produced(REQUESTED, this, e);
 }

 if (WIP.decrementAndGet(this) == 0) {
 break;
 }
   }
}

忽略中间的一些控制流程,则该方法的主要过程如下:

  1. 通过一个while循环从内部队列中取出消息,并调用Subscriber的onNext方法将该消息发送给Subscriber进行处理,直到发送的消息个数达到记录的requested的数量或者队列为空。
  2. 从requested中减去已发送的消息的数量。

五、背压样例

背压就是流量控制。Reactor提供的背压策略由OverflowStrategy枚举指定:

  • IGNORE:完全忽略下游背压请求。

  • ERROR:当下游无法跟上时,发出IllegalStateException信号。

  • DROP:如果下游没有准备好接收传入信号,则丢弃传入。

  • LATEST:下游将仅获得来自上游的最新信号。

  • BUFFER:如果下游跟不上,缓冲所有信号。

    @Test
    public void test() {
    CountDownLatch countDownLatch = new CountDownLatch(1);
    final int[] a = {0};
    Flux.push(t-> {
    for (int i=0;i<10;i++){
    t.next(a[0]++);
    try {
    TimeUnit.MICROSECONDS.sleep(10);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }
    System.out.println("generate thread:"+Thread.currentThread().getName());
    t.complete();
    }, FluxSink.OverflowStrategy.BUFFER)
    .publishOn(Schedulers.newSingle("publish-thread-"),1)
    .subscribeOn(Schedulers.newSingle("subscribe-thread-"))
    .subscribe(new Subscriber<Object>() {
    private Subscription subscription = null;

    复制代码
                  @Override
                  public void onSubscribe(Subscription subscription) {
                      this.subscription = subscription;
                      subscription.request(1);
                  }
    
                  @Override
                  public void onNext(Object o) {
                      System.out.println(Thread.currentThread().getName()+":消费数据:"+o);
                      try {
                          TimeUnit.MICROSECONDS.sleep(30);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
    
                      this.subscription.request(1);
                  }
    
                  @Override
                  public void onError(Throwable throwable) {
                      System.out.println("出现错误");
                      throwable.printStackTrace();
                      countDownLatch.countDown();
                  }
    
                  @Override
                  public void onComplete() {
                      System.out.println("Complete");
                      countDownLatch.countDown();
                  }
              });
      try {
          countDownLatch.await();
      } catch (InterruptedException e) {
          throw new RuntimeException(e);
      }

    }

Flux.push是发布者,睡眠10毫秒在生产下一个数字,订阅者是睡眠30毫秒才向上游请求数据。来模拟快的发布者,慢的订阅者。同时用publishOn和subscribeOn模拟发布者和订阅者在不同线程。记得publishOn的第二个参数(预取个数设置为1)

Flux.push第二个参数是背压策略。这里设置为FluxSink.OverflowStrategy.BUFFER,执行结果:

复制代码
publish-thread--2:消费数据:0
generate thread:subscribe-thread--1
publish-thread--2:消费数据:1
publish-thread--2:消费数据:2
publish-thread--2:消费数据:3
publish-thread--2:消费数据:4
publish-thread--2:消费数据:5
publish-thread--2:消费数据:6
publish-thread--2:消费数据:7
publish-thread--2:消费数据:8
publish-thread--2:消费数据:9
Complete

所有的数据都消费了。

将Flux.push第二个参数设置为FluxSink.OverflowStrategy.DROP,执行结果:

复制代码
publish-thread--2:消费数据:0
generate thread:subscribe-thread--1
publish-thread--2:消费数据:5
Complete

除了0和5被消费外其他都被丢弃。

将Flux.push第二个参数设置为FluxSink.OverflowStrategy.LATEST,执行结果:

复制代码
publish-thread--2:消费数据:0
generate thread:subscribe-thread--1
publish-thread--2:消费数据:4
publish-thread--2:消费数据:9
Complete

每次向上游请求时都是请求最新的数据。在这里是0,4,9。

将Flux.push第二个参数设置为FluxSink.OverflowStrategy.ERROR,执行结果:

复制代码
publish-thread--2:消费数据:0
出现错误
reactor.core.Exceptions$OverflowException: The receiver is overrun by more signals than expected (bounded queue...)
	at reactor.core.Exceptions.failWithOverflow(Exceptions.java:224)
	at reactor.core.publisher.FluxCreate$ErrorAsyncSink.onOverflow(FluxCreate.java:708)
	at reactor.core.publisher.FluxCreate$NoOverflowBaseAsyncSink.next(FluxCreate.java:673)
	at com.example.FluxTest.lambda$test$71(FluxTest.java:609)
	at reactor.core.publisher.FluxCreate.subscribe(FluxCreate.java:95)
	at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:62)
	at reactor.core.publisher.FluxSubscribeOn$SubscribeOnSubscriber.run(FluxSubscribeOn.java:194)
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:84)
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:37)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base/java.lang.Thread.run(Thread.java:833)

抛出异常:reactor.core.Exceptions$OverflowException: The receiver is overrun by more signals than expected (bounded queue...)

将Flux.push第二个参数设置为FluxSink.OverflowStrategy.IGNORE,执行结果:

复制代码
publish-thread--2:消费数据:0
generate thread:subscribe-thread--1
publish-thread--2:消费数据:1
出现错误
reactor.core.Exceptions$OverflowException: Queue is full: Reactive Streams source doesn't respect backpressure
	at reactor.core.Exceptions.failWithOverflow(Exceptions.java:237)
	at reactor.core.publisher.FluxPublishOn$PublishOnSubscriber.onNext(FluxPublishOn.java:233)
	at reactor.core.publisher.FluxCreate$IgnoreSink.next(FluxCreate.java:639)
	at com.example.FluxTest.lambda$test$71(FluxTest.java:609)
	at reactor.core.publisher.FluxCreate.subscribe(FluxCreate.java:95)
	at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:62)
	at reactor.core.publisher.FluxSubscribeOn$SubscribeOnSubscriber.run(FluxSubscribeOn.java:194)
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:84)
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:37)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base/java.lang.Thread.run(Thread.java:833)

抛出异常:reactor.core.Exceptions$OverflowException: Queue is full: Reactive Streams source doesn't respect backpressure

将Flux.push第二个参数去掉,Reactor还提供了背压的操作来实现背压策略:

  • onBackpressureBuffer()实现了FluxSink.OverflowStrategy.BUFFER
  • onBackpressureDrop()实现了FluxSink.OverflowStrategy.DROP
  • onBackpressureLatest()实现了FluxSink.OverflowStrategy.LATEST
  • onBackpressureError()实现了FluxSink.OverflowStrategy.ERROR

Reactor文档的示意图更加直观:

onBackpressureBuffer,对于来自其下游的request采取"缓存"策略。

onBackpressureDrop,元素就绪时,根据下游是否有未满足的request来判断是否发出当前元素。

onBackpressureLatest,当有新的request到来的时候,将最新的元素发出。

onBackpressureError,当有多余元素就绪时,发出错误信号。

相关推荐
wangchunting2 小时前
数据结构-散列表
java·数据结构·散列表
啥咕啦呛2 小时前
java打卡学习6:集合框架 Collection
java·windows·学习
曹牧2 小时前
Tomcat连接池异常排查
java·tomcat
RisunJan2 小时前
Linux命令-mv(移动或重命名文件和目录)
linux·运维·服务器
cool32002 小时前
Kubernetes集群节点扩容实战-kubeasz
java·开发语言·kubernetes
笑笑先生2 小时前
从接口搬运工到研发控制平面,BFF 到底在解决什么?
前端·架构·node.js
霪霖笙箫2 小时前
「JS全栈AI Agent学习」二、反思、工具使用、规划——让 Agent 从"执行者"变成"自主完成者"
前端·agent·ai编程
前端缘梦2 小时前
Next.js全栈项目部署全流程|从0到1解决数据库、WebSocket、图片上传所有坑
前端·全栈·next.js
www_stdio2 小时前
🚀 从 Event Loop 到 AI Agent:我的 Node.js 全栈进阶之路
前端·node.js·nestjs