一:背景

1. 讲故事

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

二: MySql.Data 到底怎么了

1. 祸根溯源

早期版本的 MySql.Data 访问数据库都是以同步的方式进行,比如:ExecuteReader 而不是 ExecuteReaderAsync,随着项目的升级改造需要提升MySql.Data的版本, MySql为了向前兼容保留了同步方法,下面引用最新的 MySql.Data 9.1.0 截图和参考代码如下:


// MySql.Data, Version=9.1.0.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d
// MySql.Data.MySqlClient.MySqlConnection
using System.Threading; public override void Open()
{
OpenAsync(execAsync: false, CancellationToken.None).GetAwaiter().GetResult();
} // MySql.Data, Version=9.1.0.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d
// MySql.Data.MySqlClient.MySqlCommand
using System.Data;
using System.Threading; public new MySqlDataReader ExecuteReader()
{
return ExecuteReaderAsync(CommandBehavior.Default, execAsync: false, CancellationToken.None).GetAwaiter().GetResult();
} public override object ExecuteScalar()
{
return ExecuteScalarAsync(execAsync: false, CancellationToken.None).GetAwaiter().GetResult();
}

仔细看上面这段代码,不觉让人吸了一口凉气,所谓的同步方式竟然是用异步方法简单包装 而来的,这种异步混用同步的方式很容易导致线程饥饿,即线程池中已无可用线程来唤醒 GetResult() 下的 Event 事件,这个我准备后面用一篇文章详细来聊一下线程饥饿,这里用C#内功修炼训练营中的一张图来演示下.NET8 中异步在线程池中的走法。

2. 线程饥饿的现场

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

  1. 某考试系统的故障

看故障现象比较简单,使用 !tp!tpq 即可,输出如下:


0:000> !tp
Using the Portable thread pool. CPU utilization: 1%
Workers Total: 268
Workers Running: 268
Workers Idle: 0
Worker Min Limit: 4
Worker Max Limit: 32767 0:000> !sos tpq
global work item queue________________________________
0x000002410E750218 Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT<Microsoft.AspNetCore.Hosting.HostingApplication+Context>
0x000002410E7505A0 Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT<Microsoft.AspNetCore.Hosting.HostingApplication+Context>
0x000002410E750928 Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT<Microsoft.AspNetCore.Hosting.HostingApplication+Context>
...
local per thread work items_____________________________________
0x0000024114903310 System.Runtime.CompilerServices.AsyncTaskMethodBuilder<MySql.Data.MySqlClient.MySqlPool>+AsyncStateMachineBox<MySql.Data.MySqlClient.MySqlPoolManager+<GetPoolAsync>d__23>

从卦中可以看到线程池中目前有268个线程,此时都处于运行状态,并且线程池的全局队列积压了1000+的任务没有处理,接下来使用 ~*e !clrstack 观察每个线程都在做什么。


