前言
学习Disruptor前,首先要有一个清晰的认识:Disruptor类似于BlockingQueue,但会有诸多不同,最明显的不同是BlockingQueue一个节点只能被一个消费者消费,而Disruptor一个节点可被多个消费者消费。本文主要翻译字Github上Disruptor简介,英文好的同学可以直接查看 原文。本文主要帮助英文不太好的同学学习,尽自己所能、翻译的最贴近原文,易于理解。
1、简介
理解Disruptor是什么的最好方法是将其与理解度很好并且目的相当相似的东西进行比较。 在Disruptor的情况下,这将是Java的BlockingQueue。 与队列类似,Disruptor的目的是在同一进程内的线程之间移动数据(例如消息或事件)。 但是,Disruptor提供的一些关键特性可以将其与队列区分开来。他们是:
- 对消费者的多播事件,以及消费者依赖图;
- 为事件预先分配内存;
- 可选择无锁
2、核心概念
在我们理解Disruptor如何工作之前,有必要定义一些在整个文档和代码中使用的术语。 对于那些有DDD倾向的人来说,可以将其视为Disruptor域的无处不在的语言。
- Ring Buffer(环形缓冲区):Ring Buffer 通常被认为是Disruptor的主要方面,但从Disruptor 3.0开始,Ring Buffer 只负责存储和更新 Disruptor 的数据(事件)。 对于某些高级用例,用户可以完全替代。
- Sequence(序列):Disruptor使用 Sequence 作为识别特定组件在何处的手段。 每个消费者(EventProcessor)都像Disruptor本身一样维护一个Sequence。 大部分并发代码依赖于这些Sequence值的移动,因此Sequence支持AtomicLong的许多当前特性。 事实上,Disruptor 2 和 Disruptor 3之间唯一真正的区别是序列包含额外的功能,以防止序列和其他值之间的错误共享。
- Sequencer(定序器):Sequencer 是Disruptor的真正核心。 这个接口的2个实现(单生产者,多生产者)实现了所有的并发算法,用于在生产者和消费者之间快速正确地传递数据。
- Sequence Barrier(序列屏障):Sequence Barrier 由序列发生器产生,并包含对来自序列发生器的主要发布序列和任何相关消费者的序列的引用。 它包含确定消费者是否有任何事件可供处理的逻辑。
- Wait Strategy(等待策略):Wait Strategy 决定消费者如何等待生产者将事件置于Disruptor中。 更多细节,将在 可选择无锁 的章节中提供了更多详细信息。
- Event(事件):从生产者传递给消费者的数据单位。 Event 没有特定的代码表示,因为它完全由用户定义。
- EventProcessor(时间处理器):用于处理来自Disruptor的事件的主事件循环,并拥有消费者序列的所有权。 有一个称为BatchEventProcessor的表示,它包含一个有效的事件循环实现,并将回调到EventHandler接口的已用提供的实现上。
- EventHandler:由用户实现并代表Disruptor的使用者的接口。
- Producer(生产者):这是调用Disruptor插入事件的用户代码。 这个概念在代码中也没有表示。
为了将这些元素置于上下文中,下面是LMAX如何在其高性能核心服务中使用Disruptor的例子:
图1. 有多个消费者的Disruptor
多播事件
这是队列和Disruptor之间最大的行为差异。当多个用户在同一个Disruptor上侦听时,所有事件都会发布给所有使用者;对应的,队列是一个事件只发送给某一个使用者。 Disruptor的行为旨在用于需要对相同数据进行独立的多个并行操作的情况。 LMAX的典型例子是我们有三个操作,journalling(将输入数据写入持久日志文件),复制(将输入数据发送到另一台机器以确保存在数据的远程副本)以及业务逻辑(真正的处理工作)。 Executor风格的事件处理,通过使用WorkerPool,可以通过并行处理不同事件来查找规模。请注意,它是在现有的Disruptor类的顶部,而不是用相同的第一类支持处理,因此它可能不是实现该特定目标的最有效方式。
从图1中可以看到有3个事件处理器在侦听Disruptor中RingBuffer (JournalConsumer,ReplicationConsumer和ApplicationConsumer),这些事件处理器中的每一个都会收到Disruptor中所有可用的消息(以相同的顺序)。 这允许每个消费者并行操作。
消费者依赖图
为了支持并行处理行为的实际应用,有必要支持消费者之间的协调。 回顾上述示例,必须防止业务逻辑消费者在日程安排和复制消费者完成其任务之前取得进展。 我们称这种概念为门控(Gating),或者更准确地说,这种行为的超级集合的功能称为门控。 门控发生在两个地方:
- 首先,我们需要确保生产者不会超过消费者。 这是通过调用RingBuffer.addGatingConsumers()将相关消费者添加到Disruptor来处理的。
- 其次,之前提到的情况是通过构建包含必须首先完成处理的组件序列的SequenceBarrier来实现的。
图1中有3个消费者正在从环缓冲器中侦听事件。在这个例子中有一个依赖关系图。 ApplicationConsumer依赖于JournalConsumer和ReplicationConsumer。这意味着JournalConsumer和ReplicationConsumer可以相互并行自由运行。依赖关系可以通过从ApplicationConsumer的SequenceBarrier到JournalConsumer和ReplicationConsumer的序列的连接来看到。值得注意的是Sequencer与下游消费者之间的关系。其中一个角色是确保发布不包装环缓冲区。要做到这一点,下游用户可能没有一个序列低于环缓冲区的序列减去环缓冲区的大小。然而,使用依赖关系图可以进行有趣的优化。由于ApplicationConsumers Sequence保证小于或等于JournalConsumer和ReplicationConsumer(这是依赖关系确保的内容),因此Sequencer只需查看ApplicationConsumer的Sequence。在更一般的意义上,Sequencer只需要知道依赖关系树中叶节点的消费者的序列。
事件预分配
Disruptor的目标之一是在低延迟环境下使用。 在低延迟系统中,有必要减少或删除内存分配。 在基于Java的系统中,目的是为了减少由于垃圾收集造成停顿的次数(在低延迟的C / C ++系统中,由于存储器分配器上存在争用而造成的大量内存分配也存在问题)。
为了支持这一点,用户能够预先分配Disruptor事件所需的存储空间。 在构建过程中,EventFactory由用户提供,并将针对Disruptor的 Ring Buffer 中的每个条目进行调用。 将新数据发布到Disruptor时,API将允许用户获取构造的对象,以便它们可以调用该方法或更新该存储对象上的字段。 Disruptor提供保证,只要正确实施这些操作将是并发安全的。
可选择无锁
另一个由低延迟需求推动的关键实现细节是在Disruptor中广泛使用的无锁算法。 所有的内存可见性和正确性都是由于使用内存屏障 和/或 比较和交换操作的实现来保证的。
只有BlockingWaitStrategy中一个用例需要实际的锁: 这仅仅是为了使用条件的目的而完成的,以便在等待新事件到达时消耗线程可以停放。 许多低延迟系统将使用繁忙等待来避免使用条件可能导致的抖动,但系统繁忙等待操作的数量可能导致性能显着下降,特别是在CPU资源严重受限的情况下。 例如。 虚拟化环境中的Web服务器。
Disruptor 工作流程
知乎上有一篇 文章,详细介绍了Disruptor原理及工作流程,包括:
- push
- pull
- 伪共享
强烈推荐没有相关基础的同学前往学习。
网友评论