Disruptor系列(一)— disruptor介绍

本文翻译自Disruptor在github上的wiki文章Introduction,原文可以看这里。


### 一.前言

作为程序猿大多数都有对技术的执着,想在这个方面有所提升。对于优秀的事物保持积极学习的心态,并发编程是开发中一大难题,无论是底层的各种理论还是上层的各种关于并发组件的实现,都非常的晦涩难懂。并发之所以难,就是因为"多"而难以控制。大多数都会使用"锁"这种技术进行控制,但是"锁"这种技术往往和性能又是背道而驰。为了能够将性能最大化,无锁 去锁显然是提升并发性能的关键。前人的智慧还真是慧深莫测,还真在这块领域有所突破,用一种不同以往的设计方式实现了无锁的高性能并发队列 — disruptor

本系列就对disruptor进行学习:

  • diruptor介绍
  • disruptor使用
  • disruptor原理
  • disruptor在开源中的使用

系列将从以上几个方面由浅入深,层层递进式学习。disruptor是open source的,源代码被托管在github上LMAX-Exchange/disruptor

其中wiki有很多非常优秀的文章介绍,是非常不错的学习资源。但都是英文,索性这里将其介绍篇使用篇翻译一遍。


### 二.是什么

LMAX Disruptor是高性能的线程内部通信的消息库。它源于LMAX对并发、性能、非阻塞算法的研究,目前已经成为LMAX的交易基础设施的核心部分。

理解Disruptor是什么的最好方式就是将其与现有的比较好理解的东西比较。Disruptor就相当于Java中BlockingQueue。同队列一样,Disruptor的目的就是在同一进程的线程间进行数据交互。然而Disruptor又提供了一些关键的不同于队列的特征:

  • 广播事件至消费者,并且能遵循消费者依赖关系
  • 为事件预分配内存
  • 可选择的无锁

### 三.核心概念

在理解Disruptor如何工作之前,定义一些普遍存在文档和源代码中的术语是非常有价值的。对于倾向DDD的人而言,它们就是Disruptor领域的无处不在的语言。

  • Ring Buffer(环形缓冲):RingBuffer是Disruptor的主要概念。但从Diruptor 3.0开始,RingBuffer只负责存储和更新Disruptor的数据。一些高级的使用场景,可以被用户替换。

  • Sequence(序列):Disruptor使用Sequence来标记特定组件到达什么位置的。每个消费者(EventProcessor)内部都维护一个Sequence来标记自己消费到的位置。大多数并发代码都是依赖于Sequence的移动,因此Sequence支持大量AtomicLong的特征。实际上两者之间的不同在于Sequence实现了额外的阻止伪共享的功能。

  • Sequencer(序列器):Sequencer是Disruptor中的实际核心。其中两个实现(Single producer,multi producer)全都实现了在生产者与消费者间进行快速正确的传递数据的算法。

  • Sequence Barrier(sequence屏障):Sequence Barrier由Sequencer创建,它包含了来自Sequencer的已经发布的主要sequence的引用,或者包含了依赖的消费者的sequence。同时也包含决定是否有时间可达共消费者处理的逻辑。

  • Wait Strategy(等待策略):Wait Strategy决定了消费者以何种方式等待生产者将事件放进Disruptor。

  • Event(事件):从生产者传到消费者的数据单元。Event通常由用户定义,没有特定的代码规约。

  • Event Processor(事件处理器):处理来自Disruptor的事件的主要事件循环,且包含了消费者的sequence。有一个实现为BatchEventProcessor,包含了高效的事件循环,将不断回调被提供的EventHandler接口。

  • EventHandler(事件处理逻辑):一个接口,由用户实现定义消费者的处理逻辑。

  • Producer(生产者):这也是用于实现的用户代码,调用Disruptor入队事件。没有任何代码规约。

为了能够将这些概念关联起来,即放在一个上下文中表示,下图是个例子,展示LMAX在高性能的核心服务中使用Disruptor的场景:

