软件性能优化最佳实践

软件性能优化最佳实践
(作者:a_yu)
1 经验总结
1.1 应用程序设计优化
大业务量数据存储设计建议
当系统中业务数据量较大时,避免业务变更的历史数据和正式的业务数据同时存储,由于业务新增的数据及历史业务变更产生的数据,会到时存储表越来越庞大,而且历史数据会逐步增多,一段时间后,历史数据往往会大于正式数据,而正式数据是要经常使用的,历史数据只会在个别查询时会用到,庞大的数据存储会直接导致正式业务数据的查询性能及程序处理复杂度,同时,如果系统未来进行升级时,庞大的数据量会大大增大数据迁移割接的难度,增加数据迁移的时间;建议将正式数据独立存储,业务修改后的历史数据采用日志存储或新建表进行存储,保证正式数据的查询处理效率。
对于不可变字符类型char和可变字符类型varchar 最大长度都是8000字节,char查询快,但是耗存储空间,varchar查询相对慢一些但是节省存储空间。在设计字段的时候可以灵活选择,例如用户名、密码等长度变化不大的字段可以选择CHAR,对于评论等长度变化大的字段可以选择VARCHAR。
数据行的长度不要超过8020字节,如果超过这个长度的话在物理页中这条数据会占用两行从而造成存储碎片,降低查询效率。
能够用数字类型的字段尽量选择数字类型而不用字符串类型的(电话号码),这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接回逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
字段的长度在最大限度的满足可能的需要的前提下,应该尽可能的设得短一些,这样可以提高查询的效率,而且在建立索引的时候也可以减少资源的消耗。
历史数据存储建议
当数据库中的某种业务数据增加量很大时,如短信业务或用户交易业务,往往涉及最近记录和历史记录查询问题,建议建立与正式业务表属性相同的历史记录表,同时采用定时程序或存储过程等方法将正式业务表的数据按过期规则迁移至历史记录表,在用户进行记录查询是分表查询,一般用户比较关注近期的记录,这样直接查询正式业务表,数据量不是很大,保证的较高的查询响应效率,个别情况下用户会查询历史记录,系统会从历史记录表中查询;使用这样的方式可以保证大部分时间用户的查询效率
消息服务的应用
J2EE规范在JMS中提供了内置的异步处理服务。当涉及到系统需求时,应该了解在什么情况下应该采用JMS进行异步处理的设计。一旦确定要执行一些异步处理,那么同步处理的任务就应该越少越好,将数据库密集的操作安排在稍后的异步处理中完成。
大字段处理
当业务表中包括用来存储附件、图片、大文本的大字段的数据类型时,在列表页面一般情况下不要获取大字段内容,如列表页面获取大字段,却不进行展现,如数据量增大后,会大大影响页面展示速度,在查看页面加载大字段类型,可在对象实体类中增加一个钩子函数,只把基本属性传入,这样列表页面就不会获取到大字段,当然,也可以通过Hibernate配置解决这个问题,hibernate2可以将BLOB字段与基本信息分离,生成两个PO,这样我们可以通过延迟加载特性以提高效率,字段单独拆分出来,以提高数据库操作的性能。Hibernate3中对于POJO的属性提供了延迟加载, 只要设置属性的的lazy="true",以后通过getXXX才能真正从数据库中读取数据,这样可以有效地提高我们读取部分表字段的性能。
流的关闭
对于应用中的I/O流、网络传输流、数据库连接等要及时关闭和释放,否则会造成内存溢出,会话数占满等问题。在流关闭的时候要注意关闭的顺序。
1.2 数据库优化
连接池配置
目前数据库连接池开源组件是非常多的,DBCP、C3P0、Proxool、BoneCP等都是非常优秀的产品。连接池的性能和稳定性会对我们的程序造成极大的影响,因此,有必要对这些连接池产品进行一些选择。另外,连接池的配置是否恰当,将会决定该连接池的性能和稳定性表现,Hibernate开发组推荐使用c3p0,spring开发组推荐使用dbcp(dbcp连接池有weblogic连接池同样的问题,就是强行关闭连接或数据库重启后,无法reconnect,但可通过配置来解决),Hibernate in action推荐使用c3p0和proxool。三个产品都能很方便地整合到Springframework中,也都可以配置为JNDI资源,因此,它们可以随意更换,不会影响程序代码。根据网络上各种比较结果分析,在性能上,BoneCP>Proxool>C3P0>DBCP,在稳定性上DBCP>C3P0>Proxool。所以在项目中要根据实际情况,选择适当的连接池。针对大批量的数据处理建议使用JDBC。
缓存配置
Hibernate提供了两级缓存,第一级是Session的缓存。由于Session对象的生命周期通常对应一个数据库事务或者一个应用事务,因此它的缓存是事务范围的缓存。第一级缓存是必需的,不但而且事实上也无法被卸除。在第一级缓存中,持久化类的每个实例都具有唯一的OID。第二级缓存是一个可插拔的的缓存插件,它是由SessionFactory负责管理。由于SessionFactory对象的生命周期和应用程序的整个过程对应,因此第二级缓存是进程范围或者集群范围的缓存。这个缓存中存放的对象的松散数据。第二级对象有可能出现并发问题,因此需要采用适当的并发访问策略,该策略为被缓存的数据提供了事务隔离级别。缓存适配器用于把具体的缓存实现软件与Hibernate集成。第二级缓存是可选的,可以在每个类或每个集合的粒度上配置第二级缓存。
正确的利用二级缓存,可极大的提高系统的查询效率。
以下数据适合存放到第二级缓存中:
 很少被修改的数据
 不是很重要的数据,但出现偶然并发的数据
 不会被并发访问的数据
 参考数据
