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