在进行了一段时间的调研后,本周开始着手进行性能优化工作。现在在优化工作工作之前,我总结一下调研了的一些信息。
1.背景
客户这是一个03年的时候开发的系统了,所以使用的是.NET 1.1+SQL Server 2000,操作系统用的是Windows2003,使用了这么几年,只是对操作系统进行了升级(从当时的Windows2000升级到2003的)以及对系统进行维护,.Net环境和数据库并没有改变。由于系统中记录了几年的数据,有些表有几百万行的数据,当初没有建立索引和系统程序上考虑的不足造成目前系统运行十分缓慢,需要进行性能优化。我现在都开始用VS2008开发和使用SQL Server 2008数据库了,突然接收这样一个老项目,还真有点不适应,SQL2000我好久没有碰过了。
2.硬件
在硬件环境上,客户的服务器还是很不错的,Web服务器和数据库服务器各一台,都是4个双核CPU、8G内存、1G网络、300G的RAID5硬盘,总体来说感觉很好了。在得知客户是8G的内存时我第一反应就是客户肯定浪费内存了!结果实际一看,果然如此,系统是8G的内存,但是所有程序加起来用的内存才2G左右,而且一直上不去,为什么呢?因为客户使用的是32位的操作系统,所以默认只能支持4G的内存,有2G用于操作系统,另外2G用于应用程序,在任务管理器中可以看到SQL Server只占用了1.7G左右的内存,而不能再占用更多就是这样一个原因。SQL Server是一个做大量数据处理的程序,内存的速度比硬盘快很多,若要处理的数据如果都是在内存中将会比读取硬盘进行处理快的多,所以SQL Server占用的内存越多越好。要突破32位操作系统对应用程序2G的内存限制,可以打开3GB开关,将操作系统的内存使用改为1G,应用程序使用的内存改为3G。当然这里是8G的内存,所以打开3GB开关是不够的,这里就需要打开系统的APE开关,使用SQL Server的AWE功能。另外一种解决办法就是换成64位的操作系统和SQL Server。
3.数据访问
我简单的Review了一下程序代码,该系统是BS程序,三层架构,数据库访问主要是采用SqlHelper调用存储过程和SQL语句,然后使用DataReader最终返回一个对象或对象集合。由于系统是在不断的需求变化中完成了,所以也犯了很多项目的一个通病,为了赶时间,大家就不顾代码的规范和架构,有的是使用存储过程,有的是使用SQL语句,有的是直接在UI层写SQL语句,然后将SQL语句传到业务层,最后再传到数据层去执行。我已经不是第一次看到这样的程序了,我大学实习时开始做的一个项目中也出现过这样的代码,所以感觉可以理解。
在Review数据访问层的代码时,我看到了大量的读取一个DataReader用于填充一个对象的代码如下:
private void fillRegionLevel(RegionLevelMod level, SqlDataReader reader)
{
// 行政级别代码
if (!reader.IsDBNull(0))
level.LevelCode = reader.GetString(0);
// 行政级别名称
if (!reader.IsDBNull(1))
level.LevelName = reader.GetString(1);
// 行政级别描述
if (!reader.IsDBNull(2))
level.LevelDescribe = reader.GetString(2);
}
这样写程序也不会有什么错误,但是整个程序太依赖数据库返回对象的顺序了,如果修改了数据库,在最前面多提供了一列,那么reader.GetString(0)就会错位读取到其他的列,以后的读取也全部错位,所以扩展相对比较麻烦。我个人比较建议的写法是通过列名来读取DataReader中的内容,比如:
if (Convert.IsDBNull(reader["Code"]))
{
level.LevelCode = reader["Code"].ToString();
}
当然其实并不需要这么麻烦,在项目中如果通过DataReader填充一个对象的话,我一般采用反射的方式,在实体类定义的时候就为每个字段添加Attribute,然后统一使用一个方法通过反射方式就可以将类中的字段与DataReader返回的列进行映射了。具体参见示例代码: /Files/studyzy/LoadDbToObjectDemo.rar
还有一个问题就是对于字典表(比如地市区域、类型等),应该使用缓存,将数据保存在Web服务器的内存中,不需要每次都读取数据库。
4.图表
该系统中要使用大量的Chart,为此开发人员自己写了一个服务器控件,通过设置各个属性,然后传入数据,最后执行DataBind()即可。这儿抽象出一个控件是相当不错的,实现了代码功能的复用。整个Chart的展现思路是这样的:
(1)系统将数据传入Chart控件,执行其DataBind方法,该控件将使用GDI+进行绘图。
(2)将绘出的图根据当前的时间等属性保存到服务器硬盘的某个文件夹中。
(3)将绘出的图的路径与<img>代码组合,将这段HTML代码Render出来。
这样功能是实现了,但是也存在几个问题:
- Chart的绘制是在服务器上进行了,有大量的Chart需要展示时将增加服务器的负担。
- Chart保存到了硬盘中,然后在img标签中指向该硬盘地址,为了这个图片就做了2次硬盘的IO操作。
- 整个Chart数据读取和绘制与Page_Load是同一个线程,如果一个Chart在数据读取或绘制过程需要很长时间,则会导致整个页面响应被阻塞。
经过我实际跟踪测试,发现确实如此,如果绘制一个Chart要5秒,一个页面打开时间就必然超过5秒,只有等Chart绘制完成了,整个系统才会Response出来。对于以上提出的问题,我们可以采用以下的解决办法:
- 将Chart的绘制从服务器转移到客户端,具体做法就是采用Flash或者SilverLight(其实使用JavaScript也可以绘制Chart)。客户端只需要下载了Flash后,系统将要绘制Chart的数据集通过XML、AMF等方式传送给Flash,然后由Flash负责将整个Chart绘制出来。使用Flash后一方面可以减小服务器压力,另一方面可以提供访问页面的速度,另外也可以做出很好的效果提高用户体验。
- 不进行硬盘的IO操作,在服务器绘制了Chart后,就直接使用一个页面Response出来即可。
- 将Chart的数据读取和绘制放在另外一个页面中进行,展现Chart的页面只需要输出<img src='Chart.aspx?type=1&data=1,3,5,7'/>这样的HTML即可。所有的数据读取和绘制操作将在Chart.aspx页面进行。这样如果绘制一个Chart要5秒钟,由于页面的线程中并没有执行绘图,所以可以很快返回,浏览器在载入了页面后才会去请求Chart.aspx页面,这个时候才进行绘图。
这里个人更推荐第一种方法,在新版系统中可以考虑采用。不过对于现有系统的优化,当然不能进行这么大的改动,采用第三点即可。
5.HTML
该系统的首页存在性能问题,每次打开都需要很长时间,如果网络情况不好的话就等的更久了。使用FireFox中的FireBug可以跟踪首页打开时请求的所有资源的大小、响应时间等,也可以使用网络抓包工具进行分析。经过我抓包分析发现,首页的HTML有180K左右,其实也不算大,引用的图片、CSS、JS也都不多,但是整个响应很慢除了系统进行大量数据库操作之外与HTML也存在很大关系。查看其HTML发现以下问题:
- HTML中存在很大的ViewState,但是首页主要是只读的数据绑定,所以很多控件如DataGrid都可以关闭ViewState。
- HTML中使用的是存储的Table拼接方式,这种方式将导致系统必须把整个大Table载入完成后才会呈现,可以改用div+css的方式,这样每获得了一块DIV就可以呈现一块内容。
- 没有使用AJAX技术,页面上的很多内容特别耗时间,完全可以通过AJAX进行异步绑定的。
6.数据库
数据库是我本次优化的重点,由于是SQL Server 2000的数据库,所以没有DMV、没有SSMS用的性能监视器、没有包含索引……太多好用的功能没有啊,十分的不方便。对数据库的性能优化一般来说我觉得主要是3个方向:
- 优化索引,建立该建的索引去掉没有用的索引。
- 改写查询,使之符合SARG。
- 减少阻塞和资源等待,避免死锁。
我接下来的工作就是围绕这3个方向展开。在SQL Server性能优化上必不可少的工具就是SQL Server Profiler,也就是SQL2000中的事件探测器。一种是使用Profiler抓取生产环境在业务高峰时的数据,一种是在测试环境中没有其他用户和程序干扰的情况下抓取打开某个页面或者执行某个操作时的SQL跟踪。跟踪的结果保存到数据库中,然后使用查询语句找到Reads和Duration很大的SQL语句,针对这些语句进行性能调优。
对于系统中的死锁,使用如下命令可以打开死锁跟踪记录,一旦数据库中出现了死锁,则将会把死锁信息记录到数据库的日志中。
dbcc traceon (1204, 3605, -1)
dbcc tracestatus(-1)
DBCC TRACEON
数据库优化是一门很大的学问,接下来的日子我将主要进行数据库优化的工作,具体优化过程我也将尽量详细的记录下来,希望对大家也有所帮助。
本文转自深蓝居博客园博客,原文链接:http://www.cnblogs.com/studyzy/archive/2008/10/25/1319156.html,如需转载请自行联系原作者