生产环境中在流量高峰期出现pod内存使用率很高,pod批量重启,错误日志中还有OOM相关信息。
Pod使用的内存不能直接在pod中通过top命令查看,这种方式看到的是pod所在node的资源使用情况。想查看pod的资源使用情况需要用kubectl top pod xxx
命令。但这也只能看到pod的内存使用。
那如何查看jvm的堆内存使用呢?
通过kubectl exec -it pod-name /bin/bash
连接到pod内部的shell,然后执行jmap,jstat等命令来查看JVM堆内存情况。
如果接入了Prometheus,则可以通过
sum(jvm_memory_used_bytes{pod="$pod", area="heap"})
sum(jvm_memory_committed_bytes{pod="$pod", area="heap"})
sum(jvm_memory_max_bytes{pod="$pod", area="heap"})
这几项指标来查看当前正在使用,已申请的和最大的堆内存大小。
通过查看指标发现堆内存很小,大约为pod内存的1/4,这是一个很小值,显然是不合理的。
在应用容器化之前,应用部署在物理机器上,通常会通过-Xms
和-Xmx
来设置堆的最小值和最大值。但出现容器化之后,通过这种静态值来设置堆内存大小就不合适了,因为pod的内存大小是可设置的,不像物理机一样是固定的。
因此,JDK8U191为了适配docker容器新增了几个参数,分别是:
MaxRAMPercentage
,InitialRAMPercentage
和MinRAMPercentage
,同时将InitialRAMFraction
,MaxRAMFraction
和MinRAMFraction
标记为deprecated状态。
比较违反直觉的是,MinRAMPercentage
并不是最小堆内存的意思,而是当实际内存小于96m的时候,用于计算最大堆内存,也就是说,最大堆内存计算方式如下:
phys_mem * MinRAMPercentage / 100 (if this value is less than 96M)
MAX(phys_mem * MaxRAMPercentage / 100, 96M)
通常情况下实际内存不可能小于96m,忽略MinRAMPercentage
即可。那实际生效的就是初始化大小比例和最大比例两个参数生效。这两个参数的默认是多少呢?
我们进入pod,运行命令
java -XX:+PrintFlagsFinal -version | grep RAMPercentage
可以看到结果如下:
double InitialRAMPercentage = 1.562500
double MaxRAMPercentage = 25.000000
double MinRAMPercentage = 50.000000
最大堆内存为实际内存的25%,这也是为什么在生产环境中使用的堆内存始终只有pod内存的1/4。
因为堆内存太小,当流量高峰出现时OOM就可以理解了。但还是无法解释pod内存使用率很高的问题(堆外内存应该占用不了多少内存)。
分析了OOM原因之后,添加了两个参数:
-XX:InitialRAMPercentage=25
-XX:MaxRAMPercentage=70
同时也将pod内存从1.5G调大到了2G。
重新部署之后服务正常,不再出现pod重启。每个pod的堆内存使用在1个G左右,而不是原来300多M。
优化后,理论上每个pod的最大堆内存可使用至1.4G,是够用的。
但是pod内存使用率还是保持高位,从而导致根据pod设置的hpa扩缩容策略失效,只会扩容,不会缩容。
直接给出原因:JVM在流量高峰期使用了较高的内存,随后该内存一直由JVM管理,并不会归还给操作系统。
这似乎有点难以理解,在我之前的认知里面,通过垃圾回收器回收的内存应该要归还给操作系统,但其实不是这样的。实际上,JVM向操作系统申请内存是有代价的,如果每次gc后将内存归还,然后用到的时候再申请,这将极大损耗JVM的性能。
因此默认情况下,JVM是不会在短时间内主动归还未使用的内存的。当然,具体的机制取决于JDK的版本和垃圾回收器。明确的官方说明我并没有从google上搜索到,但有一点是可以确认的,OpenJDK遵循这一原则1,并且在OpenJDK12版本中引入了一个新功能:鼓励G1垃圾回收器自动将未使用的内存归还给操作系统。如下所示:
JVM中有两个参数,分别是MinHeapFreeRatio
和MaxHeapFreeRatio
,含义是允许JVM保留的最小和最大的未使用内存比率,默认值分别是40和70。其中MaxHeapFreeRatio
在OpenJDK11之后的版本中被废弃。但有一些其他参数可以控制堆空间的收缩行为:
但即使发生堆收缩,未使用的内存是否会归还给操作系统,还是取决于具体的JVM和垃圾回收器。除非进行显式的System.gc()的调用。
最终未找到合适的将JVM内存归还给操作系统的方法,因此只能去掉了HPA中memory的策略,只保留CPU策略。
只保留CPU扩缩容策略引起了一些其他的问题,当然,这不在本文讨论的范围之内。
[1].https://docs.openshift.com/container-platform/4.8/nodes/clusters/nodes-cluster-resource-configure.html#nodes-cluster-resource-configure-jdk-unused_nodes-cluster-resource-configure:~:text=to%20be%20calculated.-,Understanding%20how%20to%20encourage%20the%20JVM%20to%20release%20unused%20memory%20to%20the%20operating%20system,-By%20default%2C%20the
å