构建高性能服务(三)Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue--转载-程序员宅基地

技术标签: java  数据结构与算法  数据库  

原文地址:http://maoyidao.iteye.com/blog/1663193

一个仅仅部署在4台服务器上的服务,每秒向Database写入数据超过100万行数据,每分钟产生超过1G的数据。而每台服务器(8核12G)上CPU占用不到100%,load不超过5。这是怎么做到呢?下面将给你描述这个架构,它的核心是一个高效缓冲区设计,我们对它的要求是:

1,该缓存区要尽量简单

2,尽量避免生产者线程和消费者线程锁

3,尽量避免大量GC

缓冲 vs 性能瓶颈

提高硬盘写入IO的银弹无疑是批量顺序写,无论是在业界流行的分布式文件系统或数据,HBase,GFS和HDFS,还是以磁盘文件为持久化方式的消息队列Kafka都采用了在内存缓存数据然后再批量写入的策略。这一个策略的性能核心就是内存中缓冲区设计。这是一个经典的数据产生者和消费者场景,缓冲区的要求是当同步写入和读出时:(1)写满则不写(2)读空则不读(3)不丢失数据(4)不读重复数据。最直接也是常用的方式就是JDK自带的LinkedBlockingQueue。LinkedBlockingQueue是一个带锁的消息队列,写入和读出时加锁,完全满缓冲区上面的四个要求。但是当你的程序跑起来之后,看看那个线程CPU消耗最高?往往就是在线程读LinkedBlockingQueue锁的时候,这也成为很多对吞吐要求很高的程序的性能瓶颈。

Disruptor

解决加锁队列产生的性能问题?Disruptor是一个选择。Disruptor是什么?看看开源它的公司LMAX自己是怎么介绍的:

 

我们花费了大量的精力去实现更高性能的队列,但是,事实证明队列作为一种基础的数据结构带有它的局限性——在生产者、消费者、以及它们的数据存储之间的合并设计问题。Disruptor就是我们在构建这样一种能够清晰地分割这些关注问题的数据结构过程中所诞生的成果。

 

OK,Disruptor是用来解决我们这个场景的问题的,而且它不是队列。那么它是什么并且如何实现高效呢?我这里不做过多介绍,网上类似资料很多,简单的总结:

1,Disruptor使用了一个RingBuffer替代队列,用生产者消费者指针替代锁。

2,生产者消费者指针使用CPU支持的整数自增,无需加锁并且速度很快。Java的实现在Unsafe package中。

 

使用Disruptor,首先需要构建一个RingBuffer,并指定一个大小,注意如果RingBuffer里面数据超过了这个大小则会覆盖旧数据。这可能是一个风险,但Disruptor提供了检查RingBuffer是否写满的机制用于规避这个问题。而且根据maoyidao测试结果,写满的可能性不大,因为Disrutpor确实高效,除非你的消费线程太慢。

 

并且使用一个单独的线程去处理RingBuffer中的数据:

 

Java代码   收藏代码
  1. RingBuffer ringBuffer = new RingBuffer<ValueEvent>(ValueEvent.EVENT_FACTORY,  
  2.          new SingleThreadedClaimStrategy(RING_SIZE),  
  3.          new SleepingWaitStrategy());  
  4.   
  5.     SequenceBarrier barrier = ringBuffer.newBarrier();  
  6.   
  7.     BatchEventProcessor<ValueEvent> eventProcessor = new BatchEventProcessor<ValueEvent>(ringBuffer, barrier, handler);  
  8.     ringBuffer.setGatingSequences(eventProcessor.getSequence());  
  9.     // only support single thread  
  10.     new Thread(eventProcessor).start();  

 

ValueEvent通常是个自定义的类,用于封装你自己的数据:

 

