转:构建可扩展的Java EE应用
转:构建可扩展的Java EE应用
2011年05月21日
原文地址: http://www.theserverside.com/news/1320914/Scaling- Your-Java-EE-Applications-Part-2 对于一个具备使用价值的应用而言,其使用者有可能会在一段时间内疯狂的增长。随着越来越多的关键性质的应用在Java EE上运行,很多的Java开发者也开始关注可扩展性的问题了。但目前来说,大部分的web 2.0站点是基于script语言编写的,对于Java应用可扩展能力,很多人都抱着质疑的态度。在这篇文章中,Wang Yu基于他本身在实验室项目的经验来展示如何构建可扩展的java应用,同时,基于一些在可扩展性上做的比较失败的项目给读者带来构建可扩展java应用的实践、理论、算法、框架和经验。
我一直为一家互联网性质的实验室工作,这个实验室采用我们公司最新的大型服务器环境为合作伙伴的产品和解决方案免费做性能测试,我工作的部分就是帮助他们在强大的CMT和SMP服务器上进行性能调优。
这些年来,我已经为不同的解决方案测试了数十种java应用。许多的产品都是为了解决同样的领域问题,因此这些产品的功能基本都是类似的,但在可扩展性上表现的却非常不同,其中有些不能扩展到64 CPU的服务器上运行,但可以扩展到20台服务器做集群运行,有些则只能运行在不超过2 CPU的机器上。
造成这些差别的原因在于设计产品时的架构愿景,所有的具备良好扩展性的java应用从需求需求阶段、系统设计阶段以及实现阶段都为可扩展性做了考虑,所以,你所编写的java应用的可扩展能力完全取决于你的愿景。
可扩展性作为系统的属性之一,是个很难定义的名词,经常会与性能混淆。当然,可扩展性和性能是有关系的,它的目的是为了达到高性能。但是衡量可扩展性和性能的方法是不一样的,在这篇文章中,我们采用wikipedia中的定义:
可扩展性是系统、网络或进程的可选属性之一,它表达的含义是可以以一种优雅的方式来处理不断增长的工作,或者以一种很明白的方式进行扩充。例如:它可以用来表示系统具备随着资源(典型的有硬件)的增加提升吞吐量的能力。
垂直扩展的意思是给系统中的单节点增加资源,典型的是给机器增加CPU或内存,垂直扩展为操作系统和应用模块提供了更多可共用的资源,因此它使得虚拟化的技术(应该是指在一台机器上运行多个虚拟机)能够运行的更加有效。
水平扩展的意思是指给系统增加更多的节点,例如为一个分布式的软件系统增加新的机器,一个更清晰的例子是将一台web服务器增加为三台。随着计算机价格的不断降低以及性能的不断提升,以往需要依靠超级计算机来进行的高性能计算的应用(例如:地震分析、生物计算等)现在可以采用这种多个低成本的应用来完成。由上百台普通机器构成的集群可以达到传统的基于RISC处理器的科学计算机所具备的计算能力。
这篇文章的第一部分来讨论下垂直扩展Java应用。
如何让Java EE应用垂直扩展
很多的软件设计人员和开发人员都认为功能是产品中最重要的因素,而性能和可扩展性是附加的特性和功能完成后才做的工作。他们中大部分人认为可以借助昂贵的硬件来缩小性能问题。
但有时候他们是错的,上个月,我们实验室中有一个紧急的项目,合作伙伴提供的产品在他们客户提供的CPU的机器上测试未达到性能的要求,因此合作伙伴希望在更多CPU(8 CPU)的机器上测试他们的产品,但结果却是在8 CPU的机器上性能反而比4 CPU的机器更差。
为什么会这样呢?首先,如果你的系统是多进程或多线程的,并且已经用尽了CPU的资源,那么在这种情况下增加CPU通常能让应用很好的得到扩展。
基于java技术的应用可以很简单的使用线程,Java语言不仅可以用来支持编写多线程的应用,同时JVM本身在对java应用的执行管理和内存管理上采用的也是多线程的方式,因此通常来说Java应用在多CPU的机器上可以运行的更好,例如Bea weblogic、IBM Websphere、开源的Glassfish和Tomcat等应用服务器,运行在Java EE应用服务器中的应用可以立刻从CMT和SMP技术中获取到好处。
但在我的实验室中,我发现很多的产品并不能充分的使用CPU,有些应用在8 CPU的服务器上只能使用到不到20%的CPU,像这类应用即使增加CPU也提升不了多少的。
热锁(Hot Lock)是可扩展性的关键障碍
在Java程序中,用来协调线程的最重要的工具就是 synchronized这个关键字了。由于java所采用的规则,包括缓存刷新和失效,Java语言中的synchronized块通常都会其他平台提供的类似的机制更加的昂贵。即使程序只是一个运行在单处理器上的单线程程序,一个synchronized的方法调用也会比非同步的方法调用慢。
要检查问题是否为采用synchronized关键字造成的,只需要像JVM进程发送一个QUIT指令(译者注:在linux上也可以用kill -3 PID的方式)来获取线程堆栈信息。如果你看到类似下面线程堆栈的信息,那么就意味着你的系统出现了热锁的问题: ..
"Thread-0"prio=10tid=0x08222eb0nid=0x9waitingformo nitorentry[0xf927b000..0xf927bdb8]
attestthread.WaitThread.run(WaitThread.java:39)
-waitingtolock(ajava.lang.Object)
-locked(ajava.util.ArrayList)
atjava.lang.Thread.run(Thread.java:595) synchronized 关键字强制执行器串行的执行synchronized中的动作。如果很多线程竞争同样的同步对象,那么只有一个线程能够执行同步块,而其他的线程就只能进入blocked状态了,如果此时没有其他需要执行的线程,那么处理器就进入空闲状态了,在这种情况下,增加CPU也带来不了多少性能提升。
热锁可能会导致更多线程的切换和系统的调用。当多个线程竞争同一个monitor时,JVM必须维护一个竞争此monitor的线程队列(同样,这个队列也必须同步),这也就意味着更多的时间需要花费在JVM或OS的代码执行上,而更少的时间是用在你的程序上的。
要避免热锁现象,以下的建议能带来一些帮助:
尽可能的缩短同步块
当你将线程中持有锁的时间尽量缩短后,其他线程竞争锁的时间也就变得更短。因此当你需要采用同步块来操作共享的变量时,应该将线程安全的代码放在同步块的外面,来看以下代码的例子:
Code list 1: publicbooleanupdateSchema(HashMapnodeTree){
synchronized(schema){
StringnodeName=(String)nodeTree.get("nodeName");
StringnodeAttributes=(List)nodeTree.get("attribute s");
if(nodeName==null)
returnfalse;
else
returnschema.update(nodeName,nodeAttributes);
}
} 上面的代码片段是为了当更新"schema"变量时保护这个共享的变量。但获取attribute值部分的代码是线程安全的。因此我们可以将这部分移至同步块的外面,让同步块变得更短一些:
Code list 2: publicbooleanupdateSchema(HashMapnodeTree){
StringnodeName=(String)nodeTree.get("nodeName");
StringnodeAttributes=(List)nodeTree.get("attribute s");
synchronized(schema){
if(nodeName==null)
returnfalse;
else
returnschema.update(nodeName,nodeAttributes);
}
} 减小锁的粒度
当你使用"synchronized"时,有两种粒度可选择:"方法锁"或"块锁"。如果你将"synchronized"放在方法上,那么也就意味着锁定了"this"对象。
Code list 3: publicclassSchema