LMAX Disruptor

2023-10-09 21:50
文章标签 lmax disruptor

本文主要是介绍LMAX Disruptor,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

LMAX Disruptor 已经是好几年前的框架了,无论是官方还是网上已经有不少其原理分析的文章了,都2020年了,为什么还要写关于它的文章呢?

几年前了解到 LMAX Disruptor 架构后,我便产生了将它移植到 C++ 上的想法。一方面是想看如此高性能的框架,移植到 C++ 会不会性能更高;另一方面是想通过源码移植了解它架构中更为基本的东西。后来虽然移植也完成了,文章也看了,但我发现它框架中最基本的东西依然没有理解。网上的文章,包括官方的,对于其无锁队列设计的解析,都是围绕它本身的结构(ring_buffer、sequence、sequencer、barrier、wait_stragety 等 ),理解起来稍显复杂,我认为一定存在不涉及这么多概念的精简结构。直到我去年读到一篇文章,如醍醐灌顶,这不就是 LMAX Disruptor 无锁队列架构的基本原理吗。网上还没有看到结合文章中的数据结构来讲 LMAX Disruptor 的文章,因此决定写下来。

Arrary Based Lock-free Circurlar Queue

文章中提到的队列,可以用如下简图表示:

可以看到,队列是基于数组的,且包含如下三个索引:

readIndex:队首索引。

maxReadIndex:队尾索引。

writeIndex:写入索引。

上面三个索引最好是用64位整数表示,否则需要处理数值溢出会造成的问题。而使用 64 位整数,以目前单机每秒的吞吐量,也很难遇到溢出问题。

入队

入队操作与 LMAX Disruptor 如出一辙,分两步:写入和提交

  1. 先增加 writeIndex ,获得写入点
  2. 写入数据
  3. 增加 maxReadIndex ,提交数据

出队

简单的增加 readIndex 即可。

从 Arrary Based Lock-free Circurlar Queue 到 LMAX Disruptor

我们可以把整个队列分为三个逻辑部分:生产者、消费者及数据存储。生产者负责操作 writeIndex 与 maxReadIndex;消费者负责操作 readIndex;数据存储负责根据索引读取/写入数据。LMAX Disruptor 架构的抽象过程也可以看成是基于这三块进行,只不过这三大块下,分的更细。

Sequence

参考文章的实现可以看到,原子操作无处不在,而操作的目的就是为了修改索引。在 LMAX Disruptor 中,索引被称为 Cursor,由 Sequence 类提供对其的原子操作。

RingBuffer

RingBuffer 主要负责存取数据,就是个 Buffer。由于不负责维护队首和队尾索引,因此它里面的数据是可以被覆盖的。

Sequencer

Sequencer 是对生产者的抽象,主要维护 maxReadIndex,也就是队尾索引。虽然没有显式的维护队首索引,为了防止数据覆盖,Sequencer 还是维护了一个叫 Gating Sequences 的 Sequence 数组,这个数组中的最小值就是队首索引。有了队首和队尾索引,Sequencer 就知道队列是满是空了。当队列满时,Sequencer 则采用一种近乎忙等的方式等待队列有空。按 LMAX 的话说,队列设计中,消费者不应该成为瓶颈。因此,队列大部分情况下都应该有空,忙等自然算是划得来的做法。虽然 Sequencer 也还是维护了队首和队尾索引,要注意的是,Gating Sequences 数组是个数组,它是可以为空数组的。这种情况下,由于队首索引不存在了,往队列写数据会发生覆盖。按 LMAX 的说法,有时候是有这种需求的,比如日志。

Sequencer 按使用场景又分 MultiProducerSequencer 和 SingleProducerSequencer,其中 SingleProducerSequencer 工作机制与文章中的方式非常像,也维护了 writeIndex(不过并不使用原子操作)。以 SingleProducerSequencer 为例,它的 Claim 及 publish 工作方式如下:

图中,index1,index2,... 就是 Gating Sequences 数组; wrap 操作是减去 bufferSize,这样就方便比较 expectedIndex 有没有覆盖到队首。

