这是我参与「第四届青训营 」笔记创作活动的的第3天
今天我们要学习的,在异步系统中流量控制和反向压力的问题。
如下面这部分代码
public static ExecutorService createExecutor(int nThreads, String threadNamePrefix) {
return Executors.newFixedThreadPool(nThreads, threadNameThreadFactory(threadNamePrefix));
}
final private Executor decoderExecutor = createExecutor(2, "decoder");
final private Executor ectExecutor = createExecutor(8, "ect");
final private Executor senderExecutor = createExecutor(2, "sender");
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest req) {
CompletableFuture
.supplyAsync(() -> this.decode(ctx, req), this.decoderExecutor)
.thenApplyAsync(e -> this.doExtractCleanTransform(ctx, req, e), this.ectExecutor)
.thenApplyAsync(e -> this.send(ctx, req, e), this.senderExecutor);
}
从上面的代码可以看出,我们在进行请求处理时,采用了 CompletableFuture 类提供的异步执行框架。在整个执行过程中,请求的处理逻辑都是提交给每个步骤各自的执行器,来进行处理,比如 decoderExecutor、ectExecutor 和 senderExecutor。
仔细分析下这些执行器你就会发现,在上面异步执行的过程中,没有任何阻塞的地方。只不过每个步骤都将它要处理的任务,存放在了执行器的任务队列中。每个执行器,如果它处理得足够快,那么任务队列里的任务都会被及时处理。这种情况下不存在什么问题。
但是,一旦有某个步骤处理的速度比较慢,比如在图 1 中,process 的速度比不上 decode 的速度,那么,消息就会在 process 的输入队列中积压。而由于执行器的任务队列,默认是非阻塞且不限容量的。这样,任务队列里积压的任务,就会越来越多。终有一刻,JVM 的内存会被耗尽,然后抛出 OOM 异常,程序就退出了。
所以,为了避免 OOM 的问题,我们必须对上游输出给下游的速度做流量控制。那怎么进行流量控制呢?
一种方式,是严格控制上游的发送速度。比如,控制上游每秒钟只能发送 1000 条消息。这种方法是可行的,但是非常低效。如果实际下游每秒钟能够处理 2000 条消息,那么,上游每秒钟发送 1000 条消息,就会使得下游一半的性能没有发挥出来。如果下游因为某种原因,性能降级为每秒钟只能处理 500 条消息,那么在一段时间后,同样会发生 OOM 问题。
所以,我们该如何进行流量控制呢?这里有一种更优雅的方法,也就是反向压力。
反向压力原理
在反向压力的方案中,上游能够根据下游的处理能力,动态地调整输出速度。当下游处理不过来时,上游就减慢发送速度,当下游处理能力提高时,上游就加快发送速度。
当下游的消息订阅者,从上游的消息发布者接收消息前,会先通知消息发布者自己能够接收多少消息。然后消息发布者就按照这个数量,向下游的消息订阅者发送消息。这样,整个消息传递的过程都是量力而行的,就不存在上下游之间因为处理速度不匹配,而造成的 OOM 问题了。
目前,一些主流的异步框架都开始支持 Reactive Streams 标准,比如 RxJava、Reactor、Akka Streams、Vert.x 等。
实现反向压力
现在,我们回到 Netty 数据采集服务器。那究竟该怎样为这个服务器加上反向压力的功能呢?
前面我们分析了异步执行的过程,之所以会出现 OOM 问题,主要还是因为,接收线程在接收到新的请求后,触发了一系列任务。这些任务都会被存放在任务队列中,并且这些任务队列,都是非阻塞且不限容量的。
因此,要实现反向压力的功能,只需要从两个方面来进行控制。
-
其一是,执行器的任务队列,它的容量必须是有限的。
-
其二是,当执行器的任务队列已经满了时,就阻止上游继续提交新的任务,直到任务队列,重新有新的空间可用为止。
先到这吧。