作者:董伟柯,腾讯 CSIG 高级工程师

问题背景

前段时间,某客户线上运行的大作业(并行度 200 左右)遇到了 TaskManager JVM 内存超限问题(实际内存用量 4.1G > 容器设定的最大阈值 4.0G),被 YARN 的 pmem-check 机制检测到并发送了 SIGTERM(kill)信号终止该 container,最终导致作业出现崩溃。这个问题近期出现了好几次,客户希望能找到解决方案,避免国庆期间线上业务受到影响。

在 Flink 配置项中,提供了很多内存参数设定。我们逐一检查了客户作业的配置,发现各个内存配置的最大值之和也只有 3.75GB 左右(不含 JVM 自身 Native 内存区),离设定的 4.0GB 阈值还有 256MB 的空间。

用户作业并没有用到 RocksDB、GZip 等常见的需要使用 Native 内存且容易造成内存泄漏的第三方库,而且从 GC 日志来看,堆内各个区域远远没有用满,说明余量还是比较充足的。

那究竟是什么原因造成实际内存用量(RSS)超限了呢?

Flink 内存模型

要分析问题,首先要了解 Flink 和 JVM 的内存模型。官方文档 [1] 和很多第三方博客 [2] [3] 都对此有较为详尽的分析,这里只做流程的简单说明,不再详尽描述每个区域的具体计算过程。

下图展示了 Flink 内存各个区域的配置参数,其中左边是 Flink 配置项中的内存参数,中间是参数对应的内存区域,右边是这个作业配置的参数值。

图中深绿色的文字(taskmanager.memory.process.size)表示 JVM 所在容器内存的硬限制,例如 Kubernetes Pod YAML 的 resource limits。它的相关类为 ClusterSpecification,里面描述了 JobManager、TaskManager 容器所允许的最大内存用量,以及每个 TaskManager 的 Slot(运行槽)数等。

TaskManager 各个区域的内存用量是由 TaskExecutorProcessSpec 类来描述的。首先 Flink 的 ResourceManager 会调用 TaskExecutorFlinkMemoryUtils 工具类,从用户和系统的各项配置 Configuration 中获取各个内存区域的大小( TaskExecutorFlinkMemory 对象,不含 Metaspace 和 Overhead 部分)。这中间要考虑到旧版本参数的兼容性,所以有很多绕来绕去的封装代码。总而言之,优先级是 新配置 > 旧配置 > 无配置(计算推导 + 默认值)。随后再根据配置和上述的计算结果,推导出 JvmMetaspaceAndOverhead,最终封装为包含各个区域内存大小定义的 TaskExecutorProcessSpec 对象。

图中最右边浅绿色文字表示 Flink 内存参数最终翻译成的 JVM 参数(例如堆区域的 -Xmx、-Xms,Direct 内存区的 -XX:MaxDirectMemorySize 等),他们是 JVM 进程最终运行时的内存区域划分依据,是 ProcessMemoryUtils 这个工具类从上述的 TaskExecutorProcessSpec 对象中生成的。

堆内内存的分析

堆内内存(JVM Heap),指的是上图的 Framework Heap 和 Task Heap 部分。Task Heap 是 Flink 作业内存分配的重点区域,也是 JVM OutOfMemoryError: Java heap space 问题的发生地,当 OOM 问题发生时如下图:

如果这个区域内存占满了,也会出现不停的 GC,尤其是 Full GC。这些可以从监控指标面板看到,也可以通过 jstat 等命令查看。如果我们通过 Arthas、async-profiler [4] 等工具对 JVM 进行运行时火焰图采样的话,也可以看到类似下面的结果:GC 相关的线程占了很大的时间片比例:

对于堆内内存的泄漏分析,如果进程即将崩溃但是还存活,可以使用 jmap 来获取一份堆内存的 dump:

jmap -dump:live,format=b,file=/tmp/dump.hprof 进程PID(先做一次 Full GC 再 dump)
jmap -dump:format=b,file=/tmp/dump.hprof 进程PID(直接 dump)

如果进程崩溃难以捕捉,可以在 Flink 配置的 JVM 启动参数中增加:

env.java.opts.taskmanager: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/taskmanager.hprof

这样 JVM 在发生 OOM 的时刻,会将堆内存 dump 保存到指定路径后再退出。

