LoadTest中内存和线程Troubleshooting实战

在端午节放假的三天中,我对正在开发的Service进行了LoadTest,尝试在增大压力的条件下发现问题。

该Service为独立进程的WCF服务,宿主于WindowsService,其接收其他Service的调用,并根据业务流程调用不同的依赖Service。

问题现象

果真,假期归来,问题来了。内存正常在200M左右,已经膨胀到4.5G,同时正常线程数为40-60左右,已经占用了560个。

基于在测试之前已经准备好了PerformanceCounter记录,从报表数据来看,CPU使用率不高,并且仍然在偶尔活跃,可以猜测只是部分业务Block,而非全部。

在此期间,内存不断攀升,而且比想象的要快。

但奇怪的是,线程数在6月8日7:30左右才开始爆发。

所以大概可以分析,内存的增加与线程的增加,没有之间的联系,线程数虽在500以上,但按照业务代码核算,不至于产生4.5G以上的内存使用。

所以猜测,应该有两个问题:

  1. 在代码某处有持续的内存泄漏。
  2. 在代码某处有单点故障导致Block线程。

分析内存泄漏

好的消息是,进程还活着,虽然不再进行正经的业务。此时,祭出神器 WinDbg,尝试看看内存中有什么。

!dumpheap -stat

  • MT :对应 Method Table 地址。
  • Count : 该对象的数量。
  • TotalSize :该对象的总大小,Bytes。

TriggerHit和TriggerProgress都是业务类型,可以大概看一下TriggerHit类型的大小,使用:

!dumpheap -mt 000007ff006775a0

!dumpobj 0000000005cbbf78

可以明确判断,每个AsyncToken实例只占48Bytes。而其为ConcurrentDictionary中的一个Node,代表一个KeyValue键值对。

综合以上可以看出,几种类型 TriggerProgress、TriggerHit、long、AsyncToken 类型最多。从业务设计上看 TriggerProgress/TriggerHit 业务类实例的个数是相同的,从图上看也是相同的,但数量肯定不至于有507万个。每个类型的占用内存大概在240M以上。而有差不多数量的 AsyncToken 类型,占用也在240M左右。而 long 类型,存放在 ConcurrentDictionary 中,应该是作为某种索引,居然有1000万条,空间占用480M以上。这几个类型的占用总和已经达到了3G左右。所以说,这里应该是内存泄漏的根源。

此时,查看对应的数据库表 Triggers,

居然有700多万条数据处在Enabled状态。此处的Trigger是个业务概念,其代表该Service与另外一个Service之间的Transaction,如果其为活跃,则证明在所依赖的Service并没有明确回复该Transaction已结束。而从业务设计角度,每秒会产生100-200个Transaction,每个Transaction通常在5分钟左右的时间结束。进一步在Service的日志Log中确认是否有Exception出现,排除处理Transaction异常的情况,发现没有。也就是说,有大量的Transaction未接收到回复,所依赖的Service应该出了问题。

走到此处,在LoadTest中内存泄漏的问题算是被确认了,而后来经过与相应的Service开发人员沟通,确认所依赖的Service确实有性能问题,因为在Load很高的情况下,发送的Transaction数量已经超过了对方Service承受的能力。后续的补充解决办法是为Transaction增加Timeout超时检测。

好,问题解决一个,线程数高的问题还没有解决,前面已经描述了,内存高与线程多没有关系。那么,那560个线程每个走在做什么工作呢?

分析线程使用

显然需要继续使用WinDbg,因为此时使用ProcessExplorer只能帮助确认线程在使用那些DLLs,没有更多帮助。

由于有500多个线程,所以也不知道从哪个开始,只能从头一个一个来。一般1-10可能是进程启动时启用的线程,比如MainThread、GC回收线程等。同时,业务中的一些常规Timer也会先启动占用线程。

当查到第22个线程时,

~22 e !clrstack

这个很有帮助,发现第22个线程在 SemaphoreSlim.Wait() 等待一个信号量释放,而其为类 SmartQueue 中的代码。还好加了PDB文件,直接显示到 SmartQueue.cs 的第 301 行。直接看看代码,

发现这句代码在等待信号量的释放,并且没有指定超时时间,所以如果信号量在占用线程不被释放,这里将一直阻塞着。那这是一个被阻塞的线程,而不是引发阻塞的线程。

下一个线程,23号线程也在做同样的事情,等待信号量释放,所以猜测有很多线程在等待同一个信号量释放。

问题是,系统中有至少10个SmartQueue在工作,而此处无法看出是哪个SmartQueue被阻塞了。但根据业务设计和配置文件中的配置,

