反向压力:异步系统中的OOM问题
导
Lead
语
异步系统中为什么会出现OOM问题?如何避免?揭秘时刻到啦!今天小编带大家了解异步系统中的OOM问题。
在Java编程中,我们经常使用ExecutorService线程池服务类来处理请求异步执行的问题。在这种方案中,请求的具体逻辑实际上是交给了各个步骤的执行器(executor)进行处理。在这整个过程中没有任何阻塞的地方,各个步骤待处理的任务都被隐式地存放在了各个执行器的任务队列中。如果各执行器处理得足够快,那么它们的任务队列都能被及时消费,这样不会存在问题。但是,一旦有某个步骤的处理速度比不上请求接收线程接收新请求的速度,那么必定有部分执行器任务队列中的任务会不停增长。由于执行器任务队列默认是非阻塞且不限容量的,这样当任务队列里积压的任务越来越多时,终有一刻,JVM的内存会被耗尽,抛出OOM系统错误后程序异常退出。
图1: 任务在各个Executor队列中积压
实际上,这也是所有异步系统都普遍存在,而且必须引起我们重视的问题。在纤程里,可以通过指定最大纤程数量来限制内存的使用量,非常自然地控制了内存和流量。但是在一般的异步系统里,如果不对执行的各个环节做流量控制,就很容易出现前面所说的OOM问题。因为当每个环节都不管其下游环节处理速度是否跟得上,不停将其输出塞给下游的任务队列时,只要上游输出速度超过下游处理速度的状况持续一段时间,必然会导致内存不断被占用,直至最终耗尽,抛出OOM灾难性系统错误。
为了避免OOM问题,我们必须对上游输出给下游的速度做流量控制。一种方式是严格控制上游的发送速度,比如每秒控制其只能发1000条消息。但是这种粗糙的处理方案会非常低效。比如如果实际下游能够每秒处理2000条消息,那上游每秒1000条消息的速度就使得下游一半的性能没发挥出来。再比如如果下游因为某种原因性能降级为每秒只能处理500条,那在一段时间后同样会发生OOM问题。
更优雅的一种解决方法是被称为反向压力的方案,即上游能够根据下游的处理能力动态调整输出速度。当下游处理不过来时,上游就减慢发送速度;当下游处理能力提高时,上游就加快发送速度。反向压力的思想,实际上正逐渐成为流计算领域的共识,比如与反向压力相关的标准Reactive Streams正在形成过程中。图2演示了Reactive Streams的工作原理,下游的消息订阅从上游的消息发布者接收消息前,会先通知消息发布者自己能够接收多少消息,消息发布者之后就按照这个数量向下游的消息订阅者发送消息。这样,整个消息传递的过程都是量力而行的,就不存在上下游处理能力不匹配造成的OOM问题了。
图2:Reactive Streams工作原理
那在Java中具体该怎样实现反向压力功能呢?我们继续用ExecutorService线程池来进行说明。由于请求接收线程接收的新请求及其触发的各项任务是被隐式地存放在各步骤的执行器任务队列中,并且执行器默认使用的任务队列是非阻塞和不限容量的。因此要加上反向压力的功能,只需要从两个方面来控制:
第一,执行器任务队列容量必须有限。
第二,当执行器任务队列中的任务已满时,就阻塞上游继续向其提交新的任务,直到任务队列重新有空间可用为止。
图3: 使用容量有限的阻塞队列实现反向压力
按照上面这种思路,我们可以很容易地实现反向压力。图3展示了使用容量有限的阻塞队列实现反向压力的过程,当process这个步骤比decode步骤慢时,位于process前的容量有限的阻塞队列会被塞满。当decode继续要往其写入消息时,就会被阻塞,直到process从队列中取走消息为止。
作者简介:周爽,本硕毕业于华中科技大学,先后在华为2012实验室高斯部门和上海行邑信息科技有限公司工作。开发过实时分析型内存数据库RTANA、华为公有云RDS服务、移动反欺诈MoFA等产品。目前担任公司技术部架构师一职。著有《实时流计算系统设计与实现》一书。
扫码了解详情并购买
↓↓↓点击“阅读原文”直达开学季促销专场