聊一聊坑人的 C# MySql.Data SDK

打印 上一主题 下一主题

主题 817|帖子 817|积分 2451

一:配景

1. 讲故事

为什么说这东西比较坑人呢?是因为近来一个月接到了两个dump,都反应步调卡死无响应,最后分析下来是因为线程饥饿导致,那什么原因导致的线程饥饿呢?进一步分析发现罪魁罪魁是 MySql.Data,这就让人无语了,而且反馈都是升级了MySql.Data驱动引发,接下来我们简单聊一下。
二: MySql.Data 到底怎么了

1. 祸根溯源

早期版本的 MySql.Data 访问数据库都是以同步的方式进行,比如:ExecuteReader 而不是 ExecuteReaderAsync,随着项目的升级改造需要提升MySql.Data的版本, MySql为了向前兼容保留了同步方法,下面引用最新的 MySql.Data 9.1.0 截图和参考代码如下:
  1. // MySql.Data, Version=9.1.0.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d
  2. // MySql.Data.MySqlClient.MySqlConnection
  3. using System.Threading;
  4. public override void Open()
  5. {
  6.         OpenAsync(execAsync: false, CancellationToken.None).GetAwaiter().GetResult();
  7. }
  8. // MySql.Data, Version=9.1.0.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d
  9. // MySql.Data.MySqlClient.MySqlCommand
  10. using System.Data;
  11. using System.Threading;
  12. public new MySqlDataReader ExecuteReader()
  13. {
  14.         return ExecuteReaderAsync(CommandBehavior.Default, execAsync: false, CancellationToken.None).GetAwaiter().GetResult();
  15. }
  16. public override object ExecuteScalar()
  17. {
  18.         return ExecuteScalarAsync(execAsync: false, CancellationToken.None).GetAwaiter().GetResult();
  19. }
复制代码
仔细看上面这段代码,不觉让人吸了一口凉气,所谓的同步方式竟然是用异步方法简单包装 而来的,这种异步混用同步的方式很容易导致线程饥饿,即线程池中已无可用线程来叫醒 GetResult() 下的 Event 事件,这个我准备后面用一篇文章详细来聊一下线程饥饿,这里用C#内功修炼训练营中的一张图来演示下.NET8 中异步在线程池中的走法。

2. 线程饥饿的现场

题目方法给大家列出来的,接下来用 windbg 看下dump中的故障现场吧。

  • 某考试系统的故障
看故障现象比较简单,使用 !tp 和 !tpq 即可,输出如下:
  1. 0:000> !tp
  2. Using the Portable thread pool.
  3. CPU utilization:  1%
  4. Workers Total:    268
  5. Workers Running:  268
  6. Workers Idle:     0
  7. Worker Min Limit: 4
  8. Worker Max Limit: 32767
  9. 0:000> !sos tpq
  10. global work item queue________________________________
  11. 0x000002410E750218 Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT<Microsoft.AspNetCore.Hosting.HostingApplication+Context>
  12. 0x000002410E7505A0 Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT<Microsoft.AspNetCore.Hosting.HostingApplication+Context>
  13. 0x000002410E750928 Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT<Microsoft.AspNetCore.Hosting.HostingApplication+Context>
  14. ...
  15. local per thread work items_____________________________________
  16. 0x0000024114903310 System.Runtime.CompilerServices.AsyncTaskMethodBuilder<MySql.Data.MySqlClient.MySqlPool>+AsyncStateMachineBox<MySql.Data.MySqlClient.MySqlPoolManager+<GetPoolAsync>d__23>
复制代码