以下数据不适合存放到第二级缓存中:
 经常被修改的数据
 财务数据,绝对不能出现并发
 与其他应用共享的数据。
SQL优化
我们要做到不但会写 SQL,还要做到写出性能优良的 SQL。SQL优化需要注意及遵循的准则很多,本文只列举核心部分:
1) 尽量少用IN操作符,基本上所有的IN操作符都可以用EXISTS代替。
2) 不用NOT IN操作符,可以用NOT EXISTS或者外连接+(外连接+判断为空)替代。
3) 不用“<>”或者“!=”操作符。对不等于操作符的处理会造成全表扫描,可以用“<” or “>”代替。例如:a<>0 改为 a>0 or a<0,a<>’ ’ 改为 a>’ ’
4) Where子句中出现IS NULL或者IS NOT NULL时,Oracle会停止使用索引而执行全表扫描。可以考虑在设计表时,对索引列设置为NOT NULL。这样就可以用其他操作来取代判断NULL的操作。
5) 当通配符“%”或者“_”作为查询字符串的第一个字符时,索引不会被使用,因此一般不要作为第一个字符出现。
6) 对于有连接的列“||”,最后一个连接列索引会无效。尽量避免连接,可以分开连接或者使用不作用在列上的函数替代。
7) 如果索引不是基于函数的,那么当在Where子句中对索引列使用函数时,索引不再起作用。
8) Where子句中避免在索引列上使用计算,否则将导致索引失效而进行全表扫描。
9) 对数据类型不同的列进行比较时,会使索引失效。
10) 用“>=”替代“>”。
11) UNION操作符会对结果进行筛选,消除重复,数据量大的情况下可能会引起磁盘排序。如果不需要删除重复记录,应该使用UNION ALL。
12) Oracle从下到上处理Where子句中多个查询条件,所以表连接语句应写在其他Where条件前,可以过滤掉最大数量记录的条件必须写在Where子句的末尾。
13) Oracle从右到左处理From子句中的表名,所以在From子句中包含多个表的情况下,将记录最少的表放在最后。
14) Order By语句中的非索引列会降低性能,可以通过添加索引的方式处理。严格控制在Order By语句中使用表达式。
15) 不同区域出现的相同的Sql语句,要保证查询字符完全相同,以利用SGA共享池,防止相同的Sql语句被多次分析。
16) 多利用内部函数提高Sql效率。
17) 当在Sql语句中连接多个表时,使用表的别名,并将之作为每列的前缀。这样可以减少解析时间。
18) 根据SQL不同设定优化模式的方式,选择不同的优化策略,通过SELECT /*+ALL+_ROWS*/ ……;来设定。可用的HINT包括/*+ALL_ROWS*/、/*+FIRST_ROWS*/、/*+CHOOSE*/、/*+RULE*/ 等一般在SQL前加first_rows策略,速度都会提高,特殊情况下改用choose策略。
19) 对于大表查询中的列应尽量避免进行诸如To_char,to_date,to_number等转换。
20) 有索引的尽量用索引,有用到索引的条件写在前面 。
21) 如有可能和有必要就建立一些索引 ,在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
22) 尽量避免进行全表扫描,限制条件尽可能多,以便更快搜索到要查询的数据。
23) Select子句中尽量避免使用‘*’,当你想在SELECT子句中列出所有的COLUMN时,使用动态SQL列引用 ‘*’ 是一个方便的方法。但是,这是一个非常低效的方法。实际上,ORACLE在解析的过程中,会将‘*’ 依次转换成所有的列名,这个工作是通过查询数据字典完成的,这意味着将耗费更多的时间。合理写WHERE子句,不要写没有WHERE的SQL语句。
24) 减少访问数据的次数,当执行每条SQL语句时, 数据库在内部执行了许多工作: 解析SQL语句, 估算索引的利用率, 绑定变量 , 读数据块等等。 由此可见, 减少访问数据库的次数 , 就能实际上减少数据库的工作量。
25) 查询的模糊匹配,尽量避免在一个复杂查询里面使用 LIKE '%parm1%'——百分号会导致相关列的索引无法使用,最好不要用。解决办法:其实只需要对该脚本略做改进,查询速度便会提高近百倍。改进方法如下:
a、修改前台程序——把查询条件的供应商名称一栏由原来的文本输入改为下拉列表,用户模糊输入供应商名称时,直接在前台就帮忙定位到具体的供应商,这样在调用后台程序时,这列就可以直接用等于来关联了。
b、直接修改后台——根据输入条件,先查出符合条件的供应商,并把相关记录保存在一个临时表里头,然后再用临时表去做复杂关联。
尽量避免在索引过的字符数据中,使用非打头字母搜索。这也使得引擎无法利用索引。 见如下例子:
SELECT * FROM T1 WHERE NAME LIKE ‘%L%’
SELECT * FROM T1 WHERE SUBSTING(NAME,2,1)=’L’
SELECT * FROM T1 WHERE NAME LIKE ‘L%’
即使NAME字段建有索引,前两个查询依然无法利用索引完成加快操作,引擎不得不对全表所有数据逐条操作来完成任务。而第三个查询能够使用索引来加快操作。
26) 避免使用耗费资源的操作,带有DISTINCT,UNION,MINUS,INTERSECT,ORDER BY的SQL语句会启动SQL引擎 执行,耗费资源的排序(SORT)功能. DISTINCT需要一次排序操作, 而其他的至少需要执行两次排序。
27) 尽量减少重复工作,控制同一条语句的多次执行,减少多次的数据转换,减少不必要的子查询和连接表,合并对同一张表的多次update操作,update操作不要拆成delete+insert操作,虽然功能相同,但性能差别很大。
28) 多表连接的连接条件对索引选择有重要意义,在写连接条件时要特别注意。多表连接时,连接条件必须写全,尽量使用聚集索引。
充分利用连接条件,在某种情况下,两个表之间可能不只一个的连接条件,这时在 WHERE 子句中将连接条件完整的写上,有可能大大提高查询速度。
例:
SELECT SUM(A.AMOUNT) FROM ACCOUNT A,CARD B WHERE A.CARD_NO = B.CARD_NO
SELECT SUM(A.AMOUNT) FROM ACCOUNT A,CARD B WHERE A.CARD_NO = B.CARD_NO AND A.ACCOUNT_NO=B.ACCOUNT_NO
第二句将比第一句执行快得多。
29) 避免使用不兼容的数据类型。例如float和int、char和varchar、binary和varbinary是不兼容的。数据类型的不兼容可能使优化器无法执行一些本来可以进行的优化操作。例如:
SELECT name FROM employee WHERE salary > 60000
在这条语句中,如salary字段是money型的,则优化器很难对其进行优化,因为60000是个整型数。我们应当在编程时将整型转化成为钱币型,而不要等到运行时转化。
30) 尽量不要用SELECT INTO语句。
SELECT INTO 语句会导致表锁定,阻止其他用户访问该表。
31) 使用视图加速查询。
把表的一个子集进行排序并创建视图,有时能加速查询。它有助于避免多重排序操作,而且在其他方面还能简化优化器的工作。视图中的行要比主表中的行少,而且物理顺序就是所要求的顺序,减少了磁盘I/O,所以查询工作量可以得到大幅减少。
建立高效的索引
索引是从数据库中获取数据的最高效方式之一。95% 的数据库性能问题都可以采用索引技术得到解决。
不必要的全表搜索导致大量不必要的I/O,从而拖慢整个数据库的性能。调优专家首先会根据查询返回的行数目来评价 SQL。在一个有序的表中,如果查询返回少于40%的行,或者在一个无序的表中,返回少于7%的行,那么这个查询都可以调整为使用一个索引来代替全表搜索。对于不必要的全表搜索来说,最常见的调优方法是增加索引。可以在表中加入标准的B树索引,也可以加入bitmap和基于函数的索引。要决定是否消除一个全表搜索,你可以仔细检查索引搜索的I/O开销和全表搜索的开销,它们的开销和数据块的读取和可能的并行执行有关,并将两者作对比。在一些情况下,一些不必要的全表搜索的消除可以通过强制使用一个index来达到,只需要在SQL语句中加入一个索引的提示就可以了。J2EE的架构设计工程师和开发人员通常不是SQL专家或经验丰富的数据库管理员。首先应该确保SQL使用了数据库提供的索引支持。在关键查询属性上增加索引,会极大的提高数据库表查询速率,在某些情况下,将数据库的索引和数据分开存放会提高性能。但要知道,增加额外的索引可以提高SELECT性能但也会降低INSERT的性能。
在做性能跟踪分析过程中,经常发现有不少后台程序的性能问题是因为缺少合适索引造成的,有些表甚至一个索引都没有。这种情况往往都是因为在设计表时,没去定义索引,而开发初期,由于表记录很少,索引创建与否,可能对性能没啥影响,开发人员因此也未多加重视。然一旦程序发布到生产环境,随着时间的推移,表记录越来越多这时缺少索引,对性能的影响便会越来越大了。这个问题需要数据库设计人员和开发人员共同关注。
法则:不要在建立的索引的数据列上进行下列操作:
◆避免对索引字段进行计算操作
◆避免在索引字段上使用not,<>,!=
◆避免在索引列上使用IS NULL和IS NOT NULL
◆避免在索引列上出现数据类型转换
◆避免在索引字段上使用函数
◆避免建立索引的列中使用空值。
作为一条规则,通常对逻辑主键使用唯一的聚集索引,对系统键(作为存储过程)采用唯一的非聚集索引,对任何外键列[字段]采用非聚集索引。
存储过程使用
对于比较复杂的业务,建议使用存储过程,可以提高运行效率,减少网络流量。但是存储过程也要慎用,对于非常消耗数据库性能的情况尽量使用存储过程,如果要考虑开发的业务逻辑,且后期的业务变化比较频繁的,不要使用存储过程,会增加维护成本,所以存储过程推荐使用,但也要有个度。
I/O读写分离
由于对于同一段数据,如果要同时写入和读出,会存在锁的问题,影响效率,建议把I/O读写频繁的数据进行分离,可以采用分库、分表等方式进行分离。
分表空间、分表、分区
对于数据量比较大,业务类型清楚的表空间和表,可以进行分表空间、分表和分区的方式,有减小数据规模等的好处,能极大的提高数据库的使用效率。对数据量很大的Table和Index使用分区,放在不同的Tablespace中。
冗余设计
冗余设计包括字段冗余、表冗余、视图以及建立临时表。对于有大量关联查询的情况,建议采用冗余的方式,或者增加冗余字段,或者增加中间表,或者增加视图,或者创建临时表,以牺牲空间的方式来换取时间效率。
提前计算
对于报表统计等大数据量的统计操作,如果速度太慢,可以提前进行计算,比如在晚上将统计结果查询统计好后放在临时区域,在第二天用的时候直接取临时数据,会大大节省效率。
事务控制 
事务方面对性能有影响的主要包括:事务方式的选用,事务隔离级别以及锁的选用。
a)事务方式选用:如果不涉及多个事务管理器事务的话,不需要使用JTA,只有JDBC的事务控制就可以。
b)事务隔离级别:参见标准的SQL事务隔离级别
c)锁的选用:悲观锁(一般由具体的事务管理器实现),对于长事务效率低,但安全。乐观锁(一般在应用级别实现),如在HIBERNATE中可以定义 VERSION字段,显然,如果有多个应用操作数据,且这些应用不是用同一种乐观锁机制,则乐观锁会失效。
集群
如果系统的瓶颈主要在数据库的并发操作比较高,可以考虑采用数据库集群的方式来提高数据库操作的性能。数据库集群是利用一个集群中的多台机器共同完成同一件任务,使得完成任务的速度和可靠性都远高于单机运行的效果。数据库集群在数据量大,计算复杂的环境用的比较多。
1.3 部署环境优化
应用服务器配置优化
针对你系统特点定制应用服务器配置。应用服务器设置对性能和吞吐量有重要的影响,包括:
         数据库连接池大小。这对吞吐量有至关重要的影响。连接池太小会导致线程因等待连接释放而阻塞;而太大的连接池在集群环境中可能带来问题,数据库可能耗光连接监听器。
         线程池大小。线程池太小会浪费CPU能力,太大的线程池可能反而降低性能。
         使用SLSB(无状态会话bean)时的实例池大小。它的影响和线程池相似。不要把它设置得比线程池更大。
         使用SLSB时的事务描述符。除非业务对象的所有方法都是需要是事务的,不要对所有方法都使用同样的事务声明。确定每个方法都有合适的事务属性:如果不需要事务就是none。
         HTTP session复制选项。根据你的应用程序选择是把session保存到数据库还是保持在内存中,仔细检查你的应用服务器提供的选项。
         JTA选项。假若只使用一个数据库,必须确保你没有使用开销巨大的XA事务。
         log设置。应用服务器和应用程序一样都会产生log输出,这可能造成巨大的开销,记住让你的服务器只产生必要的信息log。
