2011年7月22日
http://files.cnblogs.com/ivenxu/Windbg_SOS.pptx
2011年4月19日
问题:在生产环境中,在高峰期某个运行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. 得到如下输出

从这里下载完整的线程堆栈列表 http://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非常精准的定位到那几个线程获得了锁,这些就是最有可能死锁的线程。
2011年3月4日
背景:生产环境中升级最新版本后,系统中最重要的一个Windows 服务在某种情况下会异常退出。查看日志,只是模糊的说在一个Oracle DataAccess发生了异常。
开始动手吧。
1. 用adplus来捕获异常模式
adplus -crash -pn PayMedia.Background2.WindowsService.Billing.exe -o c:\crashdump
其中PayMedia.Background2.WindowsService.Billing.exe是Window Service的名字
2. 耐心等待系统崩溃.......
3. 获得Dump 文件,在开发机器用Windbg打开dump文件,
在Windbg中运行:
.loadby sos mscorwks -- 用.loadby sos clr替代如果你的程序运行在.net 4.0上
~*e!clrstack
如何使用Windbg命令,请详细参见http://support.microsoft.com/kb/892277
在这个个案中,获得了3个dump文件,这里就只截取其中有问题的一部分:

发现其中有一条“HandleOracleException”,堆栈往上看,马上我们代码中实际有问题的函数名字。
在原代码中快速查找,发现如下代码:
public void UpdateStatus(IBackgroundService backgroundService)
{
using (IDataAccess da = DataAccessFactory.CreateDataAccess("SYSTEMDATA", "SYSTEMDATA"))
{
if (backgroundService.Id == 0)
{
backgroundService.Id = GetServiceId(backgroundService, da); // scope
}
if (backgroundService.Id == 0)
{
InsertService(backgroundService, da); // scope
backgroundService.Id = GetServiceId(backgroundService, da);
}
else
{
UpdateService(backgroundService, da); // scope
}
}
}
private static void UpdateService(IBackgroundService backgroundService, IDataAccess da) // DataAccessScope scope
{
da.Parameters.Add("status", backgroundService.Status);
da.Parameters.Add("id", backgroundService.Id);
da.ExecuteNonQuery(SQL_UPDATE_SERVICE);
}
很明显我们看到代码没有处理任何异常,所以我们的错误信息没有很好的被log。如何处理异常是体外话,我们要找到问题的根源。我们快速的改一下代码把异常log下来。
public void UpdateStatus(IBackgroundService backgroundService)
{
try
{
using (IDataAccess da = DataAccessFactory.CreateDataAccess("SYSTEMDATA", "SYSTEMDATA"))
{
if (backgroundService.Id == 0)
{
backgroundService.Id = GetServiceId(backgroundService, da); // scope
}
if (backgroundService.Id == 0)
{
InsertService(backgroundService, da); // scope
backgroundService.Id = GetServiceId(backgroundService, da);
}
else
{
UpdateService(backgroundService, da); // scope
}
}
}
catch (Exception e)
{
Logger.Write(Logger.Category.Exception, e.Message, TraceEventType.Critical, e);
throw e;
}
}
替代生产环境中的程序,等待再次crash。很快我们在Event Log中发现了问题的本质线索:
Failed to execute non-query. (ORA-02290: check constraint (SYSTEMDATA.CK_BACKGROUND_SERVICE_7) violated) --> Failed to execute non-query. (ORA-02290: check constraint (SYSTEMDATA.CK_BACKGROUND_SERVICE_7) violated) --> ORA-02290: check constraint (SYSTEMDATA.CK_BACKGROUND_SERVICE_7) violated
继续查看这个数据的Constraint
ALTER TABLE SYSTEMDATA_EDB.BACKGROUND_SERVICE ADD (
CONSTRAINT CK_BACKGROUND_SERVICE_7
CHECK (REQUESTED_STATUS in (0,1,2)));
根据业务规则,这个Status是用来标识我们背景线程的状态的,在代码中有5中状态,可能由于更新不一致问题,数据库没有及时更新。
2011-05-31更新
在步骤3中用~*e!clrstack列出所有线程的堆栈是然后人肉看这些线程是非常无聊的,在这里我有另外一个更好的命令(真是不学累死人):
!pe
这样我们得到的知识有异常的线程堆栈

