Spark on Kubernetes 在多点DMALL的探索实践

摘要:多点在云原生建设中,全力推动Spark on k8s及大数据调度平台、日志采集、集群监控等组件的升级迭代,打造出了一套云中立,更具性价比的存算分离架构体系,平稳支撑了公司To B业务的开展。

一、背景

1.1 Hadoop 生态:短板凸显

多点 DMALL 最初使用的是传统的 Hadoop 生态搭建大数据集群。使用 HDFS + Hive + Spark + Flink + Yarn 的经典组合,通过Cloudera发行版的 CDH 统一管理,满足从底层存储、即席查询、在离线任务到资源调度的全方位大数据计算支持。在这个闭环服务中,各个服务没有明显短板,运行也相对稳定。

e46f569a730d37722fc5ac4afaa8d0d2.png

可惜世界上没有最完美的技术。Hadoop 生态紧密利用底层服务器,间接捆绑了磁盘存储和 CPU 算力。当二者的扩展速度随着公司的发展差异越来越大时,传统的大数据架构的短板——成本居高不下,资源预估困难等开始显现。除此之外,当我们尝试使用 Spark 替代 Hive 提升计算效率时,在当前的架构下遇到了不小的困境:

  1. Spark版本难以统一,管理困难

基于历史原因,多点 DMALL 最初为用户提供 Spark2.4.6 和 Spark2.3.1 版本。后为了将资源占用较多的 Hive 查询引导到 Spark SQL 中,我们在架构中引入 Apache Kyuubi ,伴随引入了 Spark3.x 。历次 Spark 升级部分 API 和 SQL 语法的不同导致无法对历史任务一刀切,传统的 Hadoop 生态下也无法提供独立调试环境辅助进行数据层的“蓝绿组”测试,这也就导致我们只能在集群中同时支持多个 Spark 版本,运维管理难度很大。

  1. CDH限制太严格,无法局部更新

CDH 套件对原生的各个组件做过部分代码的适配,对其版本要求有着严格的限制,无法单独对某些组件进行升级。而这些组件的版本往往限制了大数据计算效率的上限。举个例子,Hive MetaStore(简称HMS)的版本导致 Spark 版本升级后仍需要向下兼容适配,带来了隐含的风险,HMS 缺失的新特性也导致高版本的 Spark 无法发挥最大效能。

  1. Yarn潮汐现象严重,高峰时期任务执行资源难以保证

Yarn 潮汐现象属于老生常谈了。大数据集群的通病就是夜晚的资源占用率最高,而白天会较为清闲,这样的现象就好比木桶效应,只能先满足最短的木条(夜晚的资源使用),而无视白天的空闲,造成了一定的闲置。另一方面,Spark任务的资源由用户自定义,当晚上大部分数据开始计算时,任务的资源申请往往难以保证,随时“横空出世”一个大资源占用的任务,就可以堵住整个流程。虽然我们从平台功能的角度进行了限制和监控,但风险依旧存在。

1.2 云原生:优势乍现

为了解决不断凸显的 Hadoop 生态架构短板,我们将目光放在了云原生上。经过一段时间的调研分析,我们认为云原生架构可以解决当前多点 DMALL 大数据面临的困境。恰巧此时,是一个绝佳的架构升级的契机:

  • 从业务上来看,随着多点 DMALL 全面 To B 转型,为越来越多的 B 端客户提供零售全渠道解决方案,需要具备在多云部署环境下提供更具性价比、可复用的大数据底层基座和平台工具链。

  • 从技术上来看,Spark 对 Kubernetes 的适配越来越成熟,开源社区也涌现了多种多样的管理工具辅助大数据上云。这些技术让我们得以全面拥抱云原生。

52e0f9e1fc9afe63e1892f63add50bf3.png

于是,在云原生的新架构中,基于 Kubernetes 的基座,我们灵活混部了大数据集群和 OLAP 集群,甩开 CDH 的版本限制,剥离 Hadoop 生态的强依赖,充分利用对象存储,简化运维和部署,降低用户使用成本。

多点 DMALL 的大数据架构从传统的 Hadoop 生态升级到云原生版本经历了好几个月的打磨,尤其是围绕核心离线数据计算层的 Spark on Kubernetes。本文中,我们希望通过阐述 Spark on Kubernetes 最终设计及过程中的适配和踩坑历程,为也在做这方面尝试的大数据团队提供借鉴和经验。

