注:本文仅针对 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>
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
2 {
3 if (!applyMemCached)
4 {
5 //检查并移除相应的缓存项
6 instance = CachesFileMonitor.CheckAndRemoveCache(instance); // 这个方法里持有一个锁
7 }
8 }
看看这个方法 CachesFileMonitor.CheckAndRemoveCache
1 /// <summary>
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 方法: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:
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()持有
|
|
如果刚好有一种情况持有所的顺序跟上面相反,比如持有顺序 lockHelper —— cachelockHelper —— lockHelper ,而且这两种情况同时发生了,那死锁就这样产生了,那有没有这样的情况?有!
我们来看看 Discuz.Cache.DNTCache.cs 的 GetCacheService():
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是如何登场的,敬请期待下回分解。