最近调研一个数据分析模块中的性能问题,花费将近两周时间。期间做了许多Enhancement,而最后关于总线的性能瓶颈定位和处理值得记录一下。
首先是场景描述,一个典型的生产者消费者环境。总线Bus接收来自不同方向的数据,根据数据的分类,发送给相关订阅者。在数据交互过程中,使用JDK中的阻塞队列ArrayBlockingQueue:生产者的提供Object存入总线中的BlockingQueue;总线经计算判断后将Object存入对应消费者的BlockingQueue。为了不丢失数据,当某个消费者队列满时,总线的put操作会阻塞直到该消费者可用。
性能分析:
通过JRat(http://blog.csdn.net/chifengxin/article/details/6750619)、JProbe工具显示,CPU利用率主要消耗在消费者处。耗费大量精力优化消费者,包括增加多线程处理等。当消费者提高到极限后,通过Unix命令发现总线Bus线程居然占满到一个核(top命令后,shift+h查看单核线程列表,找到占用cpu最多的java线程,将该id用计算器转换成16进制。然后执行JDK的jstack得到堆栈信息,在堆栈中查找该16进制线程id对应的线程)。
于是Focus在总线Bus。最后结果是,此处占用CPU的竟然不是普通处理操作,而是在BlockingQueue的put方法。在Queue总是未满的情况,put方法仍然占用大量的资源。
验证:
写了个模拟线程验证,单线程占满一核CPU后,BlockingQueue极限写速度为1500K/s。因而加上其它处理,导致本程序中Bus的性能极限只有300K/s。
解决:
1、 在JDK提供的BlockingQueue中,ArrayBlockingQueue是效率最高和消耗最少的。尝试换用双缓冲队列(参照:http://www.iteye.com/topic/477412),提高很小
2、 考虑到DougLea对锁的优化,放弃重写queue
3、 最终解决方案-Pack包裹:
封装了一个简单数组容器继承原Object接口(或者使用其它任意的JDK容器):
public interface IPackObject extends IObject { public boolean add(IObject obj); public IObject get(int index); public int size(); public boolean full(); } |
该容器的构造方法需要传入一个capacity参数作为数组values的初始大小;实现数组写入和读取的方法。使用时,在往BlockingQueue放入对象前,先把对象放入该容器,直到容器满后才将容器put到BlockingQueue,从而避免在put过程中的资源消耗。消费者取到后,加上判断IObject是否是instanceof IPackObject,如果是容器则循环取出对象后再操作。
理论上,当容器容量为100时,在BlockingQueue上的资源消耗将减少99%。
注:需要考虑当容器处于不满状态的超时。最简单的方法是定期(如每秒)将所有未满的容器put到queue。
附:简易非安全对象容器
public class ObjectArray<E> { /** * A simple Object container */ private Object []values; private int capacity; private int size; private int cursor; public ObjectArray() { this(10); } public ObjectArray(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; values = new Object[capacity]; } public void add(E object) { if(size >= capacity) { capacity *=2; Object [] oldValues = values; values = new Object[capacity]; System.arraycopy(oldValues, 0, values, 0, oldValues.length); } values[size++] = object; } public int size() { return size; } public void resetCursor() { this.cursor=0; } public boolean hasNext() { return cursor<size; } public E next() { return (E) values[cursor++]; } }