Disruptor为何这么快

标签: disruptor | 发表时间:2020-04-18 09:39 | 作者:Rayjun
出处:https://juejin.im/welcome/backend

Disruptor 是一个开源并且高效的 生产者-消费者框架,很难直接解释这个框架是做什么的,但是可以把这个框架理解成 Java 中的 BlockingQueue。这样理解起来是不是轻松多了,这就是一个生产者-消费者队列,只不过它的性能要比 BloockingQueue 好很多,号称单机器可以有百万的 TPS。

Disruptor 的特点

Disruptor 有以下主要三个特点:

  • 事件多播
  • 为事件提前分配内存
  • 无锁操作

一般我们在使用队列的时候,队列中的消息只会被一个 消费者使用,但是在 Disruptor 中,同一个消息可以被多个消费者同时处理,多个消费者之间是并行的。

因为可能在同一时间,一份数据要在多个地方被用到,比如一个请求数据需要同时 被存入日志备份到远程机器进行业务处理,如下图所示:

如果是一条消息只能被一个消费者处理,那么上面说的三个处理逻辑就得线性的完成或者放到一个消费者中再异步进行处理。如果可以把这些操作拆成多个消费者来 并行消费同一条消息,处理效率就会提高很多。

这样要确保一条消息在都被所有消费者处理之后才能开始处理下一条,不能出现消费者在同时处理不同的消息,所以需要类似 Java 中 CyclicBarrier 一样的工具来保证所有消费者能够同时处理下一个消息,Disruptor 中实现了 SequenceBarrier 来完成这个功能。

Disruptor 目标是应用于低延迟的环境中。在低延迟的系统中需要减少或者完全不进行内存分配,在 Java 中,就是要减少垃圾回收所带来的停顿时间。在 Disruptor 中,使用 RingBuffer 来达成这个目标,在 RingBuffer 中提前创建好对象,后续通过反复利用这些对象来避免垃圾回收,这个实现是线程安全的。

在 Disruptor 中,实现线程安全基本不使用锁,而是使用 CAS 等无锁机制来保证线程安全。

核心概念

在正式了解 Disruptor 之前,我们需要了解一些核心的概念,Disruptor 的代码并不复杂,基本是围绕这些核心概念来展开的。

  • Producer: 数据的生产者,生产者本身与 Disruptor 无关,可以是任何生产数据的代码,甚至可以是一个 for 循环
  • Event: Producer 产生的数据,用户可以根据自己的需要自行进行定义
  • RingBuffer: 用来存储 Event 的数据结构,提前分配好内存,避免在程序运行的过程中创建对象
  • EventHandler: 消费者,由用户自己实现
  • Sequence: 用来标识 Disruptor 中的组件,多个组件之间的协同依靠它来实现
  • Sequencer: Disruptor 中的核心机制,实现了核心的并发算法,保证消息在生产者和消费之间正确的进行传递
  • SequenceBarrier: 用来保证所有的消费这能够同时处理新的消息
  • WaitStrategy: 消费者等待策略
  • EventProcessor: 将消息传递到消费者的具体实现

上面的这些组件组成了 Disruptor,整个框架的代码量其实很少,应该不到 7000 行,而且代码很干净。代码基本没有使用继承,而是使用了 面向接口编程以及 组合,所以代码之间的耦合度很低。

RingBuffer 和 Sequencer 是其中最重要的两个组件,前者用于存储消息,后者控制消息有序的生产和消费。

Disruptor 的核心目标就是提升程序的吞吐量,所以程序也是围绕这些目标来实现的,主要做了如下的事情:

  • 减少垃圾回收
  • 让消息可以通过被多个消费者并行处理
  • 使用无锁算法来实现并发
  • 缓存行填充

RingBuffer 是用来存储消息的容器,内部实现使用了一个数组:

  private final Object[] entries;
复制代码

在 Disruptor 启动之前,需要指定数据的大小以及,初始化这个数组:

  private void fill(EventFactory eventFactory)
{
    for (int i = 0; i < bufferSize; i++)
    {
        entries[BUFFER_PAD + i] = eventFactory.newInstance();
    }
}
复制代码

数组在初始化之后就不再回收了,所有的消息会循环利用这些已经创建好的对象,所以这是一个 循环数组,RingBuffer 的实现如下图所示:

那么在对循环数组进行操作的时候,需要对生产者和消费者对数组的访问进行控制。一方面,因为 Disruptor 支持多个生产者和多个消费者,所以要保证线程安全,为了保证性能,并没有使用锁来保证线程安全(只有 BlockingWaitStrategy 使用了锁),在对 RingBuffer 的访问控制中,主要使用 CAS 来完成:

  protected final E elementAt(long sequence)
{
    return (E) UNSAFE.getObject(entries, REF_ARRAY_BASE + ((sequence & indexMask) << REF_ELEMENT_SHIFT));
}
复制代码

另一方面对于生产者和消费者的速度进行访问,生产者不能对未消费的消息进行写入,这样会造成消息的丢失,在 RingBuffer 中,没有使用 head 和 tail 指针进行控制,而是通过 Sequence 来进行控制,生产者写入数据的时候,会通过当前的序列号加上需要写入的数据量,与消费者的位置进行对比,看看是否有足够的空间进行写入。

在 RingBuffer 中,有这样一段代码:

  abstract class RingBufferPad
{
    protected long p1, p2, p3, p4, p5, p6, p7;
}
复制代码