Java代码   收藏代码
  1. public class ValueEvent {  
  2.     private byte[] packet;  
  3.   
  4.     public byte[] getValue()  
  5.     {  
  6.         return packet;  
  7.     }  
  8.   
  9.     public void setValue(final byte[] packet)  
  10.     {  
  11.         this.packet = packet;  
  12.     }  
  13.   
  14.     public final static EventFactory<ValueEvent> EVENT_FACTORY = new EventFactory<ValueEvent>()  
  15.     {  
  16.         public ValueEvent newInstance()  
  17.         {  
  18.             return new ValueEvent();  
  19.         }  
  20.     };  
  21. }  

 

 

生产者通过RingBuffer.publish方法向buffer中添加数据,同时发出一个事件通知消费者有新数据达到,并且,,,注意我们是怎么规避数据覆盖问题的:

 

Java代码   收藏代码
  1. // Publishers claim events in sequence  
  2. long sequence = ringBuffer.next();  
  3.   
  4. // if capacity less than 10%, don't use ringbuffer anymore  
  5. if(ringBuffer.remainingCapacity() < RING_SIZE * 0.1) {  
  6.     log.warn("disruptor:ringbuffer avaliable capacity is less than 10 %");  
  7.     // do something  
  8. }  
  9. else {  
  10.     ValueEvent event = ringBuffer.get(sequence);  
  11.     event.setValue(packet); // this could be more complex with multiple fields  
  12.     // make the event available to EventProcessors  
  13.     ringBuffer.publish(sequence);  
  14. }  

 

数据消费者代码在EventHandler中实现:

 

Java代码   收藏代码
  1. final EventHandler<ValueEvent> handler = new EventHandler<ValueEvent>()  
  2. {  
  3.     public void onEvent(final ValueEvent event, final long sequence, final boolean endOfBatch) throws Exception  
  4.     {     
  5.         byte[] packet = event.getValue();  
  6.         // do something  
  7.     }  
  8. };  

 

很好,完成!用以上代码跑个压测,结果果然比加锁队列快很多(Disruptor官网上有benchmark数据,我这里就不提供对比数据)。好,用到线上环境。。。。结果是。。。CPU反而飙升了!??

Disruptor的坑

 

书接上文,Disruptor压测良好,但上线之后CPU使用达到650%,LOAD接近300!分析diruptor源码可知,造成cpu过高的原因是 RingBuffer 的waiting策略,Disruptor官网例子使用的策略是 SleepingWaitStrategy ,这个类的策略是当没有新数据写入RingBuffer时,每1ns检查一次RingBuffer cursor。1ns!跟死循环没什么区别,因此CPU暴高。改成每100ms检查一次,CPU立刻降为7.8%。

 

为什么Disruptor官网例子使用这种有如此风险的SleepingWaitStrategy呢?原因是此策略完全不使用锁,当吞吐极高时,RingBuffer中始终有数据存在,通过轮询策略就能最大程度的把它的性能优势发挥出来。但这显然是理想状态,互联网应用有明显的高峰低谷,不可能总处于满负荷状态。因此还是BlockingWaitStrategy 这种锁通知机制更好:

 

Java代码   收藏代码
  1. RingBuffer ringBuffer = new RingBuffer<ValueEvent>(ValueEvent.EVENT_FACTORY,  
  2.                 new SingleThreadedClaimStrategy(RING_SIZE),  
  3.                 new BlockingWaitStrategy());  

 这样写入不加锁,读出加锁。相对加锁队列少了一半,性能还是有显著提高。

 

还有没有更好的方法?

Disruptor是实现缓冲区的很好选择。但它本质的目的是提供线程间交换数据的高效实现,这是一个很好的通用选择。那么真对我们数据异步批量落地的场景,还有没有更好的选择呢?答案是:Yes,we have!我最终设计了一个非常简单的buffer,原因是:

1,Disruptor很好,但毕竟多引入了一个依赖,对于新同学也有学习成本。

2,Disruptor不能很好的解决GC过多的问题。

那么更好的缓存是什么呢?这首先要从场景说起。