拿到堆内存 dump 文件以后,我们可以使用 MAT [5] 这个开源的小工具来分析潜在的内存泄漏情况,并输出报表。

如果 MAT 不能满足需求,还有 JProfiler 等更全面的工具可以进行堆内存的高级分析。

当然,很不幸的是,这个出问题的作业的堆内存区域并没有用满,GC 日志看起来一切正常,堆内存泄漏的可能性排除。那么还需要进一步涉足堆外内存的各个神秘区域。

堆外内存的分析

JVM 堆外内存又分为多个区域,例如 Flink HybridMemorySegment 会用到 Java NIO 的 DirectByteBuffer 使用的 Direct 内存区(MaxDirectMemorySize 参数限制的区域),类加载等使用的 Metaspace 区(MaxMetaspaceSize 参数限制的区域,JDK 8 以前叫做 PermGen)。

如果 Direct 内存区发生了 OOM,JVM 会报出 OutOfMemoryError: Direct buffer memory 错误;而 Metaspace 区 OOM 则会报出 OutOfMemoryError: Metaspace 错误。但是这个作业日志中并没有看到任何 OutOfMemoryError 的错误,因此这些地方内存泄漏的可能性也不大。

使用 Native Memory Tracking 查看 JVM 的各个内存区域用量

JVM 自带了一个很有用的详细内存分配追踪工具:Native Memory Tracking(NMT)[6],可以通过配置 JVM 启动参数来开启(可能造成 10% ~ 20% 的性能下降,线上慎用):

-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics -XX:NativeMemoryTracking=summary

随后可以对运行中的 JVM 进程执行:

jcmd 进程 VM.native_memory summary

来获取此时此刻的 JVM 各区域的内存用量报表。

