原文地址http://mechanitis.blogspot.com/2011/07/dissecting-disruptor-writing-to-ring.html

 

这是Disruptor end-to-end view中漏掉的部分。提起精神来,这块比较长。不过为了让你们能在一个地方看到所有内容我决定把它们写在一个博文中。
重要的地方是:不要让环重叠;通知消费者;批量的生产者;还有多生产者如何工作。
生产者障碍
Disruptor有消费者接口和辅助类,但是没有生产者的,关于如何写入到ring buffer的。因为除了你需要知道你的生产者之外,没有别人需要访问它。尽管如此,和消费那边相似,ring buffer会创建一个ProducerBarrier,你的生产者会通过它来写入ring buffer。
写入ring buffer涉及到两个步骤的提交。第一,你的生产者需要指明buffer中的下一个槽。然后,当生产者写入槽完成的时候,它会调用ProducerBarrier的提交方法。
那么让我们来看看第一块。它听起来很容易=“给我ring buffer中的下一个槽”。好吧,从你的生产者角度来看,它是简单。你简单地调用ProducerBarrier中的nextEntry()方法。这样会返回给你一个Entry对象,这个对象基本上就是ring buffer中的下一个槽。
ProducerBarrier确保ring buffer不会重叠
ProducerBarrier在幕后做所有交涉来找出下一个槽,如果允许的话还可以向它写入。


(我不相信shiny new graphics tablet有助于我的图片的清晰度,但是它用起来很有意思)。
对于这张插图,我们假设只有一个生产者向ring buffer写入。我们等会儿会处理多生产者的复杂问题。
ConsumerTrackingProducerBarrier持有所有正在访问ring buffer的消费者的列表。现在我觉得这看起来有点儿奇怪-我不会期望ProducerBarrier知道任何有关消费那边的事情。但是等等,有一个原因。因为我们不想把ring buffer和队列“混为一谈”(队列需要追踪头和尾,它们有时候是同一个),我们的消费者负责知道自己属于哪个序列号,而不是ring buffer。所以如果想确认我们没有重叠 buffer,我们需要确定消费者该去哪里。
在上面的图表中,有一个消费者很顺利地在最高序列号的位置(12,用红/粉色高亮)。另一个消费者有点儿落后-可能它在做I/O操作之类的事情-它在序列号3的位置。因此2号消费者在赶上1号之前有整个buffer长度的路程要走。
生产者想要写入到ring buffer中序列3占据的槽,因为这个槽是ring buffer当前游标的下一个。但是ProducerBarrier知道它不能写因为有一个消费者正在使用它。所以ProducerBarrier坐下来原地打转,等待,直到那个消费者前进。
领取下一个槽
现在想像2号消费者已经做完了那一批条目,并且序列号向前进了。也许它到达了序列9(因为消费者批量工作的方式,现实中我会预期它到达12,但是如果那样的话这个例子就不够有趣了)。


上面的插图显示了当2号消费者更新到序列号9的时候发生的情况。在这张图中我已经简化了ConsumerBarrier,因为它们在这里用不到。
ProducerBarier看下一个槽,也就是序列号3已经可用了。它果断抢占这个槽的Entry(我还没有特别讲过Entry类,但是它基本上就是一个盛放那些你想扔到有序列号的ring buffer槽中的东西的桶),设置Entry上的序列号为下一个序列号(13),然后把这个Entry返回给你的生产者。然后生产者可以往这个Entry里写任何值。
提交新值
两步提交中的第二步是,好吧,是提交。


绿色代表新更新的Entry,序列号13-耶,不好意思,我是个红绿色色盲。但是其他颜色甚至更烂。
当生产者完成向那个Entry写东西的时候,它告诉ProducerBarrier来提交。
ProducerBarrier等待ring buffer游标来赶上我们现在的地方(对于单个生产者这有点无意义-比如我们知道游标已经在12了,没有其他东西在往ring buffer写入)。然后ProducerBarrier把ring buffer游标更新到刚刚更新过的Entry上-我们这里是13.接下来ProducerBarrier让消费者知道buffer中有新东西了。它会戳一下ConsumerBarrier中的WaitStrategy并说-“喂,醒醒!有事情发生了!”(注意-不同的WaitStrategy实现以不同的方式来做这事,看它是否阻塞了。)
现在1号消费者可以获取Entry13,2号消费者可以获取13和它以下的所有东西了,并且它们以后会顺利地存在。
ProducerBarrier批处理
有趣的是disruptor在生产者和消费者两边都可以批次处理。还记得随着程序运行,第二消费者最后到达了序列9吗?ProducerBarrier在这里可以做一件很滑头的事-它知道buffer的大小,也知道最慢的消费者在哪里。因此它可以知道哪些槽是可用的。