2011年3月1日
最近恶补算法的知识,写些常用的算法来练练手。先上源代码http://files.cnblogs.com/ivenxu/algorithm.zip,这个源代码包括了整个系列的代码,将不断更新。
有人在博客上做过推测,世界上只有10%的程序员能一次无错的把折半查找算法写出来。http://reprog.wordpress.com/2010/04/19/are-you-one-of-the-10-percent/ 这就是编程的魅力,非常简单东西做得完美也不容易。我也手发痒,所以就从这个算法开始。结果很不幸,我不是那10%的人:)
private static int RecurseSearch(int[] src, int subject, int low, int high)
{
int mid = (low + high) / 2;
if (low > high) return -1;
if (low == high)
{
if (src[mid] == subject) return mid;
}
if (src[mid] < subject)
{
return RecurseSearch(src, subject, mid + 1, high);
}
else if (src[mid] > subject)
{
return RecurseSearch(src, subject, low, mid - 1);
}
else
{
return mid;
}
}
public static int Search(int[] src, int subject)
{
int low = 0, high = src.Length - 1;
while (low <= high)
{
int mid = (low + high) / 2;
if (src[mid] < subject)
{
low = mid + 1;
}
else if (src[mid] > subject)
{
high = mid - 1;
}
else
{
return mid;
}
}
return -1;
}
有一道网上的面试题衍生出来的。
//有一个N*N的矩阵, 里面有N*N个数,这个矩阵的每一行,每一列都是排序好的,下面是一
//个例子
//1 3 7 9
//2 5 13 14
//6 8 25 26
//20 24 30 40
这是一道变了的折半查找,思想是非常类似的。折半的经典是分治,按一半一半的来分。那么如何来分一个数组呢?下面上两幅图来说明:


