乐趣区

关于spark:提速-10-倍深度解读字节跳动新型云原生-Spark-History-Server

更多技术交换、求职机会,欢送关注字节跳动数据平台微信公众号,回复【1】进入官网交换群

前不久,在 6 月 29 日 Databricks 举办的 Data + AI Summit 上,火山引擎向大家首次介绍了 UIMeta,一款致力于监控、剖析和优化的新型云原生 Spark History Server,相比于传统的事件日志文件,它在放大了近乎 10 倍体积的根底上,竟然还实现了提速 10 倍!!!

目前,UIMeta Service 曾经取代了原有的 History Server,为字节跳动每天数百万的作业提供服务,并且成为火山引擎 湖仓一体剖析服务 LAS(LakeHouse Analytics Service)的默认服务。

实际上,对于 History Server 来说,事件日志蕴含太多冗余信息,长时间运行的应用程序可能会带来微小的事件日志,这可能须要大量保护并且须要很长时间能力重构 UI 数据从而提供服务。在大规模生产中,作业的数量可能很大,会给历史服务器带来惨重的累赘。接下来,火山引擎 LAS 团队将向大家具体介绍字节跳动外部是怎么基于 UIMeta 实现海量数据业务的安稳和高效运行,让技术驱动业务一直倒退。

业务背景

开源 Spark History Server 架构

为了可能更好了解本次重构的背景和意义,首先对原生 Spark History Server 原理做个简略的介绍。

开源 Spark History Server 流程图

Spark History 建设在 Spark 事件(Spark Event)体系之上。在 Spark 工作运行期间会产生大量蕴含运行信息的 SparkListenerEvent,例如 ApplicationStart / StageCompleted / MetricsUpdate 等等,都有对应的 SparkListenerEvent 实现。所有的 event 会发送到 ListenerBus 中,被注册在 ListenerBus 中的所有 listener 监听。其中 EventLoggingListener 是专门用于生成 event log 的监听器。它会将 event 序列化为 Json 格局的 event log 文件,写到文件系统中(如 HDFS)。通常一个机房的工作的文件都存储在一个门路下。

在 History Server 侧,外围逻辑在 FsHistoryProvider 中。FsHistoryProvider 会维持一个线程间歇扫描配置好的 event log 存储门路,遍历其中的 event log 文件,提取其中概要信息(次要是 appliaction_id, user, status, start_time, end_time, event_log_path),保护一个列表。当用户拜访 UI,会从列表中查找申请所需的工作,如果存在,就残缺读取对应的 event log 文件,进行解析。解析的过程就是一个回放过程(replay)。Event log 文件中的每一行是一个序列化的 event,将它们逐行反序列化,并应用 ReplayListener 将其中信息反馈到 KVStore 中,还原工作的状态。

无论运行时还是 History Server,工作状态都存储在无限几个类的实例中,而它们则存储在 KVStore 中,KVStore 是 Spark 中基于内存的 KV 存储,能够存储任意的类实例。前端会从 KVStore 查问所需的对象,实现页面的渲染。

痛点

存储空间开销大

Spark 的事件体系十分具体,导致 event log 记录的事件数量十分大,对于 UI 显示来说,大部分 event 是无用的。并且 event log 个别应用 json 明文存储,空间占用较大。对于比较复杂或工夫长的工作,event log 能够达到几十 GB。字节外部 7 天的 event log 占用约 3.2 PB 的 HDFS 存储空间。

回放效率差,提早高

History Server 采纳回放解析 event log 的形式还原 Spark UI,有大量的计算开销,当工作较大就会有显著的响应提早,响应提早是指从用户发动前端拜访到页面 UI 齐全渲染进去的期待时长。作业完结之后,用户可能要等十几分钟甚至半小时能力通过 History Server 看到作业历史。而大型作业完结后,用户往往心愿尽快看到作业历史从而依据作业历史进行问题诊断和作业优化,用户期待 UI 实现渲染工夫过长,十分影响用户体验。

扩展性差

如上所述,History Server 的 FsHistoryProvider 在回放解析文件之前,须要先扫描配置的 event log 门路,遍历其中的 event log,将所有文件的元信息加载到内存中,这使得原生服务成为了有状态的服务。因而每次服务重启,都须要从新加载整个门路,能力对外服务。每个工作在实现后,也须要期待下一轮扫描能力被拜访到。

