LMAX Disruptor 原理

LMAX需要搭建high performance的交易平台, 所以需要基于并发编程模型 (并发编程模型和访问控制
当然他们也关注类似Actor或SEDA模型, 并进行了测试, 从而发现了性能瓶颈-- 对于队列的管理

如图这样比较简单的处理流程, 就需要4个queue和大量的message发送, disruptor设计了一种高效的替代方案

解决如下问题,

 

1, 用何种数据机构来实现Queue

如何使用Disruptor(一)Ringbuffer的特别之处

实现queue首先想到链表, 但使用链表有下列问题, 
- 节点分散, 不利于cache预读 
- 节点每次需要分配和释放, 需要大量的垃圾回收, 低效 
- 不利于批量读取 
- 竞争点较多, head指针, tail指针, size 
   由于producer和consumer很难同步, 所以大部分queue都是满或空状态, 这样会导致大量的竞争, 比较低效 
- 而且习惯的编程方式导致head指针, tail指针, size常常在一个cacheline中, 造成伪共享问题

 

那么用数组实现, 可以部分解决前3点问题, 但仍然无法解决竞争点问题, 以及由于数组的fix size, 带来扩展性问题

Disruptor采用特殊的ring buffer来作为queue实现的数据结构, 解决了上述的问题 
并且这种ring buffer只用了一个标志指针, 即标志下一个写入位置 
求余操作本身也是一种高耗费的操作, 所以ringbuffer的size设成2的n次方, 可以利用位操作来高效实现求余

 

2, 减少竞争点, 分离关注

对于传统的3个竞争点, Disruptor成功的通过ring buffer将其降低到1个, 提高了效率 
只有producer需要关注这个写入标志位, 如果只有一个producer的话, 那么完全就不需要lock, 当然如果有多个producer的时候, 就需要通过ProducerBarrier在写入标志位上做互斥 
对于consumer, 每个consumer各自记录读入标志位, 并且通过ConsumerBarrier不停的侦听当前最大可读标志位, 即写入标志位 
这样的设计成功的将关注点分离

 

3, Lock-free

前面说了disruptor减少竞争点, 但是不可能完全消除竞争, 对于写入标志位, 当多个producer的时候仍然存在竞争, 竞争就需要加锁.

剖析Disruptor:为什么会这么快?(一)锁的缺点 

锁是很低效的, 论文中的3.1讲的比较清晰, 并通过实验数据证明了这点, 使用锁会慢1000倍 
- 系统态的锁会导致线程cache丢失. 锁竞争的时候需要进行仲裁. 这个仲裁会涉及到操作系统的内核切换, 并且在此过程中操作系统需要做一系列操作, 导致原有线程的指令缓存和数据缓很可能被丢掉 
- 用户态的锁往往是通过自旋锁来实现(自旋即忙等), 而自旋在竞争激烈的时候开销是很大的(一直在消耗CPU资源)

那么disruptor的怎么做? lock-free, 不使用锁, 使用CAS(Compare And Swap/Set) 
严格意义上说仍然是使用锁, 因为CAS本质上也是一种乐观锁, 只不过是CPU级别指令, 不涉及到操作系统, 所以效率很高 
Java提供CAS操作的支持, AtomicLong

 

CAS依赖于处理器的支持, 当然大部分现代处理器都支持. 
CAS相对于锁是非常高效的, 因为它不需要涉及内核上下文切换进行仲裁. 
但CAS并不是免费的, 它会涉及到对指令pipeline加锁, 并且会用到内存barrier(用来刷新内存状态,简单理解就是把缓存中,寄存器中的数据同步到内存中去)

CAS的问题就是更为复杂, 比使用lock更难于理解, 并且虽然相对于lock已经很高效, 但是由于上面提到的耗费, 仍然比不使用任何锁机制要慢的多 
所以对于disruptor, 如果能保证只有一个producer就可以完全不使用lock, 甚至CAS, 是很高效的方案 
当然在不得不使用多个producer的情况下, 只能使用CAS

 

4, 解决伪共享(False Sharing)

剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充

剖析Disruptor:为什么会这么快?(三)伪共享

前面提到, CPU cache的预读会大大提高执行效率, 这也是为什么选择数组来替代链表的很重要的原因, 因为数组集中存储可以通过预读大大提高效率 
上面谈到lock的耗费, 主要也是由于内核的切换导致cache的丢失

所以cache是优化的关键, cache越接近core就越快,也越小 
可以看出对于L1, L2级别的cache是每个core都独立的 

cache-line(缓存行)

缓存是由缓存行组成的, 通常是64字节, 一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量. 
缓存行是缓存更新的基本单位, 就算你只读一个变量, 系统也会预读其余7个, 并cache这一行, 并且这行中的任一变量发生改变, 都需要重新加载整行, 而非仅仅重新加载一个变量.

这里谈的伪共享问题, 也是一种主要的cache丢失的case, 需要通过缓存行填充来解决

上面的提到的cache-line, 对于象数组这样连续存储的数据结构非常高效, 但是不能保证所有结构都是连续存储的, 比如对于链表, 就很容易出现伪共享问题, 即这种预读反而使效率降低. 
底下是典型伪共享的例子, 在链表中往往会连续定义head和tail指针, 所以对于cache-line的预读, 很有可能会导致head和tail在同一cache-line
在实际使用中, 往往producer线程会持续更改tail指针, 而consumer线程会持续更改head指针 
当producer线程和consumer线程分别被分配到core2和core1, 就会出现以下状况, 
由于core1不断改变h, 导致该cache-line过期, 对于core2, 虽然他不需要读h, 或者t也没有改变, 但是由于cache-line的整行更新, 所以core2仍然需要不停的更新它的cache 
core2的缓存未命中被一个和它本身完全不相干的值h, 而被大大提高, 导致cache效率底下 
而实际情况下, core1会不断更新h, 而core2会不断更新t, 导致core1和core2都需要频繁的重新load cache, 这就是伪共享问题

那么如何解决这个问题? 
既然预读反而降低效率, 解决办法就是消除系统预读的影响 
简单的办法就是缓存行填充, 来保证这个cache-line只存储这一个数据, 从而避免其他数据的更改对该cache-line的影响

当然显而易见, 这种缓存行填充是非常浪费的, cache本身就是很昂贵的资源, 所以必须慎用 
Disruptor里我们对RingBuffer的cursor和BatchEventProcessor的序列进行了缓存行填充

public long p1, p2, p3, p4, p5, p6, p7; // cache line padding
    private volatile long cursor = INITIAL_CURSOR_VALUE;
    public long p8, p9, p10, p11, p12, p13, p14; // cache line padding

以cursor为例, 本身是独立的变量, 和其他的数据没有关联关系, 并且cursor会频繁的被所有线程读取, 所以如果由于其他不相关的变量的更改而导致cursor所在的cache-line被频繁reload, 是非常低效的. 
所以, disruptor在cursor前后都pading了7个long, 从而避免cursor和任意其他的变量在同一个cache-line 
使用缓存行填充的准则,  
独立变量, 变量被大量线程touch, 会被频繁使用和读取

 

5, 使用内存屏障

http://ifeve.com/linux-memory-barriers/, 非常详细的介绍了内存屏障的原理

剖析Disruptor:为什么会这么快?(四)揭秘内存屏障

聊聊并发(一)深入分析Volatile的实现原理

首先, 内存屏障本身不是一种优化方式, 而是你使用lock-free(CAS)的时候, 必须要配合使用内存屏障

因为CPU和memory之间有多级cache, CPU core只会更新cache-line, 而cache-line什么时候flush到memory, 这个是有一定延时的 
在这个延时当中, 其他CPU core是无法得知你的更新的, 因为只有把cache-line flush到memory后, 其他core中的相应的cache-line才会被置为过期数据

所以如果要保证使用CAS能保证线程间互斥, 即乐观锁, 必须当一个core发生更新后, 其他所有core立刻知道并把相应的cache-line设为过期, 否则在这些core上执行CAS读到的都是过期数据 
系统提供内存屏障就是做这个事的, 当设置内存屏障, 会立刻将cache-line flush到memory, 而没有延时

Java中用volatile来实现内存屏障

Java语言规范第三版中对volatile的定义如下: java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了 volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的

volatile保证了线程间的可见性, 但是同样如果要实现互斥, 必须借助CAS, 以避免读取到更新之间的数据变更

volatile的实现实质,

- 将当前处理器缓存行的数据会写回到系统内存 
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效

 

内存屏障另一种用途, CPU出于对执行指令和数据加载的优化会调整执行顺序, 所以在代码里面先写的指令不一定会被先执行, 当然是在保证逻辑一致性的前提下. 
但内存屏障, 可以限制这种调整, 屏障之前的命令必须先于屏障执行, 而屏障之后的必须后于屏障执行, 很形象.

所以可以看到内存屏障, 虽然和lock比是高效的, 但毕竟限制了CPU的优化并会强制flush cache-line, 所以仍然是比较昂贵的操作.

 

6, 如何使用Disruptor替代Queue

解析Disruptor关系组装

我本来以为是用一个ringbuffer替代一个queue, 原来是用一个ringbuffer替代所有的queue, 怎么实现的?

     

如图, 所有consumer都是从RingBuffer里面读数据 
而C3, 依赖于C1和C2的执行结果, 那么通过设置ConsumerBarrier2来监控C1和C2的执行序号

那么有个问题是C3, 如何获得C1和C2的执行结果? 
答案是, C1和C2执行完后, 会把结果写回Ringbuffer中原来的entry中

如图, 当C3拿到Entry时, 里面有3个值, 本来的value, C1处理的结果, C2处理的结果, 并且不同的consumer写的字段不一样来避免冲突 
而Producer在监控consumer消费序号时, 只需要监控最后一层的, 即C3的, 因为只有C3处理完, 这个entry才能被覆盖.

 

看起来非常的复杂, 但是在使用时, 对用户很多机制其实是透明的, 比如上面的workflow的代码如下

ConsumerBarrier consumerBarrier1 =
    ringBuffer.createConsumerBarrier();
BatchConsumer consumer1 =
    new BatchConsumer(consumerBarrier1, handler1);
BatchConsumer consumer2 =
    new BatchConsumer(consumerBarrier1, handler2);
ConsumerBarrier consumerBarrier2 =
    ringBuffer.createConsumerBarrier(consumer1, consumer2);
BatchConsumer consumer3 =
    new BatchConsumer(consumerBarrier2, handler3);
ProducerBarrier producerBarrier =
    ringBuffer.createProducerBarrier(consumer3);

对用户而言, 只需要知道ConsumerBarrier, Consumer, ProducerBarrier 

 

总结

总体来说, disprutor从两个方面来对Actor模式的queue做了优化

最重要的是, Mechanical Sympathy(机械的共鸣), 了解硬件的工作方式来编写和硬件完美结合的软件, 很高的境界 
通过利用CAS+内存屏障实现lock-free, 并使用缓存行填充来解决伪共享, 可见虽然编程语言已经发展到很高级的地步, 但是如果要追求效率的机制, 必须要具有Mechanical Sympathy, 人剑合一

其次, 是通过ringbuffer来实现queue来替代链表的实现, 尤其当场景比较复杂需要很多queue的时候, 效率应该会得到很大的提高

 

其实, disruptor并没有实现queue的互斥consumer, 每个consumer都是自己保持序号, 各读各得, 但是对于普通queue, 被一个线程pop掉的数据, 其他线程是无法读到的

本文章摘自博客园,原文发布日期:2013-07-09

时间: 2024-11-01 04:34:05

LMAX Disruptor 原理的相关文章

LMAX Disruptor——一个高性能、低延迟且简单的框架

原文地址:LMAX Disruptor – High Performance, Low Latency and Simple Too 翻译:杨帆 校对:丁一 Disruptor是一个用于在线程间通信的高效低延时的消息组件,它像个增强的队列,并且它是让LMAX Exchange跑的如此之快的一个关键创新.关于什么是Disruptor.为何它很重要以及它的工作原理方面的信息都呈爆炸性增长 -- 这些文章很适合开始学习Disruptor,还可跟着LMAX BLOG深入学习.这里还有一份更详细的白皮书.

Java Concurrency In Practice

线程安全 定义 A class is thread-safe if it behaves correctly when accessed from multiple threads, regardless of the scheduling or interleaving of the execution of those threads by the runtime environment, and with no additional synchronization or other coo

Apache Storm内部原理分析

本文算是个人对Storm应用和学习的一个总结,由于不太懂Clojure语言,所以无法更多地从源码分析,但是参考了官网.好多朋友的文章,以及<Storm Applied: Strategies for real-time event processing>这本书,以及结合自己使用Storm的经历,希望对于想深入一点了解Storm原理的朋友能有所帮助,有不足之处欢迎拍砖交流. Storm集群架构 Storm集群采用主从架构方式,主节点是Nimbus,从节点是Supervisor,有关调度相关的信息

并发框架Disruptor译文

Martin Fowler在自己网站上写了一篇LMAX架构的 文章,在文章中他介绍了LMAX是一种新型零售金融交易平台,它能够以很低的延迟产生大量交易.这个系统是建立在JVM平台上,其核心是一个业务逻辑处理 器,它能够在一个线程里每秒处理6百万订单.业务逻辑处理器完全是运行在内存中,使用事件源驱动方式.业务逻辑处理器的核心是Disruptor. Disruptor它是一个开源的并发框架,并获得2011 Duke's 程序框架创新奖,能够在无锁的情况下实现网络的Queue并发操作.本文是Disru

剖析Disruptor:为什么会这么快?(一)Ringbuffer的特别之处

原文地址:http://ifeve.com/ringbuffer/ 作者:Trisha    译者:寒桐  校对:方腾飞 最近,我们开源了LMAX Disruptor,它是我们的交易系统吞吐量快(LMAX是一个新型的交易平台,号称能够单线程每秒处理数百万的订单)的关键原因.为什么我们要将其开源?我们意识到对高性能编程领域的一些传统观点,有点不对劲.我们找到了一种更好.更快地在线程间共享数据的方法,如果不公开于业界共享的话,那未免太自私了.同时开源也让我们觉得看起来更酷. 从这个站点,你可以下载到

Dissecting the Disruptor: What’s so special about a ring buffer?

原文地址:http://mechanitis.blogspot.com/2011/06/dissecting-disruptor-whats-so-special.html(因被墙移到墙内) 作者:Trisha Recently we open sourced the LMAX Disruptor, the key to what makes our exchange so fast.  Why did we open source it?  Well, we've realised that

HBase中Disruptor使用

// 核心是一个循环缓冲区.我们的循环缓冲区是一个LMAX Disruptor.当多个线程在单个WAL竞争append和sync时,它试图最小化同步与volatile写. // Disruptor配置为处理多个生产者和仅有一个消费者(HBase中的生产者是调用append.sync的IPC Handlers).单一的消费者从环形缓冲区中 // 拉去append和sync. /** * RingBufferTruck为事件--Event * RingBufferTruck.EVENT_FACTOR

线程间共享数据无需竞争

原文 地址  作者  Trisha   译者:李同杰 LMAX Disruptor 是一个开源的并发框架,并获得2011 Duke's程序框架创新奖.本文将用图表的方式为大家介绍Disruptor是什么,用来做什么,以及简单介绍背后的实现原理. Disruptor是什么? Disruptor 是线程内通信框架,用于线程里共享数据.LMAX 创建Disruptor作为可靠消息架构的一部分并将它设计成一种在不同组件中共享数据非常快的方法. 基于Mechanical Sympathy(对于计算机底层硬

Sharing Data Among Threads Without Contention

原文地址:http://www.oraclejavamagazine-digital.com/javamagazine/20120304/?pg=56&pm=1&u1=friend  作者 Trisha The London Multi-Asset Exchange (LMAX) Disruptor is an open source concurrency framework that recently won the 2011 Duke's Choice Award for Innov