OutOfMemoryException异常解析
- 2021 年 10 月 19 日
- 筆記
- .NET, c#, OutOfMemoryException, 崩溃
一、概述
在国庆休假快结束的最后一天晚上接到了部门老大的电话,某省的服务会出现崩溃问题。需要赶紧修复,没错这次的主角依旧是上次的“远古项目”没有办法同事都在休假没有人能帮忙开电脑远程只能打车去公司。远程链接上服务器之后查看日志发现抛出的堆栈异常信息中包含了这样一句话“OutOfMemoryException”,在A.dll中。
这个“远古项目”大致情况如下:
- 1.框架版本为.net framework 4
- 2.代码结构混乱
- 3.需要通过socket连接大量的物理设备采集数据(1000台左右)
- 4.每小时采集一次,并由“远古项目”接收并转发
二、问题分析
(1)根据日志定位问题
其实日志中能给定的错误信息有限,但是也有很大帮助起码知道问题出在哪一块。这时候直接找到A类库查看源码,这时候发现项目当中这一块代码非常多大约1000行左右,这么多代码到底哪一句出了问题不得而知。同时如果我想复现的话并不能有那么多的设备去模拟测试。这时候其实是有点晕的,这时候只能硬着头皮把“OutOfMemoryException”这个异常拿去google一样,结果发现是线程方面的内存溢出问题。那么这时候缩小了查看代码的范围,就开始在代码中搜索Thread对象的使用。查看了半天果然有发现,看到了如下一段代码:
//...代码上下文
byte[] sendbytes = new byte[] { xxx };
var thread = new Thread((_) =>
{
Send(sendbytes);
});
thread.Start();
//...代码上下文
这段代码存在的地方大概是,在所有设备在每小时采集一次数据的时候会集中在某一个时间段里大量的发送数据若干次,且每台设备每次发送数据的时候都会创建线程去发送数据。看到这一段代码的时候我人都麻了。
(2)根据问题代码继续分析
在程序开发中,创建线程的代价是非常高昂的。而且都集中在一个时间点上去频繁创建线程这样的代码肯定不行。这段代码极有可能就是引发这个异常的原因之一。分析到这里突然想起之前看过的一本书,书中描述了这样一段话:
“线程栈往往都很小。windows上默认情况下栈最大为1MB,并且大多数线程通常只使用很少的栈页(stack page)。….”(书名:.NET性能分析)
基于以上理论增加了那段代码引起崩溃的可能性怀疑。那么只能抱着试一试的心态继续往下做。
三、解决方案
那么发现了可能导致异常的代码如何去解决呢?这时候又有点头疼了,因为我暂时能想到的解是:
Answer:利用生产消费者模式建立发送队列,然后开启一个常驻的发送线程慢慢发就可以了。
但是问题来了,“远古项目”中结构太过混乱牵一发动全身的连最简单的这种思路实现都会有很多阻碍。而且框架版本过于低一些新语法特性也不能使用。
这个时候在想如果想解决这个问题应该要死扣住以下几点:
- 1.不能频繁创建线程
- 2.不能对代码有过大的改动
- 3.对线程创建以及数量要有良好的控制
- 4.不能考虑使用新语法特性
ThreadPool这个对象不是刚好满足这个情况吗,这时候将代码修改为:
byte[] sendbytes = new byte[] { 0 };
ThreadPool.QueueUserWorkItem(_=>
{
Send(sendbytes);
});
其实实现这一段代码之后,心里依旧没有解决问题的喜悦。因为根据线程池的原理,如果任务量过大的话还是会开辟默认线程数量以外的新线程。但是线程池对线程管理比较好,这样的该的结果就能直接提交代码重新去服务器上部署吗?心里还是没有底我又做了如下测试:
//模拟在同一个时间点内大量开启线程模拟多设备发送数据
for (int i = 0; i < 10000; i++)
{
var thread = new Thread((_) =>
{
//模拟发送数据耗时
Thread.Sleep(1000);
});
thread.Start();
}
观察VS内存监测变化。
发现在大量创建线程的时候CPU和内存占用会陡增。那么接下来我在试一试用线程池去执行这些操作会是一个什么情况代码修改如下:
for (int i = 0; i < 100000; i++)
{
ThreadPool.QueueUserWorkItem(_=>
{
Thread.Sleep(1000);
});
}
继续观察VS内存监测变化。
发现几乎没有任何波澜,看到这个测试结果只能说感谢微软实现了一个如此优秀的线程池。这个时候由于时间紧迫只能先改一版本拿到服务器上去顶一阵肯定比上一个版本要好。
问题又来了如果再继续出现问题怎么继续排查?下一次不一定能抛出更有用的信息。这个时候想到的解决方案如下:
- 1.添加DUMP文件输出
- 2.关键敏感地方加强日志信息详细程度和适量try块捕获异常
到此耗时大约3小时左右,编译好版本部署到服务器上再做观察。就这样观察了一个多星期没有再次出现崩溃异常。其实分析下来,发现对这个问题发生原理可能还没有玩明白需要继续研究。