背景介绍
首先对物联网平台的几个概念做下名词解释
总结一下
产品是一类设施的汇合,物模型形容了这一类设施的性能,包含属性、事件、服务。
比方创维电视是一个产品,而每户家庭中的一个个创维电视则是具体设施,这些电视(设施)都具备雷同的性能,即在创维电视这个产品上定义的性能。比方以后电视的频道、亮度、音量,这些都是具体的属性;比方如果电视的温度高于50摄氏度,则能够上报报警事件;比方能够通过服务调用的形式,来管制电视的关上和敞开,等等。
从以上的示例中,能够总结出创维电视这款产品的物模型定义,包含属性、事件、服务
属性 - 电视状态(开/关)、频道、亮度、音量等等
事件 - 电视温度过高事件
服务 - 管制电视开/关、调整电视亮度
具体的物模型是非常复杂的,局部简单的产品可能蕴含几百几千个属性、事件、服务,因而残缺的物模型是十分微小的。
对于设施每次上报的属性、事件等,物联网平台都会查问出相应的物模型,对设施上报的数据进行校验。
本文记录线上环境,大量设施上报数据,进行物模型校验引起的一次内存告警剖析
以一台单机进行剖析
如上图所示,十几分钟的工夫,内存从50%一路飙升到75%,最终稳固在77%左右不再上涨。
通过监控剖析,在13:40开始,零碎流量有所增长,且都来自于一个租户
该租户是一个测试租户在压测,与相干同学分割后,进行压测,集群重启后内存恢复正常。
问题剖析
Dump剖析
能够看到,占内存的根本是guava cache,本地缓存导致了内存疯狂上涨。
为什么guava cache导致内存上涨?
guava cache本地缓存了物模型对象,size=1000,缓存工夫为一分钟。
对于物模型本地缓存,曾经上线运行了两周,运行比较稳定,为什么此次忽然呈现内存上涨?
剖析该租户下有1000个产品下的设施同时上报,且继续在上报,一个产品对应一个物模型。
本地缓存时,key=产品惟一标识符,value=物模型
每个产品的物模型十分大,有130个属性,单是文本大小曾经达到70KB,理论Java对象占用内存更大。
理论Java对象到底有多大?
shallow heap示意这个对象自身大小
retained heap示意这个对象所有援用对象
对于一个json或map对象,想计算该对象所援用的所有对象大小,应该关注的是retained heap看上图,一个guava cache的entry占用内存 1508096 B ≈ 1508 KB ≈ 1.5 MB
为什么会这么大?有1.5 M
开展来看
entry外部对象有next、valueReference、key等
其中next其实是下一个entry的大小了,图中显示为856512 B ≈ 856 KB,这里不过多关注理论重点关注valueReference
援用了一个JSONObject,这是缓存TSL对象的次要内存占用,大小为 651384 B ≈ 651 KB
即一个物模型对象在内存中的大小约为651 KB
一个物模型对象就如此之大,那么1000个产品的物模型,如果都在本地缓存,势必占用十分大的内存空间。
然而即便如此,为什么会造成内存的继续上涨?为什么GC没有回收掉?
GC日志剖析
查看GC日志,通过肯定解决后如下
分水岭
能够看到
13:40之前,每次YGC后,老年代内存增量平均值为10K左右
13:40之后,每次YGC后,老年代内存增量平均值为35000K左右
间接增长了3500倍
通过下面的GC日志,能够看到,老年代的内存在继续上涨,也就是说,每次YGC后,都有相当一部分对象降职到了老年代。这是导致内存持续增长的根本原因。
线上JVM配置
-Xms5334m
-Xmx5334m
-Xmn2000m
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:MaxDirectMemorySize=1g
-XX:SurvivorRatio=10-Xmn2000m 示意新生代总大小为2000M,从ParNew的GC日志看,新生代总大小理论为1877376K,与2000M有肯定偏差。
且eden: survivor1 : survivor2 = 10:1:1
按新生代总大小2000M计算,survivor大小约为170M
按新生代总大小1877376K计算,survivor大小约为156M
垃圾回收 - 复制算法
新生代分为Eden和2个survivor,其中两个survivor别离叫From Survior和To Survior。
每次应用Eden和From Survivor。
YGC时,将Eden和From Survivor中存活的对象复制到To Survivor空间,最初清理掉Eden和From Survivor空间。
YGC后,From Survivor和To Survivor两块区域会调换,也就是原先的To Survivor会变成下次YGC时的From Survivor区,原先的From Survivor区会变成下次YGC时的To Survivor区。
图一:初始状态
图二:在新生代创建对象
图三:YGC,Eden和From Survivor中存活的对象移到To Survivor中,而后回收Eden和From Survivor的空间。
图四:转换From Survivor和To Survivor。
循环下面的步骤
内存调配策略
对象优先在Eden区调配
大多数状况下,对象在先新生代Eden区中调配。当Eden区没有足够空间进行调配时,虚拟机将发动一次YGC
大对象间接进入老年代
JVM提供了阈值参数-XX:PretenureSizeThreshold,大于参数设置的阈值的对象间接在老年代调配。默认值为0,代表不论多大都是先在Eden中分配内存。
经排查,该参数未设置,默认是0,示意对象都在Eden调配。
对象什么时候进入老年代
策略一:大对象间接进入老年代
有一些占用大量间断内存空间的对象在被加载伊始就会间接进入老年代。这样的大对象个别是一些数组,长字符串之类的对象。
-XX:PretenureSizeThreshold
咱们能够通过这个参数设置。
这种case能够排除,因为目前默认为0,示意对象都在新生代调配。
策略二:长期存活的对象将进入老年代
在对象的对象头信息中存储着对象的年龄,如果每次YGC后对象存活了下来,则年龄会减少。当这个年龄达到15后,这个对象将会降职到老年代。
-XX:MaxTenuringThreshold
咱们能够通过这个参数设置这个年龄值,默认15次存活进入老年代。
这种case能够排除,因为guava cache中对象活不过15次YGC。这个之前认真验证过。
cache size=1000,生效工夫为1分钟。
线上一分钟内YGC 2 ~ 5次,也就是说,缓存中的对象年龄一分钟内最多会减少到5,然而一分钟后缓存生效,这些对象失去了援用,下次回收就能够回收掉这些对象了,因此在年龄没有达到15之前,会被回收掉,失去了达到15后降职到老年代的机会。
线上做过试验。
如果生效工夫改为5分钟,则会造成内存继续上涨,5分钟的时候这些对象年龄达到了15,降职到了老年代。降职到老年代后再被淘汰或者过期生效,YGC曾经回收不掉,除非是fullgc
如果生效工夫改为1分钟后,内存安稳,不再呈现继续上涨。
策略三:对象动静年龄判断
此策略产生在Survivor区。虚拟机并不是永远要求对象的年龄必须达到MaxTenuringThreshold能力降职到老年代,如果在Survivor空间中雷同年龄的对象大小大于survivor空间的一半,那么年龄大于或等于该年龄的对象就能够间接进入老年代,毋庸等到MaxTenuringThreshold要求的年龄。
这种case存在可能性,guava cache中对象,在生效前必然存在于survivor中,如果这些对象的总大小超过了survivor空间的一半,就会降职到老年代,毋庸年龄达到15
然而从GC日志来看,每次老年代的增量为35M左右,没有达到survivor空间的一半(survivor空间有170M,一半有85M左右),因而这种case也能够排除。
策略四:YGC后进行移区,survivor无奈包容的对象将进入老年代。
这是针对复制算法的。以后YGC应用的ParNew收集器,正是应用的复制算法。
新生代分为Eden和2个survivor,每次应用Eden和其中一块survivor。YGC时,将Eden和survivor中还存活的对象一次性复制到另一个survivor空间,最初清理掉Eden和方才应用的survivor空间。如果复制的时候,须要复制的对象总大小超过了survivor空间,则survivor无奈包容的对象将进入老年代。
这种case存在很大可能性,根本能够确定就是这种case引起的内存暴涨。
查看下面的GC日志,每次YGC后,新生代残余大小在170M左右,根本就是survivor填满了,而老年代内存增长了,大概率就是YGC后存活的对象,survivor中放不下了,于是间接进入老年代。
为什么内存上涨到75%后不持续上涨了
75%后,产生了fullgc,回收掉了老年代中曾经过期和曾经被淘汰的TSL对象。
能够看到,每次fullgc后,堆内存都大幅度降落。
从日志看,的确产生了fullgc,且fullgc耗时较短。
老年代应用的CMS回收器,包含4个步骤
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
从新标记(CMS remark)并发革除(CMS concurrent sweep)
其中,初始标记、从新标记这两个步骤依然须要Stop The World
从日志看,初始标记耗时0.04秒,从新标记耗时0.33秒,STW总工夫为0.37秒,对利用影响不大。
为什么fullgc堆内存升高后利用内存没有升高
应用CMS垃圾收集器,Java利用不会把内存还给操作系统。
因而从下面图片能够看到,fullgc后,堆内存明显降低了,然而利用内存还是维持在75%不变。
为什么一般的物模型没有问题,只有这次非凡租户压测出问题了
因为一般的物模型对象大小无限,基本达不到650KB,且线上不会呈现同时有数千个产品上报且这些产品的物模型对象都十分大,之前是不存在这种场景的。
从之前的GC日志来看
每次YGC后,新生代残余空间(某个survivor)在50M左右。因为存活的对象大小没有达到survivor空间的一半,因而不会触发策略三。
每次YGC后,survivor空间只有50M左右,阐明survivor有足够的空间包容存活的对象,因而不会触发策略四。
而此次非凡租户,是同时呈现了1000个产品下的设施上报数据,每次会产生1000个物模型大对象,而不只是几个,而且是在继续上报。
从GC日志剖析,触发了策略四。
为什么物模型本地缓存的size设置为1000,生效工夫设置成一分钟
线上的产品数量十分多,罕用的有数万个,随着业务增长,数量会更多。
本地缓存难以全副缓存这些产品的物模型,占用的内存空间太大,只能缓存一部分热点数据,因而size设置为1000
如果生效工夫设置较长,则这些物模型对象会活过15次YGC,进入老年代。而实际上,这些物模型对象并不是静态数据,也是会发生变化的,存在被动生效、LRU生效、缓存过期生效这3种状况,生效后这些对象在老年代,必须等fullgc能力回收。而业务上又会产生新的物模型对象,一直进入老年代,这样会造成老年代空间继续上涨。
问题总结
通过下面的剖析,能够总结问题的起因
1、大量产品下的设施同时上报,且每个产品的物模型对象都十分大。
2、guava cache援用了这些大对象,每次YGC移区时,survivor空间放不下这些大对象,间接进入了老年代。
3、继续的设施上报数据,导致一直的有大对象进入老年代。
4、物模型对象进入老年代后,只管缓存生效工夫到了,然而曾经处在老年代,YGC回收不掉,除非FullGC
后续Action
该问题是因为本地缓存和大对象引起,因而后续将从本地缓存和大对象这两个维度别离进行优化。
本地缓存调优
本地缓存务必弄清楚应用场景
为什么须要本地缓存,size设置多大,生效工夫设置为多少,大略占用多大的内存,这些都是要认真评估的。
从热点数据和静态数据别离剖析一下。
本地缓存热点数据
场景:大量的数据存在redis缓存中,数据量大,数据会变动,可能局部数据存在热点问题。
本地缓存应用:设置本地缓存max num、过期工夫。
本地缓存作用之一是避免redis热点,之火线上呈现过屡次物模型redis热点,只管对于redis服务端只是单个节点抖动,然而对于利用来说却是每台机器redis连接池都有可能被打满,这会影响整个集群的机器,如果持续时间长,将会引发严重后果。
因而本地缓存有必要。
单个survivor空间大小约为156M ~ 170M
1、束缚本地缓存生效工夫,不能让本地缓存中对象抗住15次YGC,从而降职到老年代。(如果进入老年代后才被淘汰或生效,此时YGC已无奈回收,必须FULL GC才行)
2、束缚本地缓存总大小不超过survivor空间的一半,这样不会触发策略三,即对象动静年龄判断。
3、至于是否触发了策略四,每次调优后,须要亲密察看GC日志,查看每次YGC后新生代残余对象大小,以及老年代的增量。
在放热点的场景下,能够思考将本地缓存中的K-V设置为弱援用,guava cache反对设置弱援用。一旦设置成弱援用,则在每次YGC时会将这些弱援用对象回收,确保不会进入老年代。
本地缓存静态数据
场景:静态数据缓存,数据量不大(或者有一个大略可承受的总量),数据根本不会变动。
本地缓存应用:缓存所有静态数据到本地,设置较大的max num,不设置过期工夫,缓存数据不会被淘汰。
比方本地缓存一些动态配置,这些数据总量不大,且不会变动,则能够全副缓存到本地,永不过期,永不淘汰。这些对象会全副降职到老年代,然而内存大小无限,不会引起问题。
理论也能够承受大量数据淘汰,这种场景内存增长很无限,不会造成内存问题。这种场景要充沛评估静态数据的内存占用大小。
大对象优化
大对象对于零碎整体稳定性会造成肯定影响。
从redis拉取大对象,qps一高很容易造成热点,且造成网络流量突增。
大对象超生夕灭,会减轻GC累赘。
大对象日志打印,将给磁盘IO带来影响。
产品设计上束缚
在定义物模型时,明确阐明如果超出肯定限度后,在设施上报时将不再做物模型校验。这样就不会产生大对象,从源头上限制住了。
主动降级
拉取到物模型后,程序中计算出该物模型占用的内存大小,如果大小超出阈值,则主动敞开该物模型的校验,不再缓存该大对象。
物联网平台产品介绍详情:https://www.aliyun.com/product/iot/iot_instc_public_cn
阿里云物联网平台客户交换群