Windbg调优Kafka.Client内存泄露
从来没写过Blog,想想也是,工作十多年了,搞过N多的架构、技术,不与大家分享实在是可惜了。另外,从传统地ERP行业转到互联网,也遇到了很所前所未有的问题,原来知道有一些坑,但是不知道坑太多太深。借着填坑的机会,把过程Log下来。
言归正传,先说说背景吧。Teld的业务平台中存在大量的物联网终端传感数据和车辆运行数据,这些数据中蕴含着大量的财富。So,要存储。Teld的充电终端还是很NB的,现在已经有2W+,而且每隔30S上报一次数据,当然单条数据量不会很大。这才是开始,按照国家规划,到2020年,我们要到百万级别了。擦,说的太远了!换算了一下,仅充电终端上报数据的TPS要求还是挺高的。通过2个月的研究和技术选型,我们选用Kafka作为海量数据处理的应用中间件。
好吧!选了Kafka,开始填坑吧。由于我们采用了.net技术路线,Kafka Client也必须是.net的。…(此处省略1万字),Kafka环境顺利调试成功,但是基于Kafka.Client编写的Consumer程序却出现严重的内存泄露。
Consumer程序需长时间运行,上图仅仅运行了2个小时后的内存就达到了570M。果断抓Dump,Windbg分析。
启动Windbg,设置符号文件,加载Dump。
执行下面命令:
.loadby sos clr (说明:程序是4.0的,2.0请问度娘)。
!dumpheap –stat (说明:按照类型显示堆中的对象数量和内存占用大小)
执行结果:
00007ff947e2f2e8 1215019 29160456 Kafka.Client.Common.NoOpTimer
00007ff947e2f1a8 1215019 29160456 Kafka.Client.Metrics.KafkaTimer
00007ff947e39600 1215018 38880576 Kafka.Client.Consumers.FetchRequestAndResponseMetrics
00007ff947e2df70 1215018 38880576 Kafka.Client.Common.ClientIdAndBroker
00007ff947e3a058 1215007 58320336 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[Kafka.Client.Common.ClientIdAndBroker, Kafka.Client],[Kafka.Client.Consumers.FetchRequestAndResponseMetrics, Kafka.Client]]
00007ff9a5cc3d60 1267853 86313134 System.String
通过执行结果可以看到,NoOpTimer、KafkaTimer、TetchRequestAndResponseMetrics、ConcurrentDictionary对象每类都有120w+,占用内存近200M。好吧,好像是这几个家伙的原因,矛头直指Kafka.Client。选取NoOpTimer,先看看gcroot情况吧,继续!
执行命令:(对象太多了,命令运行一会,break吧。)
!dumpheap -mt 00007ff947e2f2e8
执行结果:
000021ae62af490 00007ff947e2f2e8 24
0000021ae62af5a8 00007ff947e2f2e8 24
0000021ae62af6c0 00007ff947e2f2e8 24
0000021ae62af7c0 00007ff947e2f2e8 24
0000021ae62af890 00007ff947e2f2e8 24
0000021ae62af960 00007ff947e2f2e8 24
0000021ae62afa30 00007ff947e2f2e8 24
0000021ae62afb00 00007ff947e2f2e8 24
0000021ae62afc18 00007ff947e2f2e8 24
0000021ae62afd18 00007ff947e2f2e8 24
执行结果的第一列为NoOpTimer对象的地址。查看gcroot情况。
执行命令:
!gcroot 000021ae62af490
执行结果:
0000021ae58965a8 Teld.Core.Log.Processor.ProcessService
-> 0000021ae58966a8 System.Collections.Generic.List`1[[Teld.Core.Log.Processor.LogListener, Teld.Core.Log.Processor]]
-> 0000021ae5898068 Teld.Core.Log.Processor.LogListener[]
-> 0000021ae5897b38 Teld.Core.Log.Processor.LogListener
-> 0000021ae5897b78 Teld.Core.Log.Processor.KafkaConsumer
-> 0000021a8ac0de20 Kafka.Client.Consumers.ZookeeperConsumerConnector
-> 0000021a90839800 Kafka.Client.Consumers.ConsumerFetcherManager
-> 0000021a90839908 System.Collections.Generic.Dictionary`2[[Kafka.Client.Server.BrokerAndFetcherId, Kafka.Client],[Kafka.Client.Server.AbstractFetcherThread, Kafka.Client]]
-> 0000021a92dcd208 System.Collections.Generic.Dictionary`2+Entry[[Kafka.Client.Server.BrokerAndFetcherId, Kafka.Client],[Kafka.Client.Server.AbstractFetcherThread, Kafka.Client]][]
-> 0000021a962e2710 Kafka.Client.Consumers.ConsumerFetcherThread
-> 0000021a962e2a70 Kafka.Client.Consumers.SimpleConsumer
-> 0000021ae58fcca8 Kafka.Client.Consumers.FetchRequestAndResponseStats
-> 0000021ae58fccd8 Kafka.Client.Utils.Pool`2[[Kafka.Client.Common.ClientIdAndBroker, Kafka.Client],[Kafka.Client.Consumers.FetchRequestAndResponseMetrics, Kafka.Client]]
-> 0000021a91cb17f8 System.Collections.Concurrent.ConcurrentDictionary`2+Tables[[Kafka.Client.Common.ClientIdAndBroker, Kafka.Client],[Kafka.Client.Consumers.FetchRequestAndResponseMetrics, Kafka.Client]]
-> 0000021af64f1728 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[Kafka.Client.Common.ClientIdAndBroker, Kafka.Client],[Kafka.Client.Consumers.FetchRequestAndResponseMetrics, Kafka.Client]][]
-> 0000021a91c82b18 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[Kafka.Client.Common.ClientIdAndBroker, Kafka.Client],[Kafka.Client.Consumers.FetchRequestAndResponseMetrics, Kafka.Client]]
-> 0000021ae62af470 Kafka.Client.Consumers.FetchRequestAndResponseMetrics
-> 0000021ae62af4a8 Kafka.Client.Metrics.KafkaTimer
-> 0000021ae62af490 Kafka.Client.Common.NoOpTimer
通过执行结果可以看到,NoOpTimer对象被FetchRequestAndResponseMetric所持有,而FetchRequestAndResponseMetric好像被缓存到ConcurrentDictionary中了。ConcurrentDictionary这一坨看着这么熟悉呢,fuck!刚才!dumpheap –stat的结果里面有它!那就再分析ConCurrentDictionary类型看看吧。继续!
执行命令:(00007ff947e3a058 是第一次!dumpheap –stat 执行结果中的ConcurrentDictionary类型第一列的值(MT)。)
!dumpheap -mt 00007ff947e3a058
执行结果:(随机截取一段)
0000021aefcd5a90 00007ff947e3a058 48
0000021aefcd5c20 00007ff947e3a058 48
0000021aefcd5d60 00007ff947e3a058 48
0000021aefcd5ef0 00007ff947e3a058 48
0000021aefcd6030 00007ff947e3a058 48
0000021aefcd65e8 00007ff947e3a058 48
0000021aefcd6790 00007ff947e3a058 48
0000021aefcd68d8 00007ff947e3a058 48
0000021aefcd6a68 00007ff947e3a058 48
随机选取一个,继续查看gcroot情况。
执行命令:
!gcroot 0000021aefcd6a68
执行结果:
0000021ae58965a8 Teld.Core.Log.Processor.ProcessService
-> 0000021ae58966a8 System.Collections.Generic.List`1[[Teld.Core.Log.Processor.LogListener, Teld.Core.Log.Processor]]
-> 0000021ae5898068 Teld.Core.Log.Processor.LogListener[]
-> 0000021ae58970a8 Teld.Core.Log.Processor.LogListener
-> 0000021ae58970e8 Teld.Core.Log.Processor.KafkaConsumer
-> 0000021a8cedba08 Kafka.Client.Consumers.ZookeeperConsumerConnector
-> 0000021a94f56710 Kafka.Client.Consumers.ConsumerFetcherManager
-> 0000021a94f56818 System.Collections.Generic.Dictionary`2[[Kafka.Client.Server.BrokerAndFetcherId, Kafka.Client],[Kafka.Client.Server.AbstractFetcherThread, Kafka.Client]]
-> 0000021a94f5bd20 System.Collections.Generic.Dictionary`2+Entry[[Kafka.Client.Server.BrokerAndFetcherId, Kafka.Client],[Kafka.Client.Server.AbstractFetcherThread, Kafka.Client]][]
-> 0000021a962e5e80 Kafka.Client.Consumers.ConsumerFetcherThread
-> 0000021a962e61e0 Kafka.Client.Consumers.SimpleConsumer
-> 0000021ae58f60e8 Kafka.Client.Consumers.FetchRequestAndResponseStats
-> 0000021ae58f6118 Kafka.Client.Utils.Pool`2[[Kafka.Client.Common.ClientIdAndBroker, Kafka.Client],[Kafka.Client.Consumers.FetchRequestAndResponseMetrics, Kafka.Client]]
-> 0000021a89deda70 System.Collections.Concurrent.ConcurrentDictionary`2+Tables[[Kafka.Client.Common.ClientIdAndBroker, Kafka.Client],[Kafka.Client.Consumers.FetchRequestAndResponseMetrics, Kafka.Client]]
-> 0000021af5a43128 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[Kafka.Client.Common.ClientIdAndBroker, Kafka.Client],[Kafka.Client.Consumers.FetchRequestAndResponseMetrics, Kafka.Client]][]
-> 0000021aefcd6a68 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[Kafka.Client.Common.ClientIdAndBroker, Kafka.Client],[Kafka.Client.Consumers.FetchRequestAndResponseMetrics, Kafka.Client]]
通过结果可以看到,ConcurrentDictionary被Pool引用,而Pool又被FetchRequestAndResponseStats引用。这与NoOpTimer类型的引用情况很相似啊!
搜一下第一次!dumpheap –stat 的结果,发现FetchRequestAndResponseStats和Pool类型的对象数量只有11个。
00007ff947e387f8 11 528 Kafka.Client.Consumers.FetchRequestAndResponseStats
7ff947e397d8 11 792 Kafka.Client.Utils.Pool`2[[Kafka.Client.Common.ClientIdAndBroker, Kafka.Client],[Kafka.Client.Consumers.FetchRequestAndResponseMetrics, Kafka.Client]]
看来,100多万个对象都是从Pool上来的。果断翻开kafka.Client的源代码。
internal class FetchRequestAndResponseStats
{
private string clientId;private Func<ClientIdAndBroker, FetchRequestAndResponseMetrics> valueFactory;
private Pool<ClientIdAndBroker, FetchRequestAndResponseMetrics> stats;private FetchRequestAndResponseMetrics allBrokerStats;
public FetchRequestAndResponseStats(string clientId)
{
this.clientId = clientId;
this.valueFactory = k => new FetchRequestAndResponseMetrics(k);
this.stats = new Pool<ClientIdAndBroker, FetchRequestAndResponseMetrics>(this.valueFactory);
this.allBrokerStats = new FetchRequestAndResponseMetrics(new ClientIdAndBroker(clientId, "AllBrokers"));
}public FetchRequestAndResponseMetrics GetFetchRequestAndResponseAllBrokersStats()
{
return this.allBrokerStats;
}public FetchRequestAndResponseMetrics GetFetchRequestAndResponseStats(string brokerInfo)
{
return this.stats.GetAndMaybePut(new ClientIdAndBroker(this.clientId, brokerInfo + "-"));
}
}
Pool类型的对象是FetchRequestAndResponseStats的一个属性,并且Pool是继承自ConcurrentDictionary,Key的类型为ClientIdAndBroker。Pool的定义如下:
public class Pool<TKey, TValue> : ConcurrentDictionary<TKey, TValue>
{
public Func<TKey, TValue> ValueFactory { get; set; }public Pool(Func<TKey, TValue> valueFactory = null)
{
this.ValueFactory = valueFactory;
}public TValue GetAndMaybePut(TKey key)
{
if (this.ValueFactory == null)
{
throw new KafkaException("Empty value factory in pool");
}
return this.GetOrAdd(key, this.ValueFactory);
}}
问题来了,FetchRequestAndResponseStats.GetFetchRequestAndResponseStats方法,每次New ClientIdAndBroker 对象后,调用Pool.GetAndMaybePut方法。擦!!!每次访问都是新对象,这个对象是要作为ConcurrentDictionary的Key存入的。并且存入方法调用的是ConcurrentDictionary.GetOrAdd()。新建的对象只能从ConcurrentDictionary中Add,没有任何Get到的可能性啊。Kafka.Client中竟然会出现这么低级的问题,瞬间对开源的组件有了新的认识:开源组件的坑太深了,不填不知道啊。
抓紧把开源组件的代码改一下吧。把Pool的key类型从ClientIdAndBroker改为string。调试运行,下面是Run了2天的Consumer程序的内存占用情况,期间Consumer已经处理了60万日志。
问题终于完美解决了!最后,国际惯例,感谢JuQiang老师指导。在互联网领域,我是个新手,Blog中难免存在一些不客观,不成熟的见解,还请多多包涵!
Windbg调优Kafka.Client内存泄露的更多相关文章
- 《Apache Kafka实战》读书笔记-调优Kafka集群
<Apache Kafka实战>读书笔记-调优Kafka集群 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.确定调优目标 1>.常见的非功能性要求 一.性能( ...
- Kafka性能调优 - Kafka优化的方法
今天,我们将讨论Kafka Performance Tuning.在本文“Kafka性能调优”中,我们将描述在设置集群配置时需要注意的配置.此外,我们将讨论Tuning Kafka Producers ...
- 【总结】性能调优:JVM内存调优相关文章
[总结]性能调优:JVM内存诊断工具 [总结]性能调优:CPU消耗分析 [总结]性能调优:消耗分析 JVM性能调优
- golang kafka clinet 内存泄露问题处理
go 内存泄露 新版本服务跑上一天内存占用20g,显然是内存泄露 内存泄露的问题难在定位 技术上的定位 主要靠 pprof 生成统计文件 之前写web项目 基于net/http/pprof 可以看到运 ...
- 系统性能调优CPU与内存
CPU相关术语 处理器:插到系统插槽或者处理器版上的物理芯片,以核或者硬件线程的方式包含了一块或者多块CPU. 核:一颗多核处理器上的一个独立CPU实例.核的使用时处理器扩展的一种方式,有称为芯片级多 ...
- Spark学习之路 (十一)SparkCore的调优之Spark内存模型
摘抄自:https://www.ibm.com/developerworks/cn/analytics/library/ba-cn-apache-spark-memory-management/ind ...
- Tomcat性能调优及JVM内存工作原理
Java性能优化方向:代码运算性能.内存回收.应用配置. 注:影响Java程序主要原因是垃圾回收,下面会重点介绍这方面 代码层优化:避免过多循环嵌套.调用和复杂逻辑.Tomcat调优主要内容如下:1. ...
- 性能调优-CPU方面,内存方面
CPU调优 首先要清楚数据库应用的分类,一般分为两类:OLTP(Online Transaction Processing,在线事务处理)和OLAP(Online Analytical Process ...
- Spark学习之路 (十一)SparkCore的调优之Spark内存模型[转]
概述 Spark 作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着非常重要的角色.理解 Spark 内存管理的基本原理,有助于更好地开发 Spark 应用程序和进行性能调优.本文旨在 ...
随机推荐
- js 获取当前系统时间
Js获取当前日期时间及其它操作 var myDate = new Date();myDate.getYear(); //获取当前年份(2位)myDate.getFullYear(); //获取完整的年 ...
- JAVA 8 Lambda表达式-Lambda Expressions
Lambda表达式介绍 Lambda表达式是在java规范提案JSR 335中定义的,Java 8 中引入了Lambda表达式,并被认为是Java 8最大的新特性,Lambda表达式促进了函数式编程, ...
- Java集合框架的接口和类层次关系结构图
Collection和Collections的区别 首先要说的是,"Collection" 和 "Collections"是两个不同的概念: 如下图所示,&qu ...
- sql语句 在字段前面加0
目前数字如下: 1 2 3 10 想变成长度为5位,如果不够,前面补0, 写法如下 select right('00000'+ltrim(你的字段),5) 结果: 00001 00002 00003 ...
- Tomcat指定特定JDK版本
我们知道在Linux服务器上,可以安装多个版本的JDK,那么当有多个JDK时,是否可以为Tomcat这类应用程序指定对应的JDK版本呢?答案是可以,指定tomcat使用特定的JDK版本.今天正好碰到这 ...
- Java堆、栈和常量池
摘录自 http://www.cnblogs.com/xiohao/p/4296088.html 1. 栈(stack)与堆(heap)都是Java用来在RAM中存放数据的地方.与C++不同,Java ...
- android动态注册监听网络变化异常
在使用广播接收器监听网络变化的时候,在AndroidManifest.xml中加入<user-permission android:name="android.permission.A ...
- 今天简单说一下cdc 的使用
从08开始,sql server 提供了一种叫做 变更数据捕获 cdc(Change Data Capture) 的功能,可以通过启用这个功能,来实现查看数据库中的表对象的数据的变化情况.(我感觉就是 ...
- SSH框架配置释义
创建一个bean, 依赖注入支持的不同注入方式及具体配置方法 属性注入 通过setter()方法注入bean的属性或者依赖对象.属性注入要求bean需要提供一个默认构造函数和对应的setter方法 ...
- HBase应用开发回顾与总结系列之二:RowKey行键设计规范
2. RowKey行键设计规范 2.1. RowKey四大特性 2.1.1 字符串类型 虽然行键在HBase中是以byte[]字节数组的形式存储的,但是建议在系统开发过程中将其数据类型设置为Strin ...