Archive

Archive for the ‘$Performance’ Category

系统优化经验分享

May 12th, 2011 西坪 No comments

将要做一个有关系统优化的分享,写了个提纲。
(没什么独创内容,都是老调重弹)

Categories: $Performance Tags:

Memory leak caused by Proxool and the fix

April 5th, 2011 西坪 No comments

Update: Be cautions if you don’t apply the patch to work with Oracle JDBC. The finalize() method in the WrappedConnection instances is always invoked when they are being finalized; then the finalize() in the physical connection class would be invoked. If the JDBC driver places some code there, it could destroy the pooled connections. (Apr 10th, 2011)
============================

Someone has reported a possible memory leak related with Proxool connection pool. And we addressed the bug recently in our applications, which are Java backend applications that work with Oracle 11g through Oracle JDBC5 on IBM JDK 5 platform.

The finalize() method of the proxied OracleConnection instances are the root cause. OracleConnection classes have no explict finalize() method, but each proxied object has one generated by cglib. When such a proxied connection instance is being finalized, the stack should be as the figure shows. You can see Connection#isClosed() method is invoked, which is SYNCHRONIZED (the monitor is the connection object itself). Then the finalizer thread and the worker thread using the physical connection could block each other. Finally, the proxied connection objects in finalizing queue and all referenced objects couldn’t be garbage-collected. Sometimes Connections aren’t so busy – you are lucky – the heap could suspend or even decrease; but OOM is waiting for you at the end usually.

thread_blocked_2

I gave a fix that avoid invoking Connection#isClosed() while finalizing. Actually, I changed the line 114 of WrappedConnection from