在这里,任何一个N X N的数组都可以分成四个数组,按N的奇偶分别按图中所示划分。到N=1的时候就不能再分,这个也是递归的退出条件。这里的划分后唯一能确定的就是,任何在第一象限的数字都是小于等于划分的这个元素本身的。
代码:
private static void Search(int[][] src, int subject, int left, int top, int len, ref int row, ref int col)
{
if (src.Length > 0)
{
if (src.Length != src[0].Length) throw new ArgumentException("bad array!");
}
if (src.Length <= 0)
{
row = -1;col = -1;return;
}
if (len == 1)
{
if (src[top][left] == subject)
{
row = top;col = left;return;
}
else
{
row = -1;col = -1;return;
}
}
if (src[src.Length - 1][src.Length - 1] < subject)
{
row = -1;col = -1;return;
}
// Seperate the array to 4 sub arrary
int ulLeft, ulTop, ulLen, urLeft, urTop, urLen, llLeft, llTop, llLen, lrLeft, lrTop, lrLen;
if ((len) % 2 == 0)
{
// Up Left Quadrant
ulLeft = left;ulTop = top;ulLen = len / 2;
// Up Right Quandrant
urLeft = left + ulLen;urTop = top;urLen = len / 2;
// Low Left Quadrant
llLeft = left;llTop = top + ulLen;llLen = len / 2;
//Low Right Quadrant
lrLeft = left + ulLen;lrTop = top + ulLen;lrLen = len / 2;
}
else
{
// Up Left Quadrant
ulLeft = left;ulTop = top;ulLen = len / 2 + 1;
// Up Right Quandrant
urLeft = left + ulLen;urTop = top;urLen = len / 2;
// Low Left Quadrant
llLeft = left;llTop = top + ulLen;llLen = len / 2;
//Low Right Quadrant
lrLeft = left + ulLen;lrTop = top + ulLen;lrLen = len / 2 + 1 ;
}
if (src[ulLeft + ulLen - 1][ulLeft + ulLen - 1] == subject)
{
row = ulLeft + ulLen - 1;col = ulLeft + ulLen - 1;return;
}
if (src[ulLeft + ulLen - 1][ulLeft + ulLen - 1] > subject)
{
// Search in Up Left Quandrant
Search(src, subject, ulLeft, ulTop, ulLen, ref row, ref col);
}
else
{
// Search in Uper Right Quandrant
Search(src, subject, urLeft, urTop, urLen, ref row, ref col);
if (row == -1 && col == -1)
{
// Search in Low Left Quandrant
Search(src, subject, llLeft, llTop, llLen, ref row, ref col);
if (row == -1 && col == -1)
{
// Search in Low Right Quandrant
Search(src, subject, lrLeft, lrTop, lrLen, ref row, ref col);
}
if (row == -1 && col == -1) return;
}
}
}
}
复杂度:平均是log(N),最坏的情况是所要查找的数字始终在第四象限,复杂度为0.75log(N)。
2011年1月23日
本文Redis VS Oracle Advance Queue性能对比 (一) 的续篇。本文主要关注于多线程下面的比较。在真实的应用环境下面多个程序多个线程同时出入队的情况比较普篇,所以我们更需要看看在这样的情况下的性能对比。Oracle Advance Queue的底下是数据库的表,我们知道对同一个表太多的Session进行操作会有些锁的问题,所以在我们的测试中既有单个Queue的多线程对比,也有多个线程对多个Queue同时操作的对比。
对于Redis本身是个单进程单线程的服务程序。根据作者的建议,如果要发挥多个core (CPU)服务器的功效,我们需要开启多个实例。本文的测试服务器是一个八核的Linux,所以我们选择开启8个实例来服务8个独立的队列来对比Oracle Advance Queue的多表多队列。
这里是Redis作者原文的引用。
Simply start multiple instances of Redis in different ports in the same box and threat them as different servers! Given that Redis is a distributed database anyway in order to scale you need to think in terms of multiple computational units. At some point a single box may not be enough anyway.
In general key-value databases are very scalable because of the property that different keys can stay on different servers independently.
In Redis there are client libraries such Redis-rb (the Ruby client) that are able to handle multiple servers automatically using consistent hashing. We are going to implement consistent hashing in all the other major client libraries. If you use a different language you can implement it yourself otherwise just hash the key before to SET / GET it from a given server. For example imagine to have N Redis servers, server-0, server-1, ..., server-N. You want to store the key "foo", what's the right server where to put "foo" in order to distribute keys evenly among different servers? Just perform the crc = CRC32("foo"), then servernum = crc % N (the rest of the division for N). This will give a number between 0 and N-1 for every key. Connect to this server and store the key. The same for gets.
This is a basic way of performing key partitioning, consistent hashing is much better and this is why after Redis 1.0 will be released we'll try to implement this in every widely used client library starting from Python and PHP (Ruby already implements this support).
测试代码的设计。
static void MultiThreadEnqueue(List<IQueue> queues, List<string> messages, string queueType)
{
Dictionary<IQueue, Task> queueTaskDic = new Dictionary<IQueue, Task>();
var sw = Stopwatch.StartNew();
int i = 1;
foreach (var q in queues)
{
queueTaskDic[q] = new Task(Enqueue, new QueueTask { Queue = q, Messages = messages });
queueTaskDic[q].Start();
i++;
}
Task.WaitAll(queueTaskDic.Values.ToArray<Task>());
sw.Stop();
Console.WriteLine("{2}, Enqueue {0} messages cost {1} ms, {4}, {0}, {3:0.0000}", messages.Count() * queues.Count(), sw.ElapsedMilliseconds, queueType, messages.Count() * queues.Count() * 1000.0 / sw.ElapsedMilliseconds, queues.Count());
}
static void MultiThreadDequeue(List<IQueue> queues, int msgCount, string queueType)
{
Dictionary<IQueue, Task> queueTaskDic = new Dictionary<IQueue, Task>();
var sw = Stopwatch.StartNew();
foreach (var q in queues)
{
queueTaskDic[q] = new Task(Dequeue, new QueueTask { Queue = q, DequeueCount=msgCount });
queueTaskDic[q].Start();
}
Task.WaitAll(queueTaskDic.Values.ToArray<Task>());
sw.Stop();
Console.WriteLine("{2}, Dequeue {0} messages cost {1} ms, {4}, {0}, {3:0.0000}", msgCount * queues.Count(), sw.ElapsedMilliseconds, queueType, msgCount * queues.Count() * 1000.0 / sw.ElapsedMilliseconds, queues.Count());
}
根据传入的Queue数组的个数来建立多个Task(线程),在些线程中做入队或出队的操作。然后等待所有的线程完成,这个计数的时间就是取所有线程中花的时间最长的一个。
AQAccessor.cs
public static List<IQueue> CreateSharedTablePersistentQueues(int queueCount)
{
List<IQueue> ret = new List<IQueue>();
for (int i = 0; i < queueCount; i++)
{
ret.Add(CreatePersistentQueue(1));
}
return ret;
}
public static List<IQueue> CreateSharedTableBufferedQueues(int queueCount)
{
List<IQueue> ret = new List<IQueue>();
for (int i = 0; i < queueCount; i++)
{
ret.Add(CreateBufferedQueue(1));
}
return ret;
}
public static List<IQueue> CreateDedicatedTableBufferedQueues(int queueCount)
{
List<IQueue> ret = new List<IQueue>();
for (int i = 0; i < queueCount; i++)
{
ret.Add(CreateBufferedQueue(i % 8 +1));
}
return ret;
}
public static List<IQueue> CreateDedicatedTablePersistentQueues(int queueCount)
{
List<IQueue> ret = new List<IQueue>();
for (int i = 0; i < queueCount; i++)
{
ret.Add(CreatePersistentQueue(i % 8 + 1));
}
return ret;
}
RedisAccessor.cs
public static List<IQueue> CreateSharedServerQueues(int queueCount)
{
List<IQueue> ret = new List<IQueue>();
for (int i = 0; i < queueCount; i++)
{
ret.Add(CreateSharedServerQueue(1));
}
return ret;
}
public static List<IQueue> CreateDedicatedServerQueues(int queueCount)
{
List<IQueue> ret = new List<IQueue>();
for (int i = 0; i < queueCount; i++)
{
ret.Add(CreateDedicatedServerQueue(i % 8 + 1));
}
return ret;
}
共享的队列的意思是所有的操作多将在queue1 (对于Oracle Advance Queue来说,对应的表是queue_table1, 对于Redis来说对应于6380端口上的服务)上完成,CreateSharedTablePersistentQueues(), CreateSharedTableBufferedQueues()分别构建的是一群共享一个表的存储或内存Queue。而CreateSharedServerQueues()构建的是一群公用一个端口的Queue。
独立的队列的意思是操作是分布在8个不同的Oracle表或8个不同端口的Redis服务器上的。如果需要构建的线程超过8,按照取8的模来分布到这八个队列上。CreateDedicatedTableBufferedQueues()和CreateDedicatedTablePersistentQueues()
是为Oracle AQ构建一群队列的。CreateDedicatedServerQueues()是为Redis 构建队列的。
测试代码和上篇是同一个。
测试结果原始数据可以从http://files.cnblogs.com/ivenxu/multthreadQueueResult.zip下载。






2011年1月22日
2010年4月29日