作者:米哈游大数据开发

近年来,容器、微服务、Kubernetes 等各项云原生技能的日渐老练,越来越多的公司开端挑选拥抱云原生,并开端将 AI、大数据等类型的企业运用布置运转在云原生之上。以 Spark 为例,在云上运转 Spark 能够充沛享有公共云的弹性资源、运维管控和存储服务等,而且业界也呈现了不少 Spark on Kubernetes 的优秀实践。

在刚刚完毕的 2023 云栖大会上,米哈游数据渠道组大数据技能专家杜安明共享了米哈游大数据架构向云原生化晋级过程中的目标、探究和实践,以及怎么经过以阿里云容器服务 ACK 为底座的 Spark on K8s 架构,获得在弹性核算、本钱节省以及存算分离方面的价值。

背景简介

跟着米哈游事务的高速发展,大数据离线数据存储量和核算使命量添加敏捷,前期的大数据离线架构已不再满意新场景和需求。

为了处理原有架构缺少弹性、运维杂乱、资源运用率低一级问题,2022 年下半年,咱们着手调研将大数据根底架构云原生化,并终究在阿里云上落地了Spark on K8s OSS-HDFS 计划,现在在出产环境上已安稳运转了一年左右的时刻,并获得了弹性核算、本钱节省以及存算分离这三大收益。

1. 弹性核算

因为游戏事务会进行周期版别更新、敞开活动以及新游戏的上线等,对离线核算资源的需求与耗费动摇巨大,可能是平常水位的几十上百倍。运用 K8s 集群天然的弹性才能,将 Spark 核算使命调度到 K8s 上运转,能够比较轻松的处理这类场景下资源耗费洪峰问题。

2. 本钱节省

依托阿里云容器服务 Kubernetes 版 ACK 集群本身强壮的弹性才能,一切核算资源按量恳求、用完开释,再加上咱们对 Spark 组件的定制改造,以及充沛运用 ECI Spot 实例,在承载同等核算使命和资源耗费下,本钱节省达 50%。

3. 存算分离

Spark 运转在 K8s 之上,彻底运用 K8s 集群的核算资源,而拜访的则数据也由 HDFS、OSS 逐步切换到 OSS-HDFS 上,中心 Shuffle 数据的读写选用 Celeborn,整套架构完成了核算和存储的解耦,易于保护和扩展。

Spark on K8s架构演进

众所周知,Spark 引擎能够支撑并运转在多种资源办理器之上,比方 Yarn、K8s、Mesos 等。在大数据场景下,现在国内大多公司的 Spark 使命还是运转在 Yarn 集群之上的,Spark 在 2.3 版别初次支撑 K8s,并于 2021 年 3 月发布的 Spark3.1 版别才正式 GA。

相较于 Yarn,Spark 在 K8s 上起步较晚,尽管在老练度、安稳性等方面还存在必定的短缺,可是 Spark on K8s 能够完成弹性核算以及本钱节省等非常突出的收益,所以各大公司也都在不断进行测验和探究,在此过程中,Spark on K8s 的运转架构也在不断的向前迭代演进。

米哈游大数据云原生实践

1. 在离线混部

现在,将 Spark 使命运转在 K8s 上,大多公司选用的计划依旧是在线与离线混合布置的方法。架构规划依据的原理是,不同的事务体系会有不同的事务高峰时刻。大数据离线事务体系典型使命高峰期间会是凌晨的0点到 9 点钟,而像是各种运用微服务、Web 供给的 BI 体系等,常见的事务高峰期是白日时刻,在这个时刻以外的其它时刻中,能够将事务体系的机器 Node 加入到 Spark 所运用的 K8s NameSpace 中。如下图所示,将 Spark 与其他在线运用服务等都布置在一套 K8s 集群之上。

米哈游大数据云原生实践

该架构的长处是能够经过在离线事务的混合布置和错峰运转,来提高机器资源运用率并降低本钱,可是缺点也比较显着,即架构施行起来杂乱,保护本钱比较高,而且难以做到严格的资源隔离,尤其是网络层面的隔离,事务之间不可避免的会产生必定的相互影响,此外,咱们认为该方法也不契合云原生的理念和未来发展趋势。

2. SparkonK8s OSS-HDFS