二、架构变迁

2.1 最初的架构

c78d9df364d028ba29eb75ef22a12836.png

在最初的大数据架构中,我们主要依赖传统的 Hadoop 生态搭建集群。

任务在多点 DMALL 自研的大数据平台 UniData 配置后,通过自研的调度体系 UniData TaskCenter(后简称TaskCenter)的 Server 模块进行 DAG 的解析,定时分配到不同的调度执行器 TaskCenter Executor 上。每个执行器是单独部署在大数据集群的机器上的。收到来自 Server 的信息后,Executor 会通过本机部署的 Spark 提交 Spark Application 到 Yarn 集群。为了节省资源,调度执行器和 HDFS 的 DataNode 是混部的。Spark 操作的数据的底层存储来自 HDFS,Executor 会定时跟踪监控 Spark Application 的任务状态,检测到任务结束后通知 Server 进而通知用户进行数据和日志的查看。

另一条使用 Spark 的通道来自即席查询。为了提升查询效率,我们通过引入 Apache Kyuubi 将 Hive 查询引导到Spark SQL 中。同样的道理,Kyuubi 也是直接部署在大数据集群的机器中的。用户在即席查询页面查询一条 SQL后,页面通过 JDBC 连接的方式连接 Kyuubi Server,经由 Kyuubi 转发到对应的 Spark Application 中进行查询。最终查询结果也是通过 JDBC 的方式返回给用户展示。

可以看到,在最初的架构中,存在不少需要单独部署在集群机器上的组件。这不仅会影响同在一台机器的数据节点和计算节点,在集群搭建过程中也带来了不小的麻烦。运维同学需要跟踪关注每一台机器,如若单台机器产生故障,影响范围的预估和迁移服务等也变得复杂和繁琐。

2.2 云原生架构

64201bc3e0bad83a345d2fe07fe06301.png

为了解决之前架构中部署复杂的问题,云原生架构中我们将绝大部分应用和引擎都迁移到了 Kubernetes 上。

任务调度系统也进行了全面升级。整个调度体系拆分成了三个模块:Server、Executor 和 Watcher。TaskCenter Server 依旧负责统筹整个调度体系,解析 DAG 和用户的配置,定时组装任务派发给 TaskCenter Executor。当 TaskCenter Executor 在 Kubernetes 上提交任务后,TaskCenter Watcher 开始跟踪任务,定时记录任务状态和资源获取情况,以确保任务的正常运行。除此之外,我们还将即席查询的组件也部署在 Kubernetes 中,所有的调用均为Kubernetes 内部调用,与离线任务共用 Spark 镜像,减少架构复杂度。

集群中的底层数据不再存储在本地磁盘中,而是使用云厂商提供的海量对象存储,更方便计算数据量和成本。Spark 等计算引擎通过 JuiceFS 可以与对象存储无缝衔接,无需做额外的改造处理。

Spark Shuffle 也做了新的设计,我们引入了 Apache Celeborn 进行远程 Shuffle 接收,以更好的提升 Spark 运行效率。Celeborn 就安装在 Kubernetes 部署上,减少网络交互和开销。

总体来看,云原生架构的改造不仅彻底避免了底层服务器对上层应用和引擎的影响,通过存算分离的设计,更是将集群效率和容量限制消除殆尽。

接下来详细阐述我们在每个细节功能点的组件选择和解决方案。

2.2.1 集群混部

升级到云原生架构后,通过混部集群降低成本成为了可能。我们利用 Kubernetes 上对 Node 的污点设置,将大数据集群与 OLAP 集群混部。白天时将二者剥离,避免离线任务、即席查询等与高并发的 OLAP 查询相互抢占资源;夜间我们会将二者融合,以对抗大量的凌晨时分的离线任务资源消耗。

2.2.2 存算分离

相比于许多企业在升级架构时试图将传统的 Hadoop 搬上 Kubernetes 相比,我们做了更为彻底的调整:不去尝试将 HDFS 搬上 Docker,而是采用开源组件 JuiceFS 对接海量对象存储。JuiceFS 是一个专为云环境设计,兼容 POSIX、HDFS 和 S3 协议的分布式文件系统。只需要修改配置参数,各个引擎组件便可以以 HDFS API 的方式正常使用 JuiceFS,无缝衔接使用存储在对象存储上的数据,以此彻底剥离存储和计算,进一步优化成本结构。