首先的问题是:我需要一个buffer,但为啥要一个跨线程buffer呢?如果我用同一个线程读,再用这个线程去写,这个buffer完全是线程本地buffer,锁本身就无意义。同时异步Database落地没有严格的顺序要求,因此我是多线程同步读写,也不需要集中时的buffer来维护顺序,因此一个内置于线程中的二维byte[][]数组就可以解决全部问题!

 

Java代码   收藏代码
  1. public class ThreadLocalBoundedMQ {  
  2.     private long lastFlushTime=0L;  
  3.       
  4.     private byte[][] msgs=new byte[Constants.BATCH_INS_COUNT][];  
  5.       
  6.     private int offset=0;  
  7.       
  8.     public byte[][] getMsgs(){  
  9.         return msgs;  
  10.     }  
  11.       
  12.     public void addMsg(byte[] msg)  
  13.     {  
  14.         msgs[offset++]=msg;  
  15.     }  
  16.   
  17.     public int size() {  
  18.         return offset;  
  19.     }  
  20.   
  21.     public void clear() {  
  22.         offset=0;  
  23.         lastFlushTime=System.currentTimeMillis();  
  24.     }  
  25.       
  26.     public boolean needFlush(){  
  27.         return (System.currentTimeMillis()-lastFlushTime > Constants.MAX_BUFFER_TIME)  
  28.         && offset>0;  
  29.     }  
  30. }  

实际测试和上线效果良好(效果见本文第一节)!

总结

能够使用最简化的代码完成性能和业务要求,是最完美的方法。根据使用场景,你可以有很多假设,但不要被眼花缭乱的新技术迷惑而拿你自己的服务做小白鼠,最适合的,最简单的,就是最好的。

 

本文系maoyidao原创,转载请引用原链接:

http://maoyidao.iteye.com/blog/1663193

同时推荐本系列前2篇

 

构建高性能服务(一)ConcurrentSkipListMap和链表构建高性能Java Memcached

http://maoyidao.iteye.com/blog/1559420

构建高性能服务(二)java高并发锁的3种实现

http://maoyidao.iteye.com/blog/1563523

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_34019144/article/details/85728407

智能推荐

反激变换器衍生拓扑对比分析_非对称反激-程序员宅基地

文章浏览阅读747次,点赞15次,收藏17次。在非对称反激变换器中,上管导通时,变压器和谐振电容同时储存能量,当能量从原边向次级传输时,原边串联的谐振电容和变压器储存的能量,同时向输出负载传输,因此,变压器得到利用的利用,变压器的尺寸可以显著的减小。(1)有源箝位反激变换器的变压器需要储存输出所需的所有能量,由于输入电压通常在一定的范围内变化,因此变压器无法工作在最优的状态,变压器也无法进行最优化的设计。、有源箝位反激变换器中,当负载降低到某一值时,系统会退出有源箝位的工作方式,效率会降低,同时会对次级整流管产生高的电压应力。_非对称反激

WIFI 认证 测试_11n认证用例-程序员宅基地

文章浏览阅读7.3k次。IEEE 802.11a/b/g双频带WorldRadio设计已经完成Wi-Fi多媒体(WMM,Wi-Fi multimedia)的Wi-Fi认证。   WMM通过控制网络音频、视频与数据的优先传送次序与缩短延迟时间提供给无线多媒体应用卓越的服务品质(QoS, Quality of Service),这个认证程序同时也可以确保取得WMM 认证的产品具有互通性,将有助于Wi-Fi 技术在消费_11n认证用例

32.Python从入门到精通—Python错误输出重定向和程序终止 字符串正则匹配 访问 互联网 日期和时间-程序员宅基地

文章浏览阅读5.4k次,点赞28次,收藏24次。接下来使用re模块中的findall函数进行匹配,它会返回一个列表,其中包含了所有符合模式的子串。最后输出匹配结果,可以看到输出的结果是一个列表,其中包含了所有匹配到的单词。除了findall函数之外,re模块还提供了其他一些函数,用于进行字符串正则匹配,例如search函数、match函数、sub函数等等。除了这些基本操作之外,datetime模块还提供了许多其他的日期和时间操作,例如时区转换、日期和时间的加减、日期和时间的比较等等。在上面的代码中,如果程序遇到异常,它将打印错误信息并以状态码1退出。