考虑到在离线混合布置的弊端,咱们规划选用了一种新的、也愈加契合云原生的完成架构:底层存储选用 OSS-HDFS(JindoFs),核算集群选用阿里云的容器服务 ACK,Spark 挑选功用相对丰厚且比较安稳的 3.2.3 版别。

OSS-HDFS 彻底兼容了 HDFS 协议,除了具备 OSS 无限容量、支撑数据冷热存储等长处以外,还支撑了目录原子性、毫秒级 rename 操作,非常适用于离线数仓,能够很好的平替现有 HDFS 和 OSS。

阿里云 ACK 集群供给了高功用、可弹性的容器运用办理服务,能够支撑企业级 Kubernetes 容器化运用的生命周期办理,ECS 是大家所熟知的阿里云服务器,而弹性容器实例 ECI 是一种 Serverless 容器运转服务,能够按量秒级恳求与开释。

该架构简略易保护,底层运用 ECI 的弹性才能,Spark 使命能够较为轻松的应对高峰流量,将 Spark 的 Executor 调度在 ECI 节点上运转,可最大程度的完成核算使命弹性与最佳的降本效果,全体架构的示意图如下所示。

米哈游大数据云原生实践

云原生架构规划与完成

1. 基本原理

在论述详细完成之前,先扼要介绍一下 Spark 在 K8s 上运转的基本原理。Pod 在 K8s 中是最小的调度单元,Spark 使命的 Driver 和 Executor 都是一个独自 Pod,每个 Pod 都分配了唯一的 IP 地址,Pod 能够包含一个或多个 Container,无论是 Driver 还是 Executor 的 JVM 进程,都是在 Container 中进行发动、运转与毁掉的。

一个 Spark 使命被提交到 K8s 集群之后,首要发动的是 Driver Pod,然后 Driver 会向 Apiserver 按需恳求 Executor,并由 Executor 去履行详细的 Task,作业完成之后由 Driver 负责清理一切的 Executor Pod,以下是这几者关系的扼要示意图。

米哈游大数据云原生实践

2. 履行流程

下图展现了完好的作业履行流程,用户在完成 Spark 作业开发后,会将使命发布到调度体系上并进行相关运转参数的装备,调度体系守时将使命提交到自研的 Launcher 中心件,并由中心件来调用 spark-k8s-cli,终究由 Cli 将使命提交至 K8s 集群上。使命提交成功之后,Spark Driver Pod 最先发动,并向集群恳求分配 Executor Pod,Executor 在运转详细的 Task 时,会与外部 Hive、Iceberg、OLAP 数据库、OSS-HDFS 等诸多大数据组件进行数据的拜访与交互,而 Spark Executor 之间的数据 Shuffle 则由 CeleBorn 来完成。

米哈游大数据云原生实践

3. 使命提交

关于怎么将 Spark 使命提交到 K8s 集群上,各个公司的做法不尽相同,下面先扼要描述下现在比较惯例的做法,然后再介绍现在咱们线上所运用的使命提交和办理方法。

3.1 运用原生 spark-submit

经过 spark-submit 命令直接提交,Spark 原生就支撑这种方法,集成起来比较简略,也契合用户的习惯,可是不便利进行作业状况跟踪和办理,无法主动装备 Spark UI 的 Service 和 Ingress,使命完毕后也无法主动清理资源等,在出产环境中并不合适。

3.2 运用 spark-on-k8s-operator

这是现在较常用的一种提交作业方法,K8s 集群需求事先装置 spark-operator,客户端经过 kubectl 提交 yaml 文件来运转 Spark 作业。本质上这是对原生方法的扩展,终究提交作业依然是运用 spark-submit 方法,扩展的功用包括:作业办理,Service/Ingress 创立与清理,使命监控,Pod 增强等。此种方法可在出产环境中运用,但与大数据调度渠道集成性不太好,关于不熟悉 K8s 的用户来说,运用起来杂乱度和上手门槛相对较高。

3.3 运用 spark-k8s-cli

在出产环境上,咱们选用 spark-k8s-cli 的方法进行使命的提交。spark-k8s-cli 本质上是一个可履行的文件,根据阿里云 emr-spark-ack 提交东西咱们进行了重构、功用增强和深度的定制。