2.2.3 调度器

网上有许多开源的 Kubernetes 调度器,经过分析和对比,我们最终选用 volcano。作为历史“悠久”的项目,volcano 也是第一个被 Spark 和 Flink 官方社区对接的 Kubernetes 调度器,尤其在当前 Spark3.x 中有着很好的适配效果。volcano 支持多种调度算法,还提供了类似 Yarn 的队列支持业务层面的资源隔离,可以很好的辅助任务平滑迁移到新架构上。在使用中,我们意识到原生支持的 Node 亲和性和反亲和性的效果粒度较粗,为了能够更精细的控制各个任务 pod 的分布,我们添加了关于 pod 的亲和性配置,并已提交到开源社区。

2.2.4 任务调度

众所周知,当前提交 Spark 任务到 Kubernetes 共有两种方式:原生 Spark-Submit 和 Google Operator。

  • 原生 Spark-Submit:

    • 是 Spark 社区提供的最直接的方式,内部通过 Kubernetes API 在 Kubernetes 上创建 Driver Pod,然后此 Pod再创建管理其他 Executor Pod。

    • 优势:简单直接,对 Spark 熟悉的开发者更容易上手。

    • 劣势:Spark 与 Kubernetes 的更新频率并非同步,很多参数的处理转换并不及时,导致许多 Kubernetes 的特性无法利用。除此之外,也缺乏对 Spark 任务统一的管理组件,任务的状态、日志等处理需要额外定制。

  • Google Operator

    • Google 提供的一种提交方式。Google Operator 会在 Kubernetes 上启动一个常驻服务,用户通过API将需要提交的任务信息直接传递给该服务,该服务收到后便启动对应的 Pod。

    • 优势:拥有统一管理的服务,对于任务状态跟踪、日志处理等都有较为完善的方案。提交的任务是以Kubernetes YAML 的方式,支持绝大部分 Kubernetes 的参数,想要利用 Kubernetes 的特性更加便捷。

    • 劣势:这种方式上手不如 Spark-Submit 的方式那么直观,需要一定学习成本。而且根据我们的经验,Google Operator 的社区维护并不活跃,使用中会发现一些问题需要自主调整代码,建议上手前充分阅读源码。

在新架构中我们针对不同的场景使用了不同的方式。就离线任务而言,我们自研的调度体系 TaskCenter 采用 Google Operator 的方式,支持更多 Kubernetes 特性的使用,也可以更方便的管理正在运行的 Spark Application;至于即席查询,基于所选用的 Apache Kyuubi 组件,还是采用传统的 Spark-Submit 的方式进行任务的提交。毕竟即席查询的用户仅关注查询的结果,对于执行进度、日志等更详细的信息往往并不关心。

2.2.5 Shuffle处理

Shuffle 的处理绝对算是 Spark on Yarn 升级到 on Kubernetes 时大部分企业都会面临的问题了。传统的 on Yarn 的方式,Shuffle 可以依赖节点挂载的大容量磁盘进行处理,但如果 on Kubernetes 还是以这种方式解决,那就是视“存算分离”的目的于无物了。再加上 Spark3.x 开始支持 Dynamic Allocation,如果没有很好的处理 Shuffle 的数据,这个资源节省“利器”就无法有效利用。

经过长时间的调研测试,我们最终决定选用开源的 Apache Celeborn 组件进行处理。Celeborn 部署在大数据集群Kubernetes 中,会首先将 Shuffle 数据存储在本地磁盘中。一些集群中为了避免与其他落盘数据争抢资源,我们也会通过 PVC 的方式,使用云厂商提供的块存储等进行 Shuffle 数据存储。当本地磁盘或块存储的容量超过设定的阈值时,Celeborn 会将数据推到 HDFS 中。同样的,在这里我们使用 JuiceFS,将写 HDFS 转化为写对象存储。从此Shuffle 有海量对象存储做后备,不用担心其丢失的问题,Dynamic Resource Allocation 也可以充分利用起来,提升整体 Spark性能。

2.2.6 集群和任务监控

  1. 管理者视角:Kubernetes的整体资源监控

基于 Prometheus + Grafana 我们搭建了一套自上而下的 Kubernetes 资源监控体系。

首先,可以通过监控,快速获取 Kubernetes 集群中 Node 和 NameSpace 的资源使用,及时分析整体资源消耗。这有利于管理员判断集群资源是否已达瓶颈,以及如何搭配资源使用以降低总体成本。

