老峰的博客
=技术 ?? new 技术()

导航

 

问题:在生产环境中,在高峰期某个运行WCF程序的机器定期响应变慢,连接数线程数直接上升。其他运行同一服务的机器表现正常,数据库响应正常,无明显锁。

分析:初步分析认为程序中有线程死锁问题。

诊断步骤:

1. 通过Windows 2008自带任务管理器

2. 在开发环境中用WinDbg打开dump文件。(确保WinDbg设置正确的Symbol路径)

3. 在WinDbg中载入SOS.DLL,并显示所有线程的堆栈

.loadby sos mscorwks -- 用.loadby sos clr替代如果你的程序运行在.net 4.0上
~*e!clrstack

4. 得到如下输出

从这里下载完整的线程堆栈列表 https://files.cnblogs.com/ivenxu/wcf_threads_stack.zip,这里大概有一半的线程处在System.Threading.WaitHandle.WaitOne()中,有人可能会怀疑这里有问题,其实这个是一个非常具有迷惑性的误导。这个是非常正常的,WCF在处理每个请求是会有两个线程:

The reason for this is the way WCF runtime handles the request. You will always see two threads to process the WCF request. One thread is the CLR ThreadPool thread (which will look something like the above thread) which is the worker thread that comes from ASP.NET and the other thread is an I/O thread that is managed by the WCF IOThreadScheduler (actually created by ThreadPool.UnsafeQueueNativeOverlapped). More details on this behavior are mentioned in this blog.” 参见http://blogs.msdn.com/b/distributedservices/archive/2011/01/11/finding-out-the-thread-which-is-actually-handling-the-wcf-request-for-the-httpcontext.aspx

排除了干扰因素再仔细分析每个线程,所有线程都处在Entriq.IBS.Core.Cache.LazyLoadCache.GetCachedObject()这个方法中,我们大概知道这里是hang的地方。

打开源代码:

/// <summary>
/// Template method to retrive cached objects in a thread safe manner.
/// </summary>
/// <param name="dsnCache">Cach repository</param>
/// <param name="key">cache key of item in repository</param>
/// <param name="sync_Lock">sync lock in case item needs to be loadeds</param>
/// <param name="loadCache">Method to call if item is not incache</param>
/// <param name="criteria">criteria for load</param>
/// <returns></returns>
private static T GetCachedObject<T, C>(Hashtable dsnCache, string key,
                  
object sync_Lock, CacheObjectLoadWithCriteria<T, C> loadCache,
                 C criteria, TimeSpan minTime, TimeSpan maxTime, CacheLevel cacheLevel)
{


CacheEntry
<T> o = GetCacheEntry<T>(dsnCache, key, sync_Lock);
if (o == null)
{
lock (sync_Lock)
{
o
= (CacheEntry<T>)dsnCache[key];
if (o == null)
{
o
= new CacheEntry<T>();
o.CachedObject
= loadCache(criteria);
o.MinTimeInCache
= minTime;
o.MaxTimeInCache
= maxTime;
o.Level
= cacheLevel;
dsnCache.Add(key, o);
}
}
}
return o.CachedObject;


}

在该方法中确实有一个lock,但是只lock一个对象没有形成线程死锁的经典情况。在这个lock的范围内有一个loadCache的委托,只有在这个函数中申请另一个锁才会有可能形成死锁的情况。因为是个委托,它可以是任何符合签名的方法,这个我们找出真正的问题又增加了难度。

从新回到线程的堆栈,仔细分析我们发现有两处非常奇怪

这两个线程的特点是分别两次调用Entriq.IBS.Core.Cache.LazyLoadCache.GetCachedObject()。这个两个就成最大嫌疑了。

打开源代码:

