1、现象
收到Memory hit original limit内存告警与CPU容量水位告警,随后发生OOM,容器无限重启


2、应急解决方案
最重要的事情是先保证生产可用,并增加JVM参数用来观测,做完以下调整后对容器状态进行观察。
1)我们需要快速调高内存,内存参数调整为-Xmx3g -Xms3g。
2)加上JVM参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs/dump ,并挂载/app/logs/dump目录,确保堆内存OOM时可以将堆内存dump下来进行分析。
3)加上-XX:NativeMemoryTracking=summary,用来分析堆外内存。
3、收集相关数据信息
1)容器成功恢复健康状态,但是过了一段时间还是触发了OOMKilled,查看/app/logs/dump目录,并没有dump文件,说明有可能不是堆内存溢出,我们查看JVM状态,包括堆内存信息。
2)JVM状态分析
在容器重启之前,CPU占用在15%-30%之间,属于正常水平,堆内存也一直处于低水位状态,非堆内存也没有明显起伏,但是可以看到系统内存占用偏高,我们需要继续追踪堆外内存的情况。

3)

可以看到总的预留内存4.8GB,使用的物理内存3.56GB,而且通过在进程稳定时和内存到达上限时的快照信息分析,每项的值没有明显的变化。
4)查看容器详情
容器重启之前RSS全程没变,但是可以看到PageCache在重启之前飙升上去了。PageCache是文件缓存占用的内存,按道理来说,在内存达到瓶颈时,系统会自动回收才对。

4、问题分析,追究根因
想知道PageCache为什么在内存达到容器限制之前没被回收,我们需要知道k8s的oomkilled机制,在什么情况下会触发。
1)k8s 检测到容器内存达到memory.limit_in_bytes时且没有回收,便会触发 OOMKilled,我们找一个容器进行测试,将memory.limit_in_bytes调为524M(内部监控平台容器配额)。
查看memory.limit_in_bytes:cat /sys/fs/cgroup/memory/memory.limit_in_bytes

watch -n 1 “cat /sys/fs/cgroup/memory/memory.usage_in_bytes” 观察内存增长

找一个大文件下载,wget “xxx”,并继续观察memory.usage_in_bytes,可以看到该值在文件下载的过程中一直上升,上升到接近memory.limit_in_bytes时触发了137错误码,即oomkilled。从现象可以得知,k8s的内存限制计算,会包含RSS和PageCache,RSS包含 JVM 进程的所有内存区域(如堆、非堆、JVM 自身代码、Native Memory等),memory.limit_in_bytes = RSS + PageCache。


2)为什么没有触发PageCache的回收,PageCache回收的速度也不可能赶不上带宽下行网速,原因是PageCache的回收是基于全局物理内存压力,不是单个容器的内存限制。而我们是在docker容器内,可以用命令查看物理内存。

容器是 Cgroups 隔离的独立环境,内核无法感知容器内的内存超限风险。即使容器内存即将超限,内核也不会优先回收其 PageCache,导致容器因 PageCache 触发 OOMKilled。
5、项目分析
随着效能平台的迭代,目前已经有了很多大文件下载或转存的场景,目前主要有:

1)效能客户端 APK 数据流上传(蓝线)
使用 MultipartFile 上传文件,spring.servlet.multipart.file-size-threshold 设置为 10M 时,超过 10M 则会存到临时文件,所以当 APK 文件过大时,也有 PageCache 上升的风险。(注释翻译:将基础输出流从基于内存的流切换到由磁盘支持的流。这是我们意识到有过多数据正在写入,无法再保存在内存中,因此选择切换到基于磁盘的存储。)
2)合作伙伴平台上传的 apk url下载(黑线)
2)固件URL转存对象存储服务做长期存储(红线) 3)固件转存对象存储加速服务做OTA全球加速。
6、解决方案
1、JDK 10+ / 8u191+ 支持让 JVM 感知容器内存的参数:
-XX:MaxRAMPercentage=...、-XX:InitialRAMPercentage=...2、JDK 10+ 支持Direct I/O,可以在读写磁盘时不使用到 Pagecache 。
1)固件转存可以不下载,直接通过数据流在内存进行处理,而不涉及磁盘。
2)手动触发PageCache回收,主动进行刷盘操作。
临时快速解决方案,直接开启同步刷盘,后续再将固件转存改为纯流处理,边下载边上传。而APK是必须下载的,因为需要在服务端进行解析。大文件不使用 MultipartFile,改为上传对象存储并自己处理下载逻辑。后续可以拆开一个服务,用来做下载与上传大文件,不影响效能平台的整体带宽与 IO,且区分下载传任务的优先级。
开启后查看PageCache及网络IO、磁盘IO,一切正常,且不会再触发137 OOMKilled。


