每周源代码35- Zip压缩ASP.NET会话及缓存状态
[原文发表地址] The Weekly Source Code 35 - Zip Compressing ASP.NET Session and Cache State
[原文发表时间] 2008-10-22 15:21
最近和Jeff Atwood以及他的开发团队讨论堆栈溢出的时候,他提到他压缩了ASP.NET里的缓存和会话数据,这使它可以存储5-10倍多的数据。他们用一些辅助方法完成了压缩,我觉得我自己尝试一下可能会更好玩。
关于怎么实现这个构想,有很多选择和思路,也有很多准要求:
• 我可以创建我自己的会话状态模型,基本上完全替代默认的对话状态机制。
• 我可以创建一些HttpSessionState的扩展方法。
• 我也可以使用辅助方法,但这就需要我每次在进入和退出的时候记得使用它们,日复一日的这么做,不是我的风格。不过,这个方法的好处是这么做非常非常简单,无论什么时候我可以打包任何我想打包的东西,并能把它放在任何地方。
• 我不想一不小心把某些东西给打包了,又把它拿出来解压。我想避免冲突。
• 我主要考虑的是储存字符串(读取: 尖括号),而不是二进制序列和目标的打包问题。
• 我想把打包了的东西放进对话,应用和缓存中。我觉得这是最重要的要求。直到我开始写外围代码之后才意识到这个问题。基本上,TDD,在现实网站中使用的是虚拟的库。
我错误的开始
我最初认为我希望它是这样运行的:
1: Session.ZippedItems["foo"] = someLargeThing;
2: someLargeThing = Session.ZippedItems["foo"]; //string is implied
但是你不能扩展属性(而不是扩展方法),或者超载运算符。
然后我想试试这么做:
1: public static class ZipSessionExtension
2: {
3: public static object GetZipItem(this HttpSessionState s, string key)
4: {
5: //go go go
6: }
7: }
到处都是GetThis和SetThat,但这感觉上也不对。
它应该如何运行
当我重新考虑那些要求时,我就把上述的都去掉了。我意识到我想它这样运行:
1: Session["foo"] = "Session: this is a test of the emergency broadcast system.";
2: Zip.Session["foo"] = "ZipSession this is a test of the emergency broadcast system.";
3: string zipsession = Zip.Session["foo"];
4: Cache["foo"] = "Cache: this is a test of the emergency broadcast system.";
5: Zip.Cache["foo"] = "ZipCache: this is a test of the emergency broadcast system.";
6: string zipfoo = Zip.Cache["foo"];
我意识到它应该如何运行,于是把它们写了下来。我用了一些很有趣的东西,也重新学了一些。
我最初希望那些属性是有索引的属性,也想让他们属于"Zip."类型且有自动感应能力。我将类命名为Zip,设为不变。有两个不变的属性,分别是对话(Session)和缓存(Cache)。他们有各自的索引,使得Zip.Session[""]和Zip.Cache[""]运行。我把"zip"放到前面以避免与没有压缩的内容发生冲突,这样就好像有种假象有两个不同的地方。
1: using System.IO;
2: using System.IO.Compression;
3: using System.Diagnostics;
4: using System.Web;
5: namespace HanselZip
6: {
7: public static class Zip
8: {
9: public static readonly ZipSessionInternal Session = new ZipSessionInternal();
10: public static readonly ZipCacheInternal Cache = new ZipCacheInternal();
11: public class ZipSessionInternal
12: {
13: public string this[string index]
14: {
15: get
16: {
17: return GZipHelpers.DeCompress(HttpContext.Current.Session["zip" + index] as byte[]);
18: }
19: set
20: {
21: HttpContext.Current.Session["zip" + index] = GZipHelpers.Compress(value);
22: }
23: }
24: }
25: public class ZipCacheInternal
26: {
27: public string this[string index]
28: {
29: get
30: {
31: return GZipHelpers.DeCompress(HttpContext.Current.Cache["zip" + index] as byte[]);
32: }
33: set
34: {
35: HttpContext.Current.Cache["zip" + index] = GZipHelpers.Compress(value);
36: }
37: }
38: }
39: public static class GZipHelpers
40: {
41: public static string DeCompress(byte[] unsquishMe)
42: {
43: using (MemoryStream mem = new MemoryStream(unsquishMe))
44: {
45: using (GZipStream gz = new GZipStream(mem, CompressionMode.Decompress))
46: {
47: var sr = new StreamReader(gz);
48: return sr.ReadToEnd();
49: }
50: }
51: }
52: public static byte[] Compress(string squishMe)
53: {
54: Trace.WriteLine("GZipHelper: Size In: " + squishMe.Length);
55: byte[] compressedBuffer = null;
56: using (MemoryStream stream = new MemoryStream())
57: {
58: using (GZipStream zip = new GZipStream(stream, CompressionMode.Compress))
59: {
60: using (StreamWriter sw = new StreamWriter(zip))
61: {
62: sw.Write(squishMe);
63: }
64: //Dont get the MemoryStream data before the GZipStream is closed since it doesn’t yet contain complete compressed data.
65: //GZipStream writes additional data including footer information when its been disposed
66: }
67: compressedBuffer = stream.ToArray();
68: Trace.WriteLine("GZipHelper: Size Out:" + compressedBuffer.Length);
69: }
70: return compressedBuffer;
71: }
72: }
73: }
74: }
注意。如果你放入的字符串短于300字节,它们可能会变大。所以,尽量在字符串大于半K的时候使用压缩。通常在你有几K或者更大的时候会用到它。我觉得这对缓存大块的HTML比较有用。
作为一个与此开发团队无关的人我用Trace.WriteLine来显示压缩前和压缩后的大小。然后,在web.config中我加入了 跟踪侦听器,确保我外部数据集中的跟踪输出在ASP.NET 跟踪器中出现:
1: <system.diagnostics>
2: <trace>
3: <listeners>
4: <add name="WebPageTraceListener"
5: type="System.Web.WebPageTraceListener, System.Web,
6: Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
7: </listeners>
8: </trace>
9: </system.diagnostics>
10: <system.web>
11: <trace pageOutput="true" writeToDiagnosticsTrace="true" enabled="true"/>
12: ...
13:
追踪输出的结果是这样的:
何时压缩
看看第一个被打包的字符串是怎么变大的?我不该把它放进去的,它原始状态太小了。第二个则从1137个字节缩到了186个字节,这个很有效。再来,生存几率,这个一般没什么,除非你在缓存中存了上千的字符串,超过1k,如果你的字符串接近10k或更多,我建议你修改下值。
比如,如果我把HTML的17K放进缓存,它压缩了3756个字节,节省了78%。这些取决于标记的重复率以及你有多少访问者。如果你同时有一千名来访者,而你在缓存,比如说,每100K中20个块,乘上1000个用户,你的缓存就使用了244兆。我在银行工作时,我们同时在线的用户可能会有成千上万,我们会缓存历史记录或者保存数据,那些数据可能会达到500K或者更多的XML。4个存储账户乘以7万用户乘以XML历史的500K,那就是RAM的16千兆(在众多服务器中,每个服务器大概有一千兆或者半千兆)。把500K压缩到70K就能减少达到总共2千兆的可能。这都取决于你存储了多少,它能压缩多少(有多少重复率)以及它如何访问。
高性能 2里包含了setCompressThreshold,可以让你在它压缩前调整你想要的最小存储值。我想Velocity也会有一些相似的设置。
最后,如果你不测算,那这一切都等于零。比方说,如果一些数据经常被读取或者写入,那这样所有的内存的节省就失效了。你可能把节省空间的问题转换成了其他潜在的问题,比如保证未压缩的值和内存碎片的内存量。关键是,在没测算的情况下不要随意地启用这个压缩。
Nate Davis对计算器有一段很经典的评论,在这里我想和大家分享一下:
如果他们以这种方式缓存最终的输出页,并且已经以zip格式从网路上发送了HTTP报告,只要在从缓存中取出页面时不是首次解压,而且之后在发送报告前重新打包好,那么打包输出缓存将会非常有意义。
如果他们去检查'Accept-Encoding'头确保 zip可被支持,那么他们可以直接把打包好的输出缓存发送到报告流,把编码器标题设置为'zip'。如果Accept-Encoding不包括zip,那么缓存就必须被解压,不过这在浏览器中概率很小。
Nate指出的是你应该想好这些数据会怎么用,你是想缓存一整张页面或IFrame吗?如果是这样,你可以打包发送出去再完整回归,对浏览器来说仍是压缩状态。
不过,如果你用IIS7,你想缓存一整张页面而不是单独的用户碎片,那你考虑使用IIS7 的动态压缩吧。你符合这些特征,和ASP.NET的OutputCaching一起,系统会知道zip压缩的。它会存储打包后的版本,然后直接处理它。
我的结论?
• 在内存不足的情况下,如果我要储存大量字符串碎片,我会使用这个工具。
• 在Web服务器层面,我经常开着压缩。这解决的是另一个不同的问题,但没有理由让你的web服务器处理未经压缩的内容。不使用压缩简直是种浪费。
你的想法呢?遗漏了什么?有用吗?还是没有用?