问题背景
Flink 的 TaskManager 进程运行在 JVM 上,目前流计算 Oceanus 容器给定的内存上限是 4GB,如果超用就会被管控服务执行 OOMKilled。
理想情况下,通过设置 -Xmx、-XX:MaxDirectMemorySize、-XX:MaxMetaspaceSize 等 JVM 参数,可以将 JVM 的堆内和堆外各内存区域限制在合适的范围。然而实际上,由于 Flink 可以执行任意的 JAR 程序,用户可能有意或无意地引入了一些原生库(例如 RocksDB 等),它们的内存申请和释放并不在 JVM 管控范围内,最终造成物理内存(RSS 或 top 命令看到的RES)用量超限。
Flink 内置的内存用量检测和上报逻辑,采用的是 Java Management API 提供的
代码语言:javascript复制ManagementFactory.getMemoryMXBean()
方法,它返回一个 MemoryMXBean 对象。Flink 的 MetricUtils 通过定期访问该对象的 getHeapMemoryUsage()、getNonHeapMemoryUsage() 等方法来获取当前的 JVM 堆内存和部分堆外内存的用量值。这种方法下,堆内存用量获取的还算准确,但是堆外部分是非常不准的(严重偏小),难以用来预估实际内存用量。
问题探索
我们知道,Java 还提供了一个内存用量相关的 API:
代码语言:javascript复制Runtime.getRuntime().totalMemory()
但是通过实际验证,发现它包含了进程的虚拟内存部分,导致获取的值远大于实际物理内存用量。
通过广泛搜集资料,以及咨询熟悉 JDK 的技术专家,得知目前 JVM 的确没有提供通用的 API 来获取物理内存用量。经过充分讨论,也得到了另一条检测路径:Linux 会把进程的内存用量信息写入到 /proc/[PID]/status 虚拟文件中,我们可以读取这个文件来获取当前的物理内存用量。
从下图可以看到,status 文件中的 VmRSS 值与 top 命令获取的 RES 是一致的:
示例程序
于是我们就有了下面的代码来获取 JVM 的实际物理内存用量:
代码语言:javascript复制public static long getProcessRssInKb() throws IOException {
String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; // 获取当前进程 PID
String os = System.getProperty("os.name");
long invalidValuePlaceholder = -1L;
if (!os.equals("Linux")) {
System.err.println("This program can only be run on Linux");
return invalidValuePlaceholder;
}
String statusFile = "/proc/" pid "/status";
return Files.readAllLines(Paths.get(statusFile))
.stream()
.filter(line -> line.startsWith("VmRSS:"))
.map(line -> line.split("\s ")[1].trim())
.mapToLong(Long::parseLong)
.findFirst()
.orElse(invalidValuePlaceholder);
}
当然,这只是一个非常简单的例子,不一定在所有环境下都可以运行(例如 Windows 等非 Linux 环境就无效)。
如果需要非常高频地调用,或者在非标准的环境下使用的话,就需要针对性优化了。由于我们每分钟最多获取一次相关指标,从 benchmark 的结果来看,这种读取文件的方式并不会对进程本身的资源占用和执行速度造成影响,因此是可行的。
如果有更好的方法,欢迎一起探讨 :)