从卦中可以看到线程池中目前有268个线程,此时都处于运行状态,而且线程池的全局队列积压了1000+的任务没有处理,接下来使用 ~*e !clrstack 观察每个线程都在做什么。
  1. 0:287> !clrstack
  2. OS Thread Id: 0x39ec (287)
  3.         Child SP               IP Call Site
  4. 000000858C5FD1B8 00007ffc95ca04e4 [HelperMethodFrame_1OBJ: 000000858c5fd1b8] System.Threading.Monitor.ObjWait(Int32, System.Object)
  5. 000000858C5FD2E0 00007ffc087cccc9 System.Threading.Monitor.Wait(System.Object, Int32) [/_/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs @ 156]
  6. 000000858C5FD310 00007ffc087cd027 System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs @ 561]
  7. 000000858C5FD3D0 00007ffc087cc4f2 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3072]
  8. 000000858C5FD440 00007ffc087cc099 System.Threading.Tasks.Task.InternalWaitCore(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3007]
  9. 000000858C5FD4C0 00007ffc08796cc6 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task, System.Threading.Tasks.ConfigureAwaitOptions) [/_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs @ 111]
  10. 000000858C5FD500 00007ffc086ffbc4 xxxx.UpdateAnswerUrl(System.String, Int32, System.Collections.Generic.Dictionary`2<System.String,System.String>)
复制代码
发现这些线程都卡在 xxxx.UpdateAnswerUrl 方法上,那到底卡在方法的那边呢?可以用 !U /d 00007ffc086ffbc4 观察方法的反汇编代码,看看这个00007ffc086ffbc4停顿在那边?输出如下:
  1. 0:000> !U /d 00007ffc086ffbc4
  2. Normal JIT generated code
  3. xxx.UpdateAnswerUrl(System.String, Int32, System.Collections.Generic.Dictionary`2<System.String,System.String>)
  4. ...
  5. 00007ffc`086ffb79 ff15114bb9fe    call    qword ptr [00007ffc`07294690] (System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[[MySql.Data.MySqlClient.MySqlCommand+<ExecuteScalarAsync>d__117, MySql.Data]](<ExecuteScalarAsync>d__117 ByRef), mdToken: 000000000600646B)
  6. 00007ffc`086ffb7f 488b8c2468010000 mov     rcx,qword ptr [rsp+168h]
  7. 00007ffc`086ffb87 4885c9          test    rcx,rcx
  8. 00007ffc`086ffb8a 0f84890c0000    je      00007ffc`08700819
  9. 00007ffc`086ffb90 3809            cmp     byte ptr [rcx],cl
  10. 00007ffc`086ffb92 48898c2498010000 mov     qword ptr [rsp+198h],rcx
  11. 00007ffc`086ffb9a 488d8c2498010000 lea     rcx,[rsp+198h]
  12. 00007ffc`086ffba2 48baf02b5006fc7f0000 mov rdx,7FFC06502BF0h (MT: System.Runtime.CompilerServices.TaskAwaiter`1[[System.Object, System.Private.CoreLib]])
  13. 00007ffc`086ffbac ff158e7cdefd    call    qword ptr [00007ffc`064e7840] (System.Runtime.CompilerServices.TaskAwaiter`1[[System.__Canon, System.Private.CoreLib]].GetResult(), mdToken: 00000000060065F0)
  14. 00007ffc`086ffbb2 48898424e8000000 mov     qword ptr [rsp+0E8h],rax
  15. 00007ffc`086ffbba eb0d            jmp     00007ffc`086ffbc9
  16. 00007ffc`086ffbbc 33d2            xor     edx,edx
  17. 00007ffc`086ffbbe ff1544d4bffd    call    qword ptr [00007ffc`062fd008] (System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task, System.Threading.Tasks.ConfigureAwaitOptions), mdToken: 00000000060065E4)
  18. >>> 00007ffc`086ffbc4 e960ffffff      jmp     00007ffc`086ffb29
复制代码
从汇编代码中可以观测它是在获取 ExecuteScalarAsync 方法的 Result 效果,有了这个信息就可以翻源代码了,截图如下:

最终就发现了ExecuteScalar下面的荒诞一幕。。。

  • 某跟踪埋点系统的故障
埋点系统也是一样的题目,使用 !tp 观察到线程池有 602 个线程都处于运行状态,输出如下:
  1. 0:000> !tp
  2. Using the Portable thread pool.
  3. CPU utilization:  11%
  4. Workers Total:    602
  5. Workers Running:  602
  6. Workers Idle:     0
  7. Worker Min Limit: 32
  8. Worker Max Limit: 32767
复制代码
然后通过 ~*e !clrstack 观察发现线程都处于 Open() 方法中,输出如下:
  1. OS Thread Id: 0x1a9d4 (23)
  2.         Child SP               IP Call Site
  3. 0000007AD4DBE228 00007ff9feb70b24 [HelperMethodFrame_1OBJ: 0000007ad4dbe228] System.Threading.Monitor.ObjWait(Int32, System.Object)
  4. 0000007AD4DBE350 00007ff9b655d55e System.Threading.Monitor.Wait(System.Object, Int32) [/_/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs @ 156]
  5. 0000007AD4DBE380 00007ff9b656860e System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs @ 561]
  6. 0000007AD4DBE420 00007ff9b6581729 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3072]
  7. 0000007AD4DBE4A0 00007ff9b6581516 System.Threading.Tasks.Task.InternalWaitCore(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3007]
  8. 0000007AD4DBE520 00007ff959e9e9f4 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task, System.Threading.Tasks.ConfigureAwaitOptions) [/_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs @ 111]
  9. 0000007AD4DBE560 00007ff95752e95b MySql.Data.MySqlClient.MySqlConnection.Open()
  10. ...
复制代码
可恶的是 Open() 方法内部也是用 异步转同步 实现的,真的无语了。
3. 解决方法

要想解决这个题目,大概两种方法吧。

  • 使用纯异步写法,这也是高版本 MySql.Data 极力推荐的,否则就给你埋坑。。。
  • 退回到低版本的 MySql.Data,继续使用真正的同步版写法。
三:总结

挺意外的是 MySql.Data 项目在 github:https://github.com/mysql/mysql-connector-net 上没开 issue 栏。

这就无法让社区开发者介入,真的很奇葩,只能在这里给大家做个预警吧。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

玛卡巴卡的卡巴卡玛

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表