Share via


每周源代码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:  

追踪输出的结果是这样的:

image

何时压缩

看看第一个被打包的字符串是怎么变大的?我不该把它放进去的,它原始状态太小了。第二个则从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服务器处理未经压缩的内容。不使用压缩简直是种浪费。

你的想法呢?遗漏了什么?有用吗?还是没有用?