0:287> !clrstack
OS Thread Id: 0x39ec (287)
Child SP IP Call Site
000000858C5FD1B8 00007ffc95ca04e4 [HelperMethodFrame_1OBJ: 000000858c5fd1b8] System.Threading.Monitor.ObjWait(Int32, System.Object)
000000858C5FD2E0 00007ffc087cccc9 System.Threading.Monitor.Wait(System.Object, Int32) [/_/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs @ 156]
000000858C5FD310 00007ffc087cd027 System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs @ 561]
000000858C5FD3D0 00007ffc087cc4f2 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3072]
000000858C5FD440 00007ffc087cc099 System.Threading.Tasks.Task.InternalWaitCore(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3007]
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]
000000858C5FD500 00007ffc086ffbc4 xxxx.UpdateAnswerUrl(System.String, Int32, System.Collections.Generic.Dictionary`2<System.String,System.String>)

发现这些线程都卡在 xxxx.UpdateAnswerUrl 方法上,那到底卡在方法的何处呢?可以用 !U /d 00007ffc086ffbc4 观察方法的反汇编代码,看看这个00007ffc086ffbc4停留在何处?输出如下:

0:000> !U /d 00007ffc086ffbc4
Normal JIT generated code
xxx.UpdateAnswerUrl(System.String, Int32, System.Collections.Generic.Dictionary`2<System.String,System.String>)
...
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)
00007ffc`086ffb7f 488b8c2468010000 mov rcx,qword ptr [rsp+168h]
00007ffc`086ffb87 4885c9 test rcx,rcx
00007ffc`086ffb8a 0f84890c0000 je 00007ffc`08700819
00007ffc`086ffb90 3809 cmp byte ptr [rcx],cl
00007ffc`086ffb92 48898c2498010000 mov qword ptr [rsp+198h],rcx
00007ffc`086ffb9a 488d8c2498010000 lea rcx,[rsp+198h]
00007ffc`086ffba2 48baf02b5006fc7f0000 mov rdx,7FFC06502BF0h (MT: System.Runtime.CompilerServices.TaskAwaiter`1[[System.Object, System.Private.CoreLib]])
00007ffc`086ffbac ff158e7cdefd call qword ptr [00007ffc`064e7840] (System.Runtime.CompilerServices.TaskAwaiter`1[[System.__Canon, System.Private.CoreLib]].GetResult(), mdToken: 00000000060065F0)
00007ffc`086ffbb2 48898424e8000000 mov qword ptr [rsp+0E8h],rax
00007ffc`086ffbba eb0d jmp 00007ffc`086ffbc9
00007ffc`086ffbbc 33d2 xor edx,edx
00007ffc`086ffbbe ff1544d4bffd call qword ptr [00007ffc`062fd008] (System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task, System.Threading.Tasks.ConfigureAwaitOptions), mdToken: 00000000060065E4)
>>> 00007ffc`086ffbc4 e960ffffff jmp 00007ffc`086ffb29

从汇编代码中可以观测它是在获取 ExecuteScalarAsync 方法的 Result 结果,有了这个信息就可以翻源代码了,截图如下:

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

  1. 某跟踪埋点系统的故障

埋点系统也是一样的问题,使用 !tp 观察到线程池有 602 个线程都处于运行状态,输出如下:


0:000> !tp
Using the Portable thread pool. CPU utilization: 11%
Workers Total: 602
Workers Running: 602
Workers Idle: 0
Worker Min Limit: 32
Worker Max Limit: 32767

然后通过 ~*e !clrstack 观察发现线程都处于 Open() 方法中,输出如下:


OS Thread Id: 0x1a9d4 (23)
Child SP IP Call Site
0000007AD4DBE228 00007ff9feb70b24 [HelperMethodFrame_1OBJ: 0000007ad4dbe228] System.Threading.Monitor.ObjWait(Int32, System.Object)
0000007AD4DBE350 00007ff9b655d55e System.Threading.Monitor.Wait(System.Object, Int32) [/_/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs @ 156]
0000007AD4DBE380 00007ff9b656860e System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs @ 561]
0000007AD4DBE420 00007ff9b6581729 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3072]
0000007AD4DBE4A0 00007ff9b6581516 System.Threading.Tasks.Task.InternalWaitCore(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3007]
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]
0000007AD4DBE560 00007ff95752e95b MySql.Data.MySqlClient.MySqlConnection.Open()
...

可恶的是 Open() 方法内部也是用 异步转同步 实现的,真的无语了。

3. 解决方法

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

  1. 使用纯异步写法,这也是高版本 MySql.Data 极力推荐的,不然就给你埋坑。。。
  2. 退回到低版本的 MySql.Data,继续使用真正的同步版写法。

三:总结

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

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