针对在大数据平台上执行的各类任务,该监控体系也支持从资源和队列角度跟踪任务等待和执行,管理员不仅可以快速排查当前资源拥堵原因,及时疏通,保障任务顺利运行,也可根据历史数据判定夜间资源拥堵点,对任务的启动时间分布进行优化,保障任务的正常执行。

更进一步,用户可以通过监控体系深入观察各个任务的资源使用情况,明确申请资源和实际使用之间的差距,帮助用户更合理的配置任务资源,提高总体资源利用率。

  1. 用户视角:Spark Application的执行监控

对于 Spark Application 的执行进度跟踪,我们依旧采用 Spark History 的组件处理。得益于 eventlogs 落地无惧存储压力的对象存储,使用这样传统的方式对用户上手也更加便捷。为了安全性考虑,TaskCenter 对于 Spark UI 的链接做了 Kubernetes 内的转发处理,用户无法通过公网进入 Spark History 的 web 页面,只能通过 TaskCenter 管理页面对自己有权限的任务进行详情查看,避免了敏感信息的泄露风险。

Spark Application 的资源使用情况是通过 TaskCenter Watcher 模块采集的。为了更好的把控集群整体资源使用情况,精细化控制任务的资源使用,Watcher 模块在 Spark Application 提交之后便定时追踪任务的资源情况,对其中出现的各种异常及时反馈给用户和系统管理员进行人工处理,甚至支持一些自动处理策略。

2.2.7 日志采集

虽然 Google Operator 提供了日志采集方案,考虑到需要采集更多种任务类型(Spark、Flink、Java、Shell等)的日志,我们设计开发了独立的日志采集组件。我们选择基于 Fluent Bit 工具采集 Kubernetes 中任务输出到控制台的日志,并将日志数据同步写入 Kafka 。考虑到 Elasticsearch 的写入效率有限,我们选择异步的方式将日志写入 Elasticsearch中供下游展示和分析。

2.2.8 弹性扩缩容

大数据集群的任务多集中在夜间,白天任务执行较少。如果全部使用固定的 Kubernetes Node 从成本上并不划算,资源也存在浪费。因此,我们选择固定节点+弹性池的方式,在夜间任务多跑在大量弹性节点上,而白天仅使用搭载平台服务和引擎的少量固定节点支撑集群运行。

Kubernetes 原生并不支持按照不同时间段启停弹性池,而云厂商提供的功能也仅支持手动操作控制台。显然,这样的方式对大数据集群的 Kubernetes 运维并不方便。

深入研究后,我们设计了基于亲和性的弹性扩缩容方案。我们在所有固定节点和弹性节点上分别设置不同的 Label,当需要开启弹性池时,将任务执行的亲和性修改为更“倾向于固定节点,当固定节点无资源时可使用弹性节点”的匹配逻辑,此时 Kubernetes 会根据任务的亲和性判断,资源不够时从弹性池中启动弹性节点支持任务执行。在需要关闭弹性池时,任务的亲和性会调整为“必须为固定节点”,从而不会触发弹性节点。该时间限制是在大数据调度平台上配置的,方便人工运维管理。

三、踩经验

架构升级是一个系统工程。在不断磨合不断适配的过程中,我们也踩了许多坑。

3.1 .stop()与主动退出问题

