详解 Flink 容器化环境下的 OOM Killed

简介: 本文将解析 JVM 和 Flink 的内存模型,并总结在工作中遇到和在社区交流中了解到的造成 Flink 内存使用超出容器限制的常见原因。由于 Flink 内存使用与用户代码、部署环境、各种依赖版本等因素都有紧密关系,本文主要讨论 on YARN 部署、Oracle JDK/OpenJDK 8、Flink 1.10+ 的情况。

在生产环境中,Flink 通常会部署在 YARN 或 k8s 等资源管理系统之上,进程会以容器化(YARN 容器或 docker 等容器)的方式运行,其资源会受到资源管理系统的严格限制。另一方面,Flink 运行在 JVM 之上,而 JVM 与容器化环境并不是特别适配,尤其 JVM 复杂且可控性较弱的内存模型,容易导致进程因使用资源超标而被 kill 掉,造成 Flink 应用的不稳定甚至不可用。

针对这个问题,Flink 在 1.10 版本对内存管理模块进行了重构,设计了全新的内存参数。在大多数场景下 Flink 的内存模型和默认已经足够好用,可以帮用户屏蔽进程背后的复杂内存结构,然而一旦出现内存问题,问题的排查和修复都需要比较多的领域知识,通常令普通用户望而却步。

为此,本文将解析 JVM 和 Flink 的内存模型,并总结在工作中遇到和在社区交流中了解到的造成 Flink 内存使用超出容器限制的常见原因。由于 Flink 内存使用与用户代码、部署环境、各种依赖版本等因素都有紧密关系,本文主要讨论 on YARN 部署、Oracle JDK/OpenJDK 8、Flink 1.10+ 的情况。此外,特别感谢 @宋辛童(Flink 1.10+ 新内存架构的主要作者)和 @唐云(RocksDB StateBackend 专家)在社区的答疑,令笔者受益匪浅。

JVM 内存分区

对于大多数 Java 用户而言,日常开发中与 JVM Heap 打交道的频率远大于其他 JVM 内存分区,因此常把其他内存分区统称为 Off-Heap 内存。而对于 Flink 来说,内存超标问题通常来自 Off-Heap 内存,因此对 JVM 内存模型有更深入的理解是十分必要的。

根据 JVM 8 Spec[1],JVM 管理的内存分区如下图:

image.png

img1. JVM 8 内存模型

除了上述 Spec 规定的标准分区,在具体实现上 JVM 常常还会加入一些额外的分区供进阶功能模块使用。以 HotSopt JVM 为例,根据 Oracle NMT[5] 的标准,我们可以将 JVM 内存细分为如下区域:

● Heap: 各线程共享的内存区域,主要存放 new 操作符创建的对象,内存的释放由 GC 管理,可被用户代码或 JVM 本身使用。
● Class: 类的元数据,对应 Spec 中的 Method Area (不含 Constant Pool),Java 8 中的 Metaspace。
● Thread: 线程级别的内存区,对应 Spec 中的 PC Register、Stack 和 Natvive Stack 三者的总和。
● Compiler: JIT (Just-In-Time) 编译器使用的内存。
● Code Cache: 用于存储 JIT 编译器生成的代码的缓存。
● GC: 垃圾回收器使用的内存。
● Symbol: 存储 Symbol (比如字段名、方法签名、Interned String) 的内存,对应 Spec 中的 Constant Pool。
● Arena Chunk: JVM 申请操作系统内存的临时缓存区。
● NMT: NMT 自己使用的内存。
● Internal: 其他不符合上述分类的内存,包括用户代码申请的 Native/Direct 内存。
● Unknown: 无法分类的内存。

理想情况下,我们可以严格控制各分区内存的上限,来保证进程总体内存在容器限额之内。但是过于严格的管理会带来会有额外使用成本且缺乏灵活度,所以在实际中为了 JVM 只对其中几个暴露给用户使用的分区提供了硬性的上限,而其他分区则可以作为整体被视为 JVM 本身的内存消耗。

