disruptor是一个高性能的线程间异步通信的框架,即在同一个JVM进程中的多线程间消息传递。应用disruptor知名项目有如下的一些:Storm, Camel, Log4j2,还有目前的美团点评技术团队也有很多不少的应用,或者说有一些借鉴了它的设计机制。 下面就跟着笔者一起去领略下disruptor高性能之道吧~

disruptor是一款开源的高性能队列框架,github地址为 https://github.com/LMAX-Exchange/disruptor

分析disruptor,只要把event的生产和消费流程弄懂,基本上disruptor的七寸就已经抓住了。话不多说,赶紧上车,笔者以下面代码为例讲解disruptor:

  1. public static void main(String[] args) {
  2. Disruptor<StringEvent> disruptor = new Disruptor<>(StringEvent::new, 1024,
  3. new PrefixThreadFactory("consumer-pool-", new AtomicInteger(0)), ProducerType.MULTI,
  4. new BlockingWaitStrategy());
  5.  
  6. // 注册consumer并启动
  7. disruptor.handleEventsWith((EventHandler<StringEvent>) (event, sequence, endOfBatch) -> {
  8. System.out.println(Util.threadName() + "onEvent " + event);
  9. });
  10. disruptor.start();
  11.  
  12. // publisher逻辑
  13. Executor executor = Executors.newFixedThreadPool(2,
  14. new PrefixThreadFactory("publisher-pool-", new AtomicInteger(0)));
  15. while (true) {
  16. for (int i = 0; i < 2; i++) {
  17. executor.execute(() -> {
  18. Util.sleep(1);
  19. disruptor.publishEvent((event, sequence, arg0) -> {
  20. event.setValue(arg0 + " " + sequence);
  21. }, "hello world");
  22. });
  23. }
  24.  
  25. Util.sleep(1000);
  26. }
  27. }
  1. class StringEvent {
  2. private String value;
  3.  
  4. public String getValue() {
  5. return value;
  6. }
  7.  
  8. public void setValue(String value) {
  9. this.value = value;
  10. }
  11.  
  12. @Override
  13. public String toString() {
  14. return "StringEvent:{value=" + value + "}";
  15. }
  16. }
  17.  
  18. class PrefixThreadFactory implements ThreadFactory {
  19. private String prefix;
  20. private AtomicInteger num;
  21.  
  22. public PrefixThreadFactory(String prefix, AtomicInteger num) {
  23. this.prefix = prefix;
  24. this.num = num;
  25. }
  26.  
  27. @Override
  28. public Thread newThread(Runnable r) {
  29. return new Thread(r, prefix + num.getAndIncrement());
  30. }
  31.  
  32. }
  33.  
  34. class Util {
  35.  
  36. static String threadName() {
  37. return String.format("%-16s", Thread.currentThread().getName()) + ": ";
  38. }
  39.  
  40. static void sleep(long millis) {
  41. try {
  42. Thread.sleep(millis);
  43. } catch (InterruptedException e) {
  44. e.printStackTrace();
  45. }
  46. }
  47. }

测试相关类

event生产流程

event的生产是从 RingBuffer.publishEvent 开始的,event生产流程步骤如下:
  • 获取待插入(到ringBuffer的)位置,相当于先占个位
  • 往该位置上设置event
  • 设置sequence对应event的标志,通知consumer
  1. public <A> void publishEvent(EventTranslatorOneArg<E, A> translator, A arg0)
  2. {
  3. // 获取当前要设置的sequence序号,然后进行设置并通知消费者
  4. final long sequence = sequencer.next();
  5. translateAndPublish(translator, sequence, arg0);
  6. }
  7.  
  8. // 获取下一个sequence,直到获取到位置才返回
  9. public long next(int n) {
  10. long current;
  11. long next;
  12.  
  13. do {
  14. // 获取当前ringBuffer的可写入sequence
  15. current = cursor.get();
  16. next = current + n;
  17.  
  18. long wrapPoint = next - bufferSize;
  19. long cachedGatingSequence = gatingSequenceCache.get();
  20.  
  21. if (wrapPoint > cachedGatingSequence || cachedGatingSequence > current) {
  22. // 如果当前没有空位置写入,获取多个consumer中消费进度最小的那个的消费进度
  23. long gatingSequence = Util.getMinimumSequence(gatingSequences, current);
  24.  
  25. if (wrapPoint > gatingSequence) {
  26. // 阻塞1ns,然后continue
  27. LockSupport.parkNanos(1); // TODO, should we spin based on the wait strategy?
  28. continue;
  29. }
  30.  
  31. gatingSequenceCache.set(gatingSequence);
  32. }
  33. // cas设置ringBuffer的sequence
  34. else if (cursor.compareAndSet(current, next)) {
  35. break;
  36. }
  37. } while (true);
  38.  
  39. return next;
  40. }
  41.  
  42. private <A> void translateAndPublish(EventTranslatorOneArg<E, A> translator, long sequence, A arg0) {
  43. try {
  44. // 设置event
  45. translator.translateTo(get(sequence), sequence, arg0);
  46. } finally {
  47. sequencer.publish(sequence);
  48. }
  49. }
  50. public void publish(final long sequence) {
  51. // 1. 设置availableBuffer,表示对应的event是否设置完成,consumer线程中会用到
  52. // - 注意,到这里时,event已经设置完成,但是consumer还不知道该sequence对应的event是否设置完成,
  53. // - 所以需要设置availableBuffer中sequence对应event的sequence number
  54. // 2. 通知consumer
  55. setAvailable(sequence);
  56. waitStrategy.signalAllWhenBlocking();
  57. }

