问题跟踪
上线的一个系统,运行后客户反馈系统越来越慢,通过查看日志,得到反馈信息是sql链接超时Begin failed with SQL exception ---> System.InvalidOperationException: 超时时间已到。超时时间已到,但是尚未从池中获取连接。出现这种情况可能是因为所有池连接均在使用,并且达到了最大池大小
。
sql链接超时一般情况是sql链接没有释放造成的,重启sql服务即可。但是,此次重启并没有解决问题,于是重启iis的应用池,程序访问正常。
由此也产生了疑问,莫非是程序发生了内存泄漏,导致连接不能释放?
程序运行两三天,客户又反馈系统越来越慢,此时感觉查看进程,发现该应用占用内存由启动的200多M到现在的10个G以上,因此也猜测,既有可能是程序又了内存泄漏。
那么,如何查出是那里泄漏呢?日志没有中没有记录,系统日志也查不到有关信息。
此时,偶然看到一篇文章,介绍如何查高内存占用的问题,介绍使用 WinDbg
工具,那么我们就来试试这个工具。
解决问题
两篇文章如下:
下载 winsdksetup.exe
双击,选择Debugging Tools for Windows安装
关于如何收集内存信息请参考文章1,windbg 基本使用命令可参考文章2。
当程序占用内存再一次高大8G的时候,我使用[ProcDump](https://docs.microsoft.com/zh-cn/sysinternals/downloads/procdump)
来抓取 dump,接下来就是使用 windbg 分析 dump。
打开windbg
选择菜单File > Open Crash Dump
,如下图
由于我们是net程序,所以需要进行如下操作,
选择菜单File > Symbol File Path
,指定symbol search path 设置符号服务器与符号缓存SRV*D:\symbols*http://msdl.microsoft.com/download/symbols
见下图
加载 net4.0以上版本的模块.loadby sos clr
,回车执行命令
分析问题
按照文章1的教程敲入命令:!dumpheap -stat
显示
BUSY
表示正在执行命令中,执行结果如下image.png
显现
String
类型占用 1G左右,而 UnitOfWorkManager
占用600M之多,UnitOfWorkManager
是管理一次数据库操作事务的工具类,此处占用这么多内存十分不合理,于是对000007fe9adaed38 16628281 665131240 xxxx.UnitOfWorkManager
进行跟踪
执行命令
!dumpheap -mt 000007fe9adaed38 -min 200
查看 unitofworkmanager 超过 200k的
执行结果
image.png
结果中没有任何信息,表示
unitofworkmanager
中没有超过200k的,那么我们再执行命令,!dumpheap -mt 000007fe9adaed38
去掉 200k的限制,会发现满屏都是40byte的 unitofwork
image.png
此时只能使用
Ctrl+Break
命令中止输出。中止输出如下
image.png
占用分析
既然猜测是内存没释放,我们使用!gcroot
命令来查看类型的引用对象
执行结果如下
image.png
陡然发现了Jobs.WindsorContainerJobActivator
,莫不是因为这里使用不当,导致hangfire中的job不能正常使用?
赶紧查看此处代码
源代码分析
WindsorContainerJobActivator
主要是为了hangfire中的job能使用windsor ioc
现在怀疑是由于job使用ioc获取了类型,但是没有释放。
于是又参考了 abp hangfire模块中的做法,发现的确代码中缺少了内容,完整代码如下
/// <summary>
/// Hangfire windsor ioc
/// </summary>
public class WindsorContainerJobActivator : JobActivator
{
/// <summary>
/// The _container.
/// </summary>
private readonly IIocResolver _iocResolver;
/// <summary>
/// Initializes a new instance of the <see cref="WindsorContainerJobActivator"/> class.
/// </summary>
/// <param name="container">
/// The container.
/// </param>
public WindsorContainerJobActivator(IIocResolver iocResolver)
{
this._iocResolver = iocResolver;
}
/// <summary>
/// The activate job.
/// </summary>
/// <param name="type">
/// The type.
/// </param>
/// <returns>
/// The <see cref="object"/>.
/// </returns>
public override object ActivateJob(Type type)
{
return _iocResolver.Resolve(type);
}
public override JobActivatorScope BeginScope(JobActivatorContext context)
{
return new HangfireIocJobActivatorScope(this, _iocResolver);
}
class HangfireIocJobActivatorScope : JobActivatorScope
{
private readonly JobActivator _activator;
private readonly IIocResolver _iocResolver;
private readonly List<object> _resolvedObjects;
public HangfireIocJobActivatorScope(JobActivator activator, IIocResolver iocResolver)
{
_activator = activator;
_iocResolver = iocResolver;
_resolvedObjects = new List<object>();
}
public override object Resolve(Type type)
{
var instance = _activator.ActivateJob(type);
_resolvedObjects.Add(instance);
return instance;
}
public override void DisposeScope()
{
_resolvedObjects.ForEach(_iocResolver.Release);
}
}
}
重写JobActivator
的BeginScope
方法,提供ioc的scope即可
总结
解决这个问题大约用了两周时间,主要是通过各种日志和清空,分析不到具体的原因,只能瞎猜,期间还用了两天时间去查询sql连接池的使用情况,没有发现任何问题,不幸中的万幸是看到了windbg工具。
一边摸索windbg的使用方式,一边跟踪代码,最终也是找到问题并解决。
在解决问题过程中,基本掌握了windbg分析问题的步骤:
- 抓取dump文件
- 分析dump文件(windbg + 命令)
- 大胆猜想
- 结合源代码
- 验证
其中也暴露出了知识缺陷,多次接触ioc,并了解scope,但是在实际编码中会自觉的屏蔽掉相关知识点,造成此次问题。
阅读官方文档要仔细,官方文档中推荐的Hangfire.Windsor没有仔细阅读,事后发现和abp hangfire中的解决方案如出一辙。
网友评论