Disruptor的小史
现在要是不知道Disruptor真的已经很outer了,Disruptor是英国外汇交易公司LMAX开发的一款开源的高性能队列,LMAX Disruptor是一个高性能的线程间消息传递库,它源于LMAX对并发性,性能和非阻塞算法的研究,如今构成了其Exchange基础架构的核心部分。
稍后,包括Apache Storm、Camel、Log4j等在内的很多知名项目都集成了Disruptor。国内不少一线大厂技术团队也在用,或者借鉴了其优秀的架构思想。Disruptor通过无锁设计实现了高并发高性能,其设计思想可以扩展到分布式环境,通过无锁设计来提升服务的高性能。
Martin Fowler的布道
著名的软件设计模式专家Martin Fowler专门写了一篇文章来推广https://martinfowler.com/articles/lmax.html
LMAX是一个新的零售金融交易平台。因此,它必须以低延迟处理许多交易。该系统构建于JVM平台之上,并以业务逻辑处理器为中心,可在单个线程上处理每秒600万个订单。业务逻辑处理器使用事件源完全在内存中运行。业务逻辑处理器被Disruptors包围 - Disruptors是一个并发组件,它实现了一个无需锁定即可运行的队列网络。在设计过程中,团队得出结论,使用队列的高性能并发模型的最新方向与现代CPU设计基本不一致。
Disruptor数据结构
在原始级别,可以将Disruptor视为队列的多播图,其中生产者在其上放置对象,这些对象通过单独的下游队列发送给所有消费者以供并行使用。当你查看内部时,你会看到这个队列网络实际上是一个单一的数据结构 - 一个环形缓冲区。
每个生产者和消费者都有一个序列计数器,用于指示它当前正在处理的缓冲区中的哪个槽。每个生产者/消费者编写自己的序列计数器,但可以读取其他序列计数器。通过这种方式,生产者可以读取消费者的计数器,以确保可以在没有计数器锁定的情况下使用它想要写入的插槽。类似地,消费者可以确保它只通过观察计数器一旦另一个消费者完成消息就处理消息。
输出Disruptor类似,但它们只有两个连续的消费者用于编组和输出。输出事件被组织成几个主题,因此消息只能发送给对它们感兴趣的接收者。每个主题都有自己的Disruptor。
我所描述的Disruptor以一种生产者和多种消费者的风格使用,但这并不是对Disruptor设计的限制。Disruptor也可以与多个生产者一起工作,在这种情况下它仍然不需要锁。
Disruptor设计的一个好处是,如果消费者遇到问题而落后,它可以让消费者更容易赶上。如果解组器在插槽15上处理时出现问题并且当接收器在插槽31上时返回,则它可以从一个批次中的插槽16-30读取数据以赶上。批量读取来自Disruptor的数据使得滞后的消费者更容易赶上,从而减少总体延迟。
环形缓冲区很大:输入缓冲区有2000万个插槽,每个输出缓冲区有400万个插槽。序列计数器是64位长整数,即使在环形槽缝合时也会单调增加。缓冲区设置为2的幂的大小,因此编译器可以执行有效的模运算以从序列计数器编号映射到槽号。与系统的其他部分一样,Disruptor在一夜之间被反弹。这种反弹主要用于擦除内存,以便在交易过程中发生昂贵的垃圾收集事件的可能性较小。(我也认为定期重启是一个好习惯,所以你要排练如何在紧急情况下这样做。)
Disruptor在哪里
https://github.com/LMAX-Exchange/disruptor/
理解Disruptor是什么的最好方法是将它与目前很好理解和非常相似的东西进行比较。可以把Disruptor类比成Java的阻塞队列BlockingQueue。像队列一样,Disruptor的目的是在同一进程内的线程之间移动数据(例如消息或事件)。但是,Disruptor提供了一些将其与队列区分开来的关键功能。他们是:
1)具有消费者依赖关系图的消费者多播事件。
2)为事件预分配内存。
3)可选择无锁模式。
Disruptor核心概念
在我们理解Disruptor是如何工作之前,需要先理解一些Disruptor团队定义的术语。
为了将这些元素置于上下文中,下面是LMAX如何在其高性能核心服务(例如交换)中使用Disruptor的示例。
图1.具有一组依赖消费者的Disruptor
多播事件
这是普通队列和Disruptor之间最大的行为差异。当您有多个消费者在同一个Disruptor上监听时,所有事件都会发布给所有消费者,而不是一个事件只发送给单一消费者队列。Disruptor的行为旨在用于需要对同一数据进行独立多个并行操作的情况。
来自LMAX的规范示例是我们有三个操作,即日志记录(将输入数据写入持久性日志文件),复制(将输入数据发送到另一台机器以确保存在数据的远程副本)和业务逻辑(真正的处理工作)。
Executor风格的事件处理,通过在同一处并行处理不同的事件来找到比例,也可以使用WorkerPool。请注意,它是在现有的Disruptor类之上进行的,并且不会使用相同的第一类支持进行处理,因此它可能不是实现该特定目标的最有效方法。
查看图1可以看到有3个事件处理程序(JournalConsumer,ReplicationConsumer和ApplicationConsumer)监听Disruptor,这些事件处理程序中的每一个都将按相同的顺序接收Disruptor中可用的所有消息,允许每个消费者的工作并行进行。
消费者依赖图
为了支持并行处理行为的实际应用,有必要支持消费者之间的协调。返回参考上述示例,必须防止业务逻辑消费者在日志记录和复制消费者完成其任务之前取得进展。我们称这个概念为门控,或者更准确地说,这种行为的超集特征称为门控(concept gating)。
门控发生在两个地方。
首先,我们需要确保生产者不会超过消费者。这是通过调用RingBuffer.addGatingConsumers()将相关的使用者添加到Disruptor来处理的。
其次,先前提到的情况是通过从必须首先完成其处理的组件构造包含序列的SequenceBarrier来实现的。
参考图1有3个消费者正在收听来自Ring Buffer的事件。此示例中有一个依赖关系图。ApplicationConsumer依赖于JournalConsumer和ReplicationConsumer。这意味着JournalConsumer和ReplicationConsumer可以彼此并行的运行。从ApplicationConsumer的SequenceBarrier到JournalConsumer和ReplicationConsumer的序列的连接可以看到依赖关系。
值得注意的是Sequencer与下游消费者之间的关系。它的一个作用是确保发布不包装Ring Buffer。要做到这一点,下游消费者中没有一个可能具有低于环形缓冲区序列的序列,而不是环形缓冲区的大小。但是,使用依赖关系图可以进行优化。由于ApplicationConsumers Sequence保证小于或等于JournalConsumer和ReplicationConsumer(这是该依赖关系所确保的),因此Sequencer只需要查看ApplicationConsumer的Sequence。在更一般的意义上,Sequencer只需要知道作为依赖关系树中叶节点的使用者的序列。
事件预分配
Disruptor的设计目标之一是能在低延迟环境中使用。在低延迟系统中,必须减少或移除内存分配。在基于Java的系统中,目的是减少由于垃圾收集GC导致的系统停顿(在低延迟C / C ++系统中,由于存在于内存分配器上的争用,大量内存分配也存在问题)。
为了支持这一点,用户可以提前分配Disruptor中事件所需的存储空间。在构造期间,EventFactory由用户提供,并将在Disruptor的Ring Buffer中为每个条目调用。将新数据发布到Disruptor时,API将允许用户获取构造的对象,以便他们可以调用方法或更新该存储对象上的字段。Disruptor保证这些操作只要正确实现就是并发安全的。
可选择无锁
低延迟期望推动的另一个关键实现细节是广泛使用无锁算法来实现Disruptor。
所有内存可见性和正确性保证都是使用内存屏障(memory barriers)和CAS操作实现的。只有一个用例需要实际锁定并且在BlockingWaitStrategy中。这仅仅是为了使用条件,以便在等待新事件到达时停放消耗线程。许多低延迟系统将使用忙等待来避免使用条件可能引起的性能抖动,但是在系统忙等待操作的数量可能导致性能显着下降,尤其是在CPU资源严重受限的情况下。例如,虚拟化环境中的Web服务器。
Disruptor jar文件可从Maven,可以从那里集成到您选择的依赖管理器中。
com.lmax
disruptor
3.4.2
为了开始使用Disruptor,我们将考虑一个非常简单的例子,一个将生产者传递给消费者的Long值,消费者只需打印出该值。
首先,我们将定义携带数据的事件。
public class LongEvent
{
private long value;
public void set(long value)
{
this.value = value;
}
}
为了让Disruptor为我们预先分配这些事件,我们需要一个将执行构造的EventFactory
import com.lmax.disruptor.EventFactory;
public class LongEventFactory implements EventFactory
{
public LongEvent newInstance()
{
return new LongEvent();
}
}
一旦我们定义了事件,我们需要创建一个处理这些事件的消费者。在我们的例子中,我们要做的就是从控制台中打印出值。
import com.lmax.disruptor.EventHandler;
public class LongEventHandler implements EventHandler
{
public void onEvent(LongEvent event, long sequence, boolean endOfBatch)
{
System.out.println("Event: " + event);
}
}
我们需要这些事件的来源,为了举例,我将假设数据来自某种I/O设备,例如网络或ByteBuffer形式的文件。
使用Disruptor的3.0版本,添加了更丰富的Lambda风格的API,以帮助开发人员将这种复杂性封装在Ring Buffer中,因此3.0之后发布消息的首选方法是通过API的Event Publisher / Event Translator部分。例如
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.EventTranslatorOneArg;
public class LongEventProducerWithTranslator
{
private final RingBuffer ringBuffer;
public LongEventProducerWithTranslator(RingBuffer ringBuffer)
{
this.ringBuffer = ringBuffer;
}
private static final EventTranslatorOneArg TRANSLATOR =
new EventTranslatorOneArg()
{
public void translateTo(LongEvent event, long sequence, ByteBuffer bb)
{
event.set(bb.getLong(0));
}
};
public void onData(ByteBuffer bb)
{
ringBuffer.publishEvent(TRANSLATOR, bb);
}
}
这种方法的另一个优点是翻译器代码可以被拉入一个单独的类中,并可以轻松地单独进行单元测试。
Disruptor提供了许多不同的接口(EventTranslator,EventTranslatorOneArg,EventTranslatorTwoArg等),可以实现这些接口来提供翻译。原因是允许转换器被表示为静态类或非实例捕获lambda(non-capturing lambda)(当Java 8 rolls around)作为转换方法的参数通过Ring Buffer上的调用传递给转换器。
我们也可以使用更“原始”的方法。
import com.lmax.disruptor.RingBuffer;
public class LongEventProducer
{
private final RingBuffer ringBuffer;
public LongEventProducer(RingBuffer ringBuffer)
{
this.ringBuffer = ringBuffer;
}
public void onData(ByteBuffer bb)
{
long sequence = ringBuffer.next(); // Grab the next sequence
try
{
LongEvent event = ringBuffer.get(sequence); // Get the entry in the Disruptor
// for the sequence
event.set(bb.getLong(0)); // Fill with data
}
finally
{
ringBuffer.publish(sequence);
}
}
}
显而易见的是,事件发布变得比使用简单队列更复杂。这是由于对事件预分配的需求。它需要(在最低级别)消息发布的两阶段方法,即声明环形缓冲区中的插槽然后发布可用数据。还必须将发布包装在try/finally块中。如果我们在Ring Buffer中声明一个插槽(调用RingBuffer.next()),那么我们必须发布这个序列。如果不这样做可能会导致Disruptor状态的变坏。具体而言,在多生产者的情况下,这将导致消费者停滞并且在没有重启的情况下无法恢复。因此,建议使用EventTranslator API。
最后一步是将整个事物连接在一起。可以手动连接所有组件,但是它可能有点复杂,因此提供DSL以简化构造。一些更复杂的选项不能通过DSL获得,但它适用于大多数情况。
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.util.DaemonThreadFactory;
import java.nio.ByteBuffer;
public class LongEventMain
{
public static void main(String[] args) throws Exception
{
// The factory for the event
LongEventFactory factory = new LongEventFactory();
// Specify the size of the ring buffer, must be power of 2.
int bufferSize = 1024;
// Construct the Disruptor
Disruptor disruptor = new Disruptor<>(factory, bufferSize, DaemonThreadFactory.INSTANCE);
// Connect the handler
disruptor.handleEventsWith(new LongEventHandler());
// Start the Disruptor, starts all threads running
disruptor.start();
// Get the ring buffer from the Disruptor to be used for publishing.
RingBuffer ringBuffer = disruptor.getRingBuffer();
LongEventProducer producer = new LongEventProducer(ringBuffer);
ByteBuffer bb = ByteBuffer.allocate(8);
for (long l = 0; true; l++)
{
bb.putLong(0, l);
producer.onData(bb);
Thread.sleep(1000);
}
}
}
Disruptor API的设计影响之一是Java 8将依赖功能接口的概念作为Java Lambdas的类型声明。Disruptor API中的大多数接口定义符合功能接口的要求,因此可以使用Lambda而不是自定义类,这可以减少所需的boiler place。
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.util.DaemonThreadFactory;
import java.nio.ByteBuffer;
public class LongEventMain
{
public static void main(String[] args) throws Exception
{
// Specify the size of the ring buffer, must be power of 2.
int bufferSize = 1024;
// Construct the Disruptor
Disruptor disruptor = new Disruptor<>(LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE);
// Connect the handler
disruptor.handleEventsWith((event, sequence, endOfBatch) -> System.out.println("Event: " + event));
// Start the Disruptor, starts all threads running
disruptor.start();
// Get the ring buffer from the Disruptor to be used for publishing.
RingBuffer ringBuffer = disruptor.getRingBuffer();
ByteBuffer bb = ByteBuffer.allocate(8);
for (long l = 0; true; l++)
{
bb.putLong(0, l);
ringBuffer.publishEvent((event, sequence, buffer) -> event.set(buffer.getLong(0)), bb);
Thread.sleep(1000);
}
}
}
注意不再需要许多类(例如处理程序,翻译器)。还要注意lambda如何publishEvent()
仅用于引用传入的参数。
如果我们要将该代码编写为:
ByteBuffer bb = ByteBuffer.allocate(8);
for (long l = 0; true; l++)
{
bb.putLong(0, l);
ringBuffer.publishEvent((event, sequence) -> event.set(bb.getLong(0)));
Thread.sleep(1000);
}
这将创建一个捕获lambda,这意味着它需要实例化一个对象来保存ByteBuffer bb
变量,因为它将lambda传递给publishEvent()
调用。这将产生额外的(不必要的)垃圾,因此如果要求低GC压力,则应首选将参数传递给lambda的调用。
给那个方法引用可以用来代替匿名lamdbas,可以用这种方式重写这个例子。
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.util.DaemonThreadFactory;
import java.nio.ByteBuffer;
public class LongEventMain
{
public static void handleEvent(LongEvent event, long sequence, boolean endOfBatch)
{
System.out.println(event);
}
public static void translate(LongEvent event, long sequence, ByteBuffer buffer)
{
event.set(buffer.getLong(0));
}
public static void main(String[] args) throws Exception
{
// Specify the size of the ring buffer, must be power of 2.
int bufferSize = 1024;
// Construct the Disruptor
Disruptor disruptor = new Disruptor<>(LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE);
// Connect the handler
disruptor.handleEventsWith(LongEventMain::handleEvent);
// Start the Disruptor, starts all threads running
disruptor.start();
// Get the ring buffer from the Disruptor to be used for publishing.
RingBuffer ringBuffer = disruptor.getRingBuffer();
ByteBuffer bb = ByteBuffer.allocate(8);
for (long l = 0; true; l++)
{
bb.putLong(0, l);
ringBuffer.publishEvent(LongEventMain::translate, bb);
Thread.sleep(1000);
}
}
}
Disruptor通过精巧的无锁设计实现了在高并发情形下的高性能。
在美团内部,很多高并发场景借鉴了Disruptor的设计,减少竞争的强度。其设计思想可以扩展到分布式场景,通过无锁设计,来提升服务性能。
/**
* @description disruptor代码样例。每10ms向disruptor中插入一个元素,消费者读取数据,并打印到终端
*/
import com.lmax.disruptor.*;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import java.util.concurrent.ThreadFactory;
public class DisruptorMain
{
public static void main(String[] args) throws Exception
{
// 队列中的元素
class Element {
private int value;
public int get(){
return value;
}
public void set(int value){
this.value= value;
}
}
// 生产者的线程工厂
ThreadFactory threadFactory = new ThreadFactory(){
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "simpleThread");
}
};
// RingBuffer生产工厂,初始化RingBuffer的时候使用
EventFactory factory = new EventFactory() {
@Override
public Element newInstance() {
return new Element();
}
};
// 处理Event的handler
EventHandler handler = new EventHandler(){
@Override
public void onEvent(Element element, long sequence, boolean endOfBatch)
{
System.out.println("Element: " + element.get());
}
};
// 阻塞策略
BlockingWaitStrategy strategy = new BlockingWaitStrategy();
// 指定RingBuffer的大小
int bufferSize = 16;
// 创建disruptor,采用单生产者模式
Disruptor disruptor = new Disruptor(factory, bufferSize, threadFactory, ProducerType.SINGLE, strategy);
// 设置EventHandler
disruptor.handleEventsWith(handler);
// 启动disruptor的线程
disruptor.start();
RingBuffer ringBuffer = disruptor.getRingBuffer();
for (int l = 0; true; l++)
{
// 获取下一个可用位置的下标
long sequence = ringBuffer.next();
try
{
// 返回可用位置的元素
Element event = ringBuffer.get(sequence);
// 设置该位置元素的值
event.set(l);
}
finally
{
ringBuffer.publish(sequence);
}
Thread.sleep(10);
}
}
}
输出截图:
参考:
https://martinfowler.com/articles/lmax.html
https://tech.meituan.com/2016/11/18/disruptor.html