被测方案 |
虚拟用户数 |
GET平均响应时间(秒) |
UPDATE平均响应时间(秒) |
每秒点击数 |
php |
500 |
0.056 |
0.072 |
7759 |
1000 |
0.195 |
0.166 |
7471 |
1500 |
0.318 |
0.270 |
7471 |
单个jvm |
500 |
0.122 |
0.052 |
5330 |
集群jvm |
500 |
0.058 |
0.058 |
8500 |
1000 |
0.124 |
0.127 |
8062 |
1500 |
0.193 |
0.193 |
7763 |
2000 |
0.265 |
0.267 |
7656 |
2399 |
0.318 |
0.320 |
7593 |
3000 |
0.396 |
0.398 |
7488 |
4000 |
0.551 |
0.553 |
7338 |
5000 |
0.660 |
0.664 |
7542 |
1、单个jvm
理论上来说java的性能应当是高于php的,但是结果很让人失望,无论我如何加大压力,在单个jvm的时候,java都达不到每秒7700请求的指标。我首先想到的是对tomcat做性能调优,我先后使用了以下措施:
- 增大jvm内存 -Xmx1578m
- 使用tomcat nio http connector
- 增大tomcat处理请求的线程数量,maxThreads=2000,processorCache=2000,acceptCount=10000
- 增大jedis连接池和c3p0数据库连接池到2000
- 不使用数据库连接池
- 不使用jedis连接池
- 不使用spring mvc直接使用servlet
以上措施中只有不使用jedis连接池少量的提高了每秒请求数和响应时间,其他的措施没有多少改进,而且不用数据库连接池时,性能急剧下降,每秒请求数下降到不足之前的五分之一。为了找出性能瓶颈,找出原因,我用jprofiler分析,发现了一些问题。
1.1 commons-pool在并发时性能不佳
jedis的对象池是用commons-pool实现的,这个实现在多线程,并发情况下反倒成了性能瓶颈之一。当2000线程处理请求时,每个线程都要先锁定一个对象才能获取或归还jedis对象,导致这些线程在这个地方排队,性能降低。后来改成每个请求都重新打开一次jedis链接后,性能反倒有一点提升。
1.2 数据库连接池让人又爱又恨
不用数据库连接池是万万不行的,每次打开数据库连接,性能太差。相比之下php没有使用连接池,性能依旧很好,估计是jdbc实现的问题。使用了数据库链接池后,c3p0和commons-pool一样,在并发时,也能成了性能瓶颈,好多线程都在等待获得c3p0的锁。
1.3 诡异的线程集体空闲,集体繁忙
从jprofiler的线程运行图里能看到,tomcat的线程大部分时间不是在处理请求,而是在莫名其妙的空闲,代码停在了tomcat使用的jre自带的ThreaderExecutor一个方法上。而且,这些线程往往同时空闲持续近10秒的时间,然后突然繁忙起来,去争抢数据库的锁,运行,然后再次先后都陷入空闲。load runner的每秒请求数曲线和响应时间曲线呈现出巨大的波动,一会每秒点击高达10000,一会跌入2000以下。而从jprofiler看出,jvm的内存空间使用不超过500m,而且波动很平稳,gc表现稳定,没有大量的全垃圾收集(full gc)。
1.4 apr也打破不了僵局
因此,我怀疑是不是jvm在处理大量并发tcp连接的时候,性能不好,导致这样的局面。为此,我想到了使用tomcat native。它可以让tomcat使用c语言实现的本地代码库来处理tcp连接,而这个库也是apache httpd使用的,性能应当相当好,至少在处理网络方面应当很好吧。编译安装成功后,测试数据显示,并没有多少改变,很让人沮丧。那些线程还是那样的间歇性地集体空闲,集体繁忙。
1.5 jetty和glassfish的表现
看来问题可能处在tomcat自身,于是我用jetty和glassfish做了测试。jetty的表现和tomcat一样,一样的性能指标,一样诡异的线程。glassfish的性能指标比tomcat稍微差一点,但是它的每秒请求数和响应时间比较稳定,波动不大,很稳定,这一点比tomcat和jetty好多了。用jprofiler看了一下glassfish只有5个线程在处理http请求,而且这些线程都一直处于繁忙状态,不会像tomcat和jetty,众多线程间歇性集体空闲集体繁忙,不均匀。由此我想到,是不是线程配置得太多了,导致tomcat和jetty调度不过来,才会出现那样的诡异的现象。
1.6 tomcat的最佳线程数
我测试了不同线程数的时候tomcat的表现。和glassfish同样5个线程时,tomcat表现很糟糕,逐渐增大tomcat线程数量到50后,性能指标达到了2000线程时的性能指标,再加上去,性能变化不大。看来tomcat默认的200线程数量是很有道理的,200可能是目前大部分机器的最优配置了。
1.7 单个jvm的极限
无论怎么调线程数量,换什么样的服务器,单个jvm最多也只能达到每秒处理5300请求。看来一个jvm是有性能极限的,很可能不能完全的发挥硬件的能力。
2 集群
集群方案是这样的,前端一个apache,使用mod_jk把请求转发到后端的多个tomcat或glassfish。apache, tomcat和glassfish自然是在同一台物理机上的。由于tomcat和jetty的实现机制太像了,就没有单独测试jetty。而tomcat和glassfish在集群时性能表现一样,没有明显的差别。而且,从2个jvm逐步增大到6个,性能表现也没有明显差别。使用apache在前端转发后,tomcat也像glassfish一样变得平稳了,估计这得归功与ajp协议,和mod_jk的连接复用。
2.1 性能分析
集群后java的性能表现终于超越了php。随着压力的增大,java的响应时间增长比php缓慢。在相同的响应时间下,java每秒处理了更多的请求,峰值时多处理近10%。相同响应时间时,java能承受更大的压力,如果以虚拟用户数量来衡量,能多承受50%的压力。 无论php和java,随着压力的增大,请求响应时间和每秒请求数快速上升,但是到达一个点后,压力再增大,每秒请求数会缓慢的下降,而请求响应时间会缓慢的上升。
3 结论
3.1 单个大jvm不如多个小jvm
一个线程很多,内存很大的jvm并不能完全发挥硬件的性能,还不如多个内存小,线程少的多个jvm集群。尽管多个小jvm总体上用了更多的内存,但是每个jvm内的线程少,意味着jvm调度的复杂性减小,竞争同一个锁的线程数更少。
3.2 一定要用数据库连接池
如果不用数据库连接池,java的表现太差了。看来mysql的jdbc driver比它的php用的c语言实现重得多。
3.3 tomcat jetty glassfish
测试中使用的是tomcat7 jetty8和glassfish3.2三者的性能表现差不多,而glassfish更加有好的使用界面,更加稳定的性能数据,使它表现得更出色。
3.4 java的性能还是比php好的
在相同的硬件下,如果单个jvm不能完全发挥硬件的性能,通过多个jvm的集群可以,而且会超过php。 我想,在这个测试例子中,因为每次打开连接执行一次数据库操作,java的数据库连接池的优势没有完全体现出来,否则java应该会表现得更好。如果是cpu计算密集型的任务,java的性能应该会更好。