作者:余建涛,大数据平台产品中心高级工程师
摘要
Spark UI是查看Spark作业运行情况的重要窗口,用户经常需要根据UI上的信息来判断作业失败的原因或者分析作业如何优化。DLC团队实现了云原生的Spark UI Sevice,相较于开源的Spark History Server,存储成本降低80%,大规模作业UI加载速度提升70%。目前已在公有云多个地域上线,为DLC用户提供Spark UI服务。
背景
Spark History Server原理
Spark History Server(以下简称SHS)是Spark原生的UI服务,为了更好了解本文工作的背景,这里先简单介绍下SHS的原理。概况来讲,SHS建立在Spark事件(Spark Event)之上,通过持久化和回放Spark Event来还原Spark作业当前的状态和运行过程中的统计信息。
图1 原生Spark History Server原理
如图1左侧,在作业运行过程中,Spark Driver内部各模块会不断产生与作业运行相关的事件,如ApplicationStart/ApplicationEnd,JobStart/JobEnd,StageStart/StageEnd,TaskStart/TaskEnd等,所有事件都会发送到LiveListenerBus,然后在LiveListenerBus内部分发到各个子队列,由子队列上注册的Listener来处理。SHS实现了EventLogQueue队列和监听该队列的EventLoggingListener,EventLoggingListener负责将Event序列化为Json格式,然后由EventLogFileWriter持久化到Event Log文件。Event Log文件通常通常存储在分布式文件系统。
图1右侧是Spark History Server,在其内部FsHistoryProvider负责事件回放,即将事件反序列化后发送到ReplayListenerBus,然后由相应的Listener处理。这里主要包含两个过程,首先是Application listing,FsHistoryProvider启动一个线程间歇性地扫描Event Log目录下的所有Application目录,检查log文件是否有更新,对于有更新的Application,则读取Event log,通过AppListingListener回放部分事件提取Application运行的概要信息,存储到KVStore。其次是Loading UI,当用户访问UI时从KVStore中查找请求的Application,如果存在,则完整读取Application目录下的 Event Log 文件,再通过 AppStatusListener从事件中提取运行数据然后更新到 KVStore中,还原任务当前的状态信息。WebUI从KvStore查询所需要的数据,实现页面的渲染。
痛点
存储开销大
Spark作业运行过程中每个Task都会产生相关事件,也就说作业越复杂,Task数越多,产生的事件也会越多。其次Event以json格式序列化,导致占用空间也较大。实际生产中,一个大规模作业的Event Log可以达到数十G。
回放效率低
SHS通过解析回放Event Log来还原Spark作业的状态信息,大量事件的反序列化处理开销大,UI加载延迟明显。对于大规模的作业,从发起访问到看到UI,用户可能需要等待数分钟甚至几十分钟,体验较差。
扩展性差
SHS服务节点通过定期扫描Event log目录,在本地KVStore更新维护Application列表,是一个有状态的服务。每次服务重启,需要重新扫描整个目录,才能对外服务。当目录下积累的作业日志增多,每一次扫描的耗时也会相应增加,此外,日志文件合并、清理负担也会加大,必须对服务节点进行纵向扩容。
不支持多租户
在公有云DLC产品中,我们希望为用户提供SAAS化的Spark UI服务,用户无需自己搭建SHS。一种方案是由服务方为每个用户搭建一套SHS,显然成本会很高,同时也会增加维护的负担;如果一个地域只部署一套SHS,一方面要求服务能通过水平扩展提升处理能力,另外还要求服务支持用户间的资源隔离,比如Event log目录、访问权限,开源SHS不具备相关能力。
DLC UI Service
方案
Spark Driver在运行过程中本身就会通过AppStatusListener监听事件并将作业运行的状态数据存储到ElementTrackingStore(数据存储在基于内存的KVStore),以便跟踪作业的运行情况。History Server回放Event log其实是重复这一过程。如果在作业运行过程中直接将状态数据持久化到FileSystem,这样就不用再存储大量Event了。
org.apache.spark.status.JobDataWrapperorg.apache.spark.status.ExecutorStageSummaryWrapperorg.apache.spark.status.ApplicationInfoWrapperorg.apache.spark.status.PoolDataorg.apache.spark.status.ExecutorSummaryWrapperorg.apache.spark.status.StageDataWrapperorg.apache.spark.status.AppSummaryorg.apache.spark.status.RDDOperationGraphWrapperorg.apache.spark.status.TaskDataWrapperorg.apache.spark.status.ApplicationEnvironmentInfoWrapper
# SQLorg.apache.spark.sql.execution.ui.SQLExecutionUIDataorg.apache.spark.sql.execution.ui.SparkPlanGraphWrapper
# Structure Streamingorg.apache.spark.sql.streaming.ui.StreamingQueryDataorg.apache.spark.sql.streaming.ui.StreamingQueryProgressWrapper
图2 运行状态数据类
基于上述朴素的想法,我们设计了如下方案。
图3 DLC Spark UI Service
- UIMetaListener
UIMetaListener创建一个ElementTrackingStore实例,用作Temp Store。通过一个线程定期遍历Original ElementTrackingStore中的数据,对于每一条数据,检查Temp Store是否存在相同key的旧数据。若不存在,就将数据写入Backup Store,然后再写出到UI Meta文件;若存在则计算两条数据的MD5并进行对比,若不一致,说明数据已更新,就将新的数据写入Backup Store,然后再写出到UI Meta文件。
图4 Dump UI Meta
Temp Store用于临时存储可能还会发生变化的数据,而对于已经完成的Job/Stage/Task,其状态数据不会再变,而且已经持久化到UI Meta文件,因此需要及时将其从Temp Store清理掉,避免占用太多内存资源。UIMetaListener通过两种方式触发清理,一种是监听到TaskStart/TaskEnd事件时触发,一种是往Temp Store写入数据时触发。
- UIMetaWriter
UIMetaWriter定义了UI Meta文件的数据结构,单条结构如下:
图5 数据结构
每个UI相关的数据类实例会序列化成四个部分:类名长度(4字节整型)+ 类型(字符串类型)+ 数据长度(4字节整型)+ 序列化数据(二进制类型)。数据的序列化使用Spark自带的序列化器KVStoreSerializer,支持GZIP压缩。数据在文件中连续存放。
DLC使用对象存储COS来存储UI Meta文件,COS对Append方式写存在诸多限制,同时为了避免Streaming场景下单个文件过大,DLC Spark UI Service实现了RollingUIMetaWriter,支持按文件大小滚动写;同时也实现了SingleUIMetaWriter,适用于支持Append写和对文件个数敏感的文件系统,比如HDFS。
- UIMetaProvider
UIMetaProvider重新实现了ApplicationHistoryProvider,去掉了FsHistoryProvider里的日志路径定期扫描,不再维护全量的Application列表。当收到某个Application UI请求时,UIMetaProvider根据路径规则直接读取对应Application目录下的UI Meta文件,反序列化数据并写入KVStore。简化后的History Server只需要处理加载UI的请求,因此很容易通过水平扩展提升服务整体的处理能力。
跟FsHistoryProvider一样,UIMetaProvider也支持缓存已加载的Active UI数据。但不同的是,对于缓存中的Active UI,UIMetaProvider会定期检查对应的作业状态或日志文件是否有变化,如果有则自动读取新增的UI Meta文件,更新KVStore里的数据,无需每次都从头开始加载。
- 多租户
原生SHS没有多租户设计,默认所有的作业日志都存放在同一个目录下,ACL由每个作业在其运行参数里设置。而DLC为不同用户分配了不同的日志目录,同时希望基于公有云账号进行认证和鉴权,为此Spark UI Service做了一些改造。
用户通过DLC访问Spark UI Service时,首先跳转到公有云登陆入口,完成登陆后在请求cookie中添加userId。Spark UI Service通过HttpRequestUserFilter拦截请求,将Cookie中携带的userId保存在请求处理线程的ThreadLocal变量中。在加载UI Meta时根据userId查询用户的日志目录,然后拼接请求参数中携带的appId和attemptId组成完整的日志路径。同时在缓存Active UI时也会将userId信息随之保存,当命中缓存中UI时也要校验userId和请求中携带的userId是否一致。
测试结果
以SparkPi作为测试作业,分别在四种参数下进行测试。如下图所示,DLC Spark UI Serice相较于开源Spark History Server,日志大小减少了80%,大型作业的UI加载时间减少70%,用户体验明显改善。
图6 日志大小对比
图7 UI加载时间对比
总结
针对云原生场景下的Spark UI需求,DLC重新设计了Spark UI Service方案,并对开源Spark进行了改造,从成本上降低了日志存储开销,从用户体验上加速了UI访问,从架构上实现了服务的水平扩展。
推荐阅读
关注腾讯云大数据公众号
邀您探索数据的无限可能
点击“阅读原文”,了解相关产品最新动态
↓↓↓