如果ProducerBarrier知道ring buffer游标是12,而且最慢的消费者在9,在它需要检查消费者们的位置之前它就可以让生产者写入到3,4,5,6,7和8。
多生产者
你以为这个已经说过了,其实还有。
我在上面的图中稍稍做了点假。我暗示了ProducerBarrier用到的序列号是直接从ring buffer的游标来的。然而,如果你查看代码的话,你会明白它其实是通过ClaimStrategy来获取的。我省略这个是为了简化图表,在单生产者的情况下这个不是很重要。
多生产者的情况下,你还需要追踪序列号。这个序列号是可写入的。注意它和单纯的ring buffer游标加1不一样-如果你有一个以上的生产者在向buffer写入,就有可能出现有些Entry正在被写入但是还没有被提交的情况。


让我们来复习一下如何领取一个槽。每个生产者都会向ClaimStrategy请求下一个可用的槽。1号生产者得到序列13,和上面的单个生产者情况一样。2号生产者得到序列14,尽管ring buffer的游标还仅仅指向12,因为ClaimSequence发出序列号,并且记下分配的是什么。
因此每个生产者都拥有自己的槽和一个崭新的序列号。
我把1号生产者和它的槽着上绿色,把2号生产者和它的槽着上可疑的粉色-看着像紫色。


现在想像一下1号生产者被仙女拐走了,由于某些原因还没有提交。2号生产者已经准备好提交了,并且向ProducerBarrier请求提交。
正如我们在早前的提交图表中看到的一样,ProducerBarrier只有在ring buffer游标到达它想要提交到的那个槽的前一个时它才会提交。这样的话,游标就必须先到达13我们才能提交14.但是我们不能,因为1号生产者正盯着某些闪亮的东西而还没有提交。因此ClaimStrategy就坐在那儿原地打转直到ring buffer游标走到它应该在的位置。


现在1号生产者清醒过来了并且请求提交Entry13(从1号生产者发出的绿色箭头表示请求)。ProducerBarrier让ClaimStrategy等ring buffer游标先到达12,它当然已经到了。所以ring buffer游标增长到13,并且ProducerBarrier戳一下WaitStrategy来告诉大家ring buffer已经更新了。现在ProducerBarrier可以完成2号生产者的请求了,把ring buffer游标增长到14,并且让所有人都知道我们这样做了。
你会看到ring buffer会保持nextEntry()的最初调用顺序,尽管生产者完成时间有差别。也就是说如果一个生产者在向ring buffer写入的时候暂停了,当它解除暂停后其他任何等待中的提交事件都会立即发出。
呼。我讲这些的时候没有提到一次memory barrier。
修改:RingBuffer最近的版本隐藏了Producer Barrier。如果你在代码里没看到ProducerBarrier,那就把我提到的“producer barrier”都当成“ring buffer”。
修改2:注意2.0版本的Disruptor使用了和这篇文章提到的不一样的名字。如果你对类名不解请查看我的变更总结

 

部分评论:
philwbJul 26, 2011 05:50 PM
那个双生产者的情况下,当1号生产者提交失败时会怎么样?2号生产者会被一直阻止完成提交吗?那样的话ring buffer会出现死锁吗?
TrishaJul 27, 2011 02:44 AM
生产者应该小心应付这种情况,因为它们确实需要注意它们阻塞了其他生产者。
如果其中一个生产者因为坏掉了而提交失败,那你的整个系统会有比死锁更大的问题。但是,如果它是因为事务失败而提交失败的话,有两点需要留意:1)重试直到提交成功(其它生产者会被阻塞直到成功)或者2)向ring buffer提交一个“死消息”以使被阻塞的生产者可以继续,并且消费者会忽略这个死消息,仍然让序列号像预期的那样递增。
philwbJul 27, 2011 08:59 AM
谢谢如此之快的回复。还有一个关于disruptor的问题:你们对于耗时的消费者或生产者有应对策略吗(多生产者的情况)?
补充一下,如果能够听听LMAX架构的其他方面比如你们实现高可用性的方法,数据持久化,多处理单元的工作分配等那就更好了!
TrishaJul 27, 2011 09:32 AM
Martin Fowler的文章给出了关于LMAX架构的更多内容,但是我们目前还没有打算公开全部秘密;)
http://martinfowler.com/articles/lmax.html
先不管你的架构怎么样,如果你的消费者一直都比生产者慢,那你的系统就应该加固。解决这问题的一个方法是你可以用两个消费者来做同样的事,其中一个用偶数序列,另一个用奇数的。这样你就可以有潜力并行地处理更多东西(当然你可以扩展2个以上)。
如果你的生产者慢,这可能是你的设计中把很多事情放在一个地方做。
我们的生产者通常做简单的事情-它们从一个地方拿数据然后插入到ring buffer。如果你的生产者在做大量的工作,你可以考虑一下把那个逻辑移到一个较早的消费者中。你的生产者可以向ring buffer写入原始数据,新的消费者可以从ring buffer读取原始数据,处理它,把处理过的数据写到Entry,拥有所有依赖它的下线消费者。这也许可以给你些并行处理这个事的建议。

 posted on 2012-02-15 14:37  ﹎敏ō  阅读(1812)  评论(0编辑  收藏  举报