在端午节放假的三天中,我对正在开发的 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个线程时,

~ 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实战

    LoadTest中内存和线程Troubleshooting实战 在端午节放假的三天中,我对正在开发的Service进行了LoadTest,尝试在增大压力的条件下发现问题. 该Service为独立进程的 ...

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

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

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

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

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

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

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

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

  6. 判定你的java应用是否正常(是否内存、线程泄漏)的一个简单方法

    给大家推荐一个最简单的判定你的java应用是否正常的方法: step1:部署你的应用,让它跑起来: step2:打开jdk下bin目录下的jconsole.exe工具,连接到你的应用——以监测线程和内 ...

  7. JDK自带内存及线程分析工具

    转载:http://blog.csdn.net/kimylrong/article/details/50970493 查询.监控.分析Java应用程序的时候,我们关注的点主要集中在资源利用,比如CPU ...

  8. 【C/C++】内存对齐规则和实战

    内存对齐规则和实战 这篇文章是我的平时的一个笔记修改后来的.这里主要介绍一下内存对齐的规则,以及提供一些实战一下.几篇我觉得比较好的详细的介绍内存对齐的作用什么的博文会在文末附上. 规则 在开始实战前 ...

  9. HotSpot 自动内存管理笔记与实战

    1.对象的创建 虚拟机遇到一条new指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载.解析和初始化过.如果没有,则必须先进行相应的类的 ...

随机推荐

  1. shell_基础知识

    参考: http://blog.csdn.net/kaizi318/article/details/9343551 开头程序必须以下面的行开始(必须方在文件的第一行):#!/bin/sh符号#!用来告 ...

  2. archlinux pacman 常用选项

    pacman -S package_name #安装软件包pacman -R package_name #删除软件包 pacman -Rs package_name #顺便删除软件包相关依赖pacma ...

  3. Mysql命令行中文乱码的解决方法

    环境:Windows 8 64位,Mysql  5.0.96 for Win64 (x86) 数据库本身安装时默认已经是使用utf8编码的了,但在命令行中执行查询时,查询到的中文依然乱码,解决方法如下 ...

  4. 通过IL分析C#中的委托、事件、Func、Action、Predicate之间的区别与联系

    先说一下个人理解的结论吧: delegate是C#中的一种类型,它实际上是一个能够持有对某个方法的引用的类. delegate声明的变量与delegate声明的事件,并没有本质的区别,事件是在dele ...

  5. Maven3路程(三)用Maven创建第一个web项目(1)

    一.创建项目 1.Eclipse中用Maven创建项目 上图中Next 2.继续Next 3.选maven-archetype-webapp后,next 4.填写相应的信息,Packaged是默认创建 ...

  6. Apache Spark-1.0.1集群搭建

    欢迎经验交流!本文Blog地址:http://www.cnblogs.com/fesh/p/3866791.html Apache Spark   a fast and general engine ...

  7. [转载] 4. JebAPI 之 jeb.api.ui

    本文转载自: https://www.zybuluo.com/oro-oro/note/145250 JebInstance可以通过getUI()方法来获得jeb.api.ui.JebUI. JebU ...

  8. 校验日期函数的js

    /判断输入内容是否为空 function IsNull(){ var str = document.getElementById('str').value.trim(); if(str.length= ...

  9. hibernate执行session.createQuery(hql)时hql若有参数则报错

    项目从Jboss换位Tomcat服务器,打开如下Hql都报错: SELECT COUNT(*) FROM SystemUser WHERE STATUS != -1 解决方法:在Lib中加入antlr ...

  10. java1.7集合源码阅读: Stack

    Stack类也是List接口的一种实现,也是一个有着非常长历史的实现,从jdk1.0开始就有了这个实现. Stack是一种基于后进先出队列的实现(last-in-first-out (LIFO)),实 ...