原文地址http://mechanitis.blogspot.com/2011/06/dissecting-disruptor-whats-so-special.html
最近我们把LMAX Disruptor开源了,它是我们的交易变得如此快的原因。为什么我们要开源它?我们意识到对高性能编程的传统认知...有点不对劲。我们找到了一个更好的,更快的线程间共享数据的方法,如果不把它分享给大家是自私的。这也会使我们看起来像死聪明。
在这个网站你可以下载到一份解释什么是Disruptor和为什么它这么高明和快速的技术文档。
我甚至有写作信誉问题,可喜的是我只是在上面加了一些标点和重组了一些我不明白的句子。
我发现把这些东西一次性消化有些困难,所以我会一块块解释它们,以适合我的NADD听众。
首先-ring buffer。我对Disruptor的最初印象就是ring buffer。后来我渐渐明白,ring buffer数据结构是在这个模式的中心部位,而Disruptor控制如何访问它。
那么ring buffer到底是什么呢?
正如看上去那样-它就是一个环(圆的,环状的),你把它当作一个在场景(线程)之间传送东西的buffer来使用。
(好吧,我是用笔刷画的。我试着画草图,希望我的强迫症没有掺和进来要求我画完美的圆和直线)。
所以基本上它就是含有指向下一个可用槽的指针的数组。
如果你一直向buffer中填东西(应该也会从里面读东西),序列会一直增长,绕着这个环。
要找到数组中当前序列指向的槽,你可以用一个mod运算。
sequence mod array length = array index
对于上面的ring buffer就是(用JAVA的mod语法):12 % 10 = 2。简单。
事实上图上有10个槽完全是一个意外。2的N次方个槽会工作得更好因为计算机以二进制运算。
还等什么呢?
如果你去Wikipedia查Circular Buffers,你会看到我们的这个环有一个主要的区别-我们没有指向末尾的指针。我们只有下一个可用的序列号。这是故意的-选择ring buffer的根本原因是我们可以支持可靠的消息通信。我们需要把服务发出的消息存储起来,那么当另一个服务发来一个nak说他们没有接收到消息的时候,我们可以重新发送它们。
ring buffer在这里看起来很完美。它保存序列来显示buffer的末尾在哪里,并且当它得到一个nak的时候它可以重现从那个点到当前序列的所有东西:
我们这个ring buffer和我们通常用的队列的区别是,我们不消耗buffer中的条目-它们只会被覆盖。这是和Wikipedia上的版本相比我们不需要尾指针的原因。确定是否重叠是由数据结构之外来管理的(这是生产者和消费者行为的一部分-如果你等不及我写博来说明它,你可以检出Disruptor项目)。
它为什么这么伟大?
我们使用这种数据结构是因为它提供我们一些可靠消息传递的好特性。这就够了,虽然它还有一些其他好的特点。
首先,因为它是数组所以它比链表要快,而且有一个可预见的访问模式。这很不错,对CPU缓存也很友好-条目可以在硬件级预加载,因此机器不会经常到主内存去加载ring的下一个条目。
第二点,它是一个数组,你可以对它预分配,使得数组里的对象真正的不消亡。这意味着GC在这里几乎什么也不用做。又是一点,不像链表那样对每个添加的条目都创建对象-当这些对象不再存在链表时它们都需要被清理掉。
没提到的
我没有提到如何避免环重叠,或是如何向ring buffer读/写东西。你也会注意到我将它和链表那样的数据结构比较,我觉得没人会认为链表是现实问题的答案。
当你拿Disruptor和队列实现比较时,有趣的事就来了。队列往往注重队列的头和尾,添加和消耗条目等之类的东西。所有东西我都没有在ring buffer中真正提到。因为ring buffer本身并不负责这些事,我们把这些关注点移到了这个数据结构的外部。
到这个网站阅读文章或是检出代码以获得更详细的信息。或者查看Mike和Martin去年在QCon San Francisco的演讲。或者再等我5分钟来思考剩下的东西。
部分评论:
Flying Frog Consultancy Ltd.Jul 4, 2011 02:11 AM
如果你没有消耗你的ring buffer中的元素,那么你会保持它们一直可用,它们不会被释放。这很明显不利于GC的吞吐和延时。
在你的ring buffer中将引用写到不同地方会导致写障碍,这也不利于吞吐和延时。
我好奇当这些弊端发挥影响的时候,你们是怎么权衡的。
Michael BarkerJul 4, 2011 01:36 PM
至于使用内存,Disruptor并没有真正的权衡过。和队列不同,你可以选择如何使用内存。如果是一个实时软件系统的解决方案,减少GC暂停次数是首要的。因此你可以重复使用ring buffer中的条目,比如从ring buffer复制字节数组到网络IO缓冲或者相反(这是我们最常用的)。系统的内存使用量保持不变,所以减少了GC的频率。
还可以实现一个包含不可变对象的引用的Entry。不过这种情况下,消费者可能需要腾出消息对象的空间来减少从Eden提取的内存量。所以编程者需要多做点事来设计适当的解决方案。我们相信相比提供的灵活性,这点小小的努力是值得的。
考虑写障碍,Disruptor主要的目标是在线程间传递消息。我们没有考虑顺序和一致性,因此就需要在合适的地方使用内存障。我们做了最大努力来使它保持最低限度。尽管如此,我们的方案仍比流行的替代品要快很多倍,因为它们绝大多数都使用锁来提供一致性。
Mike.