Disruptor

Disruptor 是一个高性能的线程间消息传递库。

简介

要理解 Disruptor 是什么,最好的办法就是把它与人们理解得很透彻、目的很相似的东西进行比较。 在 Disruptor 的情况下,这将是Java的阻塞队列(BlockingQueue)。 与队列一样,Disruptor 的目的是在同一进程的线程之间移动数据(例如消息或事件)。 然而,Disruptor 提供的一些关键特性将它与队列区分开来。它们是:

  • 多播事件给消费者,消费者依赖图。

  • 预先分配内存的事件。

  • 可以选择无锁。

备注:以下直接进行意译。Disruptor 翻译为粉碎者。

核心组件

在我们了解“破坏者”是如何工作之前,有必要定义一些术语,这些术语将在整个文档和代码中使用。 对于那些有DDD倾向的人来说,可以把它看作是破坏者领域中无处不在的语言。

环缓冲区(Ring Buffer)

环缓冲区通常被认为是破坏者的主要方面,但是从3.0开始,环缓冲区只负责在破坏者中移动的数据(事件)的存储和更新。 对于一些高级的用例,可以完全由用户来代替。

序列(Sequence)

破坏者使用序列作为一种方法来识别特定组件的位置。每个使用者(EventProcessor)和破坏者本身都维护一个序列。 大多数并发代码依赖于这些序列值的移动,因此序列支持AtomicLong的许多当前特性。 实际上,两者之间唯一真正的区别在于,序列包含了额外的功能,以防止序列和其他值之间的错误共享。

音序器(Sequencer)

音序器是破坏者的核心。该接口的两个实现(单个生产者、多个生产者)实现了所有并发算法用于快速、正确地传递生产者和消费者之间的数据。

序列屏障(Sequence Barrier)

序列屏障是由测序器产生的,它包含了从序列序列和任何依赖的消费者序列中主要发布序列的引用。 它包含逻辑,以确定是否有可供消费者处理的事件。

等待策略(Wait Strategy)

等待策略决定了消费者如何等待事件被生产者放入破坏者中。有关可选无锁的更多细节可在有关部分中获得。

事件(Event)

从生产者传递到消费者的数据单元。事件没有特定的代码表示,因为它完全由用户定义。

EventProcessor

处理来自破坏者的事件的主事件循环,并拥有消费者序列的所有权。有一个称为BatchEventProcessor的表示,它包含事件循环的一个有效实现,并将回调到EventHandler接口的一个已使用的实现。

EventHandler

用户实现的接口,代表破坏者的消费者。

生产者(Producer)

这是调用破坏者的用户代码。这个概念在代码中也没有表示。

关系图

破坏者与一组依赖的消费者。

2018-07-02-disruptor-models.png

多播事件(Multicast Events)

这是排队和破坏者之间最大的行为差异。当多个消费者监听同一个破坏者时,所有事件都发布给所有消费者,而队列中单个事件只发送给一个消费者。 当您需要在相同的数据上独立多个并行操作时,干扰器的行为就会被用于这些情况。

LMAX的典型示例是我们有三种操作:日志记录(将输入数据写入一个持久日志文件)、 复制(将输入数据发送到另一台机器以确保数据的远程副本)和业务逻辑(真正的处理工作)。 使用 WorkerPool, 也可以使用执行者类型的事件处理(通过并行地处理不同的事件来找到规模)。 请注意,在现有的中断类的基础上,并没有使用相同的第一个类支持来处理,因此它可能不是实现这一特定目标的最有效的方法。

查看图1所示。可以看到有3个事件处理程序监听破坏者(JournalConsumer、ReplicationConsumer和ApplicationConsumer), 每个事件处理程序都将接收破坏者中可用的所有消息(顺序相同)。这使得每个消费者都可以并行操作。

消费者依赖图

为了支持并行处理行为的实际应用,需要支持消费者之间的协作。 回到上面描述的示例,有必要防止业务逻辑使用者取得进展,直到日志记录和复制使用者完成他们的任务。 我们称这个概念为“门控”,或者更准确地说,这种行为的超集称为“门控”。 门控发生在两个地方。首先,我们需要确保生产者不超过消费者。 通过调用 RingBuffer.addGatingConsumers(). 将相关的消费者添加到破坏者中。 其次,前面提到的情况是通过构造一个序列屏障来实现的,该屏障包含必须首先完成处理的组件的序列。

指图1。有3个使用者正在监听来自环缓冲区的事件。 在这个例子中有一个依赖图。应用程序使用者依赖于日志使用者和复制使用者。 这意味着JournalConsumer和ReplicationConsumer可以相互并行地运行。 从应用程序使用者的序列屏障到日志使用者和复制使用者的序列之间的连接,可以看出依赖关系。 同样值得注意的是,音序器与下游消费者之间的关系。它的一个角色是确保发布不会包装环缓冲区。 为了做到这一点,下游的消费者可能没有一个低于环缓冲区的序列小于环缓冲区的大小的序列。 但是,使用依赖关系图可以进行有趣的优化。 因为ApplicationConsumers序列被保证小于或等于JournalConsumer和ReplicationConsumer(这是依赖关系所保证的), 所以测序器只需要查看ApplicationConsumer的序列。 在更一般的意义上,排序器只需要知道依赖树中的叶子节点的消费者序列。

事件预先配置(Event Preallocation)

破坏者的目标之一是在低延迟环境中启用。在低延迟系统中,有必要减少或删除内存分配。 在基于java的系统中,其目的是减少由于垃圾收集而导致的停机数量(在低延迟的 C/C++系统中,由于内存分配器上的争用,大量内存分配也存在问题)。

为了支持这一点,用户可以预先分配破坏者内部事件所需的存储。 在构建过程中,用户提供 EventFactory,并将调用破坏者的环缓冲区中的每个条目。 当向破坏者发布新数据时,API将允许用户获得构造的对象,以便他们能够调用方法或更新存储对象上的字段。 破坏者提供了保证,只要这些操作正确地实现,这些操作将是安全的。

可选无锁(Optionally Lock-free)

低延迟需求推动的另一个关键实现细节是广泛使用无锁算法来实现破坏者。 所有的内存可见性和正确性保证都是通过内存屏障和/或比较和交换操作实现的。 在 BlockingWaitStrategy 中,只有一个需要实际锁的用例。 这样做完全是为了使用条件,以便在等待新事件到达时可以将一个消费线程停靠。 许多低延迟系统将使用忙碌等待来避免由于使用条件而引起的抖动,但是在系统忙碌等待操作的数量上,可能会导致性能的显著降低, 特别是在CPU资源受到严重限制的情况下。

例如,虚拟环境中的web服务器。