这次要说的点依旧不大,主要想给大家讲的是如果发现 Flink on Yarn 定位Native Memory超出限制一个排查思路加上第二篇文章讲的Direct Memory相关。第四篇我大概率会讲一个堆内存相关的案例。
背景
这次问题发生是在18年,我们开始调研Flink。当时运维帮忙搭建了一个不大的hadoop集群,提供给我们提交perjob任务。本以为可以开开心心的学习Flink,然鹅过程走的确实灰常艰难。
现象
Flink的Taskmanager跑一段时间就会挂掉 ,在Yarn Nodemanager日志就会发现Container 内存超了。无论怎么加内存, Taskmanager迟早都会挂掉。
(当时没有保存现场,这张图找离线的同学要的)
排查过程1: 跟进Jobmanager (AM) 启动时Taskmanager(Container)的内存分配
发现Jobmanager在拼接Taskmanager的启动脚本时,会将Container Memory中只要不是堆内存都会留给Direct Memory (当时测试使用的版本是1.6比较旧,高版本忘了是1.10还是1.11以上已经有配置可以预留内存给Native Memory),且会使用-XX:MaxDirectMemorySize做限制。 既然堆内存,Direct Memory 都可以被限制住,那么预留些内存分配给Native Memory 是不是Container Memory就不会超了。所以加了个配置,从Direct memory 减去一部分什么也不做就预留给Native memory。然鹅,不管预留多少内存1G 3G 5G 给Native Memory 。Container 都会因为内存超出限制被kill掉。
排查过程2 使用pmap 查看进程:
接下来就怀疑是否Native内存泄漏 ,使用pmap -x <进程号> 观察taskmanager 的内存映射。写个脚本定期打印结果,会有很多连续的 64MB的anon的内存段,且每隔一段时间这个叫anon的内存段都会一直增长, 直到内存超过 Container限制被Kill掉。
anon是什么,一顿网上搜索之后发现终于明白:
anon是glibc(为应用系统在多核心CPU和多Sockets环境中高伸缩性提供了一个动态内存分配的特性增强)引入了per thread arena内存池,多个线程之间不再共用一个arena内存区域了,这样避免内存分配时需要额外的锁来降低性能。简单的说就是glibc为了提高性能,为每个线程分配额外的内存池,减少互斥的情况。
一个32位的应用程序进程,最大可创建 2 CPU总核数个arena内存池(MALLOC_ARENA_MAX),每个arena内存池大小为1MB,一个64位的应用程序进程,最大可创建 8 CPU总核数个arena内存池(MALLOC_ARENA_MAX),每个arena内存池大小为64MB
但是glibc 部分版本存在bug 并不会按上述的逻辑生成内存池。
如果不考虑内存分配的性能,遇到这样的问题时,有两种解决方案
1.使用export MALLOC_ARENA_MAX=1禁用per thread arena (hadoop默认是设成了4,但是我们的flink服务依然是会内存泄漏)
2.更换glibc的替代方案。比如 Jemalloc , Tcmalloc
后续
去年(2020年)年底Flink社区在Flink docker镜像默认将Jemalloc替换掉了glibc。使用flink高版本(12+) on k8s 的同学应该是不必再考虑文中介绍的问题。但是如果使用flink on yarn就需要考虑这种情况了。