if (proxyConnection != null && proxyConnection.isReallyClosed()) {

to

if (proxyConnection != null && !concreteMethod.getName().equals(FINALIZE_METHOD) && proxyConnection.isReallyClosed()) {

The fix is proved to be effective in our serveral systems in production .

You can download the patch in the bug page in proxool bugtrack site.

Or you can download a compiled version of proxool-0.9.1.jar(WITHOUT WARRANT). (It’s comipled from proxool-0.9.1-source.zip)

Any questions please let me know by giving a comment.

Categories: $Performance Tags: , , ,

Proxool导致的内存泄漏

April 1st, 2011 西坪 No comments

Proxool是一个很优秀的开源连接池。我曾经比较过dbcp, c3p0proxool 这三个连接池,阅读和分析它们的代码。相比之下,proxool 用了cglib,其源码显得相当简洁优雅。 但是很不幸,在这一次,内存泄漏的帐要算在它头上。 我们使用的是proxool 的0.9.1的版本,环境是Oracle JDBC5 + IBM JDK5。

0.9.1版本是proxool 的最新版本。我们有一个应用据称运行以来,就一直处于内存溢出的阴霾之中。追赶数据时的某些巨忙的系统可能会一天内死好些次,甚至达十几次。不得不采用一些监控脚本定时重启。去年12月我接手以来,也被这个问题困扰,最糟糕的时候甚至某些硬件设施很好的部署实例的多达50+GB的堆也能在一天之内吃光。

出错的原因很简单,proxool使用了cglib,它用WrappedConnection代理实际的Conneciton。在运行WrappedConnection的方法时,包括其finalize方法,都会调用Conneciton.isClosed()方法去判断是否真的需要执行某些操作。不幸的是Oracle JDBC中的这个方法是同步的,锁是连接对象本身。于是, Finalizer线程回收刚执行过的WrappedConnection对象时就总会与还在使用Connection的各个工作线程争用锁。

贴一张简单的图,你就懂了:
thread_blocked_2

解决方案是在WrappedConnection.java的第114行,做一点小小的改动,在调用的方法为finalize方法时,不要去掉isClosed()方法。事实上,WrappedConnection#finalize()方法的调用与Connection根本没啥关系。Oracle JDBC本身没有实现finalize()方法,是cglib代理的对象自己生成了一个finalize()方法而已。
修改前的114行:

if (proxyConnection != null && proxyConnection.isReallyClosed()) {

修改后的114行:

if (proxyConnection != null && !concreteMethod.getName().equals(FINALIZE_METHOD) && proxyConnection.isReallyClosed()) {

点此查看sourceforge上的完整的WrappedConnection.java

效果是很明显的。一方面,内存消耗明显降低。之前,所有的部署实例都会隔段时间就当掉,某些部署实例的52G的堆都会溢出!现在,系统再忙其堆大小也基本上在原来那个设得老大的Xms参数之下。 第二方面,吞吐量忽然提高了许多,没有烦人的回收线程(其优先级要高点)的干扰,当然应该要跑得更快些。

感兴趣的朋友可以下载我这里提供的jar包(-target 1.5),(源码是直接从官网下的proxool-0.9.1-source.zip)。这个jar包也解决了ProxoolDataSource在Spring中作为bean配置时有三个属性(maximumConnectionLifetime,houseKeepingSleepTime,overloadWithoutRefusalLifetime)不能注入的问题。详细请看这篇博文

下载 proxool-0.9.1

也可以应用patch自己编译一个。patch我上传到Bug页面了,请去那下载。

J2EE/Oracle性能调优实录

February 18th, 2011 西坪 No comments

这两天,有一个新接手的项目性能表现较差,不能满足要求。这个项目在这之前已经写完很久了,但是一些关键部署点一直没有跟上进度。此系统是用Java编写的多线程程序的,主要是从Oracle中读取数据,然后根据业务进行统计运行,在将计算结果放回Oracle中。

这个系统的性能的直观指标,就是每天能处理多少数据量。这个指标很不理想,速度很慢,拖后与现场数据生成的速度。调优的标准,就是每日处理量。

第一次优化: 提高JDBC的fetchSize

首先,分析系统各个环节的时间消耗,弄清瓶颈在哪里。

在进行了一些综合分析后,得出主要时间耗费在数据库的查询上。而从v$session_wait中查询,发现有很多 SQL*Net message from client 事件的等待,时间很长,有数百秒之多。 显然是客户端慢了。但是因为应用端是有连接池的,是不是空闲连接导致的呢? 有一些怀疑。

接下来,在数据库上用 sqlplus 执行了相同的SQL,测量其时间其实是很快的,远远小于应用消耗在数据获取的时间。联系其上面提到的等待事件,事情变逐渐明朗: 网络传输慢了。

于是检查了代码,发现大批量的数据查询竟然没有提升fetchSize属性。在调整该属性为500以后,很快收到了正面反馈。处理速度提升了将近一倍。

回去在检查Oracle中的等待事件,发现已经降到200名以外了(是与其他应用共用数据库的)。

有关 sqlnet message from client 事件的分析,可以参考 Asktom 上的文章 AskTom上的另一篇文章,则描述了类似于连接池等待,或者是在sqlplus中打开会话不操作也会导致该事件。所以在见到这个事件时还是需要仔细甄别一下的。

结论: 适当调整 FetchSize是很有必要的。Oracle JDBC默认的fetchSize为10。FetchSize的提升,一方面是减少了客户端与服务器之间的来回交互次数;另一方面,也会有效减少数据库服务器逻辑读的数量(记得是《Troubleshooting Oracle Performance》中有讲到)。

修改完成后,除了观测到 sqlnet message from client 事件消失外,发现 sys cpu 也有小幅增加。

第二次优化:减小锁定(lock)范围

但是,接下来的观测,证明这个速度还是不合意,达不到要求。于是继续分析。

仍然是分析其主要的时间消耗在什么地方。这次发现时间主要是消耗在程序对数据的处理过程中。数据的处理大部分都是在Java进程中的运算,处理过程中还可能调用到Memcached。是不是网络速度比较慢呢?尝试测试了一下网速,千兆网络的吞吐率在应用并发时还能挤出三分之一左右用来做测试文件的传输,用ping发现其latency也不算高。

在此前的观测中,发现应用服务器上的 user cpu 比较好,在30%~70%之间,主要是被JVM进程消耗了,系统负载在5.0左右。觉得有些奇怪的,因为一般这样的应用瓶颈都出在IO上。于是做了一次代码复查,很快发现有一个在方法签名上的synchronize 关键字。这个类是一个服务bean, 在spring中管理的。这时问题变得清晰起来,所有的处理任务线程都要在这里排队。这样正好符合了此前观测到的cpu现象。

改掉这个关键字,缩小锁定范围,速度又上升了将近一倍。user cpu很快降下来,系统负载也下来了,稍稍大于1。

此时,速度虽能勉强应付,但还是不够快。此时更多的时间消耗在数据的写回上面。处理速度提升了,写回阶段的压力便也迅速提升,每单位数据的写回时间有20%左右的上浮。于是,需要第三次优化,在数据写回方面提升速度。

第三次优化

(Update Apr 6th):
没心思再仔细写过程了,简略如下:
1) Oracle是多个系统共享的,数据写入成为瓶颈。在内存问题解决后,在本地内存中缓存结果,最后再集中提交,减少更新次数。数度大增2.5倍左右。
2) 将从数据库中的查询和在内存中的处理划分为两个线程并发处理,速度提高50-70%。
3) 然后,Memcached集群成为瓶颈,内网延迟成为问题。但此时速度已经足够快了,因此暂时搁置下一步优化。如果要继续提速,则可能在本地建立一个Memcached实例,或者在程序中使用一个足够大的LRU缓存以减少与Memcached交互的次数。

至此,数据后台处理速度完全达到并超过预期。

Categories: $Performance Tags: , , , , ,

Java中的Volatile关键字

December 13th, 2010 西坪 2 comments

前两天,并行实验室发表了一篇有关volatile关键字的文章[1],而该文参考文献中的Sayonara volatile[2]则更直接地要与volatile再见。两篇文章不谋而合,从c/c++到Java/.net,将这几门语言中的volatile过了一遍。列举出各种乱象,看上去volatile是如此不堪。

确实,volatile的语义不是那么常见。不是锁,但是它又拥有锁的部分特性(可见性,Java 5以后还有顺序保证)。如果不能正确理解,确实是个容易出错的地方。但是如果理解了用对了,仅就Java 5以及以后的版本而言,在目前阶段,volatile对性能上的提升还是有帮助的。

Volatile在Java中的语义以及历史

维基百科上,对Java中的Volatile给了一个准确的定义[6]

  • (In all versions of Java) There is a global ordering on the reads and writes to a volatile variable. This implies that every thread accessing a volatile field will read its current value before continuing, instead of (potentially) using a cached value. (However, there is no guarantee about the relative ordering of volatile reads and writes with regular reads and writes, meaning that it’s generally not a useful threading construct.)
  • (In Java 5 or later) Volatile reads and writes establish a happens-before relationship, much like acquiring and releasing a mutex.

翻译过来就是:

  • (在所有的Java版本中)对volatile变量上的读和写有一个全局排序。这暗示着每个线程访问一个volatile域时在往下运行之前将读到当前的值,而不是(有可能)使用一个缓存值。(然而,在volatie变量的读写与常规的读写之间的排序没有保证,意味着volatile通常不是一个有用的线程化的构造)
  • (在Java5及之后版本中) Volatile读和写建立了一种happens-before关系,就像获取和释放一个互斥体。

在Java中,要正确地并发,必须在原子性、可见性和顺序性三个方面做出保证。在Java 5以前的版本中,volatile就已经实现了可见性,但是因为不能保证与普通变量读写之间的顺序,故而经常是没有用的。在Java 5以后的版本中,则额外保证了其与普通变量读写的顺序性。这里不是特别好懂,我们将javamx上的例子[3] [4]改造一下:

public class MyBrokenFactory {
  private static volatile MyFactory instance;
  private int field1, field2 ...
 
  public static MyBrokenFactory getFactory() {
    // This is incorrect: don't do it at home, kids!
    if (instance == null) {
      synchronized (MyBrokenFactory.class) {
        if (instance == null)
          instance = new MyBrokenFactory();
      }
    }
    return instance;
  }
 
  private MyBrokenFactory() {
    field1 = ...
    field2 = ...
  }
}

上面的代码,在Java5以前,这个例子是不能正确工作的,因为volatile不保证对field1,field2的初始化(常规读写)一定在对volatile变量instance的写之前完成。但双检查锁本身并非volatile导致的,反而是Java 1.5的volatile让双检查锁能正确工作了。并行实验室将双检查锁不正确算在volatile的头上,有点冤了。

正是因为volatile在Java 5以前没有得到正确实现,更添加了不少复杂性。在Java 5之前,volatile基本没用处。

Volatile的实现机制分析

在Hotspot JVM中,
a) 在JVM层次,对volatile变量在线程本地工作区中不做缓存,对volatile的读写总是指向堆中的引用。可以视作在一个assign指令后总是跟着一个store指令[5]
b) 在机器码执行层次,通过内存屏障指令等迫使CPU不重排序,清除缓存,详细请参考《Memory Barriers and JVM Concurrency》的分析。