Note:
原文中没有对这张图进行解释,笔者看到时一脸懵逼,也是在全局的了解了Disruptor后再来研究这张图,才弄明白其含义。这张图可谓是将Disruptor描绘的淋漓尽致。
这里笔者也不对其做过多的介绍,因为要弄懂这张图,势必要对Disruptor有个整体理解。后面介绍原理时再细致解析。
整体而言,Producer生产事件放入RingBuffer中,Consumer利用Sequence Barrier和自身包含的Sequence从RingBuffer中获取可消费的事件。


### 四.特征

1.广播事件

这是队列和Disruptor之间的一个巨大的行为差异。当你有多个消费者监听在相同的Disruptor上是,所有的事件将都被发送给所有的消费者,这点与队列不同,在队列中,一个时间将只发送给一个消费者。Disruptor更偏向用在当有多个无依赖的消费者并行处理相同数据的场景中。在LMAX中有个非常经典的案例,有三个操作journalling(写输入的数据到一个持久化的日志文件),replication(发送输入数据到另一台机器,确保远程备份)和业务逻辑处理。类似Executor风格的事件处理,在这里同时并行的处理不同的事件,可以使用WorkePool。

再看上图,有三个事件处理器(JournalConsumer, ReplicationConsumer和 ApplicationConsumer)监听Disruptor,每一个都将接受Disruptor中所有的可用消息。这个允许三个消费者并行的工作。

2.消费者依赖图

为了支持并行处理行为的实际应用,支持消费者之前的协调是很有必要的。再以上述例子来说,业务逻辑的处理必须在journalling和replication完成之后。我们把这种概念叫做gating,或者更准确的说这个行为特征的超集被称为gating。Disruptor中,Gating发生在两个地方。第一,我们需要确保生产者不要超过消费者。通过调用RingBuffer.addGatingConsumers()增加相关的消费者至Disruptor来完成。第二,就是之前所说的场景,通过构造包含需要必须先完成的消费者的Sequence的SequenceBarrier来实现。

引用上图来说,有三个消费者监听来自RingBuffer的事件。在这个例子中,有一个依赖关系图。ApplicationConsumer依赖JournalConsumer和ReplicationConsumer。这个意味着JournalConsumer和ReplicationConsumer可以自由的并发运行。依赖关系可以看成是从ApplicationConsumer的SequenceBarrier到JournalConsumer和ReplicationConsumer的Sequence的连接。还有一点值得关注,Sequencer与下游的消费者之间的关系。它的角色是确保发布不会包裹RingBuffer。为了做到这点,下游消费者的Sequence没有一个是低于RingBuffer的Sequence而不是RingBuffer的大小。然后使用依赖关系时,一个有趣的优化可以使用。因为ApplicationConsumers的Sequence被保证是低于或者等于JournalConsumer和ReplicationConsumer的Sequence,所以Sequencer只需要检查ApplicationConsumers的Sequence。在更为普遍的应用场景中,Sequencer只需要意识到消费者树中的叶子节点的的Sequence即可。

3.事件预分配

Disruptor的一个目标之一是被用在低延迟的环境中。在低延迟系统中,必须要减少或者去除内存分配。在基于Java的系统中,需要减少由于GC导致的停顿次数(在低延迟的C/C++系统中,由于内存分配器的争用,大量的内存分配也会导致问题)。

为了满足这点,用户可以在Disruptor中为事件预分配内存。在构造期间,EventFactory由用户提供,并将在Disruptor的RingBuffer中为每个条目调用。当发布新的数据至Disruptor时,API允许用户获取已经被构造的对象,以便可以调用方法或者更新在该对象的域。Disruptor将确保这些操作是线程安全。

4.可选择的无锁

对于低延迟的需求又推动了另一个关键性是广泛的使用无锁算法实现Disruptor。所有的内存可见性和正确性都使用内存屏障和CAS操作实现。只仅仅一个场景BlockingWaitStrategy中使用到了lock。这仅仅是为了使用条件,以便在等待新事件到达时停放消耗线程。许多低延迟系统都使用忙等来避免使用Condition造成的抖动。但是忙等的数量将会导致性能的下降,特别是CPU资源严重受限的情况下。例如,在虚拟环境中的Web服务器。

posted @ 2019-01-24 17:09  怀瑾握瑜XI  阅读(1845)  评论(0编辑  收藏  举报