[转载]项目优化经验——垃圾回收导致的性能问题

[转载]项目优化经验——垃圾回收导致的性能问题 – lovecindywang – 博客园.

谈谈最近优化一个网站项目的经验,首先说一下背景情况:

1) 在页面后台代码中我们把页面上大部分的HTML都使用字符串来拼接生成然后直接赋值给LiteralControl。

2) 网站CPU很高,基本都在80%左右,即使使用了StringBuilder来拼接字符串性能也不理想。

3) 为了改善性能,把整个字符串保存在memcached中,性能还是不理想。

在比较了这个网站和其它网站服务器上相关性能监视器指标后发 现有一个参数特别显眼:

image

就是其中的每秒分配字节数,这个性能比较差的网 站每秒分配2GB的内存(而且需要注意由于性能监视器是每秒更新一下,对于一个非常健康的网站这个值应该经常看到是0才对)!而其它一些网站只分配 200M左右的内存。服务器配备4G内存,而每秒分配2G内存,我想垃圾回收器一定需要不断运行来回收这些内存。观察%Time in GC可以发现,这个值一直在10%左右,也就是说上次回收到这次回收间隔10秒的话,这次垃圾回收1秒,由于回收的时间相对固定,那么这个值可以反映回收 的频繁度。

知道了这个要点就知道了方向,在项目中找可能的问题点:

1) 是否分配了大量临时的小对象

2) 是否分配了数量不多但比较大的大对象

在经历了一番查找之后,发现一个比较大的问题,虽然使用了memcached来缓存整个页面的 HTML,但是在输出之前居然进行了几次string的Replace操作,这样就产生了几个大的字符串,我们来做一个实验模拟这种场景:

public partial class _Default : System.Web.UI.Page
{
    static string template;
    protected void Page_Load(object sender, EventArgs e)
    {
        if (template == null)
        {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 10000; i++)
                sb.Append("1234567890");
            template = sb.ToString(); 
        }

        Stopwatch sw = Stopwatch.StartNew();

        for (int i = 0; i < 1; i++)
        {
            long mem1 = GC.GetTotalMemory(false);
            string s = template + i;
            long mem2 = GC.GetTotalMemory(false);
            Response.Write((mem2 - mem1).ToString("N0"));
            Response.Write("<br/>");
            GC.KeepAlive(s);
        }

        for (int i = 0; i < 100000; i++)
        {
            double d = Math.Sqrt(i);
        }

        Thread.Sleep(30);
        Response.Write(sw.ElapsedMilliseconds);
    }
}

在这段代码中:

1) 我们首先使用一个静态变量模拟缓存中的待输出的HTML

2) 我们中间的一段代码测算一下这个字符串占用的内存空间

3) 随后我们做了一些消耗CPU的运算操作来模拟页面的一些计算

4) 然后休眠一段时间

4) 最后我们输出了页面执行时间

我们这么做的目的是模拟一个比较“正常的”ASP.NET页面需要做的一些工作:

1) 内存上的分配

2) 一些计算

3) 涉及到IO访问的一些等待

来看看输出结果:

image

这里可以看到,我们这个字符串占用差不多200K的字节,字符串是字符数组,CLR中字符采用Unicode双字节存储,因此10万长度的字符串占 用200千字节,并且也可以看到这个页面执行时间30毫秒,差不多是一个正常aspx页面的时间,而200K不到的字符串也差不多相当于这个页面的 HTML片段,现在我们来改一下其中的一段代码模拟优化前进行的Replace操作带来的几个大字符串:

for (int i = 0; i < 10; i++)
{
    //long mem1 = GC.GetTotalMemory(false);
    string s = template + i;
    //long mem2 = GC.GetTotalMemory(false);
    //Response.Write((mem2 - mem1).ToString("N0"));
    //Response.Write("<br/>");
    //GC.KeepAlive(s);
}

然后使用IDE自带压力测试1000常量用户来测试这个页面:

image

可以看到每秒分配了超过400M字节(这和我们线上环境比还差点毕竟请求少),CPU占用基本在120-160左右(双核),我们去掉每秒分配内存 这个数值,来看看垃圾回收频率和CPU占用两个值的图表:

image

可以看到红色的CPU波动基本和蓝色的垃圾回收波动保持一致(这里不太准确的另外一个原因是压力测试客户端运行于本机,而为w3wp关联2个处理 器)!为什么说垃圾回收会带来CPU的波动,从理论上来说有以下原因:

1) 垃圾回收的时候会暂时挂起所有线程,然后GC会检测扫描每一个线程栈上可回收对象,然后会移动对象,并且重新设置对象指针,这整个过程首先是消耗CPU的

2) 而且在这个过程之后恢复线程执行,这个时候CPU往往会引起一个高峰因为已经有更多的请求等待了

我们把Math.Sqrt这段代码注释掉并且把w3wp和VSTestHost关联到不同的处理器来看看对于CPU计算很少的页面,上图更明显的对 比:

image

这说明垃圾回收的确会占用很多CPU资源,但这只是一部分,其实我觉得网站的CPU压力来自于几个地方:

1) 就是大量的内存分配带来的垃圾回收所占用的CPU,对于ASP.NET框架内部的很多行为无法控制,但是可以在代码中尽量避免在堆上产生很多不必要的对象

2) 是实际的CPU运算,不涉及IO的运算,这些可以通过改良算法来优化,但是优化比较有限

3) 是IO操作这块,数据量的多少很关键,还有要考虑memcached等外部缓存对象序列化反序列化的消耗

4) 虽然很多IO操作不占用CPU资源,线程处于休眠状态,但是很多时候其实是依托新线程进行的,带来的就是线程切换和线程创建消耗的消耗,这一块可以通过合 理使用多线程来优化

发现了这个问题之后优化就很简单了,把Replace操作放到memcached的Set操作之前,取出之后不产生过多大字符串,把for循环改为 一次,再来看一下:

image

image

这次内存分配明显少了很多,CPU降下来了,降的不多,但从压力测试监视器中看到页面执行平均时间从5秒变为3秒了,每秒平均请求数从170到了 200(最高从200到了300)。在这里要说明一点很多时候网站的性能优化不能光看CPU还要对比优化前后网站的负载,因为在优化之后页面执行时间降低 了,负载量就增大了CPU消耗也随之增大。并且可以看到垃圾回收频率的缩短很明显,从长期在30%到几十秒一次30%。

最后想补充几点:

1) 有的时候我们会使用GC.GetTotalMemory(true); 来得到垃圾回收之后内存分配数,类似这样涉及到垃圾回收的代码在项目上线后千万不能出现,否则很可能会% Time in GC达到80%以上大量占用CPU。

2) 对于放在缓存中的对象我们往往会觉得性能得到保障大量去使用,其实缓存实现的只是把创造这个对象过程的时间转化为空间,而在拿到这个对象之后再进行很多运 算带来的大量空间始终会进行垃圾回收。做网站和做应用程序不一样,一个操作如果申请200K堆内存,一个页面执行这个操作10次,一秒200多个请求,大 家可以自己算一下平均每秒需要分配多少内存,这个数值是相当可怕的,网站是一个多线程的环境,我们对内存的使用要考虑更多。

作者:lovecindywang
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