这与synchronized是有明显不同的,synchronized通常需要原子锁定,在SMP上要通过锁定总线等方式来实现,其代价在大多数平台上通常要比volatile高得多。

Performance

图一运行100万次
引入Volatile的主要目的,就是为了性能。我分下面几个方面来谈谈volatile在目前的Java平台中的作用。

直接性能比较

简单写了一个测试来比较一下volatile以及它的两种替代品 — 原子类型(atomic)和锁(此处指synchronized)。例子的代码改编自庄周梦蝶slides《Java NIO trick and trap》中的SystemTimer(部分代码省略,完整源码在Github上)。这个测试只是一个例子,但其实在网络库里都要做定时,原理也大抵如此。

第一个版本如下:

package me.xiping.volitileverify;
public class SystemTimerV1 {
	private final static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
	private static final long tickUnit = Long.parseLong(System.getProperty(
			"notify.systimer.tick", "50"));
	private static volatile long time = System.currentTimeMillis();
 
	private static class TimerTicker implements Runnable {
		public void run() {
			time = System.currentTimeMillis();
		}
	}
 
	public static long currentTimeMillis() {
		return time;
	}
 
	static {
		executor.scheduleAtFixedRate(new TimerTicker(), tickUnit, tickUnit,
				TimeUnit.MILLISECONDS);
		Runtime.getRuntime().addShutdownHook(new Thread() {
			@Override
			public void run() {
				executor.shutdown();
			}
		});
	}
}

