Coup de Grace

内存队列disruptor源码解读 功能点实现

起因是一位同事选型时坚称 log4j2 秒了,强无敌…

于是我翻了翻 apache 发现他们性能测试时的文字游戏

另外就是 Logback 在 appender 丰富度和Metrics 输出能力上还是很不错的, Pivotal 那帮人改方案之前我觉得完全没必要.

真实系统中的瓶颈几乎永远不会是线程间数据交换,不过Disruptor的 paper 真是令人读的神清气爽.

但是Disruptor好东西得读一下.

本文大量翻译来自这里



内存预加载减少 GC

Ring buffer的内存是在启动时预先分配的。Ring buffer要么是一个引用数组,每个元素是指向对象的引用

在使用带有垃圾回收特性语言来设计金融用的交换机(exchange)时,过多的内存分配会带来麻烦,所以,我们基于链表的队列不是一个好的解决方案。
如果用于存储各个阶段交换数据的节点(entries)可以被预先分配内存,那么垃圾回收可以被最小化;
更进一步地,如果节点被统一分配为相同大小的块,那么在遍历节点时,将会以一种缓存友好的方式进行,效率会更高。

这与对标的ArrayBlockingQueue相比就是较大的提升.

RingBuffer 是一个环形的数组,其中的对象在系统启动时即完成初始化,而后对象引用一直不变,只是单个插槽内的 detail/任务 进行变化.


独占缓存行来避免伪共享

伪共享

简而言之,常规教材上告诉我们的JMM 往往没有算上更靠近 CPU 实际核心的多级缓存

在实际的缓存中往往存在着多个缓存值再同一个缓存行中,而单个缓存值的击穿会导致整个缓存行失效.

所以我们可以通过多层继承,在目标值(Disruptor中的 Sequence 是 Long 值)的左右填充若干个8字节的值使得每个缓存值填满单个缓存行.

以此避免伪共享状态的出现.


Teasing Apart the Concerns

如下几个关注点是所有队列实现方式都需要实现的,也是为队列实现定义的接口:
  1) 队列元素的存储;
  2) 队列协调生产者声明下一个需要交换的队列元素的序号;
  3) 队列协调并告知消费者等待的元素已经就绪。

第一点在上一篇 RIngBuffer相关的环形数组啊,取大小原则啊预加载啊之类的已经解释过了

第二点会在接下来的序号栅铬中解决.

第三点则是可以理解成用mailbox 的形式隔离开消费者,也就是第一篇中的 EventProcessorEventHandler.


序号栅格消除锁和CAS

序号栅栏(SequenceBarrier)和序号(Sequence)搭配使用,协调和管理消费者与生产者的工作节奏,避免了锁和CAS的使用。

各个消费者和生产者持有自己的序号,这些序号的变化必须满足如下基本条件:

- 消费者序号数值必须小于生产者序号数值;
- 消费者序号数值必须小于其前置(依赖关系)消费者的序号数值;
- 生产者序号数值不能大于消费者中最小的序号数值,以避免生产者速度过快,将还未来得及消费的消息覆盖。

SequenceBarrier用来在消费者之间以及消费者和RingBuffer之间建立依赖关系。


生产与消费的策略

当生产者将相关的数据拷贝到RingBuffer的位置(entry)中后,生产者提交这个序号,告知消费者这个位置的数据可以消费了。
这里可以不使用CAS操作,而是用简单地自旋直到等待的其他生产者都到达了这个序号便可提交。为了避免覆盖情况发生【注:覆盖是指生产者速度快于消费者速度】,生产者在写入前会检查所有消费者最小的seq,确保写入的seq不会大于这个最小的消费者seq。

消费者在读取元素之前需要等待一个序号,该序号指示了有效的可以读取的元素。
怎么样等待这个序号,有很多种策略。
如果CPU资源比较宝贵,那么消费者可以等待某一个锁的条件变量,由生产者来唤醒消费者。这种方式明显会带来竞争,只适用于CPU资源的稀缺性比系统的延时/吞吐量重要的场景。
另外一种策略是所有消费者线程循环检查游标(cursor),该游标表示RingBuffer中当前有效的可供读取的元素的位置,这种策略使用更多消耗CPU资源来换取低时延。这种方法由于没有用锁和条件变量,因此打破了生产者和消费者之间的竞争依赖关系。

相对应的就是 WaitStrategy:

当消费者等待在SequenceBarrier上时,有许多可选的等待策略,不同的等待策略在延迟和CPU资源的占用上有所不同,可以视应用场景选择:


Batching Effect

当消费者等待RingBuffer中可用的前进游标序号时,如果消费者发现RingBuffer游标自上次检查以来已经前进了多个序号,消费者可以直接处理所有可用的多个序号,而不用引入并发机制, 这样滞后的消费者能够迅速跟上生产者的步伐,从而平衡系统,这一特性是队列(queues)不具有的。

这种类型的批处理增加了吞吐量,同时减少和平滑了延迟。 
根据我们的观察结果,无论负载如何,时延都会维持在一个常数时间值上,直到存储子系统饱和,然后根据小定律(Little’s Law),该曲线是线性的。 

这与在负载增加时观察队列所得到时延“J”曲线效应非常不同。

当生产者节奏快于消费者,消费者可以通过‘批处理效应’快速追赶,即:消费者可以一次性从RingBuffer中获取多个已经准备好的enties,从而提高效率。
public long waitFor(final long sequence) {
        checkAlert();
        long availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, this);
        if (availableSequence < sequence){
            return availableSequence;
        }
        //获取消费者可以消费的最大的可用序号,支持批处理效应,提升处理效率。
        //当availableSequence > sequence时,需要遍历 sequence --> availableSequence,找到最前一个准备就绪,可以被消费的event对应的seq。
        //最小值为:sequence-1
        return sequencer.getHighestPublishedSequence(sequence, availableSequence);
    }

Dependency Graphs

队列从本质上来讲表示一个简单的消费者和生产者之间的只具有一步的管道(pipeline)。如果消费者形成了一个链条,或者一个图状的依赖关系,那么图中的每个阶段之间都会需要一个队列。大量的队列就带来了开销。

在Disruptor设计模式中,生产者和消费者的竞争被很好得隔离开了,因此通过使用一个简单的RingBuffer也可以在消费者之间构造复杂的依赖关系。这样降低了执行时延,从而提高了吞吐量。

一个RingBuffer可以用来处理一个具有复杂的依赖关系图的流程。设计RingBuffer的时候需要特别注意,需要避免消费者之间造成的jvm伪共享。

done.