spark-k8s-cli 交融 spark-submit 和 spark-operator 两种作业提交方法的长处,使得一切作业都能经过 spark-operator 办理,支撑运转交互式 spark-shell 和本地依靠的提交,而且在运用方法上与原生 spark-submit 语法彻底一致。

在上线运用初期,咱们一切使命的 Spark Submit JVM 进程都发动在 Gateway Pod 中,在运用一段时刻后,发现该方法安稳性缺乏,一旦 Gateway Pod 反常,其上的一切正在 Spark 使命都将失利,别的 Spark 使命的日志输出也欠好办理。鉴于此种状况,咱们将 spark-k8s-cli 改成了每个使命运用独自一个 Submit Pod 的方法,由 Submit Pod 来恳求发动使命的 Driver,Submit Pod 和 Driver Pod 一样都运转在固定的 ECS 节点之上,Submit Pod 之间彻底独立,使命完毕后 Submit Pod 也会主动开释。spark-k8s-cli 的提交和运转原理如下图所示。

米哈游大数据云原生实践

关于 spark-k8s-cli,除了上述基本的使命提交以外,咱们还做了其他一些增强和定制化的功用。

  • 支撑提交使命到同地域多个不同的 K8s 集群上,完成集群之间的负载均衡和故障搬运切换
  • 完成类似 Yarn 资源缺乏时的主动排队等待功用(K8s 假如设置了资源 Quota,当 Quota 到达上限后,使命会直接失利)
  • 添加与 K8s 网络通讯等反常处理、创立或发动失利重试等,对偶发的集群颤动、网络反常进行容错
  • 支撑按照不同部门或事务线,对大规划补数使命进行限流和管控功用
  • 内嵌使命提交失利、容器创立或发动失利以及运转超时等告警功用

4. 日志采集与展现

K8s 集群本身并没有像 Yarn 那样供给日志主动聚合和展现的功用,Driver 和 Executor 的日志搜集需求用户自己来完成。现在比较常见的计划是在各个 K8s Node上布置 Agent,经过 Agent 把日志采集并落在第三方存储上,比方 ES、SLS 等,但这些方法关于习惯了在 Yarn 页面上点击检查日志的用户和开发者来说,运用起来很不便利,用户不得不跳转到第三方体系上抓取检查日志。

为完成 K8s Spark 使命日志的便捷检查,咱们对 Spark 代码进行了改造,使 Driver 和 Executor 日志终究都输出到 OSS 上,用户能够在 Spark UI 和 Spark Jobhistory 上,直接点击检查日志文件。

米哈游大数据云原生实践

上图所示为日志的搜集和展现原理,Spark 使命在发动时,Driver 和 Executor 都会首要注册一个 Shutdown Hook,当使命完毕 JVM 退出时,调用 Hook 方法把完好的日志上传到 OSS 上。此外,想要完好检查日志,还需求对 Spark 的 Job History 相关代码做下改造,需求在 History 页面显现 stdout 和 stderr,并在点击日志时,从 OSS 上拉取对应 Driver 或 Executor 的日志文件,终究由浏览器烘托检查。别的,关于正在运转中的使命,咱们会供给一个 Spark Running Web UI 给用户,使命提交成功后,spark-operator会主动生成的 Service 和 Ingress 供用户检查运转详情,此时日志的获取经过拜访 K8s 的 api 拉取对应 Pod 的运转日志即可。

5. 弹性与降本

根据 ACK 集群供给的弹性弹性才能,再加上对 ECI 的充沛运用,同等规划量级下的Spark 使命,运转在 K8s 的总本钱要显着低于在 Yarn 固定集群上,一起也大大提高了资源运用率。

弹性容器实例 ECI 是一种 Serverless 容器运转服务,ECI 和 ECS 最大的不同就在于 ECI 是按量秒级计费的,恳求与开释速度也是秒级的,所以 ECI 很合适 Spark 这一类负载峰谷显着的核算场景。

米哈游大数据云原生实践

上图示意了 Spark 使命在 ACK 集群上怎么恳求和运用 ECI,运用前提是在集群中装置 ack-virtual-node 组件,并装备好 Vswitch 等信息,在使命运转时,Executor 被调度到虚拟节点上,并由虚拟节点恳求创立和办理 ECI。