具体可以用于限制分区内存的 JVM 参数如下表所示(值得注意的是,业界对于 JVM Native 内存并没有准确的定义,本文的 Native 内存指的是 Off-Heap 内存中非 Direct 的部分,与 Native Non-Direct 可以互换)。

image.png

从表中可以看到,使用 Heap、Metaspace 和 Direct 内存都是比较安全的,但非 Direct 的 Native 内存情况则比较复杂,可能是 JVM 本身的一些内部使用(比如下文会提到的 MemberNameTable),也可能是用户代码引入的 JNI 依赖,还有可能是用户代码自身通过 sun.misc.Unsafe 申请的 Native 内存。理论上讲,用户代码或第三方 lib 申请的 Native 内存需要用户来规划内存用量,而 Internal 的其余部分可以并入 JVM 本身的内存消耗。而实际上 Flink 的内存模型也遵循了类似的原则。

Flink TaskManager 内存模型

首先回顾下 Flink 1.10+ 的 TaskManager 内存模型。

image.png

img2. Flink TaskManager 内存模型

显然,Flink 框架本身不仅会包含 JVM 管理的 Heap 内存,也会申请自己管理 Off-Heap 的 Native 和 Direct 内存。在笔者看来,Flink 对于 Off-Heap 内存的管理策略可以分为三种:

● 硬限制(Hard Limit): 硬限制的内存分区是 Self-Contained 的,Flink 会保证其用量不会超过设置的阈值(若内存不够则抛出类似 OOM 的异常),
● 软限制(Soft Limit): 软限制意味着内存使用长期会在阈值以下,但可能短暂地超过配置的阈值。
● 预留(Reserved): 预留意味着 Flink 不会限制分区内存的使用,只是在规划内存时预留一部分空间,但不能保证实际使用会不会超额。

结合 JVM 的内存管理来看,一个 Flink 内存分区的内存溢出会导致何种后果,判断逻辑如下:

1、若是 Flink 有硬限制的分区,Flink 会报该分区内存不足。否则进入下一步。
2、若该分区属于 JVM 管理的分区,在其实际值增长导致 JVM 分区也内存耗尽时,JVM 会报其所属的 JVM 分区的 OOM (比如 java.lang.OutOfMemoryError: Jave heap space)。否则进入下一步。
3、该分区内存持续溢出,最终导致进程总体内存超出容器内存限制。在开启严格资源控制的环境下,资源管理器(YARN/k8s 等)会 kill 掉该进程。

为直观地展示 Flink 各内存分区与 JVM 内存分区间的关系,笔者整理了如下的内存分区映射表:

image.png

img3. Flink 分区及 JVM 分区内存限制关系

根据之前的逻辑,在所有的 Flink 内存分区中,只有不是 Self-Contained 且所属 JVM 分区也没有内存硬限制参数的 JVM Overhead 是有可能导致进程被 OOM kill 掉的。作为一个预留给各种不同用途的内存的大杂烩,JVM Overhead 的确容易出问题,但同时它也可以作为一个兜底的隔离缓冲区,来缓解来自其他区域的内存问题。

举个例子,Flink 内存模型在计算 Native Non-Direct 内存时有一个 trick:

Although, native non-direct memory usage can be accounted for as a part of the framework off-heap memory or task off-heap memory, it will result in a higher JVM’s direct memory limit in this case.

虽然 Task/Framework 的 Off-Heap 分区中可能含有 Native Non-Direct 内存,而这部分内存严格来说属于 JVM Overhead,不会被 JVM -XX:MaxDirectMemorySize 参数所限制,但 Flink 还是将它算入 MaxDirectMemorySize 中。这部分预留的 Direct 内存配额不会被实际使用,所以可以留给没有上限 JVM Overhead 占用,达到为 Native Non-Direct 内存预留空间的效果。

OOM Killed 常见原因

与上文分析一致,实践中导致 OOM Killed 的常见原因基本源于 Native 内存的泄漏或者过度使用。因为虚拟内存的 OOM Killed 通过资源管理器的配置很容易避免且通常不会有太大问题,所以下文只讨论物理内存的 OOM Killed。