当集群工作数量增多,每一轮扫描文件的耗时以及元信息内存占用都会减少,这也要求服务有越来越高的资源配置。如果通过拆分 event log 门路来放大单实例的压力,须要对路由规定进行革新,运维难度增大。目前,字节跳动外部通过减少 UIService 实例就能够不便的进行程度扩大。

非云原生

Spark History Server 并非是云原生的服务,在私有云场景下革新和保护老本高。首先私有云场景须要进行租户资源隔离,其次私有云场景下不同用户的 workload 差别很大,不同用户任务量有数量级的差异,会呈现大量长尾作业。为每个用户独自部署 History Server 计算和存储老本过大且不平衡,而部署对立的 History Server 无奈做到资源隔离,一旦呈现问题影响较多用户,两种形式运维老本都会很高。火山引擎湖仓一体剖析引擎 LAS(Lakehouse Analytics Service),提供了云原生的 UIService,能够无效解决上述问题。

UIService

计划

为了解决后面的三个问题,咱们尝试对 History Server 进行革新。如上所述,无论运行中的 Spark Driver 还是 History Server,都是通过监听 event,将其中蕴含的工作变动信息反映到几种 UI 相干的类的实例中,而后存入 KVStore 供 UI 渲染。也就是说,KVStore 中存储着 UI 显示所需的齐备信息。对于 History Server 的用户来说,绝大多数状况下咱们只关怀工作的最终状态,而无需关怀引起状态变动的具体 event。因而,咱们能够只将 KVStore 长久化下来,而不须要存储大量冗余的 event 信息。此外,KVStore 原生反对了 Kryo 序列化,性能显著于 Json 序列化。咱们基于此思维重写了一套新的 History Server 零碎,命名为 UIService。

UIService 框架图

实现

UIMetaStore

KVStore

中和 UI 相干的所有类实例,咱们将这些类统称为 UIMeta 类。具体包含 AppStatusStore 和 SQLAppStatusStore 中的信息(如下所列)。咱们定义一个类 UIMetaStore 来形象,一个 UIMetaStore 即一个工作所有 UI 信息的汇合。

UIMetaStore 所蕴含信息:

# AppStatusStore
org.apache.spark.status.JobDataWrapper
org.apache.spark.status.ExecutorStageSummaryWrapper
org.apache.spark.status.ApplicationInfoWrapper
org.apache.spark.status.PoolData
org.apache.spark.status.ExecutorSummaryWrapper
org.apache.spark.status.StageDataWrapper
org.apache.spark.status.AppSummary
org.apache.spark.status.RDDOperationGraphWrapper
org.apache.spark.status.TaskDataWrapper
org.apache.spark.status.ApplicationEnvironmentInfoWrapper

# SQLAppStatusStore
org.apache.spark.sql.execution.ui.SQLExecutionUIData
org.apache.spark.sql.execution.ui.SparkPlanGraphWrapper

UIMetaStore 还定义了长久化文件的数据结构,构造如下:

4-Byte Magic Number: "UI_S"
----------- Body ---------------
4_byte_length_of_class_name | class_name_str1 | 4_byte_length | serialized_of_class1_instance1
4_byte_length_of_class_name | class_name_str1 | 4_byte_length | serialized_of_class1_instance2
4_byte_length_of_class_name | class_name_str2 | 4_byte_length | serialized_of_class2_instance1
4_byte_length_of_class_name | class_name_str2 | 4_byte_length | serialized_of_class2_instance2

Magic Number 用于文件类型标识校验。

Body 是 UIMetaStore 的主体数据,应用间断存储。每一个 UI 相干的类实例,会序列化成四个片段:类名长度(4 byte long 类型)+ 类名(string 类型)+ 数据长度(4 byte long 类型)+ 序列化的数据(二进制类型)。在读取时程序读取,每个元素先读取长度信息,再依据长度读取后续相应数据进行反序列化。

应用 Spark 原生的 KVStoreSerializer 序列化,能够保障前后兼容性。

UIMetaLoggingListener

