个人空间和个人档案的优化(2008-09)
最近个人空间的性能令人担忧,一天中不时会有大量的IO,或者w3wp.exe的内存占有不断增长直到应用程序池回收,这时如果应用程序池回收了就会导致大量的IO,针对这样的情况,必须先着手解决当前的性能问题。
1、分离用户头像到独立服务器中,并且取消通过程序读取和显示头像。
在以往的实现中,是通过DisplayPicture.aspx页面的Response.WriteFile(fullname);
读取用户的头像然后显示出来的,前且还通过URL重写,提供友好的URL,例如http://profile.csdn.net/billok/picture/1.jpg
在这里面会有一定的性能和资源损耗,例如URL重写和通过asp.net读图片到内存然后又回收,这样的损耗其实没有太大的必要。所以在这次的性能优化中就直接显示为真实的地址,例如:http://avatar.profile.csdn.net/1/c/5/1_billok.jpg
其中"/1/c/5/"是MD的散列的一部分,目的是为了文件在磁盘能够更加均匀的分布。
通过这样的修改就会取消ASP.NET的托管,提高影响的能力和减少不必要的性能损耗。同时,把avatar站点迁移到另外一台负荷更少一点的服务器上,就能大大减少IO读取量,当然上传头像和其它服务调用也要一并迁移了。
2、改良文件缓存组件。
系统中最重要的缓存方式是文件缓存,所以文件缓存的调用率是十分具大的,只要有一点的改善所带来的好处就是很大的了。
缓存组件是一个通用的缓存组件,可能通过配置来切换不同的缓存类型(如:文件、MemCached、ASP.NET等),不过这里主要用到的就是文件形式,因为有点复杂这里就先不拿出来讲了,不过在下篇文章里我会把文件缓存的核心提取出一个简化版本以便大家纠一下错。这里只介绍本次优化中做了些什么事,
#资源的释放。
在检查代码的过程中发现计算MD5后竟后没有清理资源,在计算后须要调用一下Clear()操作。
{
byte[] buf = md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(k));
x = BitConverter.ToString(buf).Replace("-", string.Empty);
md5.Clear();
}
还发现原来XML序列化的时候竟然没有正确关闭,这量修正了这样的错误:
{
string str = null;
using (MemoryStream stream = new MemoryStream())
{
using (XmlTextWriter xmlWriter = new XmlTextWriter(stream, Encoding.UTF8))
{
XmlSerializer serializer = new XmlSerializer(objectToConvert.GetType());
serializer.Serialize(xmlWriter, objectToConvert);
byte[] buffer = new byte[stream.Length];
stream.Position = 0;
stream.Read(buffer, 0, (int)stream.Length);
str = Encoding.UTF8.GetString(buffer);
xmlWriter.Close();
}
stream.Close();
}
return str;
}
private static object ConvertToObject(string xml, Type objectType)
{
if (objectType == null)
return null;
object obj = null;
try
{
byte[] buffer = Encoding.UTF8.GetBytes(xml);
using (MemoryStream stream = new MemoryStream(buffer, 0, buffer.Length))
{
stream.Position = 0;
using (XmlTextReader reader = new XmlTextReader(stream))
{
XmlSerializer serializer = new XmlSerializer(objectType);
obj = serializer.Deserialize(reader);
reader.Close();
}
stream.Close();
}
}
catch { }
return obj;
}
#锁的问题是这样的,原来组件中是没有锁的,这样会导致资源争用的问题,后来我就加上了ReaderWriterLock这样的读写锁,这样资源争用的问题就基本解决了,在这次的优化中查看了一下FX3.5出来了一个新的读写锁ReaderWriterLockSlim,性能明显优于前者,这当然是优化的首选之一了,因为任何读写都会与锁发生关系的,所以调用量惊人的大,只要有一点性能提高,放大后都是巨大的。ReaderWriterLockSlim要修改的地方比较多,这里的就不列举了,可以查看前面的链接,或者文件缓存简易版。
#改善Get方法的逻辑,减少IO读取次数。
改动的地方不大,但是每次读缓存时减少了一次IO,这样对IO的改善就很大了。这里讲一下一个细节就是在判断缓存有没有过期的时候需要知道文件的最后更新时间,我们可以通过File.GetLastWriteTime(fileName)方法获取到,但是如果fileName不存在的话就会返回"1601/1/1 8:00:00"这样的时间值,所以可以通过if (dt == null || dt.Value.Year == 1601)来判断缓存是否已经存在了,这样就减少了一次的IO读取。
3、缓存默认头像。
头像服务中原来是通过 Response.WriteFile(Server.MapPath(fileName)) 方法从文件系统中读取头像然后显示出来。但是目前的情况是有头像的用户实际上是少数的,大部分的用户都是没有上传头像的,而且默认头像也就只有5个,所以如果每次访问默认头像都要读取一个IO就实在有点浪费。所以改用Response.BinaryWrite方法来从byte[]中读取显示头像,并且对头像的数据进行内存缓存,这样IO读取量就会大大减少了。下面是修改后的代码:
{
string url = Request.Url.ToString();
string mime = "image/x-icon";
string fileName = "noimg_default.ico";
if (!url.EndsWith(".ico"))
{
mime = "image/jpg";
fileName = string.Format("noimg_default_{0}.jpg", url.Substring(url.LastIndexOf('/') + 1, 1));
}
//Response.Buffer = true;
//Response.Clear();
Response.BufferOutput = true;//将服务器创建的响应进行缓存
Response.AddHeader("Content-Type", mime);
string filePath = Server.MapPath(fileName);
if (filePath.IndexOf("noimg_default.ico") > -1 ||
filePath.IndexOf("noimg_default_1.jpg") > -1 ||
filePath.IndexOf("noimg_default_2.jpg") > -1 ||
filePath.IndexOf("noimg_default_3.jpg") > -1 ||
filePath.IndexOf("noimg_default_4.jpg") > -1)
{
//Response.WriteFile(Server.MapPath(fileName));
string cacheKey = filePath;
byte[] bytes = Cache.Get(cacheKey) as byte[];
if (bytes == null)
{
bytes = GetImageBinary(filePath);
Cache.Insert(cacheKey, bytes, null, DateTime.MaxValue, TimeSpan.Zero,
CacheItemPriority.High, null);
}
Response.BinaryWrite(bytes);
}
//Response.Flush();
Response.End();
}
private byte[] GetImageBinary(string filePath)
{
byte[] bytes = null;
using (Stream stream = new FileStream(filePath, FileMode.Open,
FileAccess.Read, FileShare.ReadWrite))
{
using (BinaryReader br = new BinaryReader(stream))
{
for (Int64 x = 0; x < (br.BaseStream.Length / 10000 + 1); x++)
{
bytes = br.ReadBytes(10000);
}
br.Close();
}
stream.Close();
}
return bytes;
}
注意:
# 这里不配置Response.Flush();否则可能会出现错误"0x80072746"
#提高Web园的数目到4个,以提高该应用程序池处理请求的性能。
4、排除异常捕捉组件捕获的异常。
5、垃圾收集器的类型有两种,即工作站和服务器。选择不同的垃圾引集器类型对性能有一定的影响,默认选项为工作站形式,通过处改配置文件可以指定不同的收集器类型,对于多核服务器来说,需要修改成以服务器垃圾收集器的形式进行工作,最大限度的提高垃圾收集的效率。
<gcServer enabled="true"/>
</runtime>
如何判断当前垃圾收集器是否为服务器类型,可以通过调用GCSettings.IsServerGC来判断
后续:
#HttpWebRequest潜在的问题。
在用户展示页面中,用排除法发现一处使用HttpWebRquest调用内容服务的请求可能会存在问题。经过单独提取这段调用,然后使用多线程大量请求此调用的页面后发现在内存会不断快速增长,但很难有大量的回落,结果最后达到应用程序池的内存阀值后导置回收。从现象来看,GC对于这样的调用所占用的资源可能不能很好的进行垃圾回收,或者这样的调用存在非托管资源的占用,所以对此进行了优化,并且使用了超时设置,如下:
request.Timeout = 1000;
string content = null;
try
{
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
if (response.StatusCode == HttpStatusCode.OK)
{
using (Stream stream = response.GetResponseStream())
{
using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
{
content = reader.ReadToEnd();
reader.Close();
}
stream.Close();
}
}
response.Close();
}
}
catch (System.Net.WebException)
{
}
finally
{
if (request != null)
{
request.Abort();
request = null;
}
}
这样的调用应该做到了足够的资源释放措施,但是事实上并没有很好的解决刚才提到的问题,资源还是会不断增长。
当然这样的增长可能也是正常的,不过过快地进行应用程序回收并不是一个很好方法,所以可以考虑把修改对内容服务的调用方式。
内容服务:它提供一个公共展示内容(html或者模板)的获取点,但它的内容可以来源于不同的数据源,调用方可以根据自已的需要获取自已页面所需要内容项,如果所请求的内容项在内容服务中不存在,则会自动根据配置文件从数据源(隐藏的)中自动获取并进行缓存。内容服务的更新有两种方式,一种是拉的方式,就是刚才提到的根据配置文件向数据源接口自动提取;一种是推的方式,数据源方如果有数据更新会通过MSMQ的形式传递给内容服务。通过这两种方式可以保正内容服务的内容的时效性,并且对于内容服务来说,它也是一个经过优化的统一的缓存服务,能分散IO流量。具体内容服务的框架设计会在以后的文章中详细说明。
内容服务使用WCF(MSMQ+SVC)技术,但是在内容获取上为了兼容不同平台的调用需要使用了ashx页面返回JSON内容的方式,所以就出现了刚才使用HttpWebRequest来请求内容服务的问题,其实从调用性能角度来看完全可以把这样调用也通过WCF(TCP)来提供调用接口,这样既可以提高调用性能,也可能会解决内存增长过快的问题。
#StringTemplate存在的资源泄漏,我们使用的是v3.1b1版本,查看原码发觉StringWriter并没有正常关闭和释放资源,这里修正原代码,以便释放占用的资源
\Antlr.StringTemplate.Language\ActionEvaluator.cs
\Antlr.StringTemplate\StringTemplate.cs
\Antlr.StringTemplate.Language\ASTExpr.cs