每个SmartQueue中最多启160个线程,而这个Queue的配置已经是最大的了,其他均小于160。这样看来,达到560,还不是一个SmartQueue在努力。

问题复杂了,一时看不出究竟,只能继续查看。当查到第30号线程时,

希望来了,这个调用占说明,在方法ObtainXXX中,正在Monitor.Enter(object),也就是一个lock在尝试锁定对象。此时继续查看线程调用栈,40/50/100/200/300/400/500线程均Block在ObtainXXX方法中。

PDB信息中已经给出的代码行,

这里__activeXXX是一个ConcurrentDictionary,这里已经进行了DoubleCheck检测,也就是获得锁的线程需要检测是否该ID已经在其他线程中增加了。但调用这个ObtainXXX方法的代码有8处,并且在不同的业务规则中,所以确实可以发生在不同的SmartQueue中的线程中,也验证了上面的猜测。

那么,问题是,既然一个线程得到了锁,而其不释放这个锁,那么就是说{}代码中应该有可阻塞调用。

发现代码中,除了Log,只有一句代码,就是这个LoadXXXByID,其功能就是从数据库中获取数据并生成对象实例。

此处我开始疑惑,Service访问数据库使用EntityFramework,此处针对这个业务对象,只有两种操作,

  • 如果数据库中不存在此业务对象ID,则Insert一条新的。
  • 如果数据库中存在此业务对象ID,则通过Select读取数据构造业务对象。

业务代码简写为:

      Balance storedBalance = Repository.Balance.QueryWithPC()
.SingleOrDefault(p => p.RelatedId == xxx); if storedBalance == null)
{
storedBalance = CreateBalanceToRepository(xxx);
}

这能产生死锁?

分析数据库

走到这一步,开始关注数据库了。因为,感觉Insert操作会产生表级锁,阻塞所有Select操作,Update操作可能产生多种类型的锁,但Insert/Update会很快处理完毕释放锁。Select操作,默认EntityFramework并不加QueryHint,所以默认查询隔离级别为ReadCommitted,即使这样,查询只会变慢,但不会死锁。所以,有些想不通。

先查看了针对数据库访问定制的PerformanceCounter,

发现在发生故障之前,CRUD操作均在0.1秒以下。在发生故障的那个时间点,针对Balance表的Insert操作的时长已经达到了60秒的量级,而Select操作也已经平均在5秒以上。

到底发生了什么?

SQL Server Management Studio 提供了些默认的报表,先看看。发现一张有用的报表 Top Transactions by Locks Count,

貌似Triggers表有死锁出现,而业务上,Balance与Trigger是一对多的关系。

查看Trigger表的PerformanceCounter监控记录,

Trigger这张数据库表,在同样的时间也发生了类似的访问时间异常。

SELECT REQUEST_MODE, REQUEST_TYPE, REQUEST_SESSION_ID
FROM sys.dm_tran_locks
WHERE RESOURCE_TYPE = 'OBJECT'
AND RESOURCE_ASSOCIATED_ENTITY_ID =(SELECT OBJECT_iD('Triggers'))

用上面这个SQL查询确认Triggers表在SessionID=77上发生了IX类型的锁,不释放。

并且使用 sp_who2 确认这个session是由我们的Service保持的,因为有多个Service在使用同一个数据库。

我在Google上搜索到了一个可以查看具体锁内容的SQL,

SELECT  L.request_session_id AS SPID,
DB_NAME(L.resource_database_id) AS DatabaseName,
SC.name AS SchemaName,
O.Name AS LockedObjectName,
P.object_id AS LockedObjectId,
L.resource_type AS LockedResource,
L.request_mode AS LockType,
ST.text AS SqlStatementText,
ES.login_name AS LoginName,
ES.host_name AS HostName,
TST.is_user_transaction as IsUserTransaction,
AT.name as TransactionName,
CN.auth_scheme as AuthenticationMethod
FROM sys.dm_tran_locks L
JOIN sys.partitions P ON P.hobt_id = L.resource_associated_entity_id
JOIN sys.objects O ON O.object_id = P.object_id
JOIN sys.schemas SC ON O.schema_id = SC.schema_id
JOIN sys.dm_exec_sessions ES ON ES.session_id = L.request_session_id
JOIN sys.dm_tran_session_transactions TST ON ES.session_id = TST.session_id
JOIN sys.dm_tran_active_transactions AT ON TST.transaction_id = AT.transaction_id
JOIN sys.dm_exec_connections CN ON CN.session_id = ES.session_id
CROSS APPLY sys.dm_exec_sql_text(CN.most_recent_sql_handle) AS ST
WHERE resource_database_id = db_id()
ORDER BY SchemaName

查询结果:

从SqlStatementText的样子可以看出,这些SQL基本上就是EntityFramework生成的。进而总结所有对Trigger表的操作SQL,

Insert Trigger:

insert [Triggers]([BalanceID], [Origin], [Offset], [Target], [TriggerId], [Category], [CreatedTime], [LastUpdatedTime], [Enabled])
values (@0, @1, @2, @3, @4, @5, @6, @7, @8)
select [ID]
from [Triggers]
where @@ROWCOUNT > 0 and [ID] = scope_identity()',N'@0 bigint,@1 bigint,@2 bigint,@3 bigint,@4 bigint,@5 smallint,@6 datetime2(7),@7 datetime2(7),@8 bit',@0=14,@1=0,@2=0,@3=2000,@4=635062714505193632,@5=0,@6='2013-06-08 07:00:19.8322818',@7='2013-06-08 07:00:19.8322818',@8=1

Update Trigger:

update [Triggers]
set [BalanceID] = @0, [Origin] = @1, [Offset] = @2, [Target] = @3, [TriggerId] = @4, [Category] = @5, [CreatedTime] = @6, [LastUpdatedTime] = @7, [Enabled] = @8
where ([ID] = @9)
',N'@0 bigint,@1 bigint,@2 bigint,@3 bigint,@4 bigint,@5 smallint,@6 datetime2(7),@7 datetime2(7),@8 bit,@9 bigint',@0=14,@1=0,@2=0,@3=266,@4=635062714505193629,@5=3,@6='2013-06-08 07:00:19.8292815',@7='2013-06-08 07:00:37.1550139',@8=0,@9=111

Select Trigger:

SELECT
[Extent1].[ID] AS [ID],
[Extent1].[BalanceID] AS [BalanceID],
[Extent1].[Origin] AS [Origin],
[Extent1].[Offset] AS [Offset],
[Extent1].[Target] AS [Target],
[Extent1].[TriggerId] AS [TriggerId],
[Extent1].[Category] AS [Category],
[Extent1].[CreatedTime] AS [CreatedTime],
[Extent1].[LastUpdatedTime] AS [LastUpdatedTime],
[Extent1].[Enabled] AS [Enabled]
FROM [Triggers] AS [Extent1]
WHERE ([Extent1].[BalanceID] = @p__linq__0) AND ([Extent1].[Enabled] = 1) AND ([Extent1].[LastUpdatedTime] > @p__linq__1)',N'@p__linq__0 bigint,@p__linq__1 datetime2(7)',@p__linq__0=22,@p__linq__1='2013-06-08 06:15:56.6259518'

这些SQL都已经是简单到不能再简单的SQL了,但根据上图描述,在多个Key上产生了X锁,在多个Page上产生了IX锁,而我猜测就是这个IX锁阻塞了所有操作。

但,我还是不知道为什么会产生DeadLock。我只能猜测,一个Update在把持一个Key-X锁,而另一个Update把持着Page-IX锁,他们互相等,而Select在等S锁,并等待X、IX结束。

然后,我尝试了一个很stupid的操作,Kill 77。我把这个session给kill掉了,瞬间,service进程没了。我破坏了现场。

我以为,然后就没有然后了。

是的,在进程死之前,居然打了句Log,真给力啊。至少它明确的告诉了我,“An existing connection was forcibly closed by the remote host.”。这就是我的Kill操作。

而且,其证明了这个session阻塞在 UnitOfWork.Commit() 方法上,也就是最终的 DbContext.SaveChanges() 上。

而这个操作是在Update这个Triggers表,也就是Transaction在这里产生了DeadLock最终没有返回,导致此处代码一直被Block,进而没有释放lock(object),进而阻塞线程,并阻止了其他线程的访问。

最后,我做了些更改,当获取Repository<Trigger>时,增加TransactionScope,并指定ReadUncommitted。

    private static T Get<T>() where T : class
{
T obj = default(T); using (var transactionScope = new TransactionScope(
TransactionScopeOption.RequiresNew,
new TransactionOptions()
{
IsolationLevel = IsolationLevel.ReadUncommitted
}))
{
obj = UnityContainerDispatcher.GetContainer().Resolve<T>();
transactionScope.Complete();
} return obj;
}

在新的一轮测试中,相同Load下,DeadLock没有再产生,只能说“貌似”解决了问题。

 