RocksDB Native 内存的不确定性

众所周知,RocksDB 通过 JNI 直接申请 Native 内存,并不受 Flink 的管控,所以实际上 Flink 通过设置 RocksDB 的内存参数间接影响其内存使用。然而,目前 Flink 是通过估算得出这些参数,并不是非常精确的值,其中有以下的几个原因。

首先是部分内存难以准确计算的问题。RocksDB 的内存占用有 4 个部分[6]:

● Block Cache: OS PageCache 之上的一层缓存,缓存未压缩的数据 Block。
● Indexes and filter blocks: 索引及布隆过滤器,用于优化读性能。
● Memtable: 类似写缓存。
● Blocks pinned by Iterator: 触发 RocksDB 遍历操作(比如遍历 RocksDBMapState 的所有 key)时,Iterator 在其生命周期内会阻止其引用到的 Block 和 Memtable 被释放,导致额外的内存占用[10]。

前三个区域的内存都是可配置的,但 Iterator 锁定的资源则要取决于应用业务使用模式,且没有提供一个硬限制,因此 Flink 在计算 RocksDB StateBackend 内存时没有将这部分纳入考虑。

其次是 RocksDB Block Cache 的一个 bug[8][9],它会导致 Cache 大小无法严格控制,有可能短时间内超出设置的内存容量,相当于软限制。

对于这个问题,通常我们只要调大 JVM Overhead 的阈值,让 Flink 预留更多内存即可,因为 RocksDB 的内存超额使用只是暂时的。

glibc Thread Arena 问题

另外一个常见的问题就是 glibc 著名的 64 MB 问题,它可能会导致 JVM 进程的内存使用大幅增长,最终被 YARN kill 掉。

具体来说,JVM 通过 glibc 申请内存,而为了提高内存分配效率和减少内存碎片,glibc 会维护称为 Arena 的内存池,包括一个共享的 Main Arena 和线程级别的 Thread Arena。当一个线程需要申请内存但 Main Arena 已经被其他线程加锁时,glibc 会分配一个大约 64 MB (64 位机器)的 Thread Arena 供线程使用。这些 Thread Arena 对于 JVM 是透明的,但会被算进进程的总体虚拟内存(VIRT)和物理内存(RSS)里。

默认情况下,Arena 的最大数目是 cpu 核数 * 8,对于一台普通的 32 核服务器来说最多占用 16 GB,不可谓不可观。为了控制总体消耗内存的总量,glibc 提供了环境变量 MALLOC_ARENA_MAX 来限制 Arena 的总量,比如 Hadoop 就默认将这个值设置为 4。然而,这个参数只是一个软限制,所有 Arena 都被加锁时,glibc 仍会新建 Thread Arena 来分配内存[11],造成意外的内存使用。

通常来说,这个问题会出现在需要频繁创建线程的应用里,比如 HDFS Client 会为每个正在写入的文件新建一个 DataStreamer 线程,所以比较容易遇到 Thread Arena 的问题。如果怀疑你的 Flink 应用遇到这个问题,比较简单的验证方法就是看进程的 pmap 是否存在很多大小为 64MB 倍数的连续 anon 段,比如下图中蓝色几个的 65536 KB 的段就很有可能是 Arena。

image.png

img4. pmap 64 MB arena

这个问题的修复办法比较简单,将 MALLOC_ARENA_MAX 设置为 1 即可,也就是禁用 Thread Arena 只使用 Main Arena。当然,这样的代价就是线程分配内存效率会降低。不过值得一提的是,使用 Flink 的进程环境变量参数(比如 containerized.taskmanager.env.MALLOC_ARENA_MAX=1)来覆盖默认的 MALLOC_ARENA_MAX 参数可能是不可行的,原因是在非白名单变量(yarn.nodemanager.env-whitelist)冲突的情况下, NodeManager 会以合并 URL 的方式来合并原有的值和追加的值,最终造成 MALLOC_ARENA_MAX="4:1" 这样的结果。

