一:背景 1. 讲故事
这个月初,星球里的一位朋友找到我,说他的程序出现了死锁,怀疑是自己的某些写法导致mongodb出现了如此尴尬的情况,截图如下:
说实话,看过这么多dump,还是第一次遇到真实的死锁,这tmd的顿时就有了兴趣。。。 上 windbg 说话。
二:Windbg 分析 1. 真的是死锁吗既然朋友说死锁,我得先验证一下,可以用命令 !syncblk 查看同步块表。
0:000> !syncblk Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner 97 000000F7B77CA1B8 107 1 000000F7D37A7210 8848 25 000000f7b853d480 System.Object 144 000000F7D39BA2A8 495 1 000000F7DA4CDA70 75e0 63 000000f7b853de48 System.Object ----------------------------- Total 603 CCW 1 RCW 1 ComClassFactory 0 Free 490从同步块表中可得知如下信息。
25号线程正持有 000000f7b853d480 锁对象。
63号线程正持有 000000f7b853de48 锁对象。
我们知道所谓的 死锁 就是两个线程都渴望得到对方持有的锁资源,谁也不让步所造成的一种僵局,如果不明白,我就画一张图:
上图就是一种死锁的僵局,顺便提一下, 在 sqlserver 中也常会遇到这种情况,那它会怎么处理的呢? 这就有点意思了,sqlserver 内部有一个调停的线程周期性执行,当检测到这种死锁僵局的时候,它会把优先级低的线程kill掉,这样另外一个线程就能顺利获取锁,被 kill 掉的线程就会出现如下异常信息:
System.Data.SqlClient.SqlException (0x80131904): 事务(进程 ID 112)与另一个进程被死锁在 锁 | 通信缓冲区 资源上,并且已被选作死锁牺牲品。请重新运行该事务。 在 System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction) 在 System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction) 在 System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose) 在 System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady) 在 System.Data.SqlClient.SqlCommand.RunExecuteNonQueryTds(String methodName, Boolean async, Int32 timeout) 在 System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, String methodName, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite) 在 System.Data.SqlClient.SqlCommand.ExecuteNonQuery() 在 Microsoft.ApplicationBlocks.Data.SqlHelper.ExecuteNonQuery(SqlConnection connection, CommandType commandType, String commandText, SqlParameter[] commandParameters) 在 Microsoft.ApplicationBlocks.Data.SqlHelper.ExecuteNonQuery(String connectionString, CommandType commandType, String commandText, SqlParameter[] commandParameters)哈哈,是不是似曾相识,好了,对死锁有了一定认识之后,我们假设一下,如果存在
25号线程想获取 000000f7b853de48 锁对象。
63号线程想获取 000000f7b853d480 锁对象。
的情况下,必然就会死锁, 对吧,接下来怎么用 windbg 验证呢? 切到 25 号线程查看线程栈及栈对象。
0:000> ~25s ntdll!NtWaitForMultipleObjects+0xa: 00007ffb`9f230c7a c3 ret 0:025> !clrstack OS Thread Id: 0x8848 (25) Child SP IP Call Site 000000F782904838 00007ffb9f230c7a [HelperMethodFrame_1OBJ: 000000f782904838] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef) 000000F782904990 00007ffb1d5e4d5c MongoDB.Driver.Core.ConnectionPools.ExclusiveConnectionPool+ListConnectionHolder.Acquire() 0:025> !dso OS Thread Id: 0x8848 (25) RSP/REG Object Name 000000F782904648 000000f7b853de48 System.Object 000000F7829046D8 000000f7b84cb508 MongoDB.Driver.ReadPreference可以清楚的看到 ReliableEnter 正在获取 000000f7b853de48 锁对象时被卡住,再切到 63号线程查看。
0:025> ~63s ntdll!NtWaitForMultipleObjects+0xa: 00007ffb`9f230c7a c3 ret 0:063> !clrstack OS Thread Id: 0x75e0 (63) Child SP IP Call Site 000000F787774EE8 00007ffb9f230c7a [HelperMethodFrame_1OBJ: 000000f787774ee8] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef) 000000F787775040 00007ffb1d5e4d5c MongoDB.Driver.Core.ConnectionPools.ExclusiveConnectionPool+ListConnectionHolder.Acquire() 0:063> !dso OS Thread Id: 0x75e0 (63) RSP/REG Object Name 000000F787774A38 000000f7b82dc750 MongoDB.Bson.BsonBoolean 000000F787774BA0 000000f7b83a9a10 System.RuntimeType 000000F787774CF8 000000f7b853d480 System.Object可以清楚的看到 ReliableEnter 正在获取 000000f7b853d480, 这就表明确实产生了死锁,没毛病。
2. 死锁原因分析