高性能队列 Disruptor

回眸只為那壹抹淺笑 提交于 2019-12-02 00:38:46

偶然中看到一篇 关于disruptor的分析,高性能低延迟的特性吸引了我。

disruptor

java实现的线程间通信的高性能低延迟消息组件,也就是消息队列,相比于BlockingQueue 最大的特点在于无锁的机制,性能提高一倍以上,常用于金融行业。

核心机制

1)无锁 CAS

使用了CPU级别的CAS指令,比os级的 lock快很多 ,避免了上下文切换。

2)数组减小GC开销

使用环形数组,而不是链表的数组结构存储数据,数组预分配,避免JavaGC的开销r

3)避免伪共享

数组顺序执行,利用了cpu的缓存特性,不至于 经常缓存失效,重新到内存读,提高缓存命中率。disruptor会尽量将两个不相关的内存隔离到两个缓存行上,不过要牺牲空间。一个缓存行可能128字节或者64字节。可重用大于替换。

  • 环形数组结构

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

  • 元素位置定位

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

9 223 372 036 854 775 807
  • 无锁设计

每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据。

 

核心组件

RingBuffer

通过使用递增的sequence 表示 访问位置,写入位置等。不用访问位置由消费者维护,而ringBuffer 自身维护了一个当前的写入值。通过CAS保证并发的安全性。而消费者维护消息指针,这样可以带来很多的功能,各个消费者可互不影响的消费。但内部也提供了如顺序执行,同步执行,等消费机制。通过取余的方式来访问数组的位置,且不需要考虑溢出的问题,因为long类型的自增,是短时间内无法一溢出。且其需要跟踪消费者的位置,来确定是否队列已满。

Sequence

最核心的组件,通过sequence达到多生产者互斥的访问,生产者与消费者之间的协调,以及消纳者之间的协调。其本质是一个递增的序号(计数器),线程间引用传递,CAS更新。线程安全。并且通过padding避免伪共享。

下面给出一个多消费者如何获取资源的源码。当生产者需要的资源超过了可用资源时抛异常。

//多生产者时获取n个生产位置
public long tryNext(int n) throws InsufficientCapacityException {
        if(n < 1) {
            throw new IllegalArgumentException("n must be > 0");
        } else {
            long current;
            long next;
            do {
                current = this.cursor.get();
                next = current + (long)n;
                if(!this.hasAvailableCapacity(this.gatingSequences, n, current)) {//检测是否超过最慢的消费者
                    throw InsufficientCapacityException.INSTANCE;
                }
            } while(!this.cursor.compareAndSet(current, next));

            return next;
        }
}
private boolean hasAvailableCapacity(Sequence[] gatingSequences, int requiredCapacity, long cursorValue) {
        long wrapPoint = cursorValue + (long)requiredCapacity - (long)this.bufferSize;
        long cachedGatingSequence = this.gatingSequenceCache.get();
        if(wrapPoint > cachedGatingSequence || cachedGatingSequence > cursorValue) {
            long minSequence = Util.getMinimumSequence(gatingSequences, cursorValue);
            this.gatingSequenceCache.set(minSequence);
            if(wrapPoint > minSequence) {
                return false;
            }
        }

        return true;
    }

SequenceBarrier

用来队列(RingBuffer)与消费者以及消费者与消费与消费者之间的依赖关系,也就是一个执行顺序的的定义。所以消费者必须 小于队列中的curosr(当前队列中队首的位置).SequenceBarrier会收集所依赖的组件的Sequencr.

WaitStrategy

当消费者等待在SequenceBarrier上时,有多种策略,在延迟和CPU资源的占用上各有不同。

  • BusySpinWaitStrategy : 自旋等待,类似Linux Kernel使用的自旋锁。低延迟但同时对CPU资源的占用也多。

  • BlockingWaitStrategy : 使用锁和条件变量。CPU资源的占用少,延迟大。

  • SleepingWaitStrategy : 在多次循环尝试不成功后,选择让出CPU,等待下次调度,多次调度后仍不成功,尝试前睡眠一个纳秒级别的时间再尝试。这种策略平衡了延迟和CPU资源占用,但延迟不均匀。

  • YieldingWaitStrategy : 在多次循环尝试不成功后,选择让出CPU,等待下次调。平衡了延迟和CPU资源占用,但延迟也比较均匀。

  • PhasedBackoffWaitStrategy : 上面多种策略的综合,CPU资源的占用少,延迟大。

BatchEvenProcessor

在Disruptor中,消费者是以EventProcessor的形式存在的。其中一类消费者是BatchEvenProcessor。每个BatchEvenProcessor有一个Sequence,来记录自己消费RingBuffer中消息的情况。所以,一个消息必然会被每一个BatchEvenProcessor消费。

WorkProcessor

另一类消费者是WorkProcessor。每个WorkProcessor也有一个Sequence,多个WorkProcessor还共享一个Sequence用于互斥的访问RingBuffer。一个消息被一个WorkProcessor消费,就不会被共享一个Sequence的其他WorkProcessor消费。这个被WorkProcessor共享的Sequence相当于尾指针。

WorkerPool

共享同一个Sequence的WorkProcessor可由一个WorkerPool管理,这时,共享的Sequence也由WorkerPool创建。

 

http://www.tuicool.com/articles/b2eUBn

http://tech.meituan.com/disruptor.html

http://ifeve.com/disruptor/

http://ziyue1987.github.io/pages/2013/09/22/disruptor-use-manual.html#onepublishertoonebatcheventprocessor

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!