注:本文仅针对 DiscuzNT3.0, sqlserver 2000版本,其他版本请勿对号入座.
经过前面的几次优化之后我们的论坛终于稳定了一段时间,大概半年之后我们的论坛迎来了每天大约50万的pv,这时候论坛有开始出现了问题。症状是这样的:
管理员发现,网站经常会打不开, 但是也不报错,好像永远一直在打开,直到浏览器认为它打不开了,这样的症状每天会出现几次,而且越来越频繁。每次发生这样的情况过后一般iis的事件查看器都会asp.net有死锁提示,于是我知道,我终于遇上传说中的死锁了,每次有死锁迹象的时候我都跟踪了一下sqlserver,发现数据库是正常的,那看来就是asp.net这边的问题了。
可是DiscuzNT这么大的一个论坛,里面包含了十几个项目,项目如此之多,代码量如此之大,到底哪里出了问题呢,一下子还真不好定位。还好微软给我们提供了两个很不错的工具,windbg 和 IIS Diagnostics,winddbg是用来调试内存的工具,而IIS Diagnostics则是抓取内存的好工具,我也正是借助这两个工具才快速定位到了问题,不过很遗憾的是我抓取的dump文件由于时间太久,竟然找不到了,所以现在暂时无法一展它们的风采。(不过后续会介绍windbg的用法,因为它真的帮了我大忙)
那到底是哪里引发的死锁呢,废话不多说,看看下面的代码就知道了,Discuz.Cache.DNTCache.cs 类文件
1 /// <summary>
2
///
构造函数
3
///
</summary>
4
private
DNTCache()
5
{
6
if
(MemCachedConfigs.GetConfig()
!=
null
&&
MemCachedConfigs.GetConfig().ApplyMemCached)
7
applyMemCached
=
true
;
8
9
if
(applyMemCached)
10
cs
=
new
MemCachedStrategy();
11
else
12
{
13
cs
=
new
DefaultCacheStrategy();
14
15
objectXmlMap
=
rootXml.CreateElement(
"
Cache
"
);
16
//
建立内部XML文档.
17
rootXml.AppendChild(objectXmlMap);
18
19
//
LogVisitor clv = new CacheLogVisitor();
20
//
cs.Accept(clv);
21
22
cacheConfigTimer.AutoReset
=
true
;
23
cacheConfigTimer.Enabled
=
true
;
24
cacheConfigTimer.Elapsed
+=
new
System.Timers.ElapsedEventHandler(
Timer_Elapsed
);
// 重点看下这个方法
25
cacheConfigTimer.Start();
26
}
27
}
看下这个方法 Timer_Elapsed
1
private
static
void
Timer_Elapsed(
object
sender, System.Timers.ElapsedEventArgs e)
2
{
3
if
(
!
applyMemCached)
4
{
5
//
检查并移除相应的缓存项
6
instance
=
CachesFileMonitor.CheckAndRemoveCache
(instance);
// 这个方法里持有一个锁
7 }
8 }
看看这个方法 CachesFileMonitor.CheckAndRemoveCache
1 /// <summary>
2
///
检查和移除缓存
3
///
</summary>
4
///
<param name="instance"></param>
5
///
<returns></returns>
6
public
static
DNTCache CheckAndRemoveCache(DNTCache instance)
//
7
{
8
//
当程序运行中cache.config发生变化时则对缓存对象做删除的操作
9
cachefilenewchange
=
System.IO.File.GetLastWriteTime(path);
10
if
(cachefileoldchange
!=
cachefilenewchange)
11
{
12
lock
(cachelockHelper)
13
{
14
if
(cachefileoldchange
!=
cachefilenewchange)
15
{
16
//
当有要清除的项时
17
DataSet dsSrc
=
new
DataSet();
18
dsSrc.ReadXml(path);
19
foreach
(DataRow dr
in
dsSrc.Tables[
0
].Rows)
20
{
21
if
(dr[
"
xpath
"
].ToString().Trim()
!=
""
)
22
{
23
DateTime removedatetime
=
DateTime.Now;
24
try
25
{
26
removedatetime
=
Convert.ToDateTime(dr[
"
removedatetime
"
].ToString().Trim());
27
}
28
catch
29
{
30
;
31
}
32
33
if
(removedatetime
>
cachefilenewchange.AddSeconds(
-
2
))
34
{
35
string
xpath
=
dr[
"
xpath
"
].ToString().Trim();
36
instance.RemoveObject
(xpath,
false
);
//
这个方法里持有第二个锁
37
}
38
}
39
}
40
41
cachefileoldchange
=
cachefilenewchange;
42
43
dsSrc.Dispose();
44
}
45
}
46
}
47
return
instance;
48
}
看看
RemoveObject 方法:
1
///
<summary>
2
///
通过指定的路径删除缓存中的对象
3
///
</summary>
4
///
<param name="xpath">
分级对象的路径
</param>
5
///
<param name="writeconfig">
是否写入文件
</param>
6
public
virtual
void
RemoveObject(
string
xpath,
bool
writeconfig)
7
{
8
lock
(lockHelper)
9
{
10
try
11
{
12
if
(applyMemCached)
13
{
14
//
移除相应的缓存项
15
cs.RemoveObject(xpath);
16
}
17
else
18
{
19
if
(writeconfig)
20
{
21
CachesFileMonitor.UpdateCacheItem
(xpath); // 这里再次持有锁
22
}
23
24
XmlNode result
=
objectXmlMap.SelectSingleNode(PrepareXpath(xpath));
25
//
检查路径是否指向一个组或一个被缓存的实例元素
26
if
(result.HasChildNodes)
27
{
28
//
删除所有对象和子结点的信息
29
XmlNodeList objects
=
result.SelectNodes(
"
*[@objectId]
"
);
30
string
objectId
=
""
;
31
foreach
(XmlNode node
in
objects)
32
{
33
objectId
=
node.Attributes[
"
objectId
"
].Value;
34
node.ParentNode.RemoveChild(node);
35
//
删除对象
36
cs.RemoveObject(objectId);
37
}
38
}
39
else
40
{
41
//
删除元素结点和相关的对象
42
string
objectId
=
result.Attributes[
"
objectId
"
].Value;
43
result.ParentNode.RemoveChild(result);
44
cs.RemoveObject(objectId);
45
}
46
}
47
48
}
49
catch
//
如出错误表明当前路径不存在
50
{}
51
52 }
53 }
再来看看方法UpdateCacheItem:
1
///
<summary>
2
///
更新或插入相应的缓存路径
3
///
</summary>
4
///
<param name="xpath"></param>
5
public
static
void
UpdateCacheItem(
string
xpath)
6
{
7
DataTable dt
=
new
DataTable(
"
cachetableremove
"
);
8
dt.Columns.Add(
"
xpath
"
, System.Type.GetType(
"
System.String
"
));
9
dt.Columns.Add(
"
removedatetime
"
, System.Type.GetType(
"
System.DateTime
"
));
10
11
//
当有要清除的项时
12
DataSet dsSrc
=
new
DataSet();
13
lock (cachelockHelper)
14
{
15
dsSrc.ReadXml(path);
16
17
bool
nohasone
=
true
;
18
foreach
(DataRow dr
in
dsSrc.Tables[
0
].Rows)
19
{
20
if
(dr[
"
xpath
"
].ToString().Trim()
==
xpath)
21
{
22
dr[
"
removedatetime
"
]
=
DateTime.Now.ToString();
23
nohasone
=
false
;
24
break
;
25
}
26
}
27
28
if
(nohasone)
29
{
30
DataRow dr
=
dsSrc.Tables[
0
].NewRow();
31
dr[
"
xpath
"
]
=
xpath;
32
dr[
"
removedatetime
"
]
=
DateTime.Now.ToString();
33
dsSrc.Tables[
0
].Rows.Add(dr);
34
}
35
36
dsSrc.WriteXml(path);
37
dsSrc.Dispose();
38 }
39 }
通过上面的代码的红字体部分我们可以看到,如果DNTCache 启动它的定时器,它将会顺序持有如下锁
cachelockHelper —— 》 CachesFileMonitor.CheckAndRemoveCache() 持有
|
|
lockHelper ——》 instance.RemoveObject()持有
|
|
cachelockHelper ——》 CachesFileMonitor.UpdateCacheItem() 持有
如果刚好有一种情况持有所的顺序跟上面相反,比如持有顺序 lockHelper —— cachelockHelper —— lockHelper ,而且这两种情况同时发生了,那死锁就这样产生了,那有没有这样的情况?有!
我们来看看 Discuz.Cache.DNTCache.cs 的 GetCacheService():
1
///
<summary>
2
///
单体模式返回当前类的实例
3
///
</summary>
4
///
<returns></returns>
5
public
static
DNTCache GetCacheService()
6
{
7
if
(instance
==
null
)
8
{
9
lock (lockHelper)
10
{
11
if
(instance
==
null
)
12
{
13
instance
=
applyMemCached
?
new
DNTCache() :
CachesFileMonitor.CheckAndRemoveCache(
new
DNTCache());
14
}
15
}
16
}
17
18
return instance;
19 }
看上面的 lock (lockHelper), 是不是很眼熟啊,对了,他刚好是上面第一种持有锁情况里面出现的第二个锁,只要这个
Discuz.Cache.DNTCache.
GetCacheService() 和 CachesFileMonitor.CheckAndRemoveCache() 同时被启动,那死锁就产生了,而
Discuz.Cache.DNTCache.
GetCacheService()是返回当前缓存的实例,可以说他时时刻刻都在被调用,你可以尝试搜索一下
Discuz.Cache.DNTCache.
GetCacheService(),你会发现他无处不在,当
Discuz.Cache.DNTCache.
GetCacheService() 和
Discuz.Cache.DNTCache.Timer_Elapsed() 同时发生,死锁也就产生了。
既然问题找到了,那该如何解决呢,我看了一下,这个
Discuz.Cache.DNTCache里面用到的lock作用就是为了保证唯一性,但是我发现若不是唯一好像也没什么影响,于是我把lock注释了,试运行一段时间之后,发现并没有什么影响,于是一直沿用至今。
本篇是本系列里针对DiscuzNT的c#代码做出优化的第一篇文章,比较遗憾的是第一大功臣windbg未能华丽登场,不过它以后还有机会。欲知windbg是如何登场的,敬请期待下回分解。