写在之前:
本篇文章写就工夫较早,因而本文所探讨的 Spark SQL 非最新版本,后续更新版本可能有局部修复和更新。
一、Spark 内存泄露
1. 高并发状况下的内存泄露的具体表现
首先得很遗憾的通知各位,Spark 在设计之初的架构就不是为了高并发申请而生的,咱们尝试在网络条件不好的集群下,进行 100 并发的查问,在压测 3 天后发现了内存泄露。
在进行大量小 SQL 的压测过程中发现,有大量的 activejob 在 spark ui 上始终处于 pending 状态,且永远不完结,如下图所示:
并且发现 driver 内存爆满:
咱们通过内存剖析工具剖析了下:
2. 高并发下 AsynchronousListenerBus 引起的 WEB UI 的内存泄露
短时间内 Spark 提交大量的 SQL,而且 SQL 外面存在大量的 union 与 join 的情景,会创立大量的 event 对象,使得这里的 event 数量超过 10000 个 event。
一旦超过 10000 个 event 就开始抛弃 event,而这个 event 是用来回收资源的,抛弃了资源就无奈回收了。针对 UI 页面的这个问题,咱们将这个队列长度的限度给勾销了。
3.AsynchronousListenerBus 自身引起的内存泄露
通过抓包咱们发现:
这些 event 是通过 post 办法传递的,并写入到队列里。
然而也是由一个单线程进行 postToAll 的:
然而在高并发状况下,单线程的 postToAll 的速度没有 post 的速度快,会导致队列沉积的 event 越来越多,如果是持续性的高并发的 SQL 查问,这里就会导致内存泄露。
接下来咱们在剖析下 postToAll 的办法外面,哪个门路是最慢的,导致事件处理最慢的逻辑是哪个?
可能有的同学都不敢相信,通过 jstack 抓取剖析,程序大部分工夫都阻塞在记录日志上。
能够通过禁用这个中央的 log 来晋升 event 的速度。
4. 高并发下的 Cleaner 的内存泄露
说道这里,Cleaner 的设计应该算是 spark 最蹩脚的设计。spark 的 ContextCleaner 是用于回收与清理曾经实现了的 播送 boradcast,shuffle 数据的。然而高并发下,咱们发现这个中央积攒的数据会越来越多,最终导致 driver 内存跑满而挂掉。
咱们先看下,是如何触发内存回收的:
没错,就是通过 System.gc() 回收的内存,如果咱们在 jvm 里配置了禁止执行 System.gc,这个逻辑就等于废掉(而且有很多 jvm 的优化参数个别都举荐配置禁止 system.gc 参数)。
这是一个单线程的逻辑,而且每次清理都要协同很多机器一起清理,清理速度相对来说比较慢,然而 SQL 并发很大的时候,产生速度超过了清理速度,整个 driver 就会产生内存泄露。而且 brocadcast 如果占用内存太多,也会应用十分多的本地磁盘小文件,咱们在测试中发现,高持续性并发的状况下本地磁盘用于存储 blockmanager 的目录占据了咱们 60% 的存储空间。
咱们再来剖析下 clean 外面,哪个逻辑最慢:
真正的瓶颈在于 blockManagerMaster 外面的 removeBroadcast,因为这部分逻辑是须要逾越多台机器的。
针对这种问题,咱们在 SQL 层加了一个 SQLWAITING 逻辑,判断了沉积长度,如果沉积长度超过了咱们的设定值,咱们这里将阻塞新的 SQL 的执行。沉积长度能够通过更改 conf 目录下的 ya100_env_default.sh 中的 ydb.sql.waiting.queue.size 的值来设置。
倡议集群的带宽要大一些,万兆网络必定会比千兆网络的清理速度快很多。给集群劳动的机会,不要始终持续性的高并发,让集群有间断的机会。增大 spark 的线程池,能够调节 conf 下的 spark-defaults.conf 的如下值来改善。
5. 线程池与 threadlocal 引起的内存泄露
发现 spark,hive,lucene 都十分钟爱应用 threadlocal 来治理长期的 session 对象,期待 SQL 执行结束后这些对象可能主动开释,然而与此同时 spark 又应用了线程池,线程池里的线程始终不完结,这些资源始终就不开释,工夫久了内存就堆积起来了。
针对这个问题,咱们批改了 spark 要害线程池的实现,更改为每 1 个小时,强制更换线程池为新的线程池,旧的线程数可能主动开释。
6. 文件泄露
这时有同学会发现,随着申请的 session 变多,spark 会在 hdfs 和本地磁盘创立海量的磁盘目录,最终会因为本地磁盘与 hdfs 上的目录过多,而导致文件系统和整个文件系统瘫痪。针对这种状况,咱们也做了对应解决。
7.deleteONExit 内存泄露
为什么会有这些对象在外面,咱们看下源码:
8.JDO 内存泄露
多达 10 万多个 JDOPersistenceManager:
9.listerner 内存泄露
通过 debug 工具监控发现,spark 的 listerner 随着工夫的积攒,告诉 (post) 速度运来越慢。
排查之后发现所有代码都卡在了 onpostevent 上:
jstack 的后果如下:
钻研下了调用逻辑如下,发现是循环调用 listerners,而且 listerner 都是空执行才会产生下面的 jstack 截图:
通过内存发现有 30 多万个 linterner 在外面:
发现都是大多数都是同一个 listener, 咱们核查下该处源码:
最终定位了问题,确系是这个中央的 BUG,每次创立 JDBC 连贯的时候,spark 就会减少一个 listener,工夫久了,listener 就会积攒越来越多。针对这个问题,我简略的批改了一行代码,开始进入下一轮的压测。
二、Spark 源码调优
测试发现,即便只有 1 条记录,应用 spark 进行一次 SQL 查问也会耗时 1 秒,对很多即席查问来说 1 秒的期待,对用户体验十分不敌对。针对这个问题,咱们在 spark 与 hive 的细节代码上进行了部分调优,调优后,响应工夫由原先的 1 秒缩减到当初的 200~300 毫秒。
以下是咱们改变过的中央
1.SessionState 的创立目录,占用较多的工夫
如果相熟 Hadoop namenode HA 的同学会留神到,如果第一个 namenode 是 standby 状态,这个中央会更慢,就不止 1 秒,所以除了改变源码外,目前在应用 namenode ha 的同学肯定要留神,将 active 状态的 node 肯定要放在后面。
2.HiveConf 的初始化过程占用太多工夫
频繁的 hiveConf 初始化,须要读取 core-default.xml,hdfs-default.xml,yarn-default.xml
,mapreduce-default.xml,hive-default.xml 等多个 xml 文件,而这些 xml 文件都是内嵌在 jar 包内的。
第一,解压这些 jar 包须要消耗较多的工夫,第二每次都对这些 xml 文件解析也消耗工夫。
3. 播送 broadcast 传递的 hadoop configuration 序列化很耗时
L-configuration 的序列化,采纳了压缩的形式进行序列化,有全局锁的问题。
L-configuration 每次序列化,传递了太多了没用的配置项了,1000 多个配置项,占用 60 多 Kb。咱们剔除了不是必须传输的配置项后,缩减到 44 个配置项,2kb 的大小。
4. 对 spark 播送数据 broadcast 的 Cleaner 的改良
因为 SPARK-3015 的 BUG,spark 的 cleaner,目前为单线程回收模式。
大家注意 spark 源码正文:
其中的 单线程瓶颈点在于播送数据的 cleaner,因为要逾越很多台机器,须要通过 akka 进行网络交互。
如果回收并发特地大,SPARK-3015 的 bug 报告会呈现网络拥挤,导致大量的 timeout 呈现。
为什么回收量特变大呢?其实是因为 cleaner 实质是通过 system.gc(), 定期执行的,默认积攒 30 分钟或者进行了 gc 后才触发 cleaner, 这样就会导致霎时,大量的 akka 并发执行,集中开释,这才造成了网络的霎时瘫痪。
然而单线程回收意味着回收速度恒定,如果查问并发很大,回收速度跟不上 cleaner 的速度,会导致 cleaner 积攒很多,会导致过程 OOM(YDB 做了批改,会限度前台查问的并发)。
不论是 OOM 还是限度并发都不是咱们心愿看到的,所以针对高并发状况下,这种单线程的回收速度是满足不了高并发的需要的。
对于官网的这样的做法,咱们示意并不是一个完满的 cleaner 计划。并发回收肯定要反对,只有解决 akka 的 timeout 问题即可。
所以这个问题要仔细分析一下,akka 为什么会 timeout,是因为 cleaner 占据了太多的资源,那么咱们是否能够管制下 cleaner 的并发呢?比如说应用 4 个并发,而不是默认将全副的并发线程都给占满呢?这样及解决了 cleaner 的回收速度,也解决了 akka 的问题不是更好么?
针对这个问题,咱们最终还是抉择了批改 spark 的 ContextCleaner 对象,将播送数据的回收,改成多线程的形式,限度了线程的并发数量,从而解决了该问题。