ECI 分为一般实例和抢占式实例,抢占式实例是一种低本钱竞价型实例,默许有 1 小时的保护期,适用于大部分 Spark 批处理场景,超出保护期后,抢占式实例可能被强制回收。为进一步提高降本效果,充沛运用抢占式实例的价格优势,咱们对 Spark 进行改造,完成了 ECI 实例类型主动转化的功用。Spark 使命的 Executor Pod 都优先运转在抢占式 ECI 实例上,当产生库存缺乏或其他原因无法恳求创立抢占式实例,则主动切换为运用一般 ECI 实例,确保使命的正常运转。详细完成原理和转化逻辑如下图所示。

米哈游大数据云原生实践

6. Celeborn

因为 K8s 节点的磁盘容量很小,而且节点都是用时恳求、用完开释的,无法保存许多的 Spark Shuffle 数据。假如对 Executor Pod 挂载云盘,挂载盘的巨细难以确定,考虑到数据倾斜等因素,磁盘的运用率也会比较低,运用起来比较杂乱。此外,尽管 Spark 社区在 3.2 供给了 Reuse PVC 等功用,可是调研下来觉得功用尚不齐备且安稳性缺乏。

为处理 Spark 在 K8s 上数据 Shuffle 的问题,在充沛调研和对比多家开源产品后,终究选用了阿里开源的 Celeborn 计划。Celeborn 是一个独立的服务,专门用于保存 Spark 的中心 Shuffle 数据,让 Executor 不再依靠本地盘,该服务 K8s 和 Yarn 均能够运用。Celeborn 选用了 Push Shuffle 的形式,Shuffle 过程为追加写、次序读,提高数据读写功用和效率。

根据开源的 Celeborn 项目,咱们内部也做了一些数据网络传输方面的功用增强、Metrics 丰厚、监控告警完善、Bug 修正等工作,现在已构成了内部安稳版别。

米哈游大数据云原生实践

7. KyuubionK8s

Kyuubi 是一个分布式和多租户的网关,能够为 Spark、Flink 或 Trino 等供给 SQL 等查询服务。在前期,咱们的 Spark Adhoc 查询是发送到 Kyuubi 上履行的。为了处理 Yarn 行列资源缺乏,用户的查询 SQL 无法提交和运转的问题,在 K8s 上咱们也支撑了 Kyuubi Server 的布置运转,当Yarn资源缺乏时,Spark 查询主动切换到 K8s 上运转。鉴于 Yarn 集群规划逐渐减缩,查询资源无法确保,以及保证相同的用户查询体验,现在咱们已将一切的 SparkSQL Adhoc 查询提交到 K8s 上履行。

为了让用户的 Adhoc 查询也能在 K8s 上畅快运转,咱们对 Kyuubi 也做了一些源码改造,包括对 Kyuubi 项目中 docker-image-tool.sh、Deployment.yaml、Dockfile 文件的改写,重定向 Log 到 OSS 上,Spark Operator 办理支撑、权限操控、便捷检查使命运转 UI 等。

米哈游大数据云原生实践

8. K8sManager

在 Spark on K8s 场景下,尽管 K8s 有集群层面的监控告警,可是还不能彻底满意咱们的需求。在出产环境中,咱们愈加重视的是在集群上的 Spark 使命、Pod 状况、资源耗费以及 ECI 等运转状况。运用 K8s 的 Watch 机制,咱们完成了自己的监控告警服务 K8s Manager,下图所示为该服务的示意图。

米哈游大数据云原生实践

K8sManager 是内部完成的一个比较轻量的 Spring Boot 服务,完成的功用就是对各个 K8s 集群上的 Pod、Quota、Service、ConfigMap、Ingress、Role 等各类资源信息监听和汇总处理,然后生成自定义的 Metrics 目标,并对目标进行展现和反常告警,其中包括集群 CPU 与 Memory 总运用量、当时运转的 Spark 使命数、Spark 使命内存资源耗费与运转时长 Top 计算、单日 Spark 使命量汇总、集群 Pod 总数、Pod 状况计算、ECI 机器类型与可用区分布计算、过期资源监控等等,这儿就不一一列举了。

9. 其他工作

9.1 调度使命主动切换

