一:背景

1. 讲故事

上个月底,有位朋友微信找到我,说他的程序 多线程处理 RabbitMQ 时过慢,帮忙分析下什么原因,截图如下:

这问题抛出来,有点懵逼,没说CPU爆高,也没说内存泄漏,也没说程序卡死。。。鬼知道为啥 Rabbitmq 处理过慢哈 。

既然没有一个缘由,那就往 freeze 这个方向去找吧,上 windbg 说话。

二:Windbg 分析

1. 寻找 freeze 原因

处理过慢原因有很多,可能是干活的人少了,也可能这些人摸鱼去了,总之工作不饱和,接下来看看这帮人有没有摸鱼,从 线程池 看起。


0:000> !tp
CPU utilization: 81%
Worker Thread: Total: 187 Running: 172 Idle: 0 MaxLimit: 32767 MinLimit: 8
Work Request in Queue: 0
--------------------------------------
Number of Timers: 0
--------------------------------------
Completion Port Thread:Total: 1 Free: 1 MaxFree: 16 CurrentLimit: 1 MaxLimit: 1000 MinLimit: 8

从输出中看,当前的 CPU=81%, 大概率是这个程序引起的,既然 CPU 过高,可能是某些计算密集型操作,可能是大量锁导致的上下文切换,也可能是 GC 频繁触发所致。

2. 是 GC 触发吗?

本系列到这里,我相信有很多朋友应该知道如何从线程栈上看是否为 GC 触发所致,用命令 ~*e !dumpstack 经过一通寻找,发现是 214 号线程触发了 GC,如下图所示:

!dumpstack 输出的信息太多,为了让信息更加简化,改用 !clrstack


0:214> !clrstack
OS Thread Id: 0x290c (214)
Child SP IP Call Site
000000bb2983c7d0 00007ffed8c3e335 System.Text.UTF8Encoding.GetString(Byte[], Int32, Int32)
000000bb2983c810 00007ffe7aaecee9 StackExchange.Redis.RedisValue.op_Implicit(StackExchange.Redis.RedisValue)
000000bb2983c860 00007ffe7aaecd92 xxxx.Util.Helper.RedisHelper.ConvertObj[[System.__Canon, mscorlib]](StackExchange.Redis.RedisValue)
000000bb2983c8f0 00007ffe7abc3997 xxxx.Util.Helper.RedisHelper+c__DisplayClass29_0`1[[System.__Canon, mscorlib]].b__0(StackExchange.Redis.IDatabase)
000000bb2983ca20 00007ffe7aaeaca0 xxxx.Util.Helper.RedisHelper.Do[[System.__Canon, mscorlib]](System.Func`2<StackExchange.Redis.IDatabase,System.__Canon>)
000000bb2983caa0 00007ffe7abc3762 xxxx.Util.Helper.RedisHelper.HashGet[[System.__Canon, mscorlib]](System.String, System.String)
000000bb2983cb40 00007ffe7abc2f3d xxxx.Implementation.xxx.GetCompany(System.String)
...
000000bb2983e170 00007ffe7ad2fadf xxx.xxx.MQ.xxx+c__DisplayClass21_0`1[[System.__Canon, mscorlib]].b__1()
000000bb2983e1c0 00007ffed8c11862 System.Threading.Tasks.Task`1[[System.__Canon, mscorlib]].InnerInvoke()
000000bb2983e220 00007ffed8c10937 System.Threading.Tasks.Task.Execute()
000000bb2983e260 00007ffed8bc674e System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
000000bb2983e330 00007ffed8bc65e7 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
000000bb2983e360 00007ffed8c10bdd System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef)
000000bb2983e410 00007ffed8c10303 System.Threading.Tasks.Task.ExecuteEntry(Boolean)
000000bb2983e450 00007ffed8bcfa10 System.Threading.ThreadPoolWorkQueue.Dispatch()
000000bb2983e8e8 00007ffed9d465d3 [DebuggerU2MCatchHandlerFrame: 000000bb2983e8e8]