SequenceBarrier

SequenceBarrier 主要负责隔离消费者与其他组件,可以看作是消费者的数据源。它为消费者提供可供消费的索引,同时也维护一个 Dependent Sequences 数组,这个数组中的最小值即表示最大可消费的索引。维护 Dependent Sequences 数组是为了实现 Pipeline,即上一个消费者处理完后下一个消费者再处理。

SequenceBarrier 获取可供消费索引的机制大致如下图:

图中 index1,index2 ... 就是 Dependent Sequences 数组;wait 的具体等待方式由 waitStrategy 提供。

EventProcessor

EventProcessor 才是真正的消费者,它维护自己的 readIndex,表示已经目前消费的索引。它依赖 SequenceBarrier 告诉它可消费的索引,每消费一笔数据,readIndex 就加一。LMAX Disruptor 实现了两个 EventProcessor:BatchEventProcessor 和 WorkProcessor,分别用于批次处理的场景和单个处理的场景。分裂 EventProcessor 与 SequenceBarrier 是为了简化 EventProcessor,让它更单纯。让一个 SequenceBarrier 搭配多个 EventProcessor,即可实现并行消费。

下图展示的是 1 个生产者 3 个消费者组成的 Pipeline 其内部工作机制:

图中,方框代表的 index 即 EventProcessor 维护的 readIndex,这里共有 3 个 EventProcessor 实例,图中省略了 SequenceBarrier 的等待流程。每个 consume 流程都需要一个 SequenceBarrier,因此总共要 3 个实例。每个 SequenceBarrier 都把上一步 EventProcessor 的 readIndex 作为 Dependent Sequence。

下图展示的是 1 个生产者 2 个消费者同时消费这种情景的内部机制:

从图中可以看出,总共需要 2 个 EventProcessor、1 个 SequenceBarrier。生产者(Sequencer)又把 2 个 EventProcessor 的 readIndex 作为 Gating Sequences,这个例子中最慢的那个 EventProcessor 的 readIndex 就是队首。

WaitStrategy

WaitStrategy 主要是为了抽象等待策略。它的 waitFor 方法是 SequenceBarrier 在用;signalAll 则主要是 Sequencer 在用,用于告诉消费者有数据到达。

总结

可以看到,LMAX Disruptor 架构把 readIndex、maxReadIndex 维护 和 数据存储外包给了不同的类,将传统队列三者合一的实现方式拆散为独立而又能自由组合的部分,从而满足复杂的场景。

首先,针对数据存储与访问抽象独立的 Buffer 类。

然后,针对 maxReadIndex 的维护问题,抽象出生产者 Sequencer。

再然后,针对 readIndex 的维护问题,委派给各个消费者自己维护。生产者通过计算 readIndex 的最小值间接知道队首在哪里。

再然后,将获取可消费索引的代码独立出来,让这个独立的组件充当代理数据源。给这个代理数据源添加依赖,实现串行消费。

最后,等待生产者生产数据是不可避免的,不同的等待方式性能不同,因此抽象出等待策略供开发人员选择。

这篇关于LMAX Disruptor的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/175808

相关文章

无锁环形队列框架Disruptor不同策略说明

* <pre>* BlockingWaitStrategy: 这是默认的策略,使用BlockingWaitStrategy和使用BlockingQueue是非常类似的,* 他们都使用锁和条件Condition进行数据的监控和线程的唤醒,因为涉及到线程的切换,BlockingWaitStrategy策略* 是最节省cpu,但是高并发情况下性能表现最差的等待策略.* SleepingW

disruptor(2)-等待策略

在生产者消费者模式中,等待策略对消费者而言,是一个获取消息感知的方式,可以用轮询,事件触发来实现。 对于生产者而言,等待策略表现在队列池已满的情况,如何等待消息被消费,在一般不重要的场景中,我们可能是就直接抛弃了。 如我们自己使用queue作为等待队列,我们消费时一般用poll()这么去等待数据到来,如果直接用while循环,那cpu会消耗的很严重。这时我们常见的解决办法是在while循环中加