在咱们的调度体系中,Spark 使命支撑装备 Yarn、K8s、Auto 三种履行战略。假如用户使命指明晰需求运转运用的资源办理器,则使命只会在 Yarn 或 K8s 上运转,若用户挑选了 Auto,则使命详细在哪里履行,取决于当时 Yarn 行列的资源运用率,如下图所示。因为总使命量较大,且 Hive 使命也在不断迁移至 Spark,现在仍然有部分使命运转在 Yarn 集群上,但终究的形状一切使命将由 K8s 来保管。

米哈游大数据云原生实践

9.2 多可用区、多交流机支撑

Spark 使命运转过程中许多运用 ECI,ECI 创立成功有两个前提条件: 1、能够恳求到 IP 地址;2、当时可用区有库存。实践上,单个交流机供给的可用IP数量有限,单个可用区拥有的抢占式实例的总个数也是有限的,因而在实践出产环境中,无论是运用一般 ECI 还是 Spot 类型的 ECI,比较好的实践方法是装备支撑多可用区、多交流机。

米哈游大数据云原生实践

9.3 本钱核算

因为在 Spark 使命提交时,都已清晰指定了每个 Executor 的 Cpu、Memory 等类型信息,在使命完毕 SparkContxt 封闭之前,咱们能够从使命的中拿到每个 Executor 的实践运转时长,再结合单价,即可核算出 Spark 使命的大致花费。因为 ECI Spot 实例是跟着市场和库存量随时变化的,该方法核算出来的单使命本钱是一个上限值,主要用于反映趋势。

9.4 优化 SparkOperator

在上线初期使命量较少时,Spark Operator 服务运转杰出,但跟着使命不断增多,Operator 处理各类Event事情的速度越来越慢,乃至集群呈现许多的 ConfigMap、Ingress、Service 等使命运转过程中产生的资源无法及时清理导致堆积的状况,新提交 Spark 使命的 Web UI 也无法翻开拜访。发现问题后,咱们调整了 Operator 的协程数量,并完成对 Pod Event 的批量处理、无关事情的过滤、TTL 删去等功用,处理了 Spark Operator 功用缺乏的问题。

9.5 晋级 SparkK8sClient

Spark3.2.2 选用 fabric8(Kubernetes Java Client)来拜访和操作 K8s 集群中的资源,默许客户端版别为 5.4.1,在此版别中,当使命完毕 Executor 集中开释时,Driver 会许多发送 Delete Pod 的 Api 恳求到 K8s Apiserver 上,对集群 Apiserver 和 ETCD 构成较大的压力,Apiserver 的 cpu 会瞬间飙高。

现在咱们的内部 Spark 版别,已将 kubernetes-client 晋级到 6.2.0,支撑 pod 的批量删去,处理 Spark 使命集中开释时,由许多的删去 Api 恳求操作的集群颤动。

问题与处理计划

在整个 Spark on K8s 的计划规划以及施行过程中,咱们也遇到了各式各样的问题、瓶颈和挑战,这儿做下简略的介绍,并给出咱们的处理计划。

1.弹性网卡开释慢

弹性网卡开释速度慢的问题,归于 ECI 大规划运用场景下的功用瓶颈,该问题会导致交流机上 IP 的剧烈耗费,终究导致 Spark 使命卡住或提交失利,详细触发原因如下图所示。现在阿里云团队现已过技能晋级改造处理,并大幅提高了开释速度和全体功用。

米哈游大数据云原生实践

2.Watcher 失效

Spark 使命在发动 Driver 时,会创立对 Executor 的事情监听器,用于实时获取一切 Executor 的运转状况,关于一些长时运转的 Spark 使命,这个监听器往往会因为资源过期、网络反常等状况而失效,因而在此状况下,需求对 Watcher 进行重置,否则使命可能会跑飞。该问题归于 Spark 的一个 Bug,当时咱们内部版别已修正,并将 PR 供给到了 Spark 社区。

米哈游大数据云原生实践

3.使命卡死

如上图所示,Driver 经过 List 和 Watch 两种方法来获取 Executor 的运转状况。Watch 选用被迫监听机制,可是因为网络等问题可能会产生事情漏接纳或漏处理,但这种概率比较低。List 选用主动恳求的方法,比方每隔 3 分钟,Driver 可向 Apiserver 恳求一次自己使命当时全量 Executor 的信息。