代码的重点在于volatile修饰的time域。time是一个只有一个线程写,其他线程都是读的域,没有并发修改,这里要解决的就是可见性。这正是volatile最适合的场合。

第二个版本使用synchronize关键字,与版本V1的不同如下:

package me.xiping.volitileverify;
public class SystemTimerV2 {
	....
	private static long time = System.currentTimeMillis();
 
	private static class TimerTicker implements Runnable {
		public void run() {
			synchronized (SystemTimerV2.class) {
				time = System.currentTimeMillis();
			}
		}
	}
 
	public static synchronized long currentTimeMillis() {
		return time;
	}
	....
}

第二个版本使用Java的内部锁来同步,既保证了原子性,也保证了可见性。第三个版本则是使用AtomicLong来实现的,如下:

package me.xiping.volitileverify;
 
public class SystemTimerV3 {
	......
	private static AtomicLong time = new AtomicLong(System.currentTimeMillis());
 
	private static class TimerTicker implements Runnable {
		public void run() {
			time.set(System.currentTimeMillis());
		}
	}
 
	public static long currentTimeMillis() {
		return time.get();
	}
	.....
}

在我的本上(cpu: 2core; OS: win7/cygwin; java:1.6.0_13)运行100万次,其性能如上面右图一所示(单位:ms)。
图二运行1千万次
运行1000万次性能如图二所示(注意纵坐标的值与图一不一样)。

结论:性能上的优越性是显而易见的。volatile比AtomicLong明细要快,相比于synchronized则有几倍几十倍的提高了。
(欢迎在不同平台下测试并在评论中给一个反馈,反馈时请包括处理器信息(平台,core数),OS和Java版本。源代码在这里。编译和运行需要Ant,具体指南请参考这里。)

[Update, Dec15th: 图片是用Google Chart API 生成的,在源代码中包含着两个自动执行测试并生成图片URL的脚本。脚本需运行在Bash环境下,Windows上需在Cygwin下运行。]

Mina/netty对volatile的利用

写博文时顺便统计了下volatile在一些性能比较重要的程序中的情况,我顺手统计了手边有的mina和netty两个项目的核心代码,数据如下:

Project sources path synchronized occurrence volatile occurrence
netty src/main/java 150 177
mina src/main/java 216 55

其中,mina是2009-3-31日Subversion的trunk,如下:

$svn info
Path: .
URL: http://svn.apache.org/repos/asf/mina/trunk
Repository Root: http://svn.apache.org/repos/asf
Repository UUID: 13f79535-47bb-0310-9956-ffa450edef68
Revision: 761900
Node Kind: directory
Schedule: normal
Last Changed Author: jvermillard
Last Changed Rev: 760493
Last Changed Date: 2009-03-31 23:49:15 +0800 (Tue, 31 Mar 2009)

netty的版本信息是:

$git log
commit 1ffb1aea75c36def56b709bd0892b19df78d9249
Author: Trustin Lee <trustin@gmail.com>
Date:   Fri Nov 12 10:20:03 2010 +0900

从侧面证明了volatile其实是很重要的,在性能很重要的场合,应用还是比较多的。

结论

总而言之,相比与Atomic和synchronized,volatile在性能上还是有显著的优势。

不过,正如文章中开头讨论的那样,volatile的缺点也显而易见,需要开发者对volatile的应用需要对内存模型的理解,否则容易误解而造成错误。在并发程序中,其仅可用于JVM支持的几个基本类型(int,short,long,double,reference, etc.),而这几个基本类型皆有对应的完整并发特性的包装原子类型(java.util.concurrent.atomic.*),虽然其性能与volatile有些差距,但大部分情形下也可代替volatile(但用synchronized来代替volatile是很不划算的)。

  1. 为什么在多核多线程程序中要慎用volatile关键字?
  2. Sayonara volatile
  3. Double-checked Locking (DCL) and how to fix it
  4. Double-checked Locking (DCL) and how to fix it (ctd)
  5. JVM specification#Threads and Locks
  6. Volatile variable#Java
  7. http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
  8. Memory Barriers and JVM Concurrency
Categories: $Performance Tags: , ,

位图存储优化

October 2nd, 2010 西坪 No comments

位图作为一种简高效使用内存的数据结构,在很多场合都能够用最省的内存表达大量的数据。我对位图最早的印象来自于《编程珠玑》中用位图结构来存储电话号码。感叹其简单、方便。本质上,位图是一个存储单个位的数组,每一个位表示一个数组元素。例如如果我们需要标记100万用户的在线状态,则可以将每个用户对应到一个位,只需要100万个位(约0.125M内存)的位图就可以表示了。如下图所示,在线的用户标记为绿色。其存储则可表示为 {01010011 0011…}。

位图

但是对于某些比较稀疏的位图,其存储效率也会有一些问题。如果100万用户平均只有100个人在线,那另外的999,900个位就浪费掉了。例如在第1位有一个用户,第1,000,000位有个用户,需要1M个位来保存。既浪费了存储空间,也带来大量无用的运算。在前不久我们的应用中就有这种问题。未经压缩的位图保存到数据库中,结果大量的IO操作对数据库的性能造成较大影响。

因为稀疏的位图比较多,比较直接地就想去掉中间的这些空白位,于是就想这样表示:存储第一个位的值(0或1),接着将接下来的所有值相同的位的数目保存为一个整数,再保存接下来另的一类连续位的数目,依此类推。如:{0000 0000 1000 0000 0000 1100 0001 0000 …… } 保存为 0,7,1,11,2,5…。 如果是一百万个位,只有第一个和最后一个是1{10000 …. …. 1},则保存为{1,999998,1}只需要三个整数就可以了!(注:实际上,早有人完整地将这种方法实现了,参考D-Gap Compression

于是对比较稀疏的位图应用这种方法,由于大部分位图都是比较稀疏,90%以上的位图的体积最终缩小了10倍。在测试过程中,发现最终存储的是大量的小整数(小于1000)。这些小整数其实可以用更短的类型来表示,于是应用变长编码来处理。其原理是每一个字节的高位做标识,其余7位存储值。1标识从这个字节开始是一个新的整数,0表示是上一个字节的延续。则区间[1, 2^7 )中的整数占用一个字节,[2^7, 2^14)中的整数占用两个字节,依次类推。

最终,90%以上的位图体积压缩了30倍以上。