背景
五一节假日值班期间,告警群里突然告警容器内存使用率高于 90%,并且后续一直有告警出现。随即登入指标监控系统查看该告警指标,如下:
该指标是通过 container_memory_working_set_bytes / container_spec_memory_limit_bytes 相除得到的。
注:
container_memory_cache -- 缓存占用的大小, 包含(total_active_file(活跃的缓存页) 和 total_inactive_file(不活跃的缓存页))。此内存是读写文件时操作系统为了提高性能而使用的内存缓存,当操作系统认为内存使用紧张时,就会回收掉,而不是读写完文件立即回收掉。
container_memory_rss -- RSS占用的大小,常驻内存集大小,分配给进程使用的实际物理内存。
container_memory_usage_bytes -- 当前使用内存,包括所有内存,不管有没有被访问
container_memory_working_set_bytes -- 当前内存工作集使用量。需要注意的是如果该值如果大于 container_spec_memory_limit_bytes,那么容器将会被 OOMKilled。注意不是 JVM OOM。
container_spec_memory_limit_bytes -- 容器限制最大内存使用量
下面这张图展示了它们之间的关系:
引用至:https://blog.csdn.net/qq_34562093/article/details/126754502
注意页面缓存下面的不活跃的缓存页应该是total_inactive_file
随即dump Jvm 堆内存,保留下现场,为了防止容器被 OOMKilled ,先重启应用。
问题排查
容器内存使用率高
通过 MAT 分析堆内存对象分布,发现可达的对象最大是一个队列,里面有90多万个对象,占用380M内存。由于该服务的配置是 4C8G,最大和最小堆内存配置的是4G,直接内存最大是 1G。由此看大对象 380M 占用似乎也不大,其余对象也没有特别大的。刚开始以为上述的波浪图是由于 JVM 垃圾回收而产生的,但是有一个特别奇怪的地方是,有一个POD节点容器使用率一直维持在54%左右,其他三个 POD 容器使用率是在 52%-92%直接循环(这个后面再解释原因)。所有 POD 节点JVM内存大小都是随着垃圾回收产生波浪线图形。因此容器的内存回收应该是和 JVM 垃圾回收没有关系。从上面的图中也可以看出,JVM 内存是属于 RSS,在java程序启动后,如果 -Xmn 和 -Xmx 配置相等后,其值在 RSS 统计中是一直保持不变的。
再看 JVM 堆外内存,使用200-300M,一直维持相对稳定的状态。因此可以判断此次容器内存使用率高告警和JVM 的内存使用是没有关系的。
通过查看 container_memory_rss 指标数据也印证了上述的结论,container_memory_rss 值一直是处于相对稳定的状态。
那么猜测是和 container_memory_cache 页面缓存指标相关。container_memory_cache 主要包含total_active_file(活跃的缓存页) 和 total_inactive_file(不活跃的缓存页),Page Cache 的主要作用是提高磁盘文件的读写性能,因为系统调用 read() 和 write() 的缺省行为都会把读过或者写过的页面存放在 Page Cache 里。该值是写在 /sys/fs/cgroup/memory/memory.stat
我们主要关注 total_active_file、total_inactive_file 两个指标数据,可以看到 total_active_file 内存使用已经到了1.6G左右了。而且这个值是在不断的变化中的。而且通过计算,container_memory_working_set_bytes 的值基本上等于 container_memory_usage_bytes - total_inactive_file 。total_inactive_file 值也在不断变化中,但是基本上浮动不大,由于 total_active_file 值在 30M ~ 2.5G 之间上下波动,导致container_memory_usage_bytes 值也会不断上下波动,而 total_inactive_file 值变化不大,最终的结果就是 container_memory_working_set_bytes 不断上下波动,也就是第一张图片展示的效果。
解释了容器内存使用率呈锯齿状的图形形成的数据来源,那是什么原因导致 total_active_file 值在不断的增加呢?上面提到了 total_active_file 活跃的页缓存主要是和读写文件有关,容器内存使用率是每 30 分钟从最低点到最高点,刚开始以为是应用有什么 30 分钟执行一次的任务有关,经过代码查看发现并没有时间间隔 30 分钟的定时任务。然后去日志目录下查看日志,发现程序输出了大量的日志文件,基本上是按每分钟输出一个 50M左右的文件。
到这里基本上就可以推断出,页缓存内存的使用高是和程序打印日志有关,然后再计算下 30 分钟周期内输出的所有日志文件大小总和,刚好和 total_active_file 最大值时差不多大小。然后再去排查程序打印日志的地方,发现了程序在不合理的地方打印了日志,导致很多无用的日志被打印了。
解决方案
解决方案就很简单了,需要控制减少日志的打印。也可以在节假日等流量高峰期修改日志打印级别,减少日志的打印,不仅能提高程序的性能,也能减少内存压力。
小结:
导致容器内存使用率(主要是 container_memory_working_set_bytes 指标)高的原因是:因为应用程序在不断的疯狂的输出日志,导致 total_active_file 活跃页缓存升高,即 container_memory_cache 页缓存指标升高,最终影响计算公式:
container_memory_working_set_bytes(升高) = container_memory_usage_bytes(升高) - total_inactive_file(基本不变)。
当页缓存使用到一定量时,操作系统发现内存紧张后,就会回收掉页缓存,从而造成了容器内存使用率呈锯齿状的原因。
为什么有一个 POD 内存使用率基本维持不变
此次问题排查还有一个比较奇怪的点:就是是有一个节点内存使用率一直是 54%左右,其他三个节点30分钟时间里内存使用率在 54%-90%之间循环。
在排除完程序执行不存在特别差异外,然后通过看 /sys/fs/cgroup/memory/memory.stat 文件发现了端倪。下面是特殊节点的文件内容:
不知道你有没有发现,对比其他节点的 memory.stat 文件,这个特殊 pod 的 total_active_file 和 total_inactive_file 值刚好是反过来的,也就是在程序疯狂写日志时,次pod 的 total_inactive_file 在不断的变化,而 total_active_file 值反而相对稳定。套用container_memory_working_set_bytes 计算公式:
container_memory_working_set_bytes(基本不变) = container_memory_usage_bytes(升高) - total_inactive_file(升高)。
就能解释通为什么次pod 的内存使用率一直维持稳定了。那为什么此节点和其他3个节点不一样呢?把猜疑的方向转到了操作系统上去,通过查看发现两个 pod 的宿主机Node 节点的操作系统不一样,此节点是华为的 EulerOS 操作系统,其他节点用的是 CentOS 系统,和运维沟通后他们说后续会全部使用 EulerOS 系统。
那也就基本断定是操作系统不一样导致的 total_inactive_file 和 total_active_file 值不一样。可能华为的 EulerOS 系统优化了写文件时使用的页缓存更多的是 total_inactive_file 。
小结
在网上很多人认为 container_memory_working_set_bytes 计算包含 total_active_file(活跃页缓存) 是不合理的,k8s 会依据 container_memory_working_set_bytes 值去 OOMKilled pod 节点(注意要和 JVM OOM 区分开),由于页缓存是可以被回收的,如果把页缓存也算进去,在大量读写文件时页缓存使用多了,操作系统如果没来得及回收,那么将发生 OOMKilled。
下面是 k8s GitHub 上的讨论:
https://github.com/kubernetes/kubernetes/issues/43916
https://github.com/kubernetes/kubernetes/issues/104533
华为的 EulerOS 的优化反而可能是人们所期望的行为,目前也不太确定次优化是否是合理的。值得注意的是指标的定义及收集是 k8s 或者其组件的行为,而页缓存是操作系统控制的,华为的优化操作并不是改变指标的定义,或者是收集指标的逻辑,而是修改了操作系统的页缓存计数,CentOS 系统在写文件时 total_active_file 值会不断变大,EulerOS 系统在写文件时 total_inactive_file 值会不断变大(仅在次问题的场景下)。
总结
- 要区分开 JVM OOM 和 容器 OOMKilled ,后者时 k8s 在发现 container_memory_working_set_bytes 值超过了container_spec_memory_limit_bytes limit 限制时,再申请内存时容器将会被 OOMKilled 掉。
- 排查容器内存使用率高时,要找到具体内存使用升高的部分,在排除掉时程序 堆外内存导致升高后(JVM 堆内存GC 是不会影响 container_spec_memory_limit_bytes 值的),就可能是页缓存导致的升高。再去排查具体原因。
- 页缓存的使用 k8s 是没有参数来控制的,因为这个是操作系统行为,当然可以通过修改操作系统相关配置来优化页缓存的使用。作为程序员来说,我们可以控制程序读写文件等相关操作,不要短时间内读写太多文件,特别是日志的打印。另外还要为 Java 程序设置合理的堆内存大小(-Xmn,-Xmx)和堆外内存的使用。还要预留合理的内存供操作系统用于页缓存,避免容器被 OOMKilled。
- 华为的 EulerOS 系统优化最起码在次场景下看起来更合理些。也许是页缓存的算法经过了优化,在其他场景下不确定此优化是否合理。
参考
- https://blog.csdn.net/qq_34562093/article/details/126754502
- https://cloud.tencent.com/developer/article/1637682
- https://cloud.tencent.com/developer/article/2070537