原文地址: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

转载于:https://www.cnblogs.com/davidwang456/p/4559180.html

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

  1. FastAPI 构建 API 服务,究竟有多快?

    FastAPI 干啥的? FastAPI 是用来构建 API 服务的一个高性能框架. 为什么选择 FastAPI ? FastAPI 是一个现代.高性能 web 框架,用于构建 APIs,基于 Pyt ...

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

    本文主要内容 学习微服务的关键特征 了解微服务是如何适应云架构的 将业务领域分解成一组微服务 使用Spring Boot实现简单的微服务 掌握基于微服务架构构建应用程序的视角 学习什么时候不应该使用微 ...

  3. Baeldung Java 周评 | 第一百零五弹(关键词:如果 Java 是今天设计的、内容丰富的 Spring 会议、JPA 测试用例模版、高性能 Java 持久化、自动化订购午餐、前端五强)

    开篇词 尤金的第 105 篇 Java 周评,诞生了! Spring 以及 Java 相关 如果 Java 是今天设计的:可同步接口 [jooq.org] 关于 Java 中 "可能是什么& ...

  4. 高性能 Java 应用层网关设计实践

    前言 上文我们简单阐述了一下接入层网关的实现原理 不少人对 Java 网关的实现也比较感兴趣,所以这篇文章我们来简单谈谈 Java 应用网关设计,本文将会从以下几个方面来阐述 Java 应用层网关的设 ...

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

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

  6. Java高性能解析器实现思路及方法

    在某些情况下,你可能需要在Java中实现你自己的数据或语言解析器,也许是这种数据格式或语言缺乏标准的Java或开源解析器可以使用.或者虽然有现成的解析器实现,但它们要么太慢,要么太占内存,要么就是没有 ...

  7. 25服务端_手把手教你使用 OpenResty 搭建高性能服务端!

    点击蓝色"架构文摘"关注我哟 加个"星标",每天上午 09:25,干货推送! 来源:https://www.jianshu.com/p/09c17230e1a ...

  8. 如何构建可持续的ChatGPT高性能服务器端架构?

    边缘计算 | 液冷服务器 | GPT-4 深度学习 | AI服务器 | ChatGPT 在上周举行的发布会上,OpenAI宣布推出了GPT-4模型.与之前的版本相比,GPT-4最大的改进是其多模态(m ...

  9. 【在线网课】Java高性能高并发秒杀系统方案优化实战

    java教程视频讲座简介: Java高性能高并发秒杀系统方案优化实战 Java秒杀系统方案优化 高性能高并发实战 以"秒杀"这一Java高性能高并发的试金石场景为例,带你通过一系列 ...

最新文章

  1. DvaJS 入门, 快速上手Dva
  2. Debian和CentOS主流系统按包含文件名称搜索软件包
  3. 织梦二次开发写php,PHP教程—DedeCMS二次开发(二)
  4. 为什么复制粘贴时html,关于javascript:为什么你应该停止复制和粘贴
  5. Java PropertyPermission equals()方法与示例
  6. 售前工程师的成长---一个老员工的经验之谈(5)
  7. JavaScript学习(四十五)—练习题
  8. 高通:蓝牙5.0将可同时连接两个设备
  9. OpenGL学习笔记(14)像素操作
  10. 如何注册Google Voice账号(电话号码)
  11. 红月服务器制作过程,红月3.8C私服架设教程
  12. 漏洞利用(CVE-2017-5638)
  13. rviz显示矩形框BoundingBox
  14. vscode背景图片设置分享
  15. MyBatis 插件之拦截器(Interceptor),拦截查询语句
  16. Android->Launcher3:桌面布局说明
  17. 使用Python+Opencv+Flask将windows端的摄像头变为网络摄像头的方法
  18. Qcon演讲实录 | XQUIC与多路径传输技术Multipath QUIC
  19. scikit-learn_LinearModel_02_峰回归和峰归类
  20. 【效能平台】接口模块——获取列表数据、查看详情数据、增加以及更新项目接口、删除接口相关功能开发(六)

热门文章

  1. java ls_linux ls 命令
  2. linux 校验文件生成,在Linux中了解如何使用MD5校验和生成和验证文件
  3. 安卓双摄像头录像_android开发之调用手机的摄像头使用MediaRecorder录像并播放_Android_脚本之家...
  4. linux下cmake命令行,深入理解CMake(1): CMake命令行参数
  5. java mytable_Mybatis-Plus之@TableField字段
  6. 构造全自动计算的CPU
  7. C#中的构造方法与对象初始化器
  8. killall 后面信号_五个你可能不了解的killall选项
  9. linux限制pptp连接数_性能调优,让你的服务器更强大!增加TCP连接最大限制
  10. java 方法引用无效_InvalidProgramException:调用方法时无效的IL代码