从translateAndPublish中看,如果用户的设置event方法抛出异常,这时event对象是不完整的,那么publish到consumer端,consumer消费的不是完整的数据怎么办呢?在translateAndPublish中需不需要在异常情况下reset event对象呢?关于这个问题笔者之前是有疑问的,关于这个问题笔者提了一个issue,可点击 https://github.com/LMAX-Exchange/disruptor/issues/244 进行查看。

笔者建议在consumer消费完event之后,进行reset event操作,这样避免下次设置event异常consumer时取到不完整的数据,比如log4j2中的AsyncLogger中处理完log4jEvent之后就会调用clear方法进行重置event。

event消费流程

event消费流程入口是BatchEventProcessor.processEvents,event消费流程步骤:
  • 获取当前consumer线程消费的offset,即nextSequence
  • 从ringBuffer获取可用的sequence,没有新的event时,会根据consmer阻塞策略进行执行某些动作
  • 获取event,然后执行event回调
  • 设置当前consumer线程的消费进度
  1. private void processEvents() {
  2. T event = null;
  3. long nextSequence = sequence.get() + 1L;
  4.  
  5. while (true) {
  6. try {
  7. // 获取可用的sequence,默认直到有可用sequence时才返回
  8. final long availableSequence = sequenceBarrier.waitFor(nextSequence);
  9. if (batchStartAware != null) {
  10. batchStartAware.onBatchStart(availableSequence - nextSequence + 1);
  11. }
  12.  
  13. // 执行消费回调动作,注意,这里获取到一个批次event,可能有多个,个数为availableSequence-nextSequence + 1
  14. // nextSequence == availableSequence表示该批次只有一个event
  15. while (nextSequence <= availableSequence) {
  16. // 获取nextSequence位置上的event
  17. event = dataProvider.get(nextSequence);
  18. // 用户自定义的event 回调
  19. eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
  20. nextSequence++;
  21. }
  22.  
  23. // 设置当前consumer线程的消费进度sequence
  24. sequence.set(availableSequence);
  25. } catch (final Throwable ex) {
  26. exceptionHandler.handleEventException(ex, nextSequence, event);
  27. sequence.set(nextSequence);
  28. nextSequence++;
  29. }
  30. }
  31. }
  32.  
  33. public long waitFor(final long sequence)
  34. throws AlertException, InterruptedException, TimeoutException{
  35. long availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, this);
  36.  
  37. if (availableSequence < sequence) {
  38. return availableSequence;
  39. }
  40.  
  41. // 获取ringBuffer中可安全读的最大的sequence number,该信息存在availableBuffer中的sequence
  42. // 在MultiProducerSequencer.publish方法中会设置
  43. return sequencer.getHighestPublishedSequence(sequence, availableSequence);
  44. }
  45.  
  46. // 默认consumer阻塞策略 BlockingWaitStrategy
  47. public long waitFor(long sequence, Sequence cursorSequence, Sequence dependentSequence, SequenceBarrier barrier)
  48. throws AlertException, InterruptedException
  49. {
  50. long availableSequence;
  51. if (cursorSequence.get() < sequence) {
  52. // 当前ringBuffer的sequence小于sequence,阻塞等待
  53. // event生产之后会唤醒
  54. synchronized (mutex) {
  55. while (cursorSequence.get() < sequence) {
  56. barrier.checkAlert();
  57. mutex.wait();
  58. }
  59. }
  60. }
  61.  
  62. while ((availableSequence = dependentSequence.get()) < sequence) {
  63. barrier.checkAlert();
  64. ThreadHints.onSpinWait();
  65. }
  66.  
  67. return availableSequence;
  68. }