最后,还有一个更彻底的可选解决方案,就是将 glibc 替换为 Google 家的 tcmalloc 或 Facebook 家的 jemalloc [12]。除了不会有 Thread Arena 问题,内存分配性能更好,碎片更少。在实际上,Flink 1.12 的官方镜像也将默认的内存分配器从 glibc 改为 jemelloc [17]。

JDK8 Native 内存泄漏

Oracle Jdk8u152 之前的版本存在一个 Native 内存泄漏的 bug[13],会造成 JVM 的 Internal 内存分区一直增长。

具体而言,JVM 会缓存字符串符号(Symbol)到方法(Method)、成员变量(Field)的映射对来加快查找,每对映射称为 MemberName,整个映射关系称为 MemeberNameTable,由 java.lang.invoke.MethodHandles 这个类负责。在 Jdk8u152 之前,MemberNameTable 是使用 Native 内存的,因此一些过时的 MemberName 不会被 GC 自动清理,造成内存泄漏。

要确认这个问题,需要通过 NMT 来查看 JVM 内存情况,比如笔者就遇到过线上一个 TaskManager 的超过 400 MB 的 MemeberNameTable。

img5. JDK8 MemberNameTable Native 内存泄漏

在 JDK-8013267[14] 以后,MemeberNameTable 从 Native 内存被移到 Java Heap 当中,修复了这个问题。然而,JVM 的 Native 内存泄漏问题不止一个,比如 C2 编译器的内存泄漏问题[15],所以对于跟笔者一样没有专门 JVM 团队的用户来说,升级到最新版本的 JDK 是修复问题的最好办法。

YARN mmap 内存算法

众所周知,YARN 会根据 /proc/${pid} 下的进程信息来计算整个 container 进程树的总体内存,但这里面有一个比较特殊的点是 mmap 的共享内存。mmap 内存会全部被算进进程的 VIRT,这点应该没有疑问,但关于 RSS 的计算则有不同标准。 依据 YARN 和 Linux smaps 的计算规则,内存页(Pages)按两种标准划分

: ● Private Pages: 只有当前进程映射(mapped)的 Pages

● Shared Pages: 与其他进程共享的 Pages

● Clean Pages: 自从被映射后没有被修改过的 Pages

● Dirty Pages: 自从被映射后已经被修改过的 Pages 在默认的实现里,YARN 根据 /proc/${pid}/status 来计算总内存,所有的 Shared Pages 都会被算入进程的 RSS,即便这些 Pages 同时被多个进程映射[16],这会导致和实际操作系统物理内存的偏差,有可能导致 Flink 进程被误杀(当然,前提是用户代码使用 mmap 且没有预留足够空间)。

为此,YARN 提供 yarn.nodemanager.container-monitor.procfs-tree.smaps-based-rss.enabled 配置选项,将其设置为 true 后,YARN 将根据更准确的 /proc/${pid}/smap 来计算内存占用,其中很关键的一个概念是 PSS。简单来说,PSS 的不同点在于计算内存时会将 Shared Pages 均分给所有使用这个 Pages 的进程,比如一个进程持有 1000 个 Private Pages 和 1000 个会分享给另外一个进程的 Shared Pages,那么该进程的总 Page 数就是 1500。 回到 YARN 的内存计算上,进程 RSS 等于其映射的所有 Pages RSS 的总和。在默认情况下,YARN 计算一个 Page RSS 公式为: ``` Page RSS = Private_Clean + Private_Dirty + Shared_Clean + Shared_Dirty ``` 因为一个 Page 要么是 Private,要么是 Shared,且要么是 Clean 要么是 Dirty,所以其实上述公示右边有至少三项为 0 。而在开启 smaps 选项后,公式变为: ``` Page RSS = Min(Shared_Dirty, PSS) + Private_Clean + Private_Dirty ``` 简单来说,新公式的结果就是去除了 Shared_Clean 部分被重复计算的影响。 虽然开启基于 smaps 计算的选项会让计算更加准确,但会引入遍历 Pages 计算内存总和的开销,不如 直接取 /proc/${pid}/status 的统计数据快,因此如果遇到 mmap 的问题,还是推荐通过提高 Flink 的 JVM Overhead 分区容量来解决。