下面是一个典型的返回结果(// 为本文备注内容,标出了占用较多内存的区域含义):

Total: reserved=5249055KB, committed=3997707KB // 总物理内存申请量为 3.81G
-                 Java Heap (reserved=3129344KB, committed=3129344KB)  // 堆内存占了 2.98G 物理内存(mmap: reserved=3129344KB, committed=3129344KB)-                     Class (reserved=1130076KB, committed=90824KB)  // 类的元数据占用了 88.7M 物理内存(classes #13501) // 加载的类数(malloc=1628KB #17097)(mmap: reserved=1128448KB, committed=89196KB)-                    Thread (reserved=136084KB, committed=136084KB)  // 线程栈占用了 132.9M 物理内存(thread #132)  // 线程数(stack: reserved=135504KB, committed=135504KB)(malloc=425KB #692)(arena=155KB #249)-                      Code (reserved=256605KB, committed=44513KB)(malloc=7005KB #11435)(mmap: reserved=249600KB, committed=37508KB)-                        GC (reserved=69038KB, committed=69038KB)(malloc=58846KB #618)(mmap: reserved=10192KB, committed=10192KB)-                  Compiler (reserved=394KB, committed=394KB)(malloc=263KB #811)(arena=131KB #18)-                  Internal (reserved=432708KB, committed=432704KB) // Direct 内存等部分占了 422.6M 物理内存(malloc=432672KB #31503)(mmap: reserved=36KB, committed=32KB)-                    Symbol (reserved=23801KB, committed=23801KB)(malloc=21875KB #165235)(arena=1926KB #1)-    Native Memory Tracking (reserved=3582KB, committed=3582KB)(malloc=20KB #226)(tracking overhead=3563KB)-               Arena Chunk (reserved=1542KB, committed=1542KB)(malloc=1542KB)-                   Unknown (reserved=65880KB, committed=65880KB)(mmap: reserved=65880KB, committed=65880KB)

可以看到,堆内存、Direct 内存等区域占据了 JVM 的大部分内存,其他区域占用量相对较小。这个 JVM 被统计到的实时内存申请量为 3.81GB.

但是,使用 top 命令查看这个 JVM 进程的实时用量时,发现 RSS(物理内存占用)已经升高到了 4.2G 左右,与上述结果不符,说明还是有部分内存没有追踪到:

使用 jemalloc 替代 ptmalloc 并统计内存动态分配

既然 JVM 自己统计的内存分配与实际占用仍然有较多偏差,而搜索了网上的各种资料时,经常会遇到因为 glibc malloc 64M 缓存造成内存超标的问题 [7]。

由于 jemalloc 并没有这个 64M 的问题,而且可以通过 profiler 来统计 malloc 调用的动态分配情况,因此决定先使用 jemalloc [8] 来替换 glibc 自带的分配函数,并进行统计。当然,使用 strace 等命令也可以拦截内存分配和释放情况(追踪 mmap、munmap、brk 等系统调用),不过结果太多了,分析起来并不方便。

下载解压 jemalloc 的发行包以后,进入相关目录,编译并安装它:

./configure --enable-prof --enable-stats --enable-debug --enable-fill && make && make install

随后在 Flink 参数里加上这些内容:

containerized.taskmanager.env.LD_PRELOAD: "/usr/local/lib/libjemalloc.so.2"
containerized.taskmanager.env.MALLOC_CONF: "prof:true,lg_prof_interval:29,lg_prof_sample:17"

重新运行作业,就可以不断地采集内存分配情况,并输出 .heap 文件到 JVM 进程的工作目录(例如 jeprof.951461.7.i7.heap)。

随后可以安装 graphviz,再使用 jemalloc 自带的 jeprof 命令对结果进行绘图(尽量在进程退出前绘图,避免地址无法解析):

yum install -y graphviz
jeprof --svg `which java` 采集的.heap文件名 > ~/result.svg

结果如下:

从左边的分支来看,71.1% 的内存分配请求主要由 Unsafe.allocateMemory() 调用的(例如 Flink MemoryManager 分配的堆外 MemorySegments),中间分支的 init 是 JVM 启动期间分配的,也是正常范围。右边分支主要是 JVM 内部的 ParNew & CMS GC、Class 解析所需的符号表、代码缓存所需的内存,也是正常的。因此并未观察到较大的第三方库造成的内存泄漏情况,因此间接引入第三方库造成内存泄漏的可能性也基本排除了。

使用 pmap 命令定期采样内存区块分配

既然 JVM NMT 上报的内存分区快照、jemalloc 统计的动态分配情况都没有找到准确的问题根源,我们还可以从底层出发,使用 pmap 命令来查看 JVM 进程的各个内存区域的分配情况,看是否有异常的条目。

可以使用下面的命令,从 Flink TaskManager 启动开始采样:

while true
dopmap -x JVM进程的PID > /tmp/pmap.`date +%Y-%m-%d-%H-%M-%S`.logsleep 30s
done

随后可以使用文件比较工具,对比不同时间点的内存分配情况(例如下图是刚启动和崩溃前的最后一个记录),看是否有大块的不能解释的分配区段:

上图中,除了堆内存区有大幅增长(只是稍微超出一些 Xmx 的限制),其他区域的增长都比较小,因此说明 JVM 内存超限基本上是因为堆内存区域随着使用自然扩展 + JVM 自身较大的 Overhead(内部所需内存)造成的。并且这部分内存在 NMT 报告里统计的并不准确,还需要进一步跟进。

初步总结

在上面的分析中,我们先从最容易分配也是占比最大的堆内存区域开始分析,逐步进入堆外内存的深水区。由于堆外内存除了 Java 自带的 NMT 机制外,并没有综合的分析工具可用,因此这里的分析过程往往繁杂而耗时,且较难得到准确原因。

本次问题的初步结论是 JVM 自身运行所需的内存(Overhead)占用较大,而用户对 Flink 的参数 taskmanager.memory.jvm-overhead.{min,fraction,max} 设定值过小(为了给堆内存留出更大空间,在这里只设置了 256MB 的阈值,而实际的内存占用不止这些)。

需要注意的是,这个参数并不意味着 Flink 能“限制”JVM 内部的内存用量。相反,它的用途是令 Flink 在计算各区域(Heap、Off-Heap、Network 等)的内存空间时,能考虑到 JVM 这部分 Overhead 空间并不能被自己使用,应当减去这部分不受控的余量后再分配。

特别地,当用到了 RocksDB 等 JNI 调用的原生库时,请务必继续调大 taskmanager.memory.jvm-overhead.fractiontaskmanager.memory.jvm-overhead.max 参数的值(例如给到 1~2GB),避免余量不够而造成的总内存用量超标的问题。

下表总结了本文所用的工具和适用场景:

工具名 简介 常用使用场景
jstat Java 自带的命令,可以查看 JVM 的统计信息,例如各类 GC 次数、时长等,各内存区域的使用量等 查看各区域内存用量,定位 GC 问题等
jmap Java 自带的命令,可以生成 JVM 堆内内存的 Dump 文件,也可以查看内存对象分布直方图等 获取堆内内存 Dump、查看堆内存中对象分布
jcmd Java 自带的命令,可以对正在运行的 JVM 发送命令,例如开启和关闭特定参数、触发 GC、查看某些统计信息等 开启内置 Flight Recorder、查看 NMT 统计信息等
Arthas 包含了一系列问题定位和 JVM 操控小工具,可用来拦截运行时调用现场,动态代码替换、查看 Classloader、Dump 内存等多种用途 各类场景,通常在线使用
JProfiler 非常强大的 JVM 剖析和问题排查工具,可以在线 attach 到远程 JVM,也可以离线分析 Dump 文件 各类场景,图形化诊断 JVM 问题
MAT Eclipse 推出的自动堆内存泄露分析工具 堆内存泄露分析
NMT Java 自带的功能,可以追踪 JVM 内部各区域的内存分配和使用情况 堆外内存分析
jemalloc + jeprof 一个通用的内存管理库,可以替代 glibc 中的 malloc,可以避免很多内存碎片问题,支持记录调用次数和分配量等信息等用于后续分析 底层 malloc 调用分析和剖析
pmap Linux 自带命令,查看某个进程的内存映射信息 进程内存映射情况分析

后续计划

由于人工排查堆外内存问题的过程相当繁琐,十分依赖定位者的直觉和经验,可复制性弱,工具不统一,效率很低。

我们正在规划将这些定位流程标准化地集成到我们的流计算 Oceanus 平台上,做到自助、自动诊断,逐步实现我们的愿景:打造大数据产品生态体系的实时化分析利器,成为一个基于 Apache Flink 构建的具备一站开发、无缝连接、亚秒延时、低廉成本、安全稳定的企业级实时大数据分析平台,实现企业数据价值最大化,加速企业实时化、数字化的建设进程。

参考阅读

[1] Flink 官方文档 · 内存模型详解 https://ci.apache.org/projects/flink/flink-docs-release-1.10/zh/ops/memory/mem_detail.html

[2] Flink内存配置 https://www.jianshu.com/p/a29b7b7feaaf

[3] Flink内存设置思路 https://www.cnblogs.com/lighten/p/13053828.html

[4] jvm-profiling-tools/async-profiler https://github.com/jvm-profiling-tools/async-profiler

[5] Memory Analyzer (MAT) https://www.eclipse.org/mat/

[6] Native Memory Tracking https://docs.oracle.com/javase/8/docs/technotes/guides/vm/nmt-8.html

[7] 疑案追踪:Spring Boot内存泄露排查记 https://mp.weixin.qq.com/s/aYwIH0TN3nSzNaMR2FN0AA

[8] jemalloc https://github.com/jemalloc/jemalloc/releases

流计算 Oceanus 限量秒杀专享活动火爆进行中↓↓

流计算 Oceanus | Flink JVM 内存超限的分析方法总结相关推荐

  1. 实时监控:基于流计算 Oceanus ( Flink ) 实现系统和应用级实时监控

    作者:吴云涛,腾讯 CSIG 高级工程师 本文描述了如何使用腾讯云大数据组件来完成实时监控系统的设计和实现,通过实时采集并分析云服务器(CVM)及其 App 应用的 CPU和内存等资源消耗数据,以短信 ...

  2. 指标统计:基于流计算 Oceanus(Flink) 实现实时 UVPV 统计

    作者:吴云涛,腾讯 CSIG 高级工程师 导语 | 最近梳理了一下如何用 Flink 来实现实时的 UV.PV 指标的统计,并和公司内微视部门的同事交流.然后针对该场景做了简化,并发现使用 Flink ...

  3. 数据分析小结:使用流计算 Oceanus(Flink) SQL 作业进行数据类型转换

    作者:吴云涛,腾讯 CSIG 高级工程师 在这个数据爆炸的时代,企业做数据分析也面临着新的挑战, 如何能够更高效地做数据准备,从而缩短整个数据分析的周期,让数据更有时效性,增加数据的价值,就变得尤为重 ...

  4. 腾讯云 AI 视觉产品基于流计算 Oceanus(Flink)的计费数据去重尝试

    | 导语: 介绍下最近使用 Flink 来对计费数据进行去重的具体做法 一. 背景 AI 视觉产品在我们腾讯云-人工智能的产品目录下,包括人脸识别.人脸特效.人脸核身.图像识别.文字识别等. 流计算 ...

  5. EventBridge 实践场景:流计算 Oceanus 告警消息实时推送

    导语 本文演示了如何捕获流计算 Oceanus (Flink) 集群状态变更,并通过事件总线(EventBridge)发送到企业微信或钉钉.飞书客户端. 背景介绍 监控与报警系统对于业务生产环境来说是 ...

  6. 基于流计算 Oceanus(Flink) CDC 做好数据集成场景

    作者:黄龙,腾讯 CSIG 高级工程师 数据时代,企业对技术创新和服务水准的要求不断提高,数据已成为企业极其重要的资产.无论是在在企业数据中台的建设,亦或者是打造一站式数据开发和数据治理的PASS平台 ...

  7. 流计算 Oceanus | 巧用 Flink 构建高性能 ClickHouse 实时数仓

    作者:董伟柯--腾讯云大数据产品中心高级工程师 概述 Apache Flink 是流式计算处理领域的领跑者.它凭借易用.高吞吐.低延迟.丰富的算子和原生状态支持等优势,多方位领先同领域的开源竞品. 同 ...

  8. 流计算 Oceanus 限量1元秒杀,立省2000元

    一.流计算 Oceanus 限量秒杀专享活动 二.什么是ETL ETL(Extract,Transform,Load)描述了将数据从源端经过抽取.转换.加载至目的端的过程. 企业收集到的原始数据通常存 ...

  9. 计算机上没有足够的可用内存无法完成扫描,windows照片查看器无法显示此图片,因为计算机上的可用内存可能不足解决方法...

    win7查看照片显示内存不足怎么办呢?有用户使用win7照片查看器打开图片时提示:windows照片查看器无法显示此图片,因为计算机上的可用内存可能不足.但是电脑硬件配置足够高,内存也不小,那么遇到这 ...

最新文章

  1. java delete 和deleteOnExit 的区别
  2. 超过AttGAN,谷歌推出生成文本到图像的新框架 TReCS
  3. POJ1321(棋盘问题)
  4. power designer 使用vba实现自动化操作学习
  5. 供应商主数据屏幕增强
  6. 如何实现tm同时监控多个状态的改变_广电机房监控系统【斯必得智慧机房】
  7. C++实现线性递归矩阵 linear recurrence matrix算法(附完整源码)
  8. Nacos,阿里开源,是真的香!!
  9. ios测试续航软件,5款iPhone测试iOS电池续航:结果这样 你还升级吗
  10. 红旗Linux职称考试模块,计算机职称考试红旗Linux Desktop 6.0考试大纲
  11. 使用Eclipse创建的第一个javabean,cannot resolved to a type
  12. win10系统安装虚拟机
  13. 【高效开发插件】06 FEHelper 数据格式化
  14. C# winform中使窗体最小化(NotifyIcon)
  15. 加勒比海盗海盗不雅镜头_土豆,海盗和……编程?
  16. 国内公有云大厂核心技术解剖
  17. 面向对象设计的新视角
  18. 第四章第九节数据资产盘点-数据资产目录分类
  19. 【python脚本】linux中编写运行python脚本
  20. PIC16F877A与Proteus仿真-PIC16F877A最小系统及开发环境搭建

热门文章

  1. 李彦宏: 《硅谷商战》 节选
  2. Win10怎么优化网络?网络优化设置
  3. Vue 计算属性缓存和方法的区别:从另一段代码来看【vue3学习笔记】
  4. Class类(基本介绍、常用方法)
  5. 国讯通用OA协同办公系统源码
  6. 杂记 SpaceX如何利用人工智能
  7. 解密|FBI不光有特工,还有智能存储系统!
  8. Github高星项目!用Python开发的一个网易云音乐
  9. 一般集合的合并(并集)
  10. VS SDK更新问题(error MSB8036: 找不到 Windows SDK 版本10.0.14393.0)