这段代码就称之为缓存行填充,说到这个就需要了解 CPU 的缓存机制,因为内存的访问速度与 CPU 的速度相差太远,所以在 CPU 和内存之间还加上了 CPU 缓存,现在一般会加上 3 级,第一级和第二级是 CPU 核独享的,第三级缓存则是多个核之间共享。

在很多情况下,我们想把一些不会变化的值缓存到 CPU 缓存中,比如 Java 中的 final 变量,这样就可以最大化的利用 CPU 缓存的速度,但是 CPU 缓存有一个特点,缓存数据的时候会以 CPU 缓存行为单位,所以如果一个 final 变量的附近定义了会变化的变量,每次变量变化的时候,数据就会重新被写回到内存中,那么 final 变量同样也不会再缓存在 CPU 缓存中了,所以在缓存行的前后部分都需要填充,确保不会缓存到其他的数据:

  abstract class RingBufferPad
{
    // 填充缓存行的前部分
    protected long p1, p2, p3, p4, p5, p6, p7;
}
abstract class RingBufferFields extends RingBufferPad{ 
    ......
    // 下面需要被缓存到 CPU 缓存的数据
    private final long indexMask; 
    private final Object[] entries; 
    protected final int bufferSize;
    protected final Sequencer sequencer; 
    ...... 
}
public final class RingBuffer extends RingBufferFields implements Cursored, EventSequencer, EventSink{ 
    ...... 
    // 填充缓存行的后部分
    protected long p1, p2, p3, p4, p5, p6, p7; 
    ......
}
复制代码

这样 RingBufferFields 的数据被加载到 CPU 缓存中之后,就不会再需要从内存中读取了。

Disruptor 通过各方面的措施来提升性能,这就是为什么这么快的原因。

原文

关注微信公众号,聊点其他的

相关 [disruptor] 推荐:

Disruptor封装

- - 开源软件 - ITeye博客
在数据交换场景,disruptor受到越来越多的欢迎. 下面是将原生disruptor封装成queue模型的代码,供参考. 抽象类Disruptor,提供pull、take等接口. 已有 0 人发表留言,猛击->> 这里<<-参与讨论. —软件人才免语言低担保 赴美带薪读研.

disruptor使用示例

- - 企业架构 - ITeye博客
LMAX 开源了一个高性能并发编程框架. 可以理解为消费者-生产者的消息发布订阅模式. 本文下载了官方示例代码,进行实验. longEvent事件数据. LongEventFactory事件工厂. import com.lmax.disruptor.EventFactory; /** * 事件生产工厂 * @author wanghao * */ public class LongEventFactory implements EventFactory {.

Disruptor 学习笔记

- - 开源软件 - ITeye博客
Disruptor 是一个高性能异步处理框架,也可以认为是一个消息框架,它实现了观察者模式. Disruptor 比传统的基于锁的消息框架的优势在于:它是无锁的、CPU友好;它不会清除缓存中的数据,只会覆盖,降低了垃圾回收机制启动的频率. Disruptor 为什么快. 通过内存屏障和原子性的CAS操作替换锁.

Disruptor使用入门

- - CSDN博客推荐文章
在最近的项目中看到同事使用到了Disruptor,以前在ifeve上看到过关于Disruptor的文章,但是没有深入研究,现在项目中用到了,就借这个机会对这个并发编程框架进行深入学习. 项目中使用到的是disruptor-2.10.4,所以下面分析到的Disruptor的代码是这个版本的. 并发编程网介绍Disruptor的文章是disruptor1.0版本,所以有一些术语在2.0版本上已经没有了或者被替代了.

并发框架Disruptor译文

- - 酷壳 - CoolShell.cn
(感谢同事 方腾飞投递本文). Martin Fowler在自己网站上写了一篇LMAX架构的文章,在文章中他介绍了LMAX是一种新型零售金融交易平台,它能够以很低的延迟产生大量交易. 这个系统是建立在JVM平台上,其核心是一个业务逻辑处理器,它能够在一个线程里每秒处理6百万订单. 业务逻辑处理器完全是运行在内存中,使用事件源驱动方式.

Disruptor 极速体验 - haiq

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

Disruptor为何这么快

- - 掘金后端
Disruptor 是一个开源并且高效的 生产者-消费者框架,很难直接解释这个框架是做什么的,但是可以把这个框架理解成 Java 中的 BlockingQueue. 这样理解起来是不是轻松多了,这就是一个生产者-消费者队列,只不过它的性能要比 BloockingQueue 好很多,号称单机器可以有百万的 TPS.

GitHub - chanjarster/artemis-disruptor-miaosha: 没有redis也能够支撑

- -
"小米在印度把亚马逊搞挂了"事件的秒杀解决方案. 小米在印度打破了多项记录:. 4分钟内卖出了超过250,000台. ---OPS:1042次抢购/S. 抢购前我们收到了100万“到货提醒”. 亚马逊每分钟收到超过500万个点击. 亚马逊在这个期间每秒收到1500个订单(这是印度电商公司所有销售中最高的).

还在用阻塞队列?读这篇文章,了解下 Disruptor 吧

- - IT瘾-dev
听到队列相信大家对其并不陌生,在我们现实生活中队列随处可见,去超市结账,你会看见大家都会一排排的站得好好的,等待结账,为什么要站得一排排的,你想象一下大家都没有素质,一窝蜂的上去结账,不仅让这个超市崩溃,还会容易造成各种踩踏事件,当然这些事其实在我们现实中也是会经常发生. 当然在计算机世界中,队列是属于一种数据结构,队列采用的FIFO(first in firstout),新元素(等待进入队列的元素)总是被插入到尾部,而读取的时候总是从头部开始读取.