乐趣区

关于缓存:GuavaCache与物模型大对象引起的内存暴涨分析设备管理运维类

背景介绍

首先对物联网平台的几个概念做下名词解释

总结一下
产品是一类设施的汇合,物模型形容了这一类设施的性能,包含属性、事件、服务。

比方创维电视是一个产品,而每户家庭中的一个个创维电视则是具体设施,这些电视(设施)都具备雷同的性能,即在创维电视这个产品上定义的性能。比方以后电视的频道、亮度、音量,这些都是具体的属性;比方如果电视的温度高于 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

            阿里云物联网平台客户交换群
退出移动版