作者:林小铂

原文链接 

本文为阿里云原创内容,未经允许不得转载

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/514456.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

哈哈!TCP泄露了操作系统信息···

作者:轩辕之风O来源: 编程技术宇宙前言大家好,我是轩辕。前几天,我在读者群里提了一个问题:这一下,大家总算停止了灌水(这群人都不用上班的,天天划水摸鱼),开…

android 自定义view控件,Android 自定义View——自定义View控件

Android给我们提供了大量的View控件,但这还是远远满足不了我们的要求,有时候开发所需要的控件形式是在Android提供的控件中是不存在,这就需要我们自己去定义一个。那么如何自定义控件?学习自定义控件,首先要先掌握Canv…

解读容器的 2020:寻找云原生的下一站

简介: “云原生”到底是什么?它就是容器和 Kubernetes 吗?虚拟机是云原生的吗?…… 2020 年注定是不凡的。它在阴霾中开始,在惊叹中结束,也让未来变得更加扑朔迷离。那么,容器与云原生的 2020 年…

如何用好云的弹性

简介: 如何用好云的弹性 1. 弹性为什么这么重要 做好弹性让IT能力轻松跟上用户的业务发展,做到多、快、好、省。 多:选择多,可以全球建站、机型选择也多、配套云服务也多。 快:部署快,自建IDC的建设时长以…

android笔试添加自定义服务,Android之Listview(item为单选题)自定义adapter,像考试时前面的10几道单选题的实现...

用于展现重复性的东西,Listview比较好用,看了别人的自定义Adapter(item是EditText,能够很好地获取到每一个item的EditText值)。又由于在做项目的需要,故特制了一个item包含RadioGroup的Listview的自定义Adapter。主要功能&#xf…

实现工具自由!开源的桌面工具箱

来源: HelloGitHubRubick,因为开源所有更自由在一切开始之前,首先要致敬 uTools!如果没有它就没有 Rubick。大家好,我是“拉比克”(Rubick)项目的作者木偶。我做的 Rubick 是一款基于 Electron 的开源桌面工具箱&#…

测试一年多,上线就崩溃!微服务到底应该怎么测试?

简介: 只有了解风险,才能及时应对,保障服务高可用。 不久前,也就是11月16日,澳大利亚交易所(Australian Securities Exchange, ASX)上线了一个新的交易系统,但因为出现故障而被迫关闭…

阿里云熊鹰:基于融合、协同系统的边缘云原生架构演进和实践

简介: 云原生和边缘计算是近两年都非常火的技术话题了,在第十届云计算标准和应用大会上,阿里云高级技术专家熊鹰分享了《基于融合、协同系统的边缘云原生架构演进和实践》,希望通过介绍现在阿里云在边缘计算和边缘云原生这些技术领…

漫画:什么是 “建造者模式” ?

作者&#xff1a;东风玖哥来源&#xff1a; 程序员小灰————— 第二天 —————————————————首先&#xff0c;我们来定义一个Product类&#xff1a;public class Product {ArrayList<String> parts new ArrayList<String>();public void add(S…

阿里云云效何勉:云原生是“精益实践”的最佳助力

简介&#xff1a; 1月15日&#xff0c;国内知名“精益产品开发”研究和实践者、阿里云云效资深技术专家何勉在阿里云《云计算情报局》线上直播栏目中&#xff0c;分享其对研发新模式的最新思考&#xff0c;提出“下一代精益开发方法”&#xff0c;助力企业在研发效能上提升10倍…

开发效率提升15倍!批流融合实时平台在好未来的应用实践