【送书福利-第二十五期】《AI时代系列书籍》-程序员宅基地

文章浏览阅读4.4w次,点赞37次,收藏35次。【送书福利-第二十五期】《AI时代系列书籍》

2020(广东)功能性农业大健康大会中国农民丰收节交易会_上海钱美梅-程序员宅基地

文章浏览阅读548次。2020(广东)功能性农业大健康大会中国农民丰收节交易会新闻中国采编 中国新闻釆编 谋定研究中国智库、经信研究 国研智库 国情讲坛 万权釆编:“倡导农业大健康以创造交易机会、打通功能性食品及特色地域地标性农副产品到达餐桌最后一公里,促进农商对接和渠道理顺,促成贸易合作为核心,加强产业扶贫成果展示展销,强化品牌营销和产销对接,集中展示农业各行业新业态新产品。是2020(广东)功能性农业·农业大健康大会暨中国农民丰收节交易会的最大亮点。”中纪委驻农业农村部原副部长宋树有在致辞中表示,这是贯彻落实中国农民丰收_上海钱美梅

Keras:Lambda 层_from keras.layers.core import lambda-程序员宅基地

文章浏览阅读1.4w次,点赞5次,收藏27次。Lambda层keras.layers.core.Lambda(function, output_shape=None, mask=None, arguments=None)本函数用以对上一层的输出施以任何Theano/TensorFlow表达式。如果你只是想对流经该层的数据做个变换,而这个变换本身没有什么需要学习的参数,那么直接用Lambda Layer是最合适的了。导入..._from keras.layers.core import lambda

随便推点

Eclipse中运行JSP程序(J2ee第三天学习记录)_eel3 shij-程序员宅基地

文章浏览阅读5.3k次,点赞8次,收藏27次。之前写的jsp学习记录,感觉没什么干货,大家随便看看吧_eel3 shij

约瑟夫问题的几种解法_yue se fu ying shi li yu ruan shi li-程序员宅基地

文章浏览阅读2.5k次。问题来历据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个_yue se fu ying shi li yu ruan shi li

win10安装pyskl配置环境,软件安装等mmdet(1)_pyskl 环境配置-程序员宅基地

文章浏览阅读658次。上周对骨骼识别的GitHub进行仿真,初步有了进展,现在对其进行复盘,首先软件是基于python语言为基础的,第一步就是安装python,对于版本是有要求的,这里先展示一下pyskl的下载库的版本要求。这个你可以试一下先pip下载cpython,cython,pip下载pycocotools ,但是一般还是有问题,所以先下载这个pycocotools的包,这是包的下载。10.mmdet=2.23.0这个是问题出的最多的地方,先是下载mmdet的whl文件,然后也放在跟mmcv-full的那个地方上面。_pyskl 环境配置

adobe dreamweaver cs5序列号_dwcs5序列号-程序员宅基地

文章浏览阅读5.2k次。Adobe Dreamweaver CS5序列号如下:1192-1973-6217-2477-6088-1657(我用的是这个)1192-1013-2621-8434-6884-01751192-1482-6532-1529-0926-55701192-1384-9698-0251-8971-98021192-1678-5764-4289-1578-82661192-1337-9219..._dwcs5序列号

超详细Anaconda安装教程-程序员宅基地

文章浏览阅读10w+次,点赞488次,收藏2.2k次。超详细最新Anaconda安装教程_anaconda安装教程

C语言中的3种注释方法_c语言注释-程序员宅基地

文章浏览阅读1.5w次,点赞5次,收藏30次。在用C语言编程时,常用的注释方式有如下几种: (1)单行注释 // … (2)多行注释 /* … */ (3)条件编译注释 #if 0…#endif_c语言注释

推荐文章

热门文章

相关标签