相似于 EventLoggingListener,为 UIMeta 开发了专用的 Listener —— UIMetaLoggingListener,用于监听事件,写 UIMeta 文件。和 EventLoggingListener 进行比照:EventLoggingListener 每承受一个 event 都会触发写,写的是序列化的 event;而 UIMetaLoggingListener 只会被特定的 event 触发,目前是只会被 stageEnd,JobEnd 事件触发,但每次写操作是批量的写,将上一阶段的 UIMetaStore 的信息残缺地长久化。做一个类比,EventLoggingListener 好比流式,一直地追加写,而 UIMetaLoggingListener 相似于批式,定期将工作状态快照下来。

UIMetaProvider

替换原先的 FsHistoryProvider,次要区别在于:

将读取 event log 文件和回放生成 KVStore 的流程改为读取 UIMetaFile,反序列化出 UIMetaStore。

去掉了 FsHistoryProvider 的门路扫描逻辑;每次 UI 拜访,依据 appid 和门路规定,间接去读取 UIMetaFile 解析。这使得 UIService 无需预加载所有文件元信息,不须要随着工作数量减少进步服务器配置,不便了程度扩大。

优化

防止反复写

因为每个 stage 实现都会触发写 UIMeta 文件,这样对于 UIMeta 的很多元素,可能会呈现反复长久化的状况,减少写入耗时和文件的大小。因而咱们在 UIMetaLoggingListener 外部保护了一个 map,记录曾经被序列化的实例。在写 UIMeta 文件时进行过滤,只写没有写过或者数据产生扭转的元素。这样能够杜绝大部分的写冗余。此外,开发期间发现,占用空间最大的是 task 级别信息 TaskDataWrapper。在一个 stage 实现触发写时。可能会将仍处于 RUNNING 状态的 stage 的 task 序列化下来,这样当 RUNNING 的 stage 实现时,task 信息会再被写一次,也会造成数据冗余,因而咱们对序列化 TaskDataWrapper 信息进行过滤,在 stage 完结时只长久化状态是 Completed 的 task 信息。

反对回退到 event log

鉴于 UIService 在初期有存在问题的危险,咱们还反对了回退机制,即拜访一个工作的 UI,优先尝试走 UIService 的门路:解析 UIMeta 文件,如果 UIMeta 文件不存在或者解析报错,会回退到读 event log 文件的门路,防止 UI 拜访失败。同时还反对将 event log 文件转换成 UIMeta 文件,这样下一次调用时就能够应用 UIService。这个性能保障咱们迁徙过程的平滑。

收益

存储收益

线上测试显示存储均匀缩小 85%,总量缩小 92.4%。

下图显示了某机房 event log 和 UIMeta 存储占用监控,能够看到 UIMeta 较 event log 在存储量上有数量级的缩小。目前字节外部 7 天的 event log 占用存储空间 3.2 PB,改用 UIMeta 后,空间占用只有 350TB。

凭借 UIService 的存储劣势,咱们能够保留更长时间的日志信息,有助于历史剖析,问题复盘。目前咱们已从保留 7 天日志进步到了保留 30 天,并能够依据需要增大保留工夫。

某机房 event log/UIMeta HDFS 存储监控比照拜访提早收益拜访提早:均匀缩短 35%,PCT90/95/99 别离缩小 84.6%/90.8%/93.7%。拜访提早百分位散布如下图所示,UIService 的 UI 拜访提早整体较比 event log 向左移,长尾工作显著缩小。

拜访提早分布图

架构收益

去掉了原生 History Server 遍历门路,预加载的耗时环节,打消从工作实现到 History Server 可拜访的工夫距离,从本来的均匀 10min 左右升高到秒级,工作实现即可立刻对外提供服务。同时使 History Server 能够程度扩大,能更好应答将来任务量增长带来的挑战。目前,字节跳动外部咱们通过减少 UIService 实例就能够不便的进行横向扩大。在火山引擎湖仓一体剖析服务 LAS 中,咱们也基于 UIService 实现了反对租户拜访隔离,云原生的,可按需伸缩的 Spark History Server。

立刻跳转火山引擎湖仓一体剖析服务 LAS 官网理解详情

退出移动版