//在线程1中:
//在GetUpgradeDowngradeForCommercialProduct()主方法中调参入
//ProductCatalogConfigurationServiceCached.sync_lock作为锁定对象
//在loadCache委托函数中通过层层调用最后调用到
//IBSReadWriteCollectionBase.GetControlFieldDefinitionsForEntity()
//在此函数中,把LazyLoadCache.SyncLock作为锁定对象
public partial class ProductCatalogConfigurationServiceCached :
ProductCatalogConfigurationServiceDecorator
{

private static object sync_lock= new object();

.......
public override UpgradeDowngradeCollection GetUpgradeDowngradeForCommercialProduct(
      
int commercialProductId, int page)
{
CacheObjectLoad
<UpgradeDowngradeCollection>load = delegate(){
        
return _GetUpgradeDowngradeForCommercialProduct(commercialProductId, page);
        };
return LazyLoadCache.GetCachedObject<UpgradeDowngradeCollection>(
            
string.Format("GetUpgradeDowngradeForCommercialProduct_{0}_{1}",
            commercialProductId.ToString(),page.ToString()), sync_lock,load);

}

......
}


public abstract class IBSControlFieldCollectionBase<T> :
    IBSReadWriteCollectionBase
<T> where T : IBSControlFieldBase
{

......

private List<ControlFieldDefinition> GetControlFieldDefinitionsForEntity(int entityId)
{
return LazyLoadCache.GetCachedObject<List<ControlFieldDefinition>,int>(
        
string.Format("ControlFieldDefFor_{0}", entityId),
       LazyLoadCache.SyncLock, LoadControlFieldDefinitionsForEntity,entityId,
      CacheLevel.SchemaData);
}
......

}

//在线程2中:
//本身方法中调用GetCachedObject,传入LazyLoadCache.SyncLock作为锁对象,
//ServiceGateway.ProductCatalog.CachedConfig.GetProductActivationRules()作为loadCache委托

public class ProductActivationProcess
{
......
protected virtual ProductActivationRules GetActivationRules()
{
CacheObjectLoad
<ProductActivationRules> l =
()
=> ServiceGateway.ProductCatalog.CachedConfig.GetProductActivationRules();
return LazyLoadCache.GetCachedObject(typeof(ProductActivationRules).ToString(),
           LazyLoadCache.SyncLock, l);

}
......
}

//在loadCache委托中锁定单独的私有变量ProductCatalogConfigurationServiceCached.sync_lock。

public partial class ProductCatalogConfigurationServiceCached :
  ProductCatalogConfigurationServiceDecorator
{

private static object sync_lock= new object();

......

public override ProductActivationRules GetProductActivationRules()
{
return LazyLoadCache.GetCachedObject<ProductActivationRules>(
            
"GetProductActivationRules",
           sync_lock,
base.GetProductActivationRules);

}

......
}

以上代码形成了经典的死锁环:线程1首先申请ProductCatalogConfigurationServiceCached.sync_lock再申请LazyLoadCache.SyncLock,相反的在线程2中首先申请LazyLoadCache.SyncLock再申请ProductCatalogConfigurationServiceCached.sync_lock。

2011-05-31更新:

在一开始的时候,对Windbg的命令不是很熟,所有用~*e!clrstack列出所有的线程的堆栈,这个方法是非常低效的,也非常消耗体力的。在服务器环境下,在线程出现死锁的情况下,线程会越积越多,很可能会上千。人肉看这上千个线程是非常枯燥的。经过进步学习,我有更好的方法了。这里我们简化上面的步骤3为:

3.1 查看现有的syncblock

~*e!clrstack

得到的输出为:

在这里我们看到了有线程的ID分别为87,86和83对比我们原来步骤3中的线程号是吻合的。

3.2 通过~Ns线程好却换当前线程

3.3 通过!clrstack打印当前线程的堆栈

总结: 通过上面的!syncblk非常精准的定位到那几个线程获得了锁,这些就是最有可能死锁的线程。

posted on 2011-04-19 16:56  iVen Xu  阅读(1260)  评论(0编辑  收藏  举报