LoadTest中内存和线程Troubleshooting实战的更多相关文章

  1. LoadTest内存和线程Troubleshooting实战

    在端午节放假的三天中,我对正在开发的 Service 进行了 LoadTest,尝试在增大压力的条件下发现问题. 该 Service 为独立进程的 WCF 服务,宿主于 WindowsService, ...

  2. .NET Core中遇到奇怪的线程死锁问题:内存与线程数不停地增长

    一个 asp.net core 站点,之前运行在Linux 服务器上,运行一段时间后有时站点会挂掉,在日志中记录很多“EMFILE too many open files”的错误: Microsoft ...

  3. MySQL中内存分为全局内存和线程内存

    首先我们来看一个公式,MySQL中内存分为全局内存和线程内存两大部分(其实并不全部,只是影响比较大的 部分): 复制代码 代码如下: per_thread_buffers=(read_buffer_s ...

  4. Java堆内存是线程共享的!面试官:你确定吗?

    Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点,所以,即使是一个Java的初学者,也一定或多或少的对JVM有一些了解.可以说,关于JVM的相关知识,基本是每个Java开发者 ...

  5. 详解线程池的作用及Java中如何使用线程池

    服务端应用程序(如数据库和 Web 服务器)需要处理来自客户端的高并发.耗时较短的请求任务,所以频繁的创建处理这些请求的所需要的线程就是一个非常消耗资源的操作.常规的方法是针对一个新的请求创建一个新线 ...

  6. Spring 中的 bean线程安全性分析

    首先:Spring 中的 bean不是线程安全的 Spring容器中的Bean是否线程安全,容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但 ...

  7. java中内存分配策略及堆和栈的比较

    Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间 ...

  8. java线程内存模型,线程、工作内存、主内存

    转自:http://rainyear.iteye.com/blog/1734311 java线程内存模型 线程.工作内存.主内存三者之间的交互关系图: key edeas 所有线程共享主内存 每个线程 ...

  9. js中各种跨域问题实战小结(二)

    这里接上篇:js中各种跨域问题实战小结(一) 后面继续学习的过程中,对上面第一篇有稍作休整.下面继续第二部分: -->5.利用iframe和location.hash -->6.windo ...

随机推荐

  1. 通信网Project之——单源单宿最短路问题

    最主要的Vertex类: #ifndef VERTEX_H #define VERTEX_H #include <climits> #include <cstddef> #de ...

  2. C#中实现WebBrowser控件的HTML源代码读写

    原文:C#中实现WebBrowser控件的HTML源代码读写 C#中实现WebBrowser控件的HTML源代码读写http://www.blogcn.com/user8/flier_lu/index ...

  3. php设计模式(一):简介及创建型模式

    我们分三篇文章来总结一下设计模式在PHP中的应用,这是第一篇创建型模式. 一.设计模式简介 首先我们来认识一下什么是设计模式: 设计模式是一套被反复使用.容易被他人理解的.可靠的代码设计经验的总结. ...

  4. 剖析Jetty实现原理

    之前写一个简单易用Jetty文章.Jetty对于做JAVA Web发展的方面来说并不陌生,他是一个servlet集装箱,只有相对Tomcat这是比较简单的设计,并且也相对简单,使用灵活,我是学习和使用 ...

  5. Oracle OS认证和口令文件认证方法

    OS认证 1.在SQLNET.ORA(位于$ORACLE_HOME/NETWORK/ADMIN文件夹中)文件里,使用vi编辑,凝视掉#SQLNET.AUTHENTICATION_SERVICES = ...

  6. js模块开发

    js模块开发(一) 现在嵌入页面里面的javascript代码越来越复杂,于是可能依赖也越来越严重,使用别人开发的js也越来越多,于是在理想情况下,我们只需要实现核心的业务逻辑,其他都可以加载别人已经 ...

  7. 探讨css中repaint和reflow

    (个人blog迁移文章.) 前言: 页面设计中,不可避免的需要浏览器进行repaint和reflow.那到底什么是repaint和reflow呢.下面谈谈自己对repaint和reflow的理解,以及 ...

  8. IOS被遗忘的知识

    IOS ARC项目使用非ARC文件 1.自己的旧项目没有使用ARC,可是引入的第三方库却是使用了ARC的. 对于第一个情况,给採用了ARC的源文件,加入-fobjc-arc选项 2.自己的新项目使用了 ...

  9. WP 开发中.xaml 与.xaml.cs

    关于 WP 开发中.xaml 与.xaml.cs 的关系   今天我们先来看一下在WP8.1开发中最长见到的几个文件之间的关系.比较论证,在看这个问题之前我们简单看看.NET平台其他两个不同的框架: ...

  10. 系统预定义委托与Lambda表达式

    NET中那些所谓的新语法之三:系统预定义委托与Lambda表达式   开篇:在上一篇中,我们了解了匿名类.匿名方法与扩展方法等所谓的新语法,这一篇我们继续征程,看看系统预定义委托(Action/Fun ...