从上面的event消费流程来看,消费线程会读取ringBuffer的sequence,然后更新本消费线程内的offset(消费进度sequence),如果有多个event的话,那么就是广播消费模式了(单consumer线程内还是顺序消费),如果不想让event被广播消费(重复消费),可使用如下方法添加consumer线程(WorkHandler是集群消费,EventHandler是广播消费):

  1. disruptor.handleEventsWithWorkerPool((WorkHandler<StringEvent>) event -> {
  2. System.out.println(Util.threadName() + "onEvent " + event);
  3. });

disruptor高性能之道

弃用锁机制改用CAS

event生产流程中获取并自增sequence时用的就是CAS,获取之后该sequence对应位置的操作只会在单线程,没有了并发问题。

集群消费模式下获取sequence之后也会使用CAS设置为sequence新值,设置本地消费进度,然后再执行获取event并执行回调逻辑。

注意,disruptor中较多地方使用了CAS,但并不代表完全没有了锁机制,比如默认consumer阻塞策略 BlockingWaitStrategy发挥作用时,consumer消费线程就会阻塞,只不过这只会出现在event生产能力不足是才会存在。如果consumer消费不足,大量event生产导致ringBuffer爆满,这时event生产线程就会轮询调用LockSupport.parkNanos(1),这里的成本也不容小觑(涉及到线程切换损耗)。

 
避免伪共享引入缓冲行填充

伪共享讲的是多个CPU时的123级缓存的问题,通常,缓存是以缓存行的方式读取数据,如果A、B两个变量被缓冲在同一行之内,那么对于其中一个的更新会导致另一个缓冲无效,需要从内存中读取,这种无法充分利用缓存行的问题就是伪共享。disruptor相关代码如下:

  1. class LhsPadding {
  2. protected long p1, p2, p3, p4, p5, p6, p7;
  3. }
  4. class Value extends LhsPadding {
  5. protected volatile long value;
  6. }
 
使用RingBuffer作为数据存储容器

ringBuffer是一个环形队列,本质是一个数组,size为2的幂次方(方便做&操作),数据位置sequence值会和size做&操作得出数组下标,然后进行数据的读写操作(只在同一个线程内,无并发问题)。

 
小结

disruptor初衷是为了解决内存队列的延迟问题,作为一个高性能队列,包括Apache Storm、Camel、Log4j 2在内的很多知名项目都在使用。disruptor的重要机制就是CAS和RingBuffer,借助于它们两个实现数据高效的生产和消费

disruptor多生产者多消费者模式下,因为RingBuffer数据的写入是分为2步的(先获取到个sequence,然后写入数据),如果获取到sequence之后,生产者写入RingBuffer较慢,consumer消费较快,那么生产者最终会拖慢consumer消费进度,这一点需注意(如果已经消费到生产者占位的前一个数据了,那么consumer会执行对应的阻塞策略)。在实际使用过程中,如果consumer消费逻辑耗时较长,可以封装成任务交给线程池来处理,避免consumer端拖慢生成者的写入速度。

disruptor的设计对于开发者来说有哪些借鉴的呢?尽量减少竞争,避免多线程对同一数据做操作,比如disruptor使用CAS获取只会在一个线程内进行读写的event对象,这种思想其实已经在JDK的thread本地内存中有所体现;尽量复用对象,避免大量的内存申请释放,增加GC损耗,disruptor通过复用event对象来保证读写时不会产生对象GC问题;选择合适数据结构,disruptor使用ringBuffer,环形数组来实现数据高效读写。

参考资料:

1、https://tech.meituan.com/disruptor.html