聊一聊坑人的 C# MySql.Data SDK的更多相关文章

  1. 关于Linux和Windows下部署mysql.data.dll的注册问题

    mysql ado.net connector下载地址: http://dev.mysql.com/downloads/connector/net/ 选择版本: Generally Available ...

  2. allow zero datetime=true导致datetime转换失败:MySql.Data.Types.MySqlDateTime”的对象无法转换为类型“System.Nullable`1[System.DateTime]

    allow zero datetime=true导致datetime转换失败:MySql.Data.Types.MySqlDateTime”的对象无法转换为类型“System.Nullable`1[S ...

  3. MySql.Data.MySqlClient.MySqlException: Parameter ‘@maxid’ must be defined

    本文涉及到的mysql知识点: mysql中的if条件语句用法: IF(expr1,expr2,expr3) mysql使用变量(mysql中变量不用事前申明) mysql事务 testcase 为了 ...

  4. MySql.Data.dll 不支持输出参数

    insert INTO stu(name) VALUES('maimai'); set @ReturnValue=@@IDENTITY; string sql="insert INTO st ...

  5. Linux安装MySql.Data for mono

    wget http://dev.mysql.com/get/Downloads/Connector-Net/mysql-connector-net-6.8.3-noinstall.zipunzip m ...

  6. Net连接mysql的公共Helper类MySqlHelper.cs带MySql.Data.dll下载

    MySqlHelper.cs代码如下: using System; using System.Collections.Generic; using System.Linq; using System. ...

  7. Could not load file or assembly 'MySql.Data.CF,

    Could not load file or assembly 'MySql.Data.CF, Version=6.4.4.0, Culture=neutral, PublicKeyToken=c56 ...

  8. Change MYSQL data directory

    For example, change mysql data directory from /var/lib/mysql to /var/data/mysql Step1: Copy the /var ...

  9. 更改ubuntu mysql data目录位置

    很多时候,mysql的数据会非常大,数据默认放在/var/lib/mysql,由于/var所划分的空间不够大,所以我们需要将mysql数据存放路径修改一下,放到大分区里面,以便可以应付mysql数据增 ...

  10. 启动 mysql 失败 Warning:The /usr/local/mysql/data directory is not owned by the 'mysql' or '_mysql'

    Warning:The /usr/local/mysql/data directory is not owned by the 'mysql' or '_mysql' 这应该是某种情况下导致/usr/ ...

随机推荐

  1. SpringBoot.3中的aot.factories到底有什么用?和以前的spring.factories一样吗?

    首先,我们来澄清一下 aot.factories 和 spring.factories 之间的区别.这两个文件不仅名称不同,而且在功能上也存在显著差异.接下来,我们将深入探讨这两个文件的具体作用以及它 ...

  2. 推荐一款开源的API开放平台,5分钟就可以搭建企业专属的API开放门户!

    前言 在过去的十年中,企业API治理并未受到广泛关注.然而,随着时间的推进,特别是在近几年,企业技术管理者对API治理的重视程度显著提高,开始将API视为企业资产的重要组成部分.API不再仅仅是技术层 ...

  3. KubeSphere 社区双周报 | KubeSphere 多项更新 | 2023.06.23-07.06

    KubeSphere 社区双周报主要整理展示新增的贡献者名单和证书.新增的讲师证书以及两周内提交过 commit 的贡献者,并对近期重要的 PR 进行解析,同时还包含了线上/线下活动和布道推广等一系列 ...

  4. 题解:GZOI2024 D2T2 乒乓球

    考场上切了,但是比较神奇的题,应该是蓝/紫. Discription 乒乓球 \(\text{ }\)时间限制:\(\bold{3}\) 秒 众所周知,一场乒乓球比赛共有两个玩家 \(A\) 和 \( ...

  5. 从0.1开始学java(2)

    string串池 现在都在堆中 "=="号的比较 字符串比较一般调用equals或者equalsignoreCase API来比较(后者忽略大小写) StringBuilder 可 ...

  6. Clickhouse副本及分片

    副本 副本的目的主要是保障数据的高可用性,即使一台 ClickHouse 节点宕机,那么也可以从其他服务器获得相同的数据 配置副本 1. zookeeper集群准备 2. Clickhouse准备两个 ...

  7. Linux进程监控系统

    目录 动态监控进程 top 基本语法 关键信息说明 第一行:系统信息 第二行:进程信息 第三行:CPU占用情况 第四行:内存信息 第五行:交换区信息 交互操作 操作选项 应用实例 监控网络状态 net ...

  8. Python 抓取猫眼电影排行

    import json import re import requests from requests.exceptions import RequestException import time # ...

  9. 如何挑选海外4G模组?这里有秘籍!

    今天我会告诉大家如何挑选海外4G模组,我会把优势给贴出作为参考.去过国外的都知道国外4G网络各种状况实在让人无力吐槽,做海外设备的朋友,是时候了解一下Air780EEN/EEU/EEJ系列海外模组-- ...

  10. delphi12 Android Edit SDK安装

    安装 delphi 12.1 后,编译 FMX Android 程序失败! 查找原因,SDK配置全是叹号! 之前用过SDK Manager.exe,直接打开即可以选择下载,但现在发现没有了,查找资料如 ...