因为 List 恳求使命一切 Pod 信息,当使命较多时,频频 List 对 K8s 的 Apiserver 和 ETCD 构成较大压力,前期咱们封闭了守时 List,只运用 Watch。当 Spark 使命运转反常,比方有许多 Executor OOM 了,有必定概率会导致 Driver Watch 的信息错误,尽管 Task 还没有运转完,可是 Driver 却不再恳求 Executor 去履行使命,产生使命卡死。对此咱们的处理计划如下:

  • 在敞开 Watch 机制的一起,也敞开 List 机制,并将 List 时刻距离拉长,设置每 5 分钟恳求一次
  • 修正 ExecutorPodsPollingSnapshotSource 相关代码,答应 Apiserver 服务端缓存,从缓存中获取全量 Pod 信息,降低 List 对集群的压力

4. Celeborn 读写超时、失利

ApacheCeleborn 是阿里开源的一款产品,前身为 RSS(Remote Shuffle Service)。在前期老练度上还略有短缺,在对网络推迟、丢包反常处理等方面处理的不够完善,导致线上呈现一些有许多 Shuffle 数据的 Spark 使命运转时刻很长、乃至使命失利,以下三点是咱们针对此问题的处理办法。

  • 优化 Celeborn,构成内部版别,完善网络包传输方面的代码
  • 调优 CelebornMaster 和 Worker 相关参数,提高 Shuffle 数据的读写功用
  • 晋级 ECI 底层镜像版别,修正 ECILinux 内核 Bug

5. 批量提交使命时,Quota 锁抵触

为了避免资源被无限运用,咱们对每个 K8s 集群都设置了 Quota 上限。在 K8s 中,Quota 也是一种资源,每一个 Pod 的恳求与开释都会修正 Quota 的内容(Cpu/Memory 值),当许多使命并发提交时,可能会产生 Quota 锁抵触,然后影响使命 Driver 的创立,使命发动失利。

应对这种状况导致的使命发动失利,咱们修正 Spark Driver Pod 的创立逻辑,添加可装备的重试参数,当检测到 Driver Pod 创立是因为 Quota 锁抵触引起时,进行重试创立。Executor Pod 的创立也可能会因为 Quota 锁抵触而失利,这种状况能够不用处理,Executor 创立失利 Driver 会主动恳求创立新的,相当所以主动重试了。

6.批量提交使命时,UnknownHost 报错

当瞬时批量提交许多使命到集群时,多个 Submit Pod 会一起发动,并向 Terway 组件恳求 IP 一起绑定弹性网卡,存在必定概率呈现以下状况,即 Pod 现已发动了,弹性网卡也绑定成功可是实践并没有彻底安排妥当,此时该 Pod 的网络通讯功用实践还无法正常运用,使命拜访 Core DNS 时,恳求无法发出去,Spark 使命报错 UnknownHost 并运转失利。该问题咱们经过下面这两个措施进行躲避和处理:

  • 为每台 ECS 节点,都分配一个 TerwayPod
  • 敞开 Terway 的缓存功用,提早分配好 IP 和弹性网卡,新 Pod 来的直接从缓存池中获取,用完之后归还到缓存池中

7. 可用区之间网络丢包

为保证库存的足够,各 K8s 集群都装备了多可用区,但跨可用区的网络通讯要比同可用区之间通讯的安稳性略差,即可用区之间就存在必定概率的丢包,表现为使命运转时长不安稳。

关于跨可用区存在网络丢包的现象,可测验将 ECI 的调度战略设定为 VSwitchOrdered,这样一个使命的一切 Executor 基本都在一个可用区,避免了不同能够区 Executor 之间的通讯反常,导致的使命运转时刻不安稳的问题。

总结与展望

最终,非常感谢阿里云容器、ECI、EMR 等相关团队的同学,在咱们整个技能计划的落地与实践迁移过程中,给予了非常多的宝贵建议和专业的技能支撑。

现在新的云原生架构已在出产环境上安稳运转了近一年左右的时刻,在未来,咱们将持续对全体架构进行优化和提高,主要围绕以下几个方面:

  1. 持续优化云原生的全体计划,进一步提高体系承载与容灾才能

  2. 云原生架构晋级,更多大数据组件容器化,让全体架构愈加彻底的云原生化

  3. 愈加细粒度的资源办理和精准的本钱操控