构建高性能服务(三)Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue--转载

原文地址: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. SequenceBarrier barrier = ringBuffer.newBarrier();
  5. BatchEventProcessor<ValueEvent> eventProcessor = new BatchEventProcessor<ValueEvent>(ringBuffer, barrier, handler);
  6. ringBuffer.setGatingSequences(eventProcessor.getSequence());
  7. // only support single thread
  8. new Thread(eventProcessor).start();

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

Java代码  

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

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

Java代码  

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

数据消费者代码在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. private byte[][] msgs=new byte[Constants.BATCH_INS_COUNT][];
  4. private int offset=0;
  5. public byte[][] getMsgs(){
  6. return msgs;
  7. }
  8. public void addMsg(byte[] msg)
  9. {
  10. msgs[offset++]=msg;
  11. }
  12. public int size() {
  13. return offset;
  14. }
  15. public void clear() {
  16. offset=0;
  17. lastFlushTime=System.currentTimeMillis();
  18. }
  19. public boolean needFlush(){
  20. return (System.currentTimeMillis()-lastFlushTime > Constants.MAX_BUFFER_TIME)
  21. && offset>0;
  22. }
  23. }

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

总结

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

本文系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

时间: 2024-12-13 04:21:56

构建高性能服务(三)Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue--转载的相关文章

Springboot &amp; Mybatis 构建restful 服务三

Springboot & Mybatis 构建restful 服务三 1 前置条件 成功执行完Springboot & Mybatis 构建restful 服务二 2 restful service 添加日志 1)新建 logback.xml文件(配置生成的日志文件的格式) src/main/resources/logback.xml <?xml version="1.0" encoding="UTF-8"?>   <!-- 设置根

使用Spring Boot构建微服务(文末福利)

本文主要内容 学习微服务的关键特征 了解微服务是如何适应云架构的 将业务领域分解成一组微服务 使用Spring Boot实现简单的微服务 掌握基于微服务架构构建应用程序的视角 学习什么时候不应该使用微服务 软件开发的历史充斥着大型开发项目崩溃的故事,这些项目可能投资了数百万美元.集中了行业里众多的顶尖人才.消耗了开发人员成千上万的工时,但从未给客户交付任何有价值的东西,最终由于其复杂性和负担而轰然倒塌. 这些庞大的项目倾向于遵循大型传统的瀑布开发方法,坚持在项目开始时界定应用的所有需求和设计.这

Springboot &amp; Mybatis 构建restful 服务四

Springboot & Mybatis 构建restful 服务四 1 前置条件 成功执行完Springboot & Mybatis 构建restful 服务三 2 restful service 添加 Apache POI生成 Excel 文件 1)修改 POM.xml文件 添加 Apache POI 的依赖 <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-oo

Node.js 构建TCP服务

构建TCP服务 三次握手连接: 1.客户端请求连接 2.服务器响应 3.开始传输 服务端 // 构建TCP服务 服务端 const net = require('net') const server = net.createServer() server.on('connection', clientSocket => { console.log('有新的客户端连接了') //服务端通过 clientSocket 监听 data 事件 clientSocket.on('data', data =

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

构建高性能服务(二)java高并发锁的3种实现 来源:http://www.xymyeah.com/?p=46 提高系统并发吞吐能力是构建高性能服务的重点和难点.通常review代码时看到synchronized是我都会想一想,这个地方可不可以优化.使用synchronized使得并发的线程变成顺序执行,对系统并发吞吐能力有极大影响,我的博文 http://maoyidao.iteye.com/blog/1149015 介绍了可以从理论上估算系统并发处理能力的方法. 那么对于必须使用synchr

Java Web高性能开发(三)

今日要闻: Clarifai:可识别视频中物体 最近几年,得益于深度学习技术的发展,谷歌和Facebook等企业的研究人员在图形识别软件领域取得了重大突破.现在,一家名为Clarifai的创业公司则提供了一项新服务,利用深度学习技术来理解视频内容. 深度学习需要借助一个模拟"神经元"的网络来处理数据,并且利用实例数据对其进行训练.Clarifai的技术源自纽约大学的研究,该公司2013年在一项著名的图片内容识别软件竞赛中取得前五名. 今日英文: http://docs.oracle.c

构建高性能服务

构建高性能服务  ConcurrentSkipListMap和链表构建高性能Java Memcached 场景 缓存服务器是互联网后端服务中常用的基础设施. 场景(一)图片服务器上存储了大量图片,为了提高图片服务的吞吐量,希望把热门的图片加载到内存中. 场景(二)分布式存储服务,为提高访问吞吐,把大量的meta信息存储在内存中. 问题 但是使用Java语言开发缓存服务,不可避免的遇到GC问题.无论使用ehcache是基于Map实现的缓存,都会产生大量Minor GC无法回收的对象,最终导致CMS

通用高性能服务框架解析

做了多年的后台服务,一直想将自己这么多年对高性能服务架构的一些粗浅认识写出来,一方面对自己这个阶段成长做个总结, 另一方面想通过这个与各位做一个交流,妄不吝赐教. 一.最初对服务架构的概念 最初接触服务端程序应该是2011年,当初基于服务架构的概念是基于这样一个模型 这是最简单的一种C/S模型结构,客户端直接连接服务端,只能适用于对效率.并发量.扩展性要求低的环境,所以当请求量逐渐上升,你会发现这种架构的系统,在处理上已经不满足业务的需求了,所以你衍生出下面一种架构. 二.多个App分流模式 这

Java高性能编程之CAS与ABA及解决方法

Java高性能编程之CAS与ABA及解决方法 前言 如果喜欢暗色调的界面或者想换换界面,可以看看我在个人博客发布的 Java高性能编程之CAS与ABA及解决方法. CAS概念 CAS,全称Compare And Swap,比较与交换. 属于硬件级别的同步原语,从处理器层面提供了内存操作的原子性. 从概念上,我们可以得出三点.第一,CAS的运作方式(通过比较与交换实现).第二,硬件层面支持,性能肯定不低(当然它也不是银弹).第三,提供原子性,那么它的功能肯定是确保原子性,从而确保线程安全. 实际使