知识卡片

迭代次数过多触发StackOverflow:递归改迭代、把序列化数据从栈挪到堆的解法

普通读书笔记卡

内容

Hulu在生产环境中遇到一个真实问题:很多机器学习算法需要进行大量轮次的迭代计算,一旦迭代次数超过某个不算很大的阈值(可能只是几百次),社区版Spark的程序就会抛出StackOverflow异常并中止执行。排查后发现根本原因是:Driver把RDD任务发送给Executor执行时,需要把RDD的依赖信息序列化后广播出去,而这个序列化过程需要递归地把它依赖的RDD也一并序列化——如果一个RDD的依赖链比较长(这在多轮迭代场景下很常见,因为每一轮迭代产生的新RDD都依赖上一轮的RDD),这个递归过程占用的线程栈帧内存就会持续累积,最终导致栈溢出。这个问题的解法思路很清晰:既然问题的根源是”用递归的方式在线程栈上处理一条可能很长的依赖链”,那就把这个处理方式从”栈上递归”改造成”堆上迭代”——具体做法是把RDD的依赖关系从原来隐式的递归结构,显式地拆分成两张映射表(rddId到depId列表的映射、depId到具体Dependency的映射),Driver端把RDD本身和这两张映射表一起序列化广播出去,Executor端收到广播消息后,再用这两张映射表重新组装出原始的依赖关系——整个过程不再依赖递归调用来遍历依赖链,而是通过显式的映射表在堆内存里完成同样的信息传递,彻底避开了栈帧内存不足的问题。这次优化需要改动RDD核心的Task接口、经过严格测试才能上线,但效果很明显——优化之后即使迭代一两千次也不再出现问题。这个案例给出了一条排查和解决”资源随着某个参数增长而线性累积、最终耗尽某种有限资源”这类问题的通用思路:一旦定位到问题根源是”某种处理方式(这里是递归)天然依赖一块有限的资源(这里是线程栈),且这个依赖会随着问题规模(这里是依赖链长度)线性增长”,最直接有效的解法往往不是想办法扩大这块有限资源的容量(比如调大栈内存),而是从根本上把处理方式换成不依赖这块有限资源的替代方案(把递归改成基于显式数据结构的迭代,把数据从栈搬到容量大得多的堆)。

参考来源

- 位置:《高可用架构(第1卷)》第6章《大数据与数据库》"6.9 大数据盘点之Spark篇"节,"6.9.2 Spark在Hulu的实践"(源文件:_epub-src/OEBPS/Text/Chapter6_9_3.xhtml) - 结论依据:原文说明"产生上述错误的原因在于Driver将RDD任务发送给Executor执行的时候需要将RDD的信息序列化后广播……这样在运行具有比较长依赖链的RDD的程序时就可能因为线程的栈帧内存不够,造成StackOverflow异常……将递归改为迭代,把原来需要递归保存在线程栈帧的序列化RDD挪到堆区进行保存……在做这种优化之后,迭代个一两千次都没有什么问题",直接支撑本卡片结论。 - 原始内容:产生上述错误的原因在于Driver将RDD任务发送给Executor执行的时候需要将RDD的信息序列化后广播……造成StackOverflow异常的结果。它的解决方法也比较直接,就是将递归改为迭代,把原来需要递归保存在线程栈帧的序列化RDD挪到堆区进行保存。