系统最近老年代的内存上升的比较快,三到四天会发生一波fullGC。于是开始对GC的情况做一波分析。
线上老年代2.7G,年轻带1.3G老年代上升较快,3天一波fullGC,并且fullGC会把内存回收,有时回收一般,有时回收全部。所以判断是不会有内存泄漏现象的,内存发生泄漏是回收不了的。第二个判断,不存在大对象,一个是基于对程序的理解,一个是对于老年代上升的速率,基本是稳固上升。不存在峰值。
我是先用jstack 命令打印出线程状态的 jstack -l pid >> 文件名 发现这么操作是无用的。jstack主要是分析线程状态的,尤其是针对占用CPU,死循环,死锁操作等。
一开始确实定位到一个线程,是我们服务每个一分钟拉去配置中心数据的线程,数据量很小,而且基本都是重复的数据。所以那会没有头绪方向不对。
后来老大说打下内存快照啊。于是就开始看下内存快照了。
1、先使用 jmap命令打印出内存快照文件。
jmap -dump:format=b,file= 文件路径 pid(进程号)
2、使用MemoryAnalyzer(windows下的分析工具)工具分析大对象
分析出来是jdbc连接对象实例太多,因为mysql的jdbc包在每次创建jdbc连接的时候,会对连接进行虚引用包装,最后放到一个ConcurrentHashMap里。而系统1分钟会进行年轻代的GC,而程序配置的链接生命周期为30分钟。所以30分钟MinGC将到达30多次,而JVM默认的配置是15。所以我们推测这个配置的不对,要么减少生命周期要么增加JVM年龄的配置。
经过测试在jdk8的环境下,年龄的阈值是不能超过15的,这就尴尬了。于是只好降低生命周期,同时设置数据源的最大连接和最小连接相等,防止数据源一直创建空闲连接,这样能方式连接数量,同时通过反射,检测ConcurrentHashMap的数量。经过上线后,发现并没有明显效果。于是乎干脆把map的数据直接清空,发现清空了上完线也并没有解决老年代上升的问题。不过老年代每次fullGC都会回收到底。于是判断清空有助于进行回收,并且老年代回收的耗时也变短了,减少到一半时间。
另一方面观测,检查了JVM年轻代 E区和S区的比例,你会认为默认是1:8,但是不是那么简单地。发现程序刚启动时确实是1比8,但是随着程序启动,MinGC,jvm默认的垃圾回收器会自动调节这个比例。只有设置CMS才不会自动调节。于是决策是由于S区分配内存太小导致,当时我们的S区稳定在30M左右,E区差不多有1.3G。于是改成了CMS垃圾回收器,发现也并没有卵用,而且老年代内存到达50%就发生了fullGC,看来CMS是可以设置回收阈值的。实在没办法了,只好换数据源了,把HikairCP换成了druid。发现换了之后就解决了,fullGC频率从4天能到达15天,ConcurrentMap的数量很少,最多也就2百的量。而之前能达到上万的量。总之不知道是因为HikairCP的原因,还是我们配置的原因。据说这个数据源号称比druid速度更优的数据源。
.
.
.
历史是何其的相似,程序性能问题又来了,我们程序为了提高tps,经过压测,发现瓶颈在于redis,当时redis是哨兵模式,读写访问一个master,于是想切换为集群。发现切换为集群后,fullGC的频率又回到了原先的地步,而且把操作切换为读集群时,压力更大,竟然再短短5小时内,打满老年代内存, 并且CPU占用也较大。没办法观测了一会,发现每5分钟上升2个百分点。代码回滚了。于是乎又开始了一波分析,代码优化。
经过分析本次上线只是添加了cluster管道批量操作的代码,代码肯定出现在这里,于是开始进行优化。
首先分析线上内存快照,发现最大占用的是 int 数组,这个不好分析,应为属于基本对象,没法分析。于是乎私底下在本地进行读集群方法的压测,再加上代码的分析,发现每次操作都会对取cluster集群的信息,这个集群的信息包含了所有节点的ip+port,以及他们对应的分槽信息,而这个分槽信息是以LinkedHashSet类型存储的,里面是满满的Integer类型的槽。一共16384个,于是基本确定了,是每次实时查询cluster集群信息导致的。发现Jedis的方法是对集群信息做了缓存的 ,但是只缓存了100毫秒。再加上我们本身程序也包含了大量的业务逻辑。估计这么一连起来就造成了YGC次数过高,或者年轻代S区空间太小。于是从两个方面去提高程序性能。
发现在修改JVM参数配比,提高年轻代S区的内存大小,并且修改垃圾回收器为CMS,发现在本机进行ab压测,最高tps到达了800。并且在并发80,访问量50000的情况下,老年代内存会上升到100M,YGC次数能够到达200。而哨兵的压测结果tps最高到达2000。而且老年代内存50M。YGC次数也在30以内。
于是对cluster的代码进行优化,把每次获取集群节点信息放到了for循环里。tps提高到1300,老年代内存稳定在90M。
我们程序另一条思路是:把集群的信息缓存到JVM,不依靠Jedis的缓存100ms。而是由我们自己缓存。但是存在一个问题,每当集群节点扩容,分槽或者主下线,从上线。我们的程序就检测不到变化了,于是在程序里开启了异步线程每个5秒刷新集群信息,而且对于每次操作cluster做了重试处理,一旦检测到节点连接创建失败,发生MOVED错误,就刷新集群信息,进行操作重试。发现使用静态缓存集群信息的方式,tps提高到2000左右,基本和哨兵持平,而且老年代内存也到达60M左右,YGC次数也降下来了。
在这些过程中,我学到了什么:
自己尝试搭建了虚拟机的redis-cluster集群(很早就搭建了,这回正好用上),redis集群分槽。
cluster管道批量操作,jdk的一些常用工具命令,还有ab的简单压测
最后重要的是分析问题的能力和思路