问题:
进程启动后,线程数迅速上升至最小线程数后,缓慢上升(线程池限制)到数千,然后由于线程过多,CPU飙升到90%。
对外表现为Api无响应或连接超时。
背景
有些数据存在于另一个机房,通过内网专线连接。一个服务程序有4个数据库,其中3个在本地机房,1个在外地。
各种排查,没有解决。
最终的处理方法
Dump进程
- 使用进程管理器,创建进程Dump文件。
- 使用VisualStudio打开该Dump文件并进行托管调试
- 查看并行堆栈,发现大部分线程均处于MySql.Data.MySqlClient.MySqlPoolManager.GetPool这个函数的调用中。并在此处进入了本机代码。处于其他调用堆栈的线程屈指可数。
代码分析
- 由于Mysql.Data.dll没有对应的pdb文件(Oracle没有提供),所以在Visual Studio中不能进入其中的代码,因此直接反编译,找到该函数,代码如下:
函数中,第一句的GetKey函数如下,其中有一个lock。其中代码仅仅是赋值,或是在集成认证的情况下才执行。所以卡住的可能性不大。
第二句是个赋值,且MysqlPoolManager.pools是个字段(field),理论上不会卡住。
第二个lock中,如果指定key对应的缓存已存在,则lock会很快返回。如果不存在,则执行new MysqlPool(setttings);函数代码如下:
其主要功能有
- 创建一个事件,用于获取连接时的异步等待
- 根据settings持久化设置
- 初始化池驱动列表、队列
- 按照配置的minSize创建指定数量的连接。
-
创建一个过程缓存,代码如下
这5个步骤中,最可能耗时较久的是步骤d。其他步骤理论上不会有问题。
步骤d中的代码,虽然就一个函数,但是代码很多。
经过不停的查看代码,发现其主要功能是根据连接字符串中的设置,创建一个指定类型的连接。其底层创建代码如下:
可以看到,任何创建Stream失败的情况都会抛出异常,最终导致连接池创建失败。
其中第一句,GetStream的底层代码如下:
开始连接(BeginConnect)后,即开始了等待。等待的超时默认值如下:
2147483s,即596h。如果有连接到数据库服务器的网络有问题或其他原因导致连接不成功,而也未触发其他导致失败的情况,则会一直等下去。如果推断正确,那么所有线程中,一定有线程的调用堆栈在如下位置:
对Dump文件中的所有线程堆栈排序,有且仅有一个线程处于该调用堆栈处。高亮行正是上述堆栈的函数名CreateSocketStream。上面一行就是WaiteOne。之后进入本机代码。
那么根本原因也就清楚了:一个连接的创建卡住了数据库连接创建,间接卡住了连接池的锁,又间接卡住了其他连接池的使用和创建。导致所有数据库连接不可用。所以,所有进入的请求经过运行,全部堆在GetPool这里。
解决方法:
- 保证网络正常(跨机房专线稳定性不可控,有人摇晃光纤玩 o(∩_∩)o 或者其他原因导致流量堵塞)
- 容易卡的数据库连接分离出去到单独的进程。这样由于不共享锁,所以不会卡住其他线程池的使用和创建。
- 需要跨机房的业务,在数据所在机房单独提供api,内网失效时可以走外网。
- 容易卡住的线程池连接字符串中设置minPoolSize=0。这样创建连接池时,不预创建连接而影响其他连接池。但是,对于突发流量增长的情况,响应可能不够及时。
- 设置一个合理的ConnectionTimeout。可以有效避免连接创建时卡住,导致api无响应和其他副作用。
其他在源代码中发现的需要注意的地方
- 连接池中空闲连接的空闲时间是180s。
- 清理周期第一次是188s,之后保持180s。
- 如果连接池中的空闲连接数大于设置的minPoolSize,则清理空闲连接直到minPoolSize。
-
ConnectionTimeout用于几个地方
- ) 连接socket时的等待超时
- ) 连接之后,连接上的读写超时。
- ) 从已空且总数达上限的连接池中,等待可用连接时的等待超时
以上所有信息基于.Net版本Mysql.Data 6.9.9版本反编译分析。