从卦中信息看,程序在 RedisHelper.HashGet 操作的过程中触发了 GC,我去,难道是从 redis 中读了什么大数据?接下来用 !clrstack -a 看看到底从redis中读了个啥?


0:214> !clrstack -a
OS Thread Id: 0x290c (214)
Child SP IP Call Site
000000bb2983c860 00007ffe7aaecd92 xxx.Util.Helper.RedisHelper.ConvertObj[[System.__Canon, mscorlib]](StackExchange.Redis.RedisValue)
PARAMETERS:
this (0x000000bb2983c8f0) = 0x000000bb182da3d0
value (0x000000bb2983c960) = 0x000000bb18440778
LOCALS:
0x000000bb2983c8c4 = 0x0000000000000000
0x000000bb2983c8a0 = 0x0000000000000000
0x000000bb2983c898 = 0x0000000000000000 0:214> !do 0x000000bb18440778
Name: System.Byte[]
MethodTable: 00007ffed8db93d0
EEClass: 00007ffed87f4dc8
Size: 6679(0x1a17) bytes
Array: Rank 1, Number of elements 6655, Type Byte (Print Array)
Content: {"ID":104x,"Code":"130x","xxx":"1304","xxx":"8888","Name":"...............","xxx":"...............","MqConnStr"

可以看到,redis 读了大概 6.6 k 的数据,才这么点数据,我还以为 几十M 呢 , 要说这点东西就能触发GC,我是不相信的 。

3. 到底是什么诱使了 GC 触发 ?

那这个 罪魁祸首 到底是谁呢? 其实仔细想一想,GC触发无非就是将对象从 gen0 推到 gen1,然后由 gen1 推到 gen2,那朋友的这个 RabbitMQ 处理慢,肯定是触发了多次GC,才有了这个肉眼可见的处理慢。

这里就有一个突破点,既然触发多次GC,那gen2上肯定有很多带根的对象,所以从这里入手比较好,用 !dumpgen 命令查看。


0:214> !dumpgen 2 -stat
Count Total Size Type
-------------------------------------------------
87,738 24,925,316 System.Int32[]
509,643 56,399,716 System.String
83,077 65,760,888 System.Reflection.Emit.__FixupData[]
2,158,346 69,067,072 System.Reflection.Emit.GenericMethodInfo
92,388 172,765,392 System.Object[]
796,870 179,587,796 **** FREE ****
14,030,441 336,730,584 System.RuntimeMethodHandle
428,376 348,743,274 System.Byte[]

我去,gen2 上居然有 1.4 kw 的对象,这太让人好奇了,接下来随便抽几个 address ,看看它的引用根是个啥?


0:214> !dumpgen 2 -type System.RuntimeMethodHandle
Object MT Size Name
-------------------------------------------------------------------
000000bac9f8fe68 00007FFED8DB9978 24 System.RuntimeMethodHandle
000000bac9f8fe80 00007FFED8DB9978 24 System.RuntimeMethodHandle
000000bac9f8fe98 00007FFED8DB9978 24 System.RuntimeMethodHandle
000000bac9f8feb0 00007FFED8DB9978 24 System.RuntimeMethodHandle
000000bac9f8fec8 00007FFED8DB9978 24 System.RuntimeMethodHandle
...
000000baca1af510 00007FFED8DB9978 24 System.RuntimeMethodHandle
000000baca1af548 00007FFED8DB9978 24 System.RuntimeMethodHandle
000000baca1af560 00007FFED8DB9978 24 System.RuntimeMethodHandle
000000baca1afd90 00007FFED8DB9978 24 System.RuntimeMethodHandle
000000baca1afde0 00007FFED8DB9978 24 System.RuntimeMethodHandle 0:214> !gcroot 000000baca1afde0
Thread 187c0:
000000baeeb5cb30 00007ffed8ba9c60 System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].FindEntry(System.__Canon)
rsi:
-> 000000baca4c6ef0 System.Collections.Generic.Dictionary`2[[System.Tuple`4[[System.String, mscorlib],[System.String, mscorlib],[System.Int32, mscorlib],[System.Int32, mscorlib]], mscorlib],[System.Delegate, mscorlib]]
-> 000000badab3b8a0 System.Collections.Generic.Dictionary`2+Entry[[System.Tuple`4[[System.String, mscorlib],[System.String, mscorlib],[System.Int32, mscorlib],[System.Int32, mscorlib]], mscorlib],[System.Delegate, mscorlib]][]
-> 000000baca1b5210 System.Func`2[[System.Data.IDataReader, System.Data],[xxx.BAR_AccountDC, xxxx]]
-> 000000baca1ae8b0 System.Reflection.Emit.DynamicMethod+RTDynamicMethod
-> 000000baca1ae7f8 System.Reflection.Emit.DynamicMethod
-> 000000baca1b51b0 System.RuntimeMethodInfoStub
-> 000000baca1b4a18 System.Reflection.Emit.DynamicResolver
-> 000000baca1aea60 System.Reflection.Emit.DynamicScope
-> 000000baca1aea78 System.Collections.Generic.List`1[[System.Object, mscorlib]]
-> 000000baca1b3b20 System.Object[]
-> 000000baca1afde0 System.RuntimeMethodHandle

