Pod的内存使用率很高的问题分析

生产环境中在流量高峰期出现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,这是一个很小值,显然是不合理的。

pod中堆内存参数

在应用容器化之前,应用部署在物理机器上,通常会通过-Xms-Xmx来设置堆的最小值和最大值。但出现容器化之后,通过这种静态值来设置堆内存大小就不合适了,因为pod的内存大小是可设置的,不像物理机一样是固定的。

因此,JDK8U191为了适配docker容器新增了几个参数,分别是:
MaxRAMPercentage,InitialRAMPercentageMinRAMPercentage,同时将InitialRAMFraction,MaxRAMFractionMinRAMFraction标记为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垃圾回收器自动将未使用的内存归还给操作系统。如下所示:
Pod的内存使用率很高的问题分析_第1张图片

如何降低pod的内存使用率

JVM中有两个参数,分别是MinHeapFreeRatioMaxHeapFreeRatio,含义是允许JVM保留的最小和最大的未使用内存比率,默认值分别是40和70。其中MaxHeapFreeRatio 在OpenJDK11之后的版本中被废弃。但有一些其他参数可以控制堆空间的收缩行为:

  • -XX:MinHeapFreeRatio:该参数用于设置最小堆空闲空间比例的阈值。当堆空间的空闲空间低于该阈值时,JVM会尝试扩展堆空间以提供更多空闲空间,在G1中不生效。
  • -XX:GCTimeRatio:该参数用于设置垃圾收集时间与应用程序执行时间的比率。较高的值将导致JVM更积极地回收内存并进行堆空间的收缩。在G1中不生效。
  • -XX:AdaptiveSizePolicy:该参数启用自适应堆大小调整策略,JVM将根据应用程序的行为动态调整堆大小。这可以包括堆空间的扩展和收缩。

但即使发生堆收缩,未使用的内存是否会归还给操作系统,还是取决于具体的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
å

你可能感兴趣的:(云原生,分布式,后端框架,云原生,K8S,1024程序员节)