服务器硬件环境优化
确保服务器网络达到设计要求。如果在硬件资源充沛的情况下,我们首先应该对硬件环境进行优化,比如更换更加高性能的服务器设备,增大内存,提高网络带宽,采用磁阵存储等。
集群、负载均衡
在系统运行时,web服务器往往要支撑大量密集的用户点击和对动态内容的需求,所以即使再高档的服务器设备,面对不断增加的用户,单位时间内所支持的访问量也会有一个限度,尤其是对于动态内容较多的情况:因为动态内容的应用需要频繁地调用数据库的数据和应用程序,会占用大量的服务器资源。这时就需要在多个服务器设备之间或多个站点之间分散服务器的负载。可以对Jboss、Tomcat等常用的应用服务器结合Apache进行负载均衡,以减轻单个web服务器的压力,提升高并发量情况下的系统性能。
1.4 其他优化
系统日志优化
将日志文件的大小设置为合理大小,以防止日志文件过大影响到性能。只记录必要的日志(如Error级别以上的日志),尽量减少日志的输出量。在代码中,删除不必要的打印语句。
静态类合理使用
Java类中的静态变量在程序运行期间,其内存空间对所有该类的对象实例而言是共享的,有些时候可以认为是全局变量。因此在某些时候为了节省系统内存开销、共享资源,可以将类中的一些变量声明为静态变量,因为静态变量生命周期较长,而且不易被系统回收,因此如果不能合理地使用静态变量,就会适得其反,造成大量的内存浪费,在高并发的时候造成stack内存溢出,所谓过犹不及。因此,建议在具备下列全部条件的情况下,尽量使用静态变量:
(1)变量所包含的对象体积较大,占用内存较多。
(2)变量所包含的对象生命周期较长。
(3)变量所包含的对象数据稳定。
(4)该类的对象实例有对该变量所包含的对象的共享需求。
页面优化
(1) 页面减肥:页面的肥瘦是影响加载速度最重要的因素 ,删除不必要的空格、注释 ,将内联引用的script和css移到外部文件 ,可以使用HTML TIDY来给HTML减肥,还可以使用一些压缩工具来给JavaScript减肥。
(2) 减少文件数量:减少页面上引用的文件数量可以减少HTTP连接数 ,许多JavaScript、CSS文件可以合并最好合并。
(3) 减少域名查询:DNS查询和解析域名也是消耗时间的,所以要减少对外部JavaScript、CSS、图片等资源的引用,不同域名的使用越少越好。
(4) 缓存重用数据:在页面上缓存重用的数据,可以减少数据库的IO操作次数,提高页面展示速度。
(5) 优化页面元素加载顺序:首先加载页面最初显示的内容和与之相关的JavaScript和CSS ,然后加载DHTML相关的东西 ,像什么不是最初显示相关的图片、flash、视频等很肥的资源就最后加载。建议把样式表放在顶部、脚本放在底部。
(6) 减少HTML内联引用javascript的数量:浏览器parser会假设内联引用JavaScript会改变页面结构,所以使用内联引用 JavaScript开销较大 ,不要使用document.write()这种输出内容的方法,使用标准W3C DOM方法来为现代浏览器处理页面内容。
(7) 使用标准的CSS和合法的标签:使用标准CSS来减少标签和图像,例如使用标准CSS+文字完全可以替代一些只有文字的图片 ,使用合法的标签避免浏览器解析HTML时做“error correction”等操作。
1.5 优化步骤
软件的性能优化应该在需求采集阶段就要了解清楚用户对软件的性能要求,比如最大的并发用户数、数据的规模、网络环境等,以便于在后续的设计和开发中对性能进行持续的优化。当在运行系统发生了性能问题后,我们往往措手不及,无从下手,其实,如果按照环境、应用、数据库这3个层面分别去进行优化,总能找到相应的解决办法。对于在运行系统,遇到性能问题后我们应该遵循“先环境后应用”、“先数据库后程序”、“先易后难”的原则进行优化。
“先环境后应用”是指遇到性能问题了,我们不要急着去查应用的问题,先看看硬件环境或者部署环境有没有出问题,比如看看网络的带宽是否正常,服务器的I/O吞吐、内存使用率、CPU使用率是否正常。然后再看看操作系统、应用服务器等是否正常,应用是否感染病毒等。这个原则适用于系统长期运行稳定,在系统使用环境没有变化的情况下,突然出现性能问题。
“先数据库后程序“是指遇到性能问题了,尤其是系统长期运行稳定,忽然由于用户量的增长或者数据量的增长导致出现性能问题的。先去查数据库的工作状况,比如会话数是否正常,有没有长期占用不释放的情况,索引是否有效,有没有索引无效而全表扫描的情况,数据库的I/O是否正常等。然后再根据数据库的排查情况先去解决数据库上的性能瓶颈,如本文提到的创建有效的索引、分表空间、分表(纵向和横向)、分区等,如果这些的办法还是行不通,再考虑去修改程序。
“先易后难”指的是在性能调优过程中,先尝试使用简单的,代价下的办法去解决问题,如索引的排查、数据库的排查、部署环境的排查,再去考虑复杂的办法,如修改程序、修改数据结构、增加集群等。

猜你喜欢

转载自zby-110.iteye.com/blog/1199239