进程RSS报警
JavaProcessRSSMemoryAlertV2Prod
背景知识 (background)
RSS ( Resident Set Size ),是 OS 统计的进程真实占用的RAM内存(不包括swap出去的)。
生产环境,托管给SRE/Docker的JVM应用通常开启了内存限额(Quota),开发可在应用大盘看到额度。

其中,Memory Quota是进程最大可用内存(内存限额),若进程内存占用(RSS)超过配额,会被操作系统OOM Killer 强制kill -9
进程被kill -9(不走优雅停机)后,会被 systemd / k8s 重新拉起,开发看到的就是节点重启了。
生产环境可能不同节点的内存/CPU额度不一样,可选择节点查看单台的准确额度。
JVM进程的RSS通常由以下部分组成:
堆内存,包括 old / young / survivor,监控狗有收集指标
非堆内存 (metaspace , codecache , direct buffers , compressed class space),监控狗有收集指标
- JVM自身内存、线程堆栈、GC内部结构、JNI/Native库、共享库/内存、Mapped Files
查看指标 (dashboard)
访问 kemonitor -> 应用 -> 监控狗 -> 节点监控,点开后,右上角可调整时间段。
展开: 进程 / 线程 / CPU / 磁盘 / IO这一行,找到监控面板:进程内存

其中,进程最大可用内存即进程的内存额度(最大可用内存),rss - resident set size为进程已使用的内存,若超过内存限额,就会被强制Kill。
止损措施 (action)
建议安排合适时机,去私有云/容器平台手动重启报警节点,一般重启会平滑摘流、优雅停机。
RSS内存占用超过了95%的阈值则会触发报警,预留了三四天的时间来处理。
事后改进(postmortem)
参考原因(cause)部分,我们建议优先排查应用代码,其次调整系统设置。
若长久无法定位或延缓RSS快速增长,可申请扩容内存。
可能的原因 (cause)
RSS是JVM进程在OS层面的真实内存占用,除了Heap/NonHeap内存之外,还包括 Native Memory,更多内容参考:Native Memory Tracking。
通常情况下,JVM进程的RSS在缓慢地增长,由于业务迭代重启等原因,不会增长到触发了RSS报警。
我们总结了三种触发RSS快速增长的原因,方便针对性检查。
一、线程数飙升,线程数量不稳定
线程是最常见的导致RSS上升的原因,即使线程stop了,也会增加少量RSS内存。
- RxJava/Reactive的Schedulers.IO/Cached,流量上升会导致创建很多线程,尽可能自定义线程池。
- 每个HttpRequest创建一个 OkHttpClient / ApacheHttpClient,尽量复用单例、连接池。
- 没有采用线程池或线程池参数不合理,不指定最大线程数,建议使用线程池,指定合理的队列大小和最大线程数。
节点指标里,展开: 进程 / 线程 / CPU / 磁盘 / IO这一行,找到监控面板线程 & Context Switch查看线程统计。

其中,JVM当前运行的线程(daemon + non-daemon)是JVM统计的线程数,和 jstack / jvisualvm 显示的一致。进程创建的OS线程是操作系统统计的进程真实的线程数,包括GC线程等。
图例里的min / max / current分别对应所选时段内的最小值、最大值、当前值。
通常来说,进程创建的线程数大于JVM正在运行的线程数,不过指标采样时间有先后,数据可能有出入。
二、JVM内存设置过大
开启了内存限额的情况下,我们建议至少保留1-1.5GB空闲内存,预留的内存除了供Native Memory用之外,也给 PageCache 留了足够空间。
| memory quota (RSS) | -Xmx |
|---|---|
| 4G | <= 2G |
| 6G | <= 4.5G |
| 8G | <= 6.5G |
| 12G | <=10G |
除了调小-Xmx之外,也可在私有云/容器平台申请扩容,调大内存配额。
另外,调小线程堆栈(-Xss)也会减少Native Memory占用,建议 256KB / 512KB。
三、Memory Fragmentation / Native Memory Leak /
Memory Fragmentation
Linux 默认使用glibc分配内存,自 glibc 2.10起,为了避免内存分配竞争,由一个主内存池调整为每个线程一组内存池。这种优化可减少多线程环境下的锁竞争,从而加速整体内存分配效率,但是会占用更多内存,造成内存碎片,更多细节参考: malloc-per-thread-arenas-in-glibc 和 hadoop-7154。
我们建议在环境变量里设置MALLOC_ARENA_MAX为4,以平衡内存分配效率和内存占用。
之前网关(i.aroute.ke.com)有设置过这个环境变量,设置后最明显的效果是虚拟内存变小了(之前几十G),但对延缓RSS增长的效果不是那么明显,建议作为最后的手段或性能优化点。
网关向部分后端Proxy请求时,TCP_NO_DELAY=false,导致RSS增长,设置为true就稳定了。
Native Memory Leak
使用Netty框架的应用,可尝试调整ResourceLeakDetector的级别,主动监控DirectMemory是否有泄露(PlatformDependent#DIRECT_MEMORY_COUNTER)。
代码里用到的所有文件/流必须确保关闭了,尤其是Zip压缩相关的,see java-native-leak-bug
另外,代码里有大量使用了Heap ByteBuffer进行IO写入的场景,也需留意。只有 Native Memory才可以传递给操作系统,因此JVM在实现时,若发现是Heap ByteBuffer,则会复制到一个临时的Direct ByteBuffer并按线程缓存这个临时的Direct ByteBuffer,如果ByteBuffer很大,则会占用很多Native Memory,看起来像 Native Memory 泄露。see https://www.evanjones.ca/java-bytebuffer-leak.html