监控与调试 监控级别Spark应用程序和作业无论是为了调试还是更好地理解应用程序在集群上的执行过程通过Spark UI和Spark日志是最方便获取监控报告的方式这些报告包括Spark应用程序的运行状态信息例如RDD转换和查询计划的执行信息等。JVMSpark在Java虚拟机JVM上运行执行器因此下一个监视层次是监控虚拟机VM以更好地理解代码的运行方式。JVM提供一些监视工具如用于跟踪堆栈的jstack用于创建堆转储的jmap用于报告时序统计信息的jstat以及用于可视化JVM属性的jconsole这些工具对于那些熟悉JVM内部机理的人员非常有用。你也可以使用像jvisualvm这样的工具来帮助分析Spark作业。其中一些JVM监视信息已经在Spark UI中提供了但对于更低层次的调试上述工具可派上用场。操作系统/主机JVM运行在主机操作系统OS上监视这些机器的运行状态也很重要这包括监控诸如CPU网络I/O等这些信息通常在集群级监控方案中也会报告但是你可以使用更专业的工具来获得更详细的监视信息这些工具包括dstatiostat和iotop集群当然你也可以监视运行Spark应用程序的集群这可能是YarnMesos或Standalone集群集群监控方案很重要如果集群不正常工作你需要很快知道一些流行的集群级监控工具包括Ganglia和Prometheus要监视什么需要监控的主要有两个方面运行应用程序的进程信息CPU使用率内存使用率等以及查询执行过程作业和任务驱动器和执行器进程当监控一个Spark应用程序时最需要注意的是驱动器进程应用程序的所有状态都会在驱动器进程上有所反映你需要确保它正确而稳定的运行。如果你只能监控一台机器或一台JVM那首选就是驱动器节点。当然了解执行器的状态对于监控Spark作业也非常重要Spark提供一个基于Dropwizard Metrics Library的可配置指标监视系统它的配置文件一般在$SPARK_HOME/conf/metrics.properties中指定可以通过更改spark.metrics.conf配置属性来自定义配置文件位置这些监控指标可以输出到包括Ganglia等多种不同的监控系统查询、作业、阶段和任务尽管监视驱动器和执行器进程很重要但有时你还需要对特定查询级别的进程进行调试。两种最常见的监视方式通过Spark日志和Spark UISpark日志获取最详细Spark监视信息的方法之一就是通过日志文件。Spark日志中记录的反常事件或在Spark应用程序中有意添加的输出都可以帮助你发现导致作业执行失败的原因。Spark UISpark UI提供了一种可视化的方式在Spark和JVM级别来监视运行中的应用程序以及Spark工作负载的性能指标。每个运行的SparkContext都将启动一个WebUI默认情况下在端口4040它将列出应用程序的有用信息例如在本地模式下运行Spark时通过访问http://localhost:4040即可在本地计算机上查看Web UI如果你运行多个应用程序他们将各自启动一个Web UI并累加端口号40414042…集群管理器还会从它自己的用户界面连接到每个应用程序的Web UI如下展示了Spark UI中所有的可用选项卡Jobs选项卡对应Spark作业Stages对应各个阶段Storage包含当前在Spark应用程序中缓存的信息和数据Environment包含有关Spark应用程序的配置等相关信息Executors提供应用程序的每个执行器的详细信息SQL对应我们提交的结构化API查询包括SQL和DataFrame调试和Spark抢救方案缓慢任务或落后者此问题在优化应用程序时非常常见这可能是由于工作负载没有被均匀分布在集群各节点上导致负载“倾斜”或者是由于某台计算节点比其他计算节点速度慢例如由于硬件问题表现形式以下都可能是该问题的表现形式Spark阶段中只剩下少数任务未完成这些任务运行了很长时间在Spark UI中可以观察到这些缓慢的任务始终在相同的数据集上发生各阶段都有这些缓慢任务扩大Spark集群规模并没有太大的效果有些任务仍然比其他任务耗时更长在Spark指标中某些执行器进程读取和写入的数据量比其他执行器进程大的多应对措施缓慢任务通常被称为“落后者”有很多原因会导致缓慢任务但最常见的原因是你的数据不均匀地分布到DataFrame或RDD分区上发生这种情况时一些执行器节点可能需要比其他执行器节点更多的工作量。一个特别常见的情况是你使用按键分组操作对应其中一个键的数据比其他键多得多。在这种情况下当你查看Spark UI时你会看到某些节点shuffle的数据比其他大得多。尝试增加分区数以减少每个分区被分配到的数据量尽可能分配给执行器进程更多的内存检查用户定义函数UDF是否在其对象分配或业务逻辑中有资源浪费的情况尝试通过另一种列组合来重新分区例如当你使用ID列进行分区时如果ID是倾斜分布的那么就容易产生落后者。或者当你使用存在许多空值的列进行分区时许多对应空值列的行都被集中分配到一台节点上也会造成落后者在后一种情况下首先筛选出空值可能会有所帮助监视有缓慢任务的执行器节点并确定该执行器节点在其他作业上也总是执行缓慢任务这说明集群中可能存在一个不健康的执行器节点例如磁盘空间不足的节点检查用户定义函数UDF是否在其对象分配或业务逻辑中有资源浪费的情况如果可能尝试将它们转换为DataFrame代码确保你的UDF或用户定义的聚合函数UDAF在足够小的数据上可以运行。通常情况下聚合操作要将大量数据存入内存以处理对某个key的聚合操作从而导致该执行器比其他执行器要完成更多的工作使用Dataset时可能会出现另一个常见问题由于Dataset执行大量的对象实例化并将记录转换为用户定义函数中的Java对象这可能会导致大量垃圾回收。如果你使用Dataset请查看Spark UI中的垃圾回收指标以确定它们是否是导致缓慢任务的原因缓慢的聚合操作如果你的聚合操作速度较慢请先查看“缓慢任务”部分的解决方案尝试过那些之后你可能会继续看到同样的问题。表现形式在执行groupby操作时产生缓慢任务聚合操作之后的作业也执行的非常缓慢应对措施这个问题不能总是能够得到解决。如果你的作业中需要对存在数据倾斜的某个key执行聚合操作那么如果你想在它们上执行聚合操作就是很慢。在聚合操作之前增加分区数量可能有助于减少每个任务中处理的不同key的数量增加执行器进程的内存配额也可以帮助缓解此问题。如果一个key拥有大量数据这将允许其执行器进程更少地与磁盘交互数据并更快完成任务尽管它可能仍然比处理其他key的执行器进程要慢得多。如果你发现聚合操作之后的任务也很慢这意味着你的数据集在聚合操作之后可能仍然不均衡。尝试调用repartition并对数据进行随机重新分区确保涉及的所有过滤操作和select操作在聚合操作之前完成这样可以保证只对需要执行聚合操作的数据进行处理避免处理无关数据。Spark的查询优化器将自动为结构化API执行此操作一些聚合操作本身也比其他聚合操作慢例如collect_list和collect_set是非常慢的聚合函数因为它们必须将所有匹配的对象返回给驱动器进程所以在代码中应该尽量避免使用这些聚合操作确保空值被正确地表示建议使用Spark的null关键字不要用“”或“Empty”之类的空值表示Spark优化器通常会在作业执行初期来跳过对null空值的处理但它无法为你自己定义的空值形式进行此优化