从引用链看,它是被 Dictionary 所持有,那我们就 dump 一下这个 dictionary,看看是个啥。


0:214> !do 000000baca4c6ef0
Name: System.Collections.Generic.Dictionary`2[[System.Tuple`4[[System.String, mscorlib],[System.String, mscorlib],[System.Int32, mscorlib],[System.Int32, mscorlib]], mscorlib],[System.Delegate, mscorlib]]
MethodTable: 00007ffe7a937f18
EEClass: 00007ffed87f7cb8
Size: 80(0x50) bytes
File: C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ffed8dc3e30 400182a 8 System.Int32[] 0 instance 000000badaaa2c10 buckets
00007ffed9874d38 400182b 10 ...non, mscorlib]][] 0 instance 000000badab3b8a0 entries
00007ffed8dc3e98 400182c 38 System.Int32 1 instance 83000 count
00007ffed8dc3e98 400182d 3c System.Int32 1 instance 83000 version
00007ffed8dc3e98 400182e 40 System.Int32 1 instance -1 freeList
00007ffed8dc3e98 400182f 44 System.Int32 1 instance 0 freeCount
00007ffed8d9a430 4001830 18 ...Canon, mscorlib]] 0 instance 000000baca2ec958 comparer
00007ffed8d9c550 4001831 20 ...Canon, mscorlib]] 0 instance 0000000000000000 keys
00007ffed8dcef28 4001832 28 ...Canon, mscorlib]] 0 instance 0000000000000000 values
00007ffed8dc1c98 4001833 30 System.Object 0 instance 0000000000000000 _syncRoot 0:214> !objsize 000000baca4c6ef0
sizeof(000000baca4c6ef0) = 1116325152 (0x4289c520) bytes (System.Collections.Generic.Dictionary`2[[System.Tuple`4[[System.String, mscorlib],[System.String, mscorlib],[System.Int32, mscorlib],[System.Int32, mscorlib]], mscorlib],[System.Delegate, mscorlib]])

可以看到,当前dict 的 size= 8.3w,总大小为 1.1G, 然后用 !mdt 抽查一下字典内容。

可以看出,字典中的item大概都是 key= select * from xxx where AccountNo= xxxxvalue = Func

4. 查看源码

!gcroot 中知道当前根在 187c0 号线程,然后我们通过线程栈去寻找下源码。


