当前位置:首页 > SEO优化 > 正文

的过往(让记忆的风缠绕过往)

  

  

  干扰器是公司英国外汇交易公司LMAX开发的高性能队列,研发的初衷是为了解决内存队列的延迟问题(性能测试中发现和I/O操作在同一个数量级)。

  

  

  

  基于干扰机开发的系统每秒可支持600万个订单,这在2010年QCon演讲后引起了业界的关注。2011年,来自企业,的应用软件专家马丁福勒写了一篇很长的介绍。同年还获得甲骨文官方杜克奖。

  

  

  

  目前包括Apache Storm、Camel、Log4j 2在内的很多知名项目都应用了Disruptor来获得高性能。

  

  

  

  特别是这里说的队列是系统,的内存队列,而不是卡夫卡那样的分布式队列。此外,本文描述的干扰物或特征仅限于3.3.4。

  

  

  

  在介绍干扰器之前,让我们看看常用的线程安全内置队列有什么问题。下表显示了Java的内置队列。

  

  

  

  队列的底层一般分为三种:数组、链表、堆。其中堆一般用于实现具有优先级特征的队列,暂时不考虑。

  

  

  

  从数组和链表的数据结构来看,基于数组线程安全的队列是ArrayBlockingQueue,主要通过锁定来保证线程安全;基于链表的线程安全队列分为两类:LinkedBlockingQueue和ConcurrentLinkedQueue。前者通过锁定也是线程安全的,而后者和上表中的LinkedTransferQueue是通过原子变量比较和交换(以下简称“CAS”)实现的,没有锁定。

  

  

  

  但是在对易变变量进行CAS操作时存在伪共享问题。详情请参阅专题文章:

  

  

  

  伪共享(图形)

  

  

  

  干扰器使用类似的方案来解决伪共享问题。

  

  

  

  在中断器中有一个重要的类序列,它包装了一个易变的修改后的长数据值,无论它是中断器中基于阵列的缓冲区环形缓冲区,还是生产或消费。它们都有自己独立的序列。在环形缓冲区中,序列指示写入进度。例如,每当生产想要将数据写入缓冲区时,它都会调用RingBuffer.next()来获取下一个可用的相对位置。对于生产和消费,序列标志着它们的事件序列号。看看序列类的源代码:

  

  

  

  从第1行到第11行,我们可以看到实际使用的变量值的前后空格由8个长变量填充,对于大小为64字节的缓存行,它刚刚被填充(一个长变量值,8字节加上前/后7个长变量,7 * 8=56,56 8=64字节)。这样,每次将变量值读入高速高速缓存时,都可以填充高速缓存行(对于大小为64字节的高速缓存行,如果高速缓存行的大小大于64字节,仍然会出现伪共享问题),以确保每次数据为处理时,都不会与其他变量发生冲突

  

  

  

  “干扰者”最常用的场景是“一个生产,多个消费"”场景,这需要顺序的处理

  

  

  

  目前业界开源组件使用的是Disruptor,包括Log4j2、Apache Storm等。它可以用作高性能的有界内存队列,并且基于生产消费模式,一个/多个生产用户对应多个消费用户。它也可以被认为是观察者模式或者发布-订阅模式的一种实现。

  

  

  

  例如,我们从MySQL的BigLog文件中顺序读取数据,然后将其写入弹性搜索。在这种情况下,BigLog要求一个文件是生产,那个文件是生产,写弹性搜索时,严格要求顺序,否则会有问题,所以一般的multi-消费thread不能解决这个问题,如果采用锁定,性能会打到折扣

  

  

  

  实战:干扰者或使用的例子

  

  

  

  让我们从一个简单的例子中学习:生产把一个长值传递给消费,而消费消费只是把它打印出来。

  

  

  

  定义事件

  

  

  

  首先,定义一个事件来包含要传输的数据:

  

  

  

  因为我们需要让干扰者为我们创建事件,所以我们还声明了一个事件工厂来实例化事件对象。

  

  

  

  定义事件处理器(干扰者将调用这个处理器的方法)

  

  

  

  我们需要一个消费,事件,即处理器事件处理器事件只是将存储在事件中的数据打印到终端:

  

  

  

  定义事件源:事件发布者来发布事件

  

  

  

  事件总是有一个事件生成源。在本例中,假设当磁盘IO或网络读取数据时触发事件,事件源使用字节缓冲区来模拟它接收的数据,即当IO读取部分数据时,事件源将触发事件(触发事件不是自动,程序员在读取数据时需要触发事件并发布):

  

  

  

  显然,当用简单的队列发布事件时,会涉及到更多的细节,因为需要提前创建事件对象。

  

  

  

  发布事件至少需要两个步骤:

  