在使用 Spark on Kubernetes 初期,我们便发现一个令人头疼的现象。原先在 on Yarn 能正常运行的任务,在 on Kubernetes 环境中之行结束后不会退出,进一步分析发现在需要在 Spark Application 代码里主动调用 sparksession.stop() 才能结束 Driver Pod 。这个“小事”在历史 Spark 任务迁移上绝对算是个“拦路虎”,也因此困扰了我们很久。经过团队的探索,后来我们通过调研发现,这是来自Spark的bug(https://github.com/apache/spark/pull/25785),升级到 Spark3.3.x 之后便可以正常使用了。

3.2 Spark UI  Service IP分配问题

使用 Google Operator 提交 Spark 任务时,默认会分配单独的 Service IP 供外网进行查看。很明显,这不符合安全管控,而且海量的 Spark 任务会造成大量资源浪费,也会损耗一部分计算效率。在调度体系设计中,Spark UI 仅能通过 TaskCenter 管理页面进行跳转查看,无需通过外网进入。我们索性修改了 Google Operator 对于这个处理的参数,将 Cluster IP 设置为“none”,从而避免了外网 IP 和端口的分配。

3.3 Spark任务清理

通过传统 Spark-Submit 提交的任务,任务执行完后 Spark Driver 不会退出。这可不是 Bug,这是 Spark 的一个特殊设计,开源社区官方考虑到用户需要通过查看日志来获得任务执行信息,如果 Driver 自动退出,日志将会全部丢失,Spark 就决定不自动删除 Driver Pod。这个决定可“苦了”所有使用这种方式提交任务的用户。Driver Pod 不清理,占用的各种资源也不会自动清理,未释放的资源越来越多。经过对网上各类解决方案的汇总,为了快速清理已完成的任务,节约资源,最终我们决定利用 Kubernetes 的 CronJob,定时清理这类“已完成,未清理”的 Pod。

3.4 自定义spark.app.name

Spark的参数设置可以通过多种方式,除了提交任务时在命令中显示添加外,在创建 Spark Session 的时候代码中也可以设置,而这一部分参数是调度平台无法掌控的。用户有时会在代码中自定义一些参数,尤其是 spark.app.name。当设置该参数为中文时,on Yarn 不会受到任何影响,但 on Kubernetes 时该参数转化成的 Spark Executor Name 就不符合 Kubernetes 的命名规则,以至于失败报错。为了解决这类问题,我们只好在 TaskCenter中强制注入参数 spark.kubernetes.executor.podNamePrefix,以确保任务的正常运行。

3.5 使用zstd压缩Shuffle报错

许多企业分享的 Spark on Kubernetes 升级中,都提到了使用 zstd 算法代替默认的 lz4 算法进行 Shuffle 数据的压缩。从我们的测试结果来看,相比默认的 lz4,在基本相同的时间内,zstd 算法确实能大大减少 Shuffle 数据量。但同时我们也发现,在部分任务中出现了以下的报错信息:

Caused by: java.io.IOException: Decompression error: Corrupted block detected
        at com.github.luben.zstd.ZstdInputStreamNoFinalizer.readInternal(ZstdInputStreamNoFinalizer.java:171)
        at com.github.luben.zstd.ZstdInputStreamNoFinalizer.read(ZstdInputStreamNoFinalizer.java:123)
        at java.base/java.io.BufferedInputStream.fill(Unknown Source)
        at java.base/java.io.BufferedInputStream.read1(Unknown Source)
        at java.base/java.io.BufferedInputStream.read(Unknown Source)
        at com.esotericsoftware.kryo.io.Input.fill(Input.java:164)
        ... 32 more

上述报错当任务存在 broadcast 时便会出现。经过调研发现是因为使用的Spark3.3.4中依赖的 zstd-jni 的包版本较早导致的:

<dependency>
        <groupId>com.github.luben</groupId>
        <artifactId>zstd-jni</artifactId>
        <version>1.5.2-1</version>
      </dependency>

在 Spark 镜像中将这个 jar 包替换成新的版本1.5.4-1就解决了。

(https://github.com/luben/zstd-jni/compare/v1.5.2-1...v1.5.4-1)

3.6 日志上报优化超大行

用户在进行日志输出时,有时会直接将数据对象信息打印在日志里,单行日志可能超过 5M 甚至更多。若这类信息下发,将会导致写入下游超时或者日志查询特别慢等不可预知的情况。因此,需要在 Fluent bit 采集时将长日志进行跳过,设置 Skip_Long_Lines = On,以提升日志的稳定性。

[INPUT]
    Name tail
    Path /var/log/containers/tc-*.log
    Tag tc.*
    Refresh_Interval 15
    multiline.parser docker, cri, java, python
    Mem_Buf_Limit 5MB
    Skip_Long_Lines On
    Skip_Empty_Lines On
    Offset_Key On

四、未来展望

目前,这套新架构已在多点 DMALL 多个公有云环境中平稳运转,不仅提升了集群搭建效率,还将Spark的特性充分利用加速了任务运行速度。接下来我们会启动原有历史基于 Hadoop 生态的存算一体集群下线,并升级为新的存算分离新架构的动作。

在稳定的 Spark on Kubernetes 环境下,我们也积极开展引入数据湖格式 Apache Iceberg 的湖仓一体搭建工作,以及引入向量化执行引擎框架快手Blaze的测试验证工作,对整体数据流转和计算效率再次升级。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值