原文地址: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中的数据:

  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通常是个自定义的类,用于封装你自己的数据:

  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中添加数据,同时发出一个事件通知消费者有新数据达到,并且,,,注意我们是怎么规避数据覆盖问题的:

  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中实现:

  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 这种锁通知机制更好:

  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[][]数组就可以解决全部问题!

  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

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

  1. 构建高性能服务 Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue

    一个仅仅部署在4台服务器上的服务,每秒向Database写入数据超过100万行数据,每分钟产生超过1G的数据.而每台服务器(8核12G)上CPU占用不到100%,load不超过5.这是怎么做到呢?下面 ...

  2. Springboot & Mybatis 构建restful 服务三

    Springboot & Mybatis 构建restful 服务三 1 前置条件 成功执行完Springboot & Mybatis 构建restful 服务二 2 restful ...

  3. Docker学习6:使用docker构建Jekyll服务和java服务

    写在前面 ## 文章Dockerfile中涉及apt-get 等操作需更换镜像 在Dockerfile中添加下列 Dockerfile源码,见下面作者githubhttps://github.com/ ...

  4. Springboot & Mybatis 构建restful 服务四

    Springboot & Mybatis 构建restful 服务四 1 前置条件 成功执行完Springboot & Mybatis 构建restful 服务三 2 restful ...

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

    构建高性能服务(二)java高并发锁的3种实现 来源:http://www.xymyeah.com/?p=46   提高系统并发吞吐能力是构建高性能服务的重点和难点.通常review代码时看到sync ...

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

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

  7. Java Web高性能开发(三)

    今日要闻: Clarifai:可识别视频中物体 最近几年,得益于深度学习技术的发展,谷歌和Facebook等企业的研究人员在图形识别软件领域取得了重大突破.现在,一家名为Clarifai的创业公司则提 ...

  8. 构建高性能高并发Java系统 .

    转:http://blog.csdn.net/nengyu/article/details/7591854 场景这里指的高性能高并发服务器是一个有状态的服务,可以理解成web或者socket服务器,每 ...

  9. 基于 IOCP 的通用异步 Windows Socket TCP 高性能服务端组件的设计与实现

    设计概述 服务端通信组件的设计是一项非常严谨的工作,其中性能.伸缩性和稳定性是必须考虑的硬性质量指标,若要把组件设计为通用组件提供给多种已知或未知的上层应用使用,则设计的难度更会大大增加,通用性.可用 ...

随机推荐

  1. 第三百五十九天 how can I 坚持

    在家待了一天,鼓捣了下linux,总算能连上网了,懂得还是少啊. 晚上去华北电力大学跑了会步,十圈,还挺有成就感呢,就是没带手环,哎. 以后学习一定要记笔记,上了这么多年学,都 没学会怎么记笔记,也是 ...

  2. 提升c++builder 代码输入流畅度的配置

    提高c++builder 代码输入流畅度 1.输入指针的函数名后,识别函数参数移动光标到括弧内,此功能太慢,有明显延迟,建议关闭.关闭以后,输入函数名不会自动添加(),需要自己手动输入括弧了,不过速度 ...

  3. (翻译)异步编程之Promise(1):初见魅力

    原文:https://www.promisejs.org/ by Forbes Lindesay 异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2) ...

  4. Fedora 16设置开机自启动程序与Ubuntu的区别

    Ubuntu设置开机自启动脚本的方法是:修改/etc/init.d/rc.local这个文件,添加需要启动的程序即可,相关函数如下: void SetSysAutoBoot() { ] = {}; ; ...

  5. [转]Torch是什么?

    Torch是一个广泛支持机器学习算法的科学计算框架.易于使用且高效,主要得益于一个简单的和快速的脚本语言LuaJIT,和底层的C / CUDA实现:Torch | Github 核心特征的总结:1. ...

  6. jsp 是什么 ,jsp 隐式对象

    google 搜索 java server page http://www.oracle.com/technetwork/java/javaee/jsp/index.html http://docs. ...

  7. js ajax post提交 ie和火狐、谷歌提交的编码不一致,导致中文乱码

    今天遇到一个问题找了很久发现: 使用js ajax post提交 ie和火狐.谷歌提交的编码不一致,导致中文乱码 //http://www.cnblogs.com/QGC88 $.ajax({ url ...

  8. Web服务器与Web系统发布

    在讨论Web系统发布之前,我们先来辨析两个概念:服务器.Web服务器. 通常,我们说的服务器,是一台提供服务的计算机,是硬件概念.这台主机有其IP地址,有服务端口,我们要访问时,就是通过IP地址唯一地 ...

  9. JQuery 的bind和unbind函数

    测试:页面代码:<body>     <input type="button" name="aaa" value="点击我" ...

  10. [转]ViewPager onPageChangeListener总结

    android ViewPager滑动事件讲解 首先ViewPager在处理滑动事件的时候要用到OnPageChangeListener OnPageChangeListener这个接口需要实现三个方 ...