简介&#xff1a; 本文由好未来资深数据平台工程师毛祥溢分享&#xff0c;主要介绍批流融合在教育行业的实践。内容包括两部分&#xff0c;第一部分是好未来在做实时平台中的几点思考&#xff0c;第二部分主要分享教育行业中特有数据分析场景。 1.背景介绍 好未来介绍 好未来是…

那些公司都配不上我,所以我选择创业

【CSDN 编者按】她&#xff0c;一个89年的农村姑娘&#xff0c;从种蘑菇到卖煤球&#xff0c;再到敲代码做云原生&#xff0c;成为企业创始人&#xff0c;一路走来她都经历了哪些困难与挑战&#xff0c;对于新生代程序员又有哪些建议&#xff1f;让我们一起来看看她是怎么说的。…

如何跑通第一个 SQL 作业

简介&#xff1a; 本文由阿里巴巴技术专家周凯波&#xff08;宝牛&#xff09;分享&#xff0c;主要介绍如何跑通第一个SQL。 一、SQL的基本概念 1.SQL 分类 SQL分为四类&#xff0c;分别是数据查询语言&#xff08;DQL&#xff09;、数据操纵语言&#xff08;DML&#xff09…

云原生DevOps的5步升级路径

简介&#xff1a; 究竟什么是云原生DevOps呢&#xff1f;我们认为&#xff1a;云原生DevOps是充分利用云原生基础设施&#xff0c;基于微服务/无服务架构体系和开源标准&#xff0c;语言和框架无关&#xff0c;具备持续交付和智能自运维能力&#xff0c;从而做到比传统DevOps更…

数据创新的四个陷阱

简介&#xff1a; 数据的重要性在当今已经无需在多言&#xff0c;所有的企业都意识到数据的重要性&#xff0c;都希望利用数据来驱动业务的发展。但是&#xff0c;很多企业信息化管理者依然存在对于数据智能&#xff0c;数据驱动的一些误解&#xff0c;这些误解会让企业的数据利…

Parallels 发布 Desktop 17版本,支持 Windows 11 和 macOS Monterey

Parallels Desktop 17 亮点速揽&#xff1a;性能提升&#xff0c;对图像效果加以改进、增强了 Windows 游戏体验&#xff0c;同时是全球首个在搭载 Apple M1 芯片的 Mac 上运行的 macOS Monterey 虚拟机&#xff0c;带来更为无缝的跨平台工作体验。 编辑 | 宋慧 头图 | Paralle…

如何将实时计算 Flink 与自身环境打通

简介&#xff1a; 如何使用实时计算 Flink 搞定数据处理难题&#xff1f;实时计算 Flink 客训练营产品、技术专家齐上阵&#xff0c;从 Flink的发展、 Flink 的技术原理、应用场景及行业案例&#xff0c;到开源Flink功能介绍和实时计算 Flink 优势详解&#xff0c;现场实操&…

一文知晓浪潮云海OS在SPEC Cloud测试中的调优实践!

日前&#xff0c;SPEC&#xff08;Standard Performance Evaluation Corporation&#xff0c;即国际标准性能评测组织&#xff09;公布了最新 Cloud IaaS 2018 Benchmark 测试成绩&#xff0c;在同规模测试场景下浪潮数据核心产品浪潮云海OS再次刷新世界纪录&#xff0c;性能得…

阿里云 EMR Delta Lake 在流利说数据接入中的架构和实践

简介&#xff1a; 为了消灭数据孤岛&#xff0c;企业往往会把各个组织的数据都接入到数据湖以提供统一的查询或分析。本文将介绍流利说当前数据接入的整个过程&#xff0c;期间遇到的挑战&#xff0c;以及delta在数据接入中产生的价值。 背景 流利说目前的离线计算任务中&…

怎么提升写代码的能力

简介&#xff1a; 对于程序员而言&#xff0c;我始终认为代码是展现能力的关键&#xff0c;一个优秀程序员写的代码&#xff0c;和一个普通程序员写的代码是很容易看出差别的&#xff0c;代码作为程序员的硬实力和名片的展示&#xff0c;怎么提升写代码的能力始终是一个关键的话…