性能和效率
- 132、提升Java性能的基本方法
- 133、若非必要,不要克隆对象
- 134、推荐使用“望闻问切”的方式诊断性能
- 135、必须定义性能衡量标准
- 136、枪打出头鸟—解决首要系统性能问题
- 137、调整JVM参数以提升性能
- 138、性能是个大“咕咚”
132、提升Java性能的基本方法
Java从诞生之日起就被质疑:字节码在JVM中运行是否会比机器码直接运行的效率会低很多?很多技术高手、权威网站都有类似的测试和争论,从而来表明Java比C(或C++)更快或效率相同。此类话题我们暂且不表(这类问题的争论没完没了,也许等到我们退休的时候,还想找个活动脑筋的方式,此类问题就会是最好的选择),我们先从如何提高Java的性能方面入手,看看怎么做才能让Java程序跑得更快,效率更高,吞吐量更大。
- (1)不要在循环条件中计算
如果在循环(如for循环、while循环)条件中计算,则每循环一遍就要计算一次,这会降低系统效率,就比如这样的代码:
//每次循环都要计算count*2while(i<count*2){//Do Something}应该替换为:
//只计算一遍inttotal=count *2;while(i<total){//Do Something}- (2)尽可能把变量、方法声明为final static类型
假设要将阿拉伯数字转换为中文数字,其定义如下:
publicStringtoChineseNum(intnum){//中文数字String[]cns={"零","壹","贰","叁","肆","伍","陆","柒","捌","玖"};returncns[num];}每次调用该方法时都会重新生成一个cns数组,注意该数组不会改变,属于不变数组,在这种情况下,把它声明为类变量,并且加上final static修饰会更合适,在类加载后就生成了该数组,每次方法调用则不再重新生成数组对象了,这有助于提高系统性能,代码如下。
//声明为类变量finalstaticString[]cns={"零","壹","贰","叁","肆","伍","陆","柒","捌","玖"};publicStringtoChineseNum(intnum){returncns[num];}- (3)缩小变量的作用范围
关于变量,能定义在方法内的就定义在方法内,能定义在一个循环体内的就定义在循环体内,能放置在一个try……catch块内的就放置在该块内,其目的是加快GC的回收。 - (4)频繁字符串操作使用StringBuilder或StringBuffer
虽然String的联接操作(“+”号)已经做了很多优化,但在大量的追加操作上StringBuilder或StringBuffer还是比“+”号的性能好很多,例如这样的代码:
Stringstr="Log file is ready......";for(inti=0;i<max;i++){//此处生成三个对象str+="log "+i;}应该修改为:
StringBuildersb=newStringBuilder(20000);sb.append("Log file is ready......");for(inti=0;i<max;i++){sb.append("log "+i);}Stringlog=sb.toString();- (5)使用非线性检索
如果在ArrayList中存储了大量的数据,使用indexOf查找元素会比java.utils. Collections. binarySearch的效率低很多,原因是binarySearch是二分搜索法,而indexOf使用的是逐个元素比对的方法。这里要注意:使用binarySearch搜索时,元素必须进行排序,否则准确性就不可靠了。 - (6)覆写Exception的fillInStackTrace方法
我们在前面提到fillInStackTrace方法是用来记录异常时的栈信息的,这是非常耗时的动作,如果我们在开发时不需要关注栈信息,则可以覆盖之,如下覆盖fillInStackTrace的自定义异常会使性能提升10倍以上:
classMyExceptionextendsException{publicThrowablefillInStackTrace(){returnthis;}}- (7)不建立冗余对象
不需要建立的对象就不能建立,说起来很容易,要完全遵循此规则难度就很大了,我们经常就会无意地创建冗余对象,例如这样一段代码:
publicvoiddoSomething(){//异常信息StringexceptionMsg="我出现异常了,快来就救我!";try{Thread.sleep(10);}catch(Exceptione){//转换为自定义运行期异常thrownewMyException(e,exceptionMsg);}}注意看变量exceptionMsg,这个字符串变量在什么时候会被用到?只有在抛出异常时它才有用武之地,那它是什么时候创建的呢?只要该方法被调用就创建,不管会不会抛出异常。我们知道异常不是我们的主逻辑,不是我们代码必须或经常要到达的区域,那为了这个不经常出现的场景就每次都多定义一个字符串变量,合适吗?而且还要占用更多的内存!所以,在catch块中定义exceptionMsg方法才是正道:需要的时候才创建对象。
我们知道运行一段程序需要三种资源:CPU、内存、I/O,提升CPU的处理速度可以加快代码的执行速度,直接表现就是返回时间缩短了,效率提高了;内存是Java程序必须考虑的问题,在32位的机器上,一个JVM最多只能使用2GB的内存,而且程序占用的内存越大,寻址效率也就越低,这也是影响效率的一个因素。I/O是程序展示和存储数据的主要通道,如果它很缓慢就会影响正常的显示效果。所以我们在编码时需要从这三个方面入手接口(当然了,任何程序优化都是从这三方面入手的)。
Java的基本优化方法非常多,这里不再罗列,相信读者也有自己的小本本,上面所罗列的性能优化方法可能远比这里多,但是随着Java的不断升级,很多看似很正确的优化策略就逐渐过时了(或者说已经失效了),这一点还需要读者注意。最基本的优化方法就是自我验证,找出最佳的优化途径,提高系统性能,不可盲目信任。
133、若非必要,不要克隆对象
通过clone方法生成一个对象时,就会不再执行构造函数了,只是在内存中进行数据块的拷贝,此方法看上去似乎应该比new方法的性能好很多,但是Java的缔造者们也认识到“二八原则”,80%(甚至更多)的对象是通过new关键字创建出来的,所以对new在生成对象(分配内存、初始化)时做了充分的性能优化,事实上,一般情况下new生成的对象比clone生成的性能方面要好很多,例如这样的代码。
privatestaticclassAppleimplementsCloneable{publicObjectclone(){try{returnsuper.clone();}catch(CloneNotSupportedExceptione){thrownewError();}}}publicstaticvoidmain(String[]args){// 循环10万次finalintmaxLoops=10*10000;intloops=0;// 开始时间longstart=System.nanoTime();// "母"对象Appleapple=newApple();while(++loops<maxLoops){apple.clone();}longmid=System.nanoTime();System.out.println("clone方法生成对象耗时:"+(mid-start)+" ns");// new生成对象while(--loops>0){newApple();}longend=System.nanoTime();System.out.println("new生成对象耗时:"+(end-mid)+" ns");}在上面的代码中,Apple是一个简单的可拷贝类,用两种方式生成了10万个苹果:一种是通过克隆技术,一种是通过直接种植(也就是new关键字),按照我们的常识想当然地会认为克隆肯定比new要快,但是结果却是这样的:
clone方法生成对象耗时:18731431nsnew生成对象耗时:2391924ns不用看具体的数字,数数位数就可以了:clone方法花费的时间是8位数,而new方法是7位数,用new生成对象比clone方法快很多!原因是Apple的构造函数非常简单,而且JVM对new做了大量的性能优化,而clone方式只是一个冷僻的生成对象方式,并不是主流,它主要用于构造函数比较复杂,对象属性比较多,通过new关键字创建一个对象比较耗时间的时候。
注意克隆对象并不比直接生成对象效率高。
134、推荐使用“望闻问切”的方式诊断性能
“望闻问切”是中医诊断疾病的必经步骤,“望”是指观气色,“闻”是指听声息,“问”是指询问症状,“切”是指摸脉象,合称“四诊”,经过这四个步骤,大夫基本上就能确认病症所在,然后加以药物调理,或能还以病人健康身躯。
一个应用系统如果出现性能问题,不管是偶发性问题还是持久性问题,都是系统“生病”的表现,需要工程师去诊断,然后对症下药。我们可以把Java的性能诊断也分为此四个过程(把我们自己想象成医生吧,只是我们的英文名字不叫Doctor,而是叫做Trouble Shooter):
望
观察性能问题的症状。有人投诉我们开发出的系统性能慢,如蜗牛爬行,执行一个操作,在等待它返回的过程中,用户已经完成了倒水、喝茶、抽烟等一系列消遣活动,但系统还是没返回结果!其实这是个好现象,至少我们能看到症状,从而可以对症下药。性能问题从表象上来看可以分为两类:- 不可(或很难)重现的偶发性问题
比如线程阻塞,在某种特殊条件下,多个线程访问共享资源时会被阻塞,但不会形成死锁,这种情况很难去重现,当用户打电话投诉时,我们自己赶到现场症状已经消失了,然后1个月内再也没有出现过,当我们都认为“磨合”期已过,系统已经正常运行的时候,又接到了类似的投诉,崩溃呀!对于这种情况,“望”已经不起作用了,不要为了看到症状而花费大量的时间和精力,可以采用后续提到的“闻问切”方式。 - 可重现的性能问题
客户打电话给我们,反映系统性能缓慢,不需要我们赶到现场,自己观察一下生产机就可以发现部分交易缓慢,CPU过高,可用内存较低等问题,在这种情况下我们至少要测试三个有性能问题的交易(或者三个与业务相关而技术无关的功能,或者与技术有关而业务无关的功能),为什么是三个呢?因为“永远不要带两块手表”,这会致使无法验证和校对。
比如三个不同的输入功能,都是用户输入信息,然后保存到数据库中,但是三个交易的性能都非常缓慢,通过初步的“望”我们就可以基本确认是与数据库或数据驱动相关的问题;若是只有一个交易缓慢,其他两个正常,那就可以大致定位到一个面:该交易的逻辑层出现问题。
- 不可(或很难)重现的偶发性问题
闻
中医上的“闻”是大夫听(或嗅)患者不自觉发出的声音和气味,在性能优化上的“闻”则是关注项目被动产生的信息,其中包括:项目组的技术能力(主要取决于技术经理的技术能力)、文化氛围、群体的习惯和习性,以及他们专注和擅长的领域等,各位读者可能要疑惑了:中医上“闻”的对象是病人,而为什么这里“闻”的对象却是开发团队呢?
我们这样来思考该问题,如果是一个人(个体)生病了,找大夫如此处理是没有任何问题的,但是如果是人类(群体)生病了,那如何追寻这个根源呢?假设人是上帝创造的,如果有一群外星生物说“人类都有自私的缺陷”,那是不是应该去观察一下上帝?了解这个缺陷是源于他的习惯性动作还是技能缺乏,或者是“文化传承”。对于一个Java应用来说,我们就是“上帝”,我们创造了他,给了他生命(能够运行),给了他尊严(用户需要它),给了他灵魂(解决了业务问题),那一旦他生病,是不是应该审视一下我们这些“上帝”呢?或者我们得自我反省一下呢?
如果项目组的技术能力很强,有资深的数据库专家,有顶尖的架构师,也有首席程序员,那性能问题产生的根源就应该定位在无意识的代码缺陷上。
如果项目组的文化氛围很糟糕,组员不交流,没有固定的代码规范,缺乏整体的架构等,那性能问题的根源就可能存在于某个配置上,或者相互的接口调用上。
如果项目组已经习惯了某一个框架,而且也习惯了框架的种种约束,那性能的根源就可能是有人越过了框架的协约。
需要注意的是,“闻”并不是主动地去了解,而是由技术(人或应用)自行挥发出的“味道”,需要我们要敏锐地抓住,这可能会对性能分析有非常大的帮助。问
“问”就是与技术人员(缔造者)和业务人员(使用者)一起探讨该问题,了解性能问题的历史状况,了解“慢”产生的前因后果,比如对于业务人员我们可以咨询:- 性能是不是一直这样慢,从何时起慢到不能忍受?
- 哪一个操作或哪一类操作最慢,大概的等待时间是多长?
- 用户的操作习惯是什么,是喜欢快捷键还是喜欢用鼠标点击?
- 在什么时间段最慢,业务高峰期是否有滞顿现象,业务低谷是否也缓慢?
- 其他访问渠道,如移动设备是否也有效率问题?
- 业务品种和数量有没有激增,操作人员是否大规模增加?
- 是否在业务上发生过重大事项或重要变更,当时的性能如何?
- 用户的操作习惯有没有改变,或者用户是否自定义了某些功能?
而对于技术人员,我们就要从技术角度来询问性能问题了,而且由于技术人员对系统了如指掌,可能会“无意识”地回避问题,我们应该有技巧地处理这类问题,例如可以这样来询问技术人员:
- 系统日志是否记录了缓慢信息,是否可以回放缓慢交易?
- 缓慢时系统的CPU、内存、I/O如何?
- 高峰期和低谷时业务并发数量、并发交易种类、连接池的数量、数据的连接数量如何?
- 最早接到用户投诉是什么时候,是如何处理的,优化后如何?
- 数据量的增长幅度如何,是否有历史数据处理策略?
- 系统是否有不稳定的情况,是否出现过宕机,是否产生过javacore文件?
- 最后一次变更是何时,变更的内容是哪些,变更后是否出现过性能问题?
- 操作系统、网络、存储、应用软件等环境是否发生过改变?
通过与技术人员和业务人员交流,我们可以对性能问题有一个整体认识,避免“管中窥豹,只见一斑”的偏见,更加有助于我们分析和定位问题。
切
“切”是“四诊”的最后一个环节,也是最重要的环节,这个环节结束我们就要给出定论:问题出在什么地方,该如何处理等。Java的性能诊断也是类似的,“切”就要我们接触真实的系统数据,需要去看设计,看代码,看日志,看系统环境,然后是思考分析,最后给出结论。在这一环节中,需要注意两点:一是所有的非一手资料(如报告、非系统信息)都不是100%可信的,二是测试环境毕竟是测试环境,它只是证明假设的辅助工具,并不能证明方法或策略的正确性。
曾经遇到过这样一个案例,有一个24小时运行的高并发系统,从获得的资料上看,在出现偶发性的性能故障前系统没有做过任何变更,网络也没变更过,业务也没有过大的变动,业务人员的形容是“一夜之间系统就变慢了”,而且该问题在测试机上不能模拟重现。接到任务后,马上进行“望闻问”,都没有太大的收获。进入到“切”环节时,对大量的日志进行跟踪分析调试,最终锁定到了加密机上:加密机属于多个系统的共享资源,当排队加密数据时就有可能出现性能问题,最终的解决方案是增加一台加密机,于是系统性能恢复正常。
性能优化是一个漫长的工作,特别是对于偶发性的性能问题,不要期望找到“名医”立刻就能见效,这是不现实的,深入思考,寻根探源,最终必然能找到根源所在。中医上有一句话“病来如山倒,病去如抽丝”,系统诊断也应该这样一个过程,切忌急躁。
注意性能诊断遵循“望闻问切”,不可过度急躁。
135、必须定义性能衡量标准
出现性能问题不可怕,可怕的是没有目标,用户只是说“我希望它非常快”,或者说“和以前一样快”,在这种情况下,我们就需要把制定性能衡量标准放在首位了,原因有两个:
- (1)性能衡量标准是技术与业务之间的契约
“非常快”是一个直观性的描述,它不具有衡量的可能性,对技术人员来说,一个请求在2秒钟之内响应就可以认为是“非常快”了,但对业务人员来说,“非常快”指的是在0.5秒内看到结果—看,出现偏差了。如果我们不解决这种偏差,就有可能出现当技术人员认为优化结束的时候,而业务人员还认为系统很慢,仍然需要提高继续性能,于是拒不签收验收文档,这就产生商务麻烦了。
- (2)性能衡量标志是技术优化的目标
性能优化是无底线的,性能优化得越厉害带来的副作用也就明显,例如代码的可读性差,可扩展性降低等,比如一个乘法计算,我们一般是这样写代码的:
inti=100*16;如果我们为了提升系统性能,使用左移的方式来计算,代码如下:
inti=100<<4;性能确实提高了,但是也带来了副作用,比如代码的可读性降低了很多,要想让其他人员看明白这个左移是何意,就需要加上注释说“把100扩大16倍”,这在项目开发中是非常不合适的。因此为了让我们的代码保持优雅,减少“坏味道”的产生,就需要定义一个优化目标:优化到什么地步才算结束。
明白了性能标准的重要性,就需要在优化前就制定好它,一个好的性能衡量标准应该包括以下KPI(KeyPerformance Indicators):
- 核心业务的响应时间。一个新闻网站的核心业务就是新闻浏览,它的衡量标准就是打开一个新闻的时间;一个邮件系统的核心业务就是邮件发送和接收速度;一个管理型系统的核心就是流程提交,这也就是它的衡量标准。
- 重要业务的响应时间。重要业务是指在系统中占据前沿地位的业务,但是不会涉及业务数据的功能, 例如一个业务系统需要登录后才能操作核心业务,这个登录交易就是它的重要交易,比如邮件系统的登录。
当然,性能衡量标准必须在一定的环境下,比如网络、操作系统、硬件设备等确定的情况下才会有意义,并且还需要限定并发数、资源数(如10万数据和1000万的数据响应时间肯定不同)等,当然很多时候我们并没有必要白纸黑字地签署一份协约,我们编写性能衡量标准更多地是为了确定一个目标,并尽快达到业务要求而已。
136、枪打出头鸟—解决首要系统性能问题
在一个系统出现性能问题的时候,很少会出现只有一个功能有性能问题(一个功能出现性能问题的情况非常容易解决,基本上不会花费什么时间),系统一旦出现性能问题,也就意味着一批的功能都出现了问题,在这种情况下,我们要做的就是统计出业务人员认为重要而且缓慢的所有功能,然后按照重要优先级和响应时间进行排序,并找出前三名,而这就是我们要找的“准出头鸟”。
“准出头鸟”找到了,然后再对这三个功能进行综合分析,运行“望闻问切”策略,找到问题的可能根源,然后只修正第一个功能的性能缺陷,再来测试检查是否解决了这个问题,紧接着是第二个、第三个,循环之。可能读者会产生疑问:为什么这里只修正第一个缺陷,而不是三个一起全部修正?这是因为第一个性能缺陷才是我们真正的出头鸟,在我做过的性能优化项目中超过80%的只要修正了第一个缺陷,其他的性能问题就会自行解决或非常容易解决,已经不成为问题了。
比如BBS系统,从用户登录到用户浏览、发帖都非常缓慢,经过逐步筛选,确定登录就是“出头鸟”,需要着重解决,代码如下:
classLoginextendsHttpServlet{publicvoiddoGet(HttpServletRequestreq,HttpServletResponseresp){//从req中获得用户名和密码StringuserName=....;Stringpasswd=...;//由登录逻辑处理登录booleanloginSuccess=loginBiz.login(user,passwd);if(loginSuccess){//登录成功,签到Checker.sign(userName);}else{//登录不成功,记录日志log.warn(userName+",登录失败!")}}}这是一个早期Web应用的典型验证代码,先验证用户是否登录成功,然后决定是否向Session中写入信息。在该BBS系统中,分析发现login和sign操作都非常耗时,那就首先跟踪login,代码如下:
classLoginBiz{publicvoidlogin(String_user,String_password){//根据用户名获得用户对象Useru=userDao.getUserByUser(_user);returnu!=null&&u.getPasswd().equals(_password);}}追踪到这里,发现从数据中取出用户对象的效率很低,但是数据库的CPU、内存、I/O都没有问题,而且没有到达最大连接数。继续追踪下去,终于发现问题了:数据库的版本和JDBC的版本不一致,虽然在进行所有的连接、执行SQL、断开等操作时都没有出现任何问题,但在多表的联合查询中速度非常慢。问题定位了,将其替换成数据库匹配的驱动程序,登录问题马上得到解决,并且其他所有性能慢的问题都解决了,归根结底其实都是数据库驱动问题引起的。
解决性能问题时,不要把所有的问题都摆在眼前,这只会“扰乱”你的思维,集中精力,找到那个“出头鸟”,解决它,在大部分情况下,一批性能问题都会迎刃而解,而且我们的用户关注最多的可能就是系统20%的功能,可能我们解决了这一部分,已经达到了用户的预期目标,也就标志着我们的优化工作可以结束了。
注意解决性能优化要“单线程”小步前进,避免关注点过多而导致精力分散。
137、调整JVM参数以提升性能
我们写的每一段Java程序都要在JVM中运行,如果程序已经优化到了极致,但还是觉得性能比较低,那JVM的优化就要提到日程上来了。不过,由于JVM又是系统运行的容器,所以稳定性也是必须考虑的,过度的优化可能就会导致系统故障频繁发生,致使系统质量大幅下降。下面提供了四个常用的JVM优化手段,供你在需要时参考。
- (1)调整堆内存大小
我们知道,在JVM中有两种内存:栈内存(Stack)和堆内存(Heap),栈内存的特点是空间比较小,速度快,用来存放对象的引用及程序中的基本类型;而堆内存的特点是空间比较大,速度慢,一般对象都会在这里生成、使用和消亡。
栈空间是由线程开辟,线程结束,栈空间由JVM回收,因此它的大小一般不会对性能有太大的影响,但是它会影响系统的稳定性,在超过栈内存的容量时,系统会报StackOverflowError错误。可以通过“java-Xss <size>”设置栈内存大小来解决此类问题。
堆内存的调整不能太随意,调整得太小,会导致Full GC频繁执行,轻则导致系统性能急速下降,重则导致系统根本无法使用;调整得太大,一则是浪费资源(当然,若设置了最小堆内存则可以避免此问题),二则是产生系统不稳定的情况,例如在32位的机器上设置超过1.8GB的内存就有可能产生莫名其妙的错误。设置初始化堆内存为1GB(也就是最小堆内存),最大堆内存为1.5GB可以用如下的参数:
java-Xmx1536m-Xms1024m- (2)调整堆内存中各分区的比例
JVM的堆内存包括三部分:新生区(Young GenerationSpace)、养老区(Tenure generation space)、永久存储区(Permanent Space),其中新生成的对象都在新生区,它又分为伊甸区(Eden Space)、幸存0区(Survivor 0 Space)和幸存1区(Survivor 1 Space),当在程序中使用了new关键字时,首先在伊甸区生成该对象,如果伊甸区满了,则用垃圾回收器先进行回收,然后把剩余的对象移动到幸存区(0区或1区),可如果幸存区也满了呢?垃圾回收器会再回收一次,然后再把剩余的对象移动到养老区,那要是养老区也满了呢?此时就会触发 Full GC(这是一个非常危险的动作,JVM会停止所有的执行,所有系统资源都会让位给垃圾回收器),会对所有的对象过滤一遍,检查是否有可以回收的对象,如果还是没有的话,就抛出OutOfMemoryError错误,系统不干了!
清楚了这个原理(若还是不清楚,请看看《JVMSpecification》),那我们就可以思考一下如何提升性能了:若扩大新生区,势必会减少养老区,这就可能产生不稳定的情况,一般情况下,新生区和养老区的比例为1:3左右,设置命令如下:
java-XX:NewSize=32m-XX:MaxNewSize=640m-XX:MaxPermSize=1280m-XX:NewRatio=5该配置指定新生代初始化为32MB(也就是新生区最小内存为32M),最大不超过640MB,养老区最大不超过 1280MB,新生区和养老区的比例为1:5。
- (3)变更GC的垃圾回收策略
Java程序性能的最大障碍就是垃圾回收,我们不知道它何时会发生,也不知道它会执行多长时间,但是我们可以想办法改变它对系统的影响,比如启用并行垃圾回收、规定并行回收的线程数量等,命令格式如下:
java-XX:+UseParallelGC-XX:ParallelGCThreads=20这里启用了并行垃圾收集机制,并且定义了20个收集线程(默认的收集线程等于CPU的数量),这对多CPU的系统是非常有帮助的,可以大大减少垃圾回收对系统的影响,提高系统性能。
当然,垃圾回收的策略还有很多属性可以修改,比如UseSerialGC(启用串行GC,默认值)、ScavengeBeforeFullGC(新生代GC优先于Full GC执行)、UseConcMarkSweepGC(对老生代采用并发标记交换算法进行GC)等,这些参数需要在系统中逐步调试。
- (4)更换JVM
如果所有的JVM优化都不见效,那只有使用最后一招了:更换JVM,目前市面上比较流行的JVM有三个产品:JavaHotSpot VM、Oracle JRockit JVM、IBM JVM,其中HotSpot是我们经常使用的,稳定性、可靠性都不错;JRockit则以效率著称,性能是它的优势,但在决定使用该JVM之前一定要做好全面的系统测试,它的某些行为可能会在JRockit上产生Bug;IBM JVM也比较稳定,而且它在AIX系统上的表现要远远好于其他操作系统。
JVM的优化不能像程序优化一样,找到Bug就可以立刻解决,JVM的优化一定是要循序渐进的,参数设置不可激进,特别是需要优化多个参数时,一定要逐步实施,确保每个优化步骤都达到了预期目标,否则会对整个系统的稳定性产生较大的风险。需要提醒的是以上带有“-XX”的JVM参数可能是不健壮的,SUN也不推荐使用,可能后续会在没有通知的情况下就不再支持它了,但是它又非常好用,这需要在系统升级、迁移时慎重考虑。
138、性能是个大“咕咚”
有一部动画片叫《咕咚来了》,其大致剧情是:三只小兔在湖边玩耍,忽然湖中传来“咕咚”一声,这奇怪的声音把小兔们吓了一大跳。小兔们刚想去看个究竟,又听到“咕咚”一声,这可把小兔们吓坏了,“快跑,咕咚来了,快逃呀!”小兔们转身就跑。
狐狸正同小鸟跳舞,与跑来的兔子碰了个满怀。狐狸一听“咕咚来了!”也紧张起来,跟着就跑。它们又惊醒了睡觉的小熊和树上的小猴,小熊和小猴也不问青红皂白,跟着它们跑起来,于是一路上跟着跑的动物越来越多,大象、河马、老虎、野猪……
岸上这阵骚乱,让湖中的青蛙感到十分惊讶,它拦住了这群吓蒙了的伙伴们,问出了什么事,大家七嘴八舌地形容“咕咚”是个多么可怕的怪物。青蛙问:“谁见到了?”大家互相推诿,谁也没有亲眼看见,于是决定回去看看明白。
回到湖边,又听见“咕咚”一声,仔细一看,原来是木瓜掉进水里发出的声音,众动物不禁大笑起来。
这寓言故事好笑吗?很好笑,但是要是发生在我们自己身上就不那么好笑了,比如说,某些Javaer一直在质疑Java系统的性能,于是我们自己也跟着怀疑Java的性能—这就是发生在我们身边的真实“咕咚”,Java系统的性能问题本就是子虚乌有的事情,是我们自己吓唬自己,其实,我们可以从四个方面分析该问题:
- (1)没有慢的系统,只有不满足业务的系统
不管是使用C开发还是Java开发的项目,最终都会有一个产品诞生,或服务于大众(如网站),或服务于企业(企业级应用),谁来决定一个系统的快慢呢?不是计算机,它只会使用毫秒、纳秒去记录时间但不会做判断,它可以计算出一个交易执行了多长时间,但它不能决定这个时间是长还是短,那谁去判断呢?是人,准确地说是使用者,即使是开发人员自己根据日志记录的时间来判断系统是慢了还是快了,那也还是以使用者的身份来判断的,对一个系统毫无了解的人员是无法判断出一个系统的快慢的。
例如一个做统计的业务人员去看计费系统,即使响应需要N秒的时间,统计人员也会觉得非常快了,那是因为统计系统的结果经常是按照小时、天来计算的。再比如即时通信系统,有1秒内的延迟是可以接受的(发送者发出消息到接受者接收消息的时间间隔为1秒),但是语音通信系统若有1秒的延迟就是不可接受的了;发送邮件N分钟后才收到,这是可以容忍的,但是对于同城银行内转账来说,这个时间就是不可容忍的,必须在秒级完成。不同的系统所要求的性能不同,因此只要一个系统达到业务要求就可以认为它足够快,我们不要期望跨系统间的性能对比,这是毫无意义的。
如果有使用者告诉你,“这个系统太慢了”,也就是在间接地提醒您:系统没有满足业务需求,尚待继续努力。
- (2)没有慢的系统,只有架构不良的系统
在做系统架构设计时,架构师有没有考虑并行计算?有没有考虑云计算技术?有没有负载均衡?……这些都是解决我们性能问题的良方,只要架构设计得当,效率就不是问题。
即使是架构初期没有考虑扩展性,那我们也有一些手段可解决性能问题。比如有一个批处理系统,系统建设时的目标是:5小时内生成2000万条业务数据,可到第3年的时候,公司发生了大规模的变化(整合了其他同类公司),需要处理的数据更多了,在5小时内需要生成8000万业务。于是就得考虑架构的扩展了。有一个很简单的处理方案,即应用服务器水平扩展,增加业务数据源的纵向切割能力,均分数据压力,这样就可以很轻松地实现大数量的生成。
再比如,一个新闻网站,刚开始上线时访问的人员不多,响应都是在毫秒级别的,随着访问量的激增,响应时间呈阶梯型增加,资深会员流失率翻倍跳跃,如何解决该问题呢?解决方案有两个:一是增加IP层的负载均衡,或者硬件设备,或者软件架构,把访问者分配到多个不同的应用服务器上,降低单台应用服务器的性能压力;二是增强系统的处理能力,增大吞吐量,比如提升数据源的响应能力,划分数据的热度(如把数据划分为Hot、Warm、Cold等区域,分配不同的硬件资源和服务等级),很多时候这两个方案配合起来使用,会很快解决性能问题。
- (3)没有慢的系统,只有懒惰的技术人员
这里的技术人员涉及面很大,可以是开发人员,也可以是维护人员,甚至是应用软件的顾问人员(如数据库顾问、App Server的顾问)等。一个系统出现问题,或者是投产前后立刻出现的性能问题,或者是运行中突发的性能问题,或者是逐渐增长的数据(用户或业务数据)导致的性能问题,只要我们肯用心查找,并且拥有适当的资源(如源码和支持资源),一般都是可以解决的。最可怕的是我们的技术人员对性能问题漠不关心,对时间效率不够敏感,导致使用者怨声载道,三人成虎,最终致使此系统成为一个“慢得无法使用的系统”。
这也要求我们在开发初期就适当考虑一下性能问题,但不要把性能排为头号任务,它不是,它只是我们的一个关注点而已。
- (4)没有慢的系统,只有不愿意投入的系统
这里的投入指的是资源,包括软硬件资源、人员资源及资金资源等,这不是项目组能够单独解决的问题,但是它会严重影响系统的性能。曾经遇到一个运行超过8年的分析系统,从1年前开始只要是高峰期它的速度就会慢下来,分析下来,发现是因为并发用户超过了许可的数量,造成系统阻塞,性能缓慢,唯一解决的法就是购买更多的许可数量,但是8年了,一个系统的生命期还能有多少呢?—所以最后采用了自由放任的办法,让其自行走到寿命的终结点,然后建立新的分析系统。
当然,我们也会碰到查不出原因的性能问题,这不可否认,毕竟现在的系统越做越大,源代码动辄就十万、百万级别,让一个人或一个小团队将其彻头彻尾地查清楚也不现实,而且性能问题涉及面非常广,如操作系统、数据库、网络、存储等,要想对这些技术都非常熟悉也很困难,但查不出问题并不代表我们解决不了,是的,这与治疗癌症相似,我们现在的科学还不知道它的发病机理,不知道为什么会产生癌细胞,但我们知道割除病变部位能够避免癌细胞扩散,性能问题也一样:我们可能不知道问题产生的原因,但我们可以有N种手段来解决它。能够解决的问题还算是问题吗?
而且,性能只是衡量系统的一个辅助指标,而不是主指标,如果您与业务人员交流,说“我们可以把系统的响应时间提升到0.001秒内,但前提是不实现您提出的需求”,您猜业务人员会同意吗?—不把我们这些“火星人”撵出门外已经算是客气的了!
注意对现代化的系统建设来说,性能就是一个大“咕咚”—看清它的本质吧。