>

  

    获取下一个事件槽,发布事件(发布事件的时候要使用try/finnally保证事件一定会被发布)。

  

    如果我们使用RingBuffer.next()获取一个事件槽,那么一定要发布对应的事件。如果不能发布事件,那么就会引起Disruptor状态的混乱。尤其是在多个事件生产者的情况下会导致事件消费者失速,从而不得不重启应用才能会恢复。

  

    Disruptor 3.0提供了lambda式的API。这样可以把一些复杂的操作放在Ring Buffer,所以在Disruptor3.0以后的版本最好使用Event Publisher或者Event Translator(事件转换器)来发布事件。

  

    Disruptor3.0以后的事件转换器(填充事件的业务数据)

  

    上面写法的另一个好处是,Translator可以分离出来并且更加容易单元测试。Disruptor提供了不同的接口(EventTranslator, EventTranslatorOneArg, EventTranslatorTwoArg, 等等)去产生一个Translator对象。很明显,Translator中方法的参数是通过RingBuffer来传递的。

  

    组装起来

  

    最后一步就是把所有的代码组合起来完成一个完整的事件处理系统。Disruptor在这方面做了简化,使用了DSL风格的代码(其实就是按照直观的写法,不太能算得上真正的DSL)。虽然DSL的写法比较简单,但是并没有提供所有的选项。如果依靠DSL已经可以处理大部分情况了。

  

    注意:这里没有使用时间转换器,而是使用简单的 事件发布器。

  

    在Java 8使用Disruptor

  

    Disruptor在自己的接口里面添加了对于Java 8 Lambda的支持。大部分Disruptor中的接口都符合Functional Interface的要求(也就是在接口中仅仅有一个方法)。所以在Disruptor中,可以广泛使用Lambda来代替自定义类。

  

    由于在Java 8中方法引用也是一个lambda,因此还可以把上面的代码改成下面的代码:

  

    Disruptor实现高性能主要体现了去掉了锁,采用CAS算法,同时内部通过环形队列实现有界队列。

  

    环形数据结构

  

    为了避免垃圾回收,采用数组而非链表。同时,数组对处理器的缓存机制更加友好。

  

    元素位置定位

  

    数组长度2^n,通过位运算,加快定位的速度。下标采取递增的形式。不用担心index溢出的问题。index是long类型,即使100万QPS的处理速度,也需要30万年才能用完。

  

    无锁设计

  

    每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据。整个过程通过原子变量CAS,保证操作的线程安全。

  

    使用Disruptor,主要用于对性能要求高、延迟低的场景,它通过“榨干”机器的性能来换取处理的高性能。如果你的项目有对性能要求高,对延迟要求低的需求,并且需要一个无锁的有界队列,来实现生产者/消费者模式,那么Disruptor是你的不二选择。

  

    RingBuffer 是一个环(首尾相连的环),用做在不同上下文(线程)间传递数据的buffer。

  

    RingBuffer 拥有一个序号,这个序号指向数组中下一个可用元素。

  

    

  

    Disruptor框架就是一个使用CAS操作的内存队列,与普通的队列不同,Disruptor框架使用的是一个基于数组实现的环形队列,无论是生产者向缓冲区里提交任务,还是消费者从缓冲区里获取任务执行,都使用CAS操作。

  

    使用环形队列的优势:

  

    第一,简化了多线程同步的复杂度。学数据结构的时候,实现队列都要两个指针head和tail来分别指向队列的头和尾,对于一般的队列是这样,想象下,如果有多个生产者同时往缓冲区队列中提交任务,某一生产者提交新任务后,tail指针都要做修改的,那么多个生产者提交任务,头指针不会做修改,但会对tail指针产生冲突,例如某一生产者P1要做写入操作,在获得tail指针指向的对象值V后,执行compareAndSet()方法前,tail指针被另一生产者P2修改了,这时生产者P1执行compareAndSet()方法,发现tail指针指向的值V和期望值E不同,导致冲突。同样,如果多个消费者不断从缓冲区中获取任务,不会修改尾指针,但会造成队列头指针head的冲突问题(因为队列的FIFO特点,出列会从头指针出开始)。

  

    环形队列的一个特点就是只有一个指针,只通过一个指针来实现出列和入列操作。如果使用两个指针head和tail来管理这个队列,有可能会出现“伪共享”问题(伪共享问题在下面我会详细说),因为创建队列时,head和tail指针变量常常在同一个缓存行中,多线程修改同一缓存行中的变量就容易出现伪共享问题。

  

    第二,由于使用的是环形队列,那么队列创建时大小就被固定了,Disruptor框架中的环形队列本来也就是基于数组实现的,使用数组的话,减少了系统对内存空间管理的压力,因为它不像链表,Java会定期回收链表中一些不再引用的对象,而数组不会出现空间的新分配和回收问题。

  

    Disruptor默认的等待策略是BlockingWaitStrategy。这个策略的内部适用一个锁和条件变量来控制线程的执行和等待(Java基本的同步方法)。BlockingWaitStrategy是最慢的等待策略,但也是CPU使用率最低和最稳定的选项。然而,可以根据不同的部署环境调整选项以提高性能。

  

    SleepingWaitStrategy

  

    和BlockingWaitStrategy一样,SpleepingWaitStrategy的CPU使用率也比较低。它的方式是循环等待并且在循环中间调用LockSupport.parkNanos(1)来睡眠,(在Linux系统上面睡眠时间60μs).然而,它的优点在于生产线程只需要计数,而不执行任何指令。并且没有条件变量的消耗。但是,事件对象从生产者到消费者传递的延迟变大了。SleepingWaitStrategy最好用在不需要低延迟,而且事件发布对于生产者的影响比较小的情况下。比如异步日志功能。

  

    YieldingWaitStrategy

  

    YieldingWaitStrategy是可以被用在低延迟系统中的两个策略之一,这种策略在减低系统延迟的同时也会增加CPU运算量。YieldingWaitStrategy策略会循环等待sequence增加到合适的值。循环中调用Thread.yield()允许其他准备好的线程执行。如果需要高性能而且事件消费者线程比逻辑内核少的时候,推荐使用YieldingWaitStrategy策略。例如:在开启超线程的时候。

  

    BusySpinW4aitStrategy

  

    BusySpinWaitStrategy是性能最高的等待策略,同时也是对部署环境要求最高的策略。这个性能最好用在事件处理线程比物理内核数目还要小的时候。例如:在禁用超线程技术的时候。

  

    单一写者模式

  

    在并发系统中提高性能最好的方式之一就是单一写者原则,对Disruptor也是适用的。如果在你的代码中仅仅有一个事件生产者,那么可以设置为单一生产者模式来提高系统的性能。

  

    

  

    一次生产,串行消费

  

    比如:现在触发一个注册Event,需要有一个Handler来存储信息,一个Hanlder来发邮件等等。

  

    

  

    菱形方式执行

  

    

  

    链式并行计算

  

    

  

    相互隔离模式

  

    

  

    航道模式

  

    串行依次执行,同时C11,C21分别有2个实例

is

有话要说...