Disruptor
1. 背景
在给Lancer用户开发第三方SDK时,发现当用户使用SDK在一段很短的时间内发送大量数据到Lancer的接收端时,如果使用Logback的AsyncAppender会存在数据丢失的情况,当切换到LogIeventDisruptor则不会出现这种情况,数据交互非常顺利。其中AsyncAppender底层使用的是ArrayBlockingQueue,Disruptor使用的是RingBuffer。
2. 找问题
我们知道当发送端要在一段很短的时间内发送出大量的数据,如果直接用线程给远端发送数据,则必然会导致阻塞的发生;这个时候我们的正常反应就是建一个队列,将要发给远端的数据缓冲起来,让线程去消费这个队列把数据发送到远端;那么问题来了,队列得使用也分场景的。
3. 队列
队列名称 | 是否阻塞 | 锁的类型 | 竞争位置 | 数据结构 |
LinkedBlockingQueue | 是 | ReentrantLock | head / last | 链表 |
ArraryBlockingQueue | 是 | ReentrantLock | takeIndex / putIndex | 数组 |
ConcurrentLinkedQueue | 否 | CAS | head / tail | 链表 |
RingBuffer | 否 | CAS | sequence | 数组 |
4. Disruptor
4.1 数组预分配,避免GC带来的运行开销
链表队列在出队的时候需要把自己第一个Node的Next指向自己,来让JVM快速的知道这个Node已经可以被删除了,然后按照某种策略进行GC操作,这必然会带来一部分开销。
数组虽然可以避免节点被删除后的GC操作,但是会存在两个问题,一个是长度的固定需要预先判断,不然就容易导致队列满了,数据就会丢失;另一个就是要知道当前的位置就必须进行一次Mod计算,在Disruptor里面用了一个技巧,当数组长度为2的N次方时可以使用位操作来获取当前位置(location & (2的N次方 - 1))
4.2 不用阻塞锁
并发时存在共享变量时,基本上伴随着就是多个线程对这个变量的竞争,一般的做法是用阻塞锁来让线程在处理这个变量时变得有序,得到一个我们想要的结果,但是速度慢已经是公认的;另一种做法就是用系统底层提供的CAS(Compare and Swap)来处理共享变量的竞争,保证每次只有一个线程可以修改共享变量的值。
4.3 避免伪共享
在一个实现队列的类里面会有多个变量是用来操作或者描述这个队列的一种属性,当多个线程并发时这些变量会被缓存到CPU的Cache中,当存在某两个变量在内存中的地址相邻,可能会存在一个CPU的核心把两个变量都读到缓存中,另一个CPU读到的缓存就无效,需要等候其他CPU把变量处理完才能重新处理这个变量,这样一来一回就导致两个CPU的核心没法真正的并发执行一段代码。
4.4优化任务依赖
在一个系统中,会存在一个业务强依赖另一个业务的完成才能继续工作,在队列处理数据时也一样,如果我们想两个业务能良好的进行下去,那么就需要建两个队列去完成这个任务,第一个队列在不断的出队来完成它的业务,接着把出队的数据丢给第二队列让它完成接下去的工作;在Disruptor中我们只需要一个RingBuffer就可以解决这个问题。
5. 原理
6. 使用
7. 结论
Disruptor可以帮助我们更好的完成工作,在高并发低延迟方面表现的非常好。
8. 展望
既然有这个好的模型和框架,当然我们就想要把他应用到我们的项目中,我们讨论过flume里面channel 作为一个缓冲,底层用的是一个LinkedBlockingDeque,所以大家都认为他效率低,那么我们是否可以使用Disruptor来对这个进行优化呢?
当然Disruptor作为一个生产消费模型非常好的实现方案其他地方也是可以用到的。