disruptor 高性能之道的更多相关文章

  1. Netty 系列之 Netty 高性能之道

    1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友通过私信告诉我,通过使用 Netty4 + Thrift 压缩二进制编解码技术,他们实现了 10 W TPS(1 K 的复杂 POJO 对象)的跨 ...

  2. Netty系列之Netty高性能之道

    转载自http://www.infoq.com/cn/articles/netty-high-performance 1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友通过私信告诉我,通过使用Ne ...

  3. Netty高性能之道

    1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友告诉我,通过使用Netty4 + Thrift压缩二进制编解码技术,他们实现了10W TPS(1K的复杂POJO对象)的跨节点远程服务调用.相比于 ...

  4. 转:Netty系列之Netty高性能之道

    1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友通过私信告诉我,通过使用Netty4 + Thrift压缩二进制编解码技术,他们实现了10W TPS(1K的复杂POJO对象)的跨节点远程服务调用 ...

  5. 【读后感】Netty 系列之 Netty 高性能之道 - 相比 Mina 怎样 ?

    [读后感]Netty 系列之 Netty 高性能之道 - 相比 Mina 怎样 ? 太阳火神的漂亮人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商 ...

  6. Netty 系列之 Netty 高性能之道 高性能的三个主题 Netty使得开发者能够轻松地接受大量打开的套接字 Java 序列化

    Netty系列之Netty高性能之道 https://www.infoq.cn/article/netty-high-performance 李林锋 2014 年 5 月 29 日 话题:性能调优语言 ...

  7. Disruptor 高性能并发框架二次封装

    Disruptor是一款java高性能无锁并发处理框架.和JDK中的BlockingQueue有相似处,但是它的处理速度非常快!!!号称“一个线程一秒钟可以处理600W个订单”(反正渣渣电脑是没体会到 ...

  8. Netty(五)Netty 高性能之道

    4.背景介绍 4.1.1 Netty 惊人的性能数据 通过使用 Netty(NIO 框架)相比于传统基于 Java 序列化+BIO(同步阻塞 IO)的通信框架,性能提升了 8 倍多.事 实上,我对这个 ...

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

    前言 秒杀架构持续优化中,基于自身认知不足之处在所难免,也请大家指正,共同进步.文章标题来自码友 简介 LMAX Disruptor是一个高性能的线程间消息库.它源于LMAX对并发性,性能和非阻塞算法 ...

随机推荐

  1. (其他)window10分盘

    由于thinkpad的一个c盘大概是一个t左右,所以我们先分一下盘.   首先找到计算机管理,然后找磁盘管理,右击比较大的磁盘,压缩卷,大概就压缩一半吧,然后新建简单卷,一直下一步,紧接着就完成了. ...

  2. 关于iframe跨域实践

    提要 项目中与到iframe子页面中需要通过top获取在父页面中的全局变量的需求,由于App部署的缘故,导致父页面和iframe子页面分别在不同的端口下,导致iframe跨域现象,通过查阅资料进行问题 ...

  3. Azure SQL Virtual Machine报Login failed for user 'NT Service\SqlIaaSExtension'. Reason: Could not find a login matching the name provided

    在一台位于HK的Azure SQL Virtual Machine上修改排序规则,重建系统数据库后,监控发现大量的登录失败告警生成,如下所示: DESCRIPTION:  Login failed f ...

  4. Linux学习历程——Centos 7 ps命令基础

    一.ps命令介绍 ps命令是Process Status的缩写,用于查看系统进程状态,ps命令输出值非常多,通常结合管道符使用. 二.实例 1.我们直接输入ps命令,不加任何参数. 可以看到默认输出4 ...

  5. Python3 socket网络编程(一)

    Socket的定义 套接字是为特定网络协议(例如TCP/IP,ICMP/IP,UDP/IP等)套件对上的网络应用程序提供者提供当前可移植标准的对象.它们允许程序接受并进行连接,如发送和接受数据.为了建 ...

  6. layui form.on('select(xxx)',function(){});绑定失败

    使用layui的form.on绑定select选中事件中, form.on()不执行, 主要原因有 1, select标签中没有写lay_filter属性,用来监听 <select id=&qu ...

  7. Object类(根类)

    Object中的方法是所有类都有的方法,每个类默认继承了Object类. boolean  equals(Object obj) : Object中默认是比较地址,可以重写equals(Object ...

  8. MATLAB简易画图

    给定一组特殊点,连线作图 作者:凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 以成绩隶属函数为例: score.m cj_x1=[ 0.1]; cj_y1= ...

  9. 力扣算法题—069x的平方根

    实现 int sqrt(int x) 函数. 计算并返回 x 的平方根,其中 x 是非负整数. 由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去. 示例 1: 输入: 4 输出: 2 示例 ...

  10. java操作elasticsearch实现条件查询(match、multiMatch、term、terms、reange)

    1.条件match query查询 //条件查询match query @Test public void test10() throws UnknownHostException { //1.指定e ...