记一次Disruptor排坑

Abstract 我们在项目中使用了Disruptor作为事件总线,实现的业务是:用户消费完成成就,完成八个成就之后自动获得第九个成就——获得前面八个成就。 这个项目不是我参与的,当时我自己封装的高性能事件总线(Electrons)已经完全能胜任上述功能,但是由于小伙伴当时对我的这个组件没有特别研究,仍然感觉我的这个就是顺序执行前面几个监听器,就没有用。 这个项目在测试环境中一直没有问题,原

Java并发编程---Disruptor体验

最近在学习中接触到Disruptor这个框架,虽然目前没有能实际运用到项目中,但是做个了解,在面试吹牛逼?的时候还能避免尴尬!学的不深,仅限于简单的使用和特性的认识。 什么是Disruptor Martin Fowler在自己网站上写了一篇LMAX架构的文章,在文章中他介绍了LMAX是一种新型零售金融交易平台,它能够以很低的延迟产生大量交易。这个系统是建立在JVM平台上,其核心是一

秒级达百万高并发框架Disruptor

1、起源 Disruptor最初由lmax.com开发,2010年在Qcon公开发表,并于2011年开源,企业应用软件专家Martin Fowler专门撰写长文介绍,同年它还获得了Oracle官方的Duke大奖。其官网定义为:“High Performance Inter-Thread Messaging Library”,即:线程间的高性能消息框架。其实JDK已经为我们提供了很多开箱即用的

处理高并发高性能队列-Disruptor

已经不记得最早接触到 Disruptor 是什么时候了,只记得发现它的时候它是以具有闪电般的速度被介绍的。于是在脑子里, Disruptor 和“闪电”一词关联了起来,然而却一直没有时间去探究一下。       最近正在进行一项对性能有很高要求的产品项目的研究,自然想起了闪电般的 Disruptor ,这必有它的用武之地,于是进行了一番探查,将成果和体会记录在案。 一、什么是 Di

Disruptor系列3:Disruptor样例实战

章节回顾: - Disruptor系列1:初识Disruptor - Disruptor系列2:Disruptor原理剖析 本章节是Disruptor样例实战,依据Disruptor的工作流依次执行的特性,实现各种样例。如果想了解Disruptor是什么,可以查看章节 Disruptor系列1:初识Disruptor ,如果想深层次了解Disruptor,可以查看章节 Disruptor系列

Disruptor系列2:Disruptor原理剖析

章节回顾: - Disruptor系列1:初识Disruptor 都说Disruptor是高性能、低延迟的内存队列,每秒可以处理600W的订单,但是它为什么这么快呢?这就需要我们从他的底层设计原理开始剖析。我觉得,学习了他的实现原理,对自身了解Java并发内存结构是有很大的好处的,因为它把如何基于Java内存结构实现高性能的并发操作,解决锁的性能开销问题发挥到了极致。 无锁(Lock-Fre

剖析Disruptor:为什么会这么快?(三)揭秘内存屏障(validate关键词解析)

主题是什么? 我写这个系列的博客主要目的是解析Disruptor是如何工作的,并深入了解下为什么这样工作。理论上,我应该从可能准备使用disruptor的开发人员的角度来写,以便在代码和技术论文[Disruptor-1.0.pdf]之间搭建一座桥梁。这篇文章提及到了内存屏障,我想弄清楚它们到底是什么,以及它们是如何应用于实践中的。 什么是内存屏障? 它是一个CPU指令。没错,又一次,我们在讨

从构建分布式秒杀系统聊聊Disruptor高性能队列

Disruptor学习网站:http://ifeve.com/disruptor-getting-started/   前言 秒杀架构持续优化中,基于自身认知不足之处在所难免,也请大家指正,共同进步。文章标题来自码友<tukangzheng>的建议,希望可以把阻塞队列ArrayBlockingQueue这个队列替换成Disruptor,由于之前曾接触过这个东西,听说很不错,正好借此机会整合进