0:089> ~~[187c0]s
ntdll!NtWaitForSingleObject+0xa:
00007ffe`e9ea06fa c3 ret
0:089> !clrstack
OS Thread Id: 0x187c0 (89)
Child SP IP Call Site
000000baeeb5caa0 00007ffed9718dfe System.Tuple`4[[System.__Canon, mscorlib],[System.__Canon, mscorlib],[System.Int32, mscorlib],[System.Int32, mscorlib]].System.Collections.IStructuralEquatable.GetHashCode(System.Collections.IEqualityComparer)
000000baeeb5cb00 00007ffed8ba90a7 System.Collections.Generic.ObjectEqualityComparer`1[[System.__Canon, mscorlib]].GetHashCode(System.__Canon)
000000baeeb5cb30 00007ffed8ba9c60 System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].FindEntry(System.__Canon)
000000baeeb5cba0 00007ffed8bc53a4 System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].TryGetValue(System.__Canon, System.__Canon ByRef)
000000baeeb5cbe0 00007ffe7abd8069 xxx.Internal.Cache`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].Get(System.__Canon, System.Func`1<System.__Canon>)
000000baeeb5cc50 00007ffe7abd6d52 xxx.Database+d__49`1[[System.__Canon, mscorlib]].MoveNext()
000000baeeb5ccd0 00007ffed733768e System.Linq.Enumerable.FirstOrDefault[[System.__Canon, mscorlib]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
000000baeeb5cd40 00007ffe7af5489e xxx.xxx.ParsePapPayDebitRecord(xxx, xxx.BAR_AccountDC ByRef, System.String ByRef, xxx ByRef, Boolean, xxx, Boolean)
...

从线程栈看,程序做了一个 db 查询的操作,源码大概如下:


public static InvalidTxnCode ParsePapPayDebitRecord(xxx)
{
sql = "select * FROM [dbo].[xxx] where AccountNO = '" + transRecord.AccountNO + "'";
IEnumerable<BAR_AccountDC> enumerable3 = new YiBll<BAR_AccountDC>(transRecord.AccountNO).QuerySql(sql);
}

问题就出现在这个 sql 拼接上,底层的框架为了提高 mapping 速度,将 sql 和与之对应的 Func<DataReader,Data> 做了缓存处理,由于每次sql都不一样,这就导致底层的 dict越来越大,直到目前的 1.1 G。

三:总结

总的来说,本次事故是由于不断增长的 dict 在不断的挤压小对象堆,加之程序多线程火力全开分配对象导致gc在小对象堆上的频繁回收造成的 rabbitmq 处理过慢。

修改办法就是将 sql 参数化。


sql = "select * FROM [dbo].[xxx] where AccountNO = '" + transRecord.AccountNO + "'";

改成


sql = "select * FROM [dbo].[xxx] where AccountNO = @AccountNO";

最后提一下,这个dump有一定的迷惑性,因为触发GC的线程并不是罪魁祸首,它只不过是压死骆驼的那最后一根稻草罢了。

RabbitMQ 处理过慢,原来是一个 SQL 缓存框架导致的 GC 频繁触发的更多相关文章

  1. Oracle数据库该如何着手优化一个SQL

    这是个终极问题,因为优化本身的复杂性实在是难以总结的,很多时候优化的方法并不是用到了什么高深莫测的技术,而只是一个思想意识层面的差异,而这些都很可能连带导致性能表现上的巨大差异. 所以有时候我们应该先 ...

  2. MySQL GROUP_CONCAT函数使用示例:如何用一个SQL查询出一个班级各个学科第N名是谁?

    如何用一个SQL查询出一个班级各个学科第N名是谁? 首先贴出建表语句,方便大家本地测试: -- 建表语句 CREATE TABLE score ( id INT NOT NULL auto_incre ...

  3. 一个 Sql语句优化的问题- STATISTICS 统计信息

    前段时间,同事遇到一个 Sql语句的问题,一个列表分页功能响应在30 s以上,看数据库里面的数据条数,数据量也不大,相关字段的一些索引也都有,可就是慢.于是找出具体的sql 语句出来分析,分页功能主要 ...

  4. 用scala实现一个sql执行引擎-(上)

    前言 在实时计算中,通常是从队列中收集原始数据,这种原始数据在内存中通常是一个java bean,把数据收集过来以后,通常会把数据落地到数据库,供后面的ETL使用.举个一个简单的例子,对一个游戏来说, ...

  5. SqlServer 中如何查看某一个Sql语句是复用了执行计划,还是重新生成了执行计划

    我们知道SqlServer的查询优化器会将所执行的Sql语句的执行计划作缓存,如果后续查询可以复用缓存中的执行计划,那么SqlServer就会为后续查询复用执行计划而不是重新生成一个新的执行计划,因为 ...

  6. 一个SQL Server 2008 R2 死锁的问题解决

    问题场景:在客户那碰到一个操作卡死的现象 问题解决: 1.如何挂钩是死锁问题:通过代码跟踪,发现是指执行一个SQL语句超时,因此猜想可能是表锁住了 2.如果确认是思索问题:通过SQL发现死锁,以下是相 ...

  7. 一个sql导致temp表空间爆掉

    Buffer sort引发的血案 今天遇到的一个问题,在线系统上,有两张表,test1大概50G,test2大概200G,需要查询出来test1表中部分记录,并且这些记录不存在test2表中.于是就写 ...

  8. 自己实现一个SQL解析引擎

    自己实现一个SQL解析引擎 功能:将用户输入的SQL语句序列转换为一个可运行的操作序列,并返回查询的结果集. SQL的解析引擎包含查询编译与查询优化和查询的执行,主要包含3个步骤: 查询分析: 制定逻 ...

  9. 一个sql的优化

    原文:一个sql的优化 目的:为了查询某天某个服务器上的登录id的个数   刚开始编写的sql: select count(a.mac) logusers from Log_MacLogin_All ...

随机推荐

  1. Golang使用swaggo自动生成Restful API文档

    #关于Swaggo 相信很多程序猿和我一样不喜欢写API文档.写代码多舒服,写文档不仅要花费大量的时间,有时候还不能做到面面具全.但API文档是必不可少的,相信其重要性就不用我说了,一份含糊的文档甚至 ...

  2. Douban Top 250爬虫

    # Ref: https://fishc.com.cn/forum.php?mod=viewthread&tid=101887&extra=page%3D1%26filter%3Dty ...

  3. Notepad++离线安装使用Markdown插件

    1.介绍 MarkdownViewerPlusPlus是Notepad++的Markdown插件, 这个Markdown插件可以在Notepad++中实时动态渲染, 可以同步查看使用Markdown的 ...

  4. ThreadLocal基本使用和内存泄漏分析

    ThreadLocal基础部分 ThreadLoal的作用 保存线程的独立变量,即每个线程维护一份.这种变量在线程的生命周期内起作用,减少同一个线程内多个函数之间公共变量传递麻烦. 使用场景 需要给不 ...

  5. feign的一个注解居然隐藏这么多知识!

    引言 最近由于业务的需要,需要接入下阿里云的一个接口,打开文档看了看这个接口看下来还是比简单的目测个把小时就可以搞定,但是接入的过程还是比较坎坷的.首先我看了看他给的示例,首先把阿里云文档推荐的dem ...

  6. Python读取网页表格数据

    学会了从网格爬取数据,就可以告别从网站一页一页复制表格数据的时代了. 说个亲身经历的事: 以前我的本科毕业论文是关于"燃放烟花爆竹和空气质量"之间关系的,就要从环保局官网查资料. ...

  7. DL4J实战之五:矩阵操作基本功

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  8. flask 之上传本地图片

    项目配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import os class Config(object): DEBUG = True SQLALCHEMY_DATABA ...

  9. 解决el-checkbox-group 的v-model无法绑定对象数组

    elementUI官方文档中el-checkbox-group组件绑定的都为一维数组,真实业务中数据绑定往往是多个键值对的对象数组,本文主要解决这个问题. 如下代码: <el-checkbox- ...

  10. 如何将jdk12的源码导入idea

    如何将jdk12的源码导入idea中 一 首先,在idea中新建一个java工程 接着,在本地找到jdk所在的文件目录,进入jdk目录,找到javasrc目录或者一个src.zip的压缩包, 在向下或 ...