今天我们将分享社区用户多点 DMALL 的案例。多点 DMALL 是亚洲领先的全渠道数字零售解决方案服务商,目前已与 380 家零售企业达成合作,覆盖 6 个国家和地区。
面对 B 端客户日益增长的企业数据,存算一体的架构显得力不从心。计算资源冗余浪费、所依靠的CDH发行版技术栈复杂、部署运维困难及计算资源潮汐现象严重等问题,迫使多点启动架构升级的进程。同时,为满足 B 端客户多样化的需求,多点需要构建一个可以在多云环境下更具性价比、可复用的大数据底层基座和平台工具链。基于此,多点的大数据团队开始搭建存算分离的云原生大数据架构。
本文深入剖析这次改造的架构设计与演进过程,分享多点 DMALL 在此过程中的经验和挑战。值得一提的是,他们利用 JuiceFS 社区版实现了与 Ranger 组件进行权限的对接,希望此经验能为其他使用 JuiceFS 的企业提供参考。
存算一体架构带来的成本和运维挑战,是大部分企业在大数据发展中一定会面对的问题。
传统的 Hadoop 生态体系中,数据存储角色与计算角色通常会部署在相同的机器上,一个占据硬盘提供存储,一个利用 CPU 和内存做计算。为此,MapReduce 和 Spark 也适应性的设计了多层级的数据本地化策略,即任务尽可能被分配到存储所需数据的对应节点上做计算,以减少中间数据交互产生的网络开销和额外的存储压力,提升整体的大数据应用效率。
可是,随着企业业务的发展,大数据存储量的增长速率与计算所需节点数量的增长速率很难保持一致。尤其是在“数据就是企业核心资产”的思想下,大量历史数据、冷数据的积累,导致企业数据存储量的增长诉求远远高于计算资源。最后企业只好不断新增机器存储更多数据,但大量计算资源得不到充分利用造成了闲置与浪费。
同样是增加存储资源,存算一体架构下会闲置部分计算资源,存算分离则不会有这个问题。
此外,数据量的不断增长还带来了 HDFS NameNode 元数据压力、集群节点规模扩张受限等问题。这些问题也时时刻刻牵动着各个大数据团队紧绷的神经。
多点DMALL 的大数据体系在构建之初,也是采用传统 Hadoop 存算一体的技术栈。除了上述企业发展中架构原生带来的困境外,面对 To B 多样化的业务场景,多点DMALL 大数据团队面临更多场景化的挑战:
随着多点DMALL 全面 To B 转型,为越来越多的 B 端客户提供零售全渠道解决方案,需要具备在多云环境下提供更具性价比、可复用的大数据底层基座和平台工具链。多点DMALL 大数据团队结合已有经验和后续业务需求,设计搭建存算分离、轻量级、可扩展、云中立大数据集群架构。
而存算分离的第一步,便是要解决数据如何从 HDFS 集群上快速切换到云服务商存储服务的问题。
在架构升级探索期,能想到最直接的方案就是通过 API 对接云厂商的对象存储。
从架构图上看这逻辑非常简洁清晰。考虑到各大云厂商都提供了稳定的对象存储服务以及完善的API,直接加以利用应该会降低架构升级的难度。为了快速检验这一思路的可行性,我们首先选择了大数据平台上,与 HDFS 会产生交互的部分功能做切换,将其换成与对象存储进行交互的方式。
快速检验的结果是,这样的设计不仅没有达到预期,反而使大数据平台开发的复杂度成倍增加。
出现问题的核心点在于:
经过验证,上述探索方案只能进行小型试点,无法支撑整个大数据架构的规模化调整,还需探寻新的解决方案。于是,JuiceFS 进入了我们的视线。
多点大数据团队很早便开始关注 JuiceFS了。在直接使用对象存储的方案宣告不可行之后,我们就一直在寻找能帮助大数据应用及引擎平滑切换到对象存储的方式。幸运的是,我们注意到了 JuiceFS 合伙人苏锐的一篇分享:从 Hadoop 到云原生, 大数据平台如何做存算分离。而后经过不断探索与验证,我们意识到这就是一直在寻找的问题解决之道。
采用 JuiceFS 的优势如下:
我们将整体架构逻辑分为以下几层:
在不断探索和尝试中,我们最终确定 JuiceFS 的引入和使用。JuiceFS 作为存储中间层,对下屏蔽了底层实际存储介质,隔离了不同的云环境,对上提供了统一的 HDFS API,保证了引擎执行和应用功能的一致性和稳定性,从而保障了集群整体对外服务的质量。
新技术的引入总是伴随着折腾的过程。在探索和使用 JuiceFS 的过程中,多点DMALL 大数据团队不出意外地踩了一些坑,幸而最终都找到了较为合理的解决方案。在此将遇到的部分典型问题整理分享出来,希望给所有计划和正在使用 JuiceFS 的同学一些启发和帮助。
开源的 JuiceFS 项目中,Hadoop Java SDK 没有安全管控的功能。因此在选择使用 JuiceFS 时,安全成为我们最关注的问题。
通过对该模块代码的细致研究,参考 HDFS 的鉴权逻辑方案,我们在 JuiceFS 的 FileSystem 的实现类中,对每个API 的实现实际操作触发前都添加了权限拦截的处理。
“权限”一词的计算机语言内涵就是“实体+动作”,Ranger 的权限设计本质也是一样的。我们将拦截的对应操作(例如创建)和相关路径转化为Ranger HDFS模块所需鉴权的动作和实体,并与操作用户组合成RangerAccessRequest
与 Ranger HDFS 模块打通进行鉴权。这个改动解决了 JuiceFS 在系统中“裸奔”的情况,为数据的安全做了一道防护。
当然,从整体的权限体系设计来讲,考虑到 Ranger一直被人所诟病的 Ranger Admin 连接风暴和策略本地化等问题,我们设计增加了权限鉴权的代理层,来进行鉴权的分流、权限映射和缓存等。但这些架构上的优化不影响 JuiceFS 接入 Ranger 的权限管控的本质目标。
除了正常的权限管控,对于可能存在的恶意使用我们也做了准备。考虑到 JuiceFS 开源代码的公开性,为了避免部分用户在了解到底层架构和引擎选择后,恶意破解调用以非法获取数据,我们对 JuiceFS 还做了额外的代码调整,包括修改核心参数的取值方式等。在保留和充分利用 JuiceFS 的核心功能前提下添加防护墙,提升整体的安全水平。
在 Spark on K8s 的云原生设计中,Shuffle 数据的处理是需要重点关注的。相比于通过机器堆出来的 YARN 集群可以直接利用超大的本地磁盘存储 Shuffle 数据而言,试图避免依赖底层机器、存算分离设计下 K8s 上的任务只能另谋他路。
在看到 JuiceFS 提供的的 K8s CSI 驱动时,我们最初以此作为突破点。在设想中,可以利用 JuiceFS K8s CSI 驱动的 writeback 模式,Shuffle 数据先放置临时存储目录,超过阈值后载入远端对象存储中。这样的逻辑下,Spark 的Shuffle 数据就无需依赖本地机器磁盘大小,有海量对象存储作为最终存储介质,理论上不再担心执行压力和数据临时存储压力。
但经过实际验证,在进行on YARN 和 on K8s 的性能测试对比时发现,使用这个方案的实际效果是:慢得不止一点点。
以下为测试中最典型的一个 Query 结果:
在深入分析研究后,我们发现 Shuffle 场景本身会存在大量小文件及随机读操作,JuiceFS K8s CSI 并不适合这种场景,会产生较大的性能瓶颈。在与 JuiceFS 社区沟通探讨后,我们开始调研开源的 Remote Shuffle Service,将 CSI 的方式切换为利用独立的 Shuffle 服务,并根据测试最终选用了 Apache Celeborn(后简称Celeborn)支持这一场景,其整体性能表现跟 on YARN 差异不大。Apache Celeborn 自身支持分级存储能力,极大提升了各类实际负载适配能力,我们将 JuiceFS 作为内存/磁盘容量不足情况下最后的兜底 Shuffle 数据存储。
上文提到,我们有一些大数据平台应用是直接通过 Hadoop Java SDK 与 HDFS 进行交互的。这些应用都是 Java 应用,在云原生的转换过程中发现以下报错:
`initial-exec TLS resolves to dynamic definition in /tmp/libjfs-amd64.7.so`
经过探索,并与 JuiceFS 社区沟通后,我们发现这个问题,是由于使用的基础镜像 openjdk-Alpine 本身的 bug,后来我们换成了eclipse-temurin 解决了问题。
在 Celeborn 的使用中发现一个漏洞。Celeborn 会自动创建其存储 Shuffle 数据的 HDFS 目录,当该服务的启动用户是 root 时,自动创建的 HDFS 目录并没有被自动设置 Owner 为 root。在上面提到的我们模仿 HDFS 鉴权思路中,对于一些目录的操作会去校验是否是这个目录的 Owner。root 没有被设置,自然后续 Celeborn 很多针对这个 HDFS 目录的操作都会被权限拦截。
虽然我们可以通过切换 Celeborn 的启动用户,或者给他单独设置权限等方式绕过这个拦截。考虑到创建后设置Owner 是合理行为,而且除了 root 外的其他用户都会被正常设置,我们还是将这个疑问向 JuiceFS 社区提出来。感谢 JuiceFS 社区第一时间的响应和支持,很快就修复了这个小漏洞。
将 CSI 驱动切换成 Celeborn 后,我们又一次开始做 Spark on YARN 和 on K8s 的性能测试。对比中发现,相同的任务和资源,on K8s 的任务总是会报错 OOM。通过细致的 Spark 内存分析,并不断对比多个环境任务和差异点后,团队内一位同学发现了 JuiceFS 的数据缓存参数设置区别,深入挖掘,最终找到答案。
Spark 任务执行时,需要特别配置juicefs.cache-dir
,不然 JuiceFS 就会默认将数据缓存放进内存中,从而对每一个 executor 多出好几百兆的额外内存占用。如果不做特殊配置,那就需要在 Spark 任务切换到on K8s的环境时,多配一些 off-heap 堆外内存,用以支持 JuiceFS 的额外数据缓存。
在使用数据缓存目录(后简称 cache 目录)的应用中,我们还遇到了另一个问题。Spark on K8s 的 Jupyter 应用中我们使用 JuiceFS K8s CSI 驱动建立的 PVC,与使用 JuiceFS Hadoop Java SDK 挂载 cache 目录,当二者使用同一个目录,会产生权限冲突的问题,在 Spark 运行日志中出现 warn 日志无法落地/获取缓存数据。仔细跟踪后发现,是因为两条链路生成的缓存文件目录默认权限不同,相互修改权限最终导致了文件写入失败,这样相当于根本没利用上 JuiceFS 客户端缓存,每次都直接与对象存储交互,这样对 Spark 任务性能而言影响很大。
该问题在反馈给 JuiceFS 社区后,社区通过对 JuiceFS K8s CSI 驱动增加参数“cache-mode
”进行了修复。
在做容量规划的时候考虑到线上集群规模,TiKV 一开始就被我们选择为 JuiceFS 的元数据存储引擎。在我们对云原生架构开发测试的大部分时间内,TiKV 的表现一直很稳定,直到我们选择 Celeborn 作为独立 Shuffle 服务。
根据 Celeborn 的功能设计,在当本地磁盘存储 Shuffle 数据满时,将把数据下推到 HDFS 中(当然我们在这里利用 JuiceFS 让其实际下推到了对象存储)。但在具体测试时发现,多个 Celeborn 的 Worker 同时写一个 JuiceFS 的目录会出现 Write Conflict 问题并触发重试操作。重试操作会有次数极限,而且不断重试很明显降低了整体Shuffle 效率延长了任务执行时长,在很长一段时间内这个问题也困扰着我们。
最终,社区的另一位 JuiceFS 用户给出了方案。Write Conflict 的根本原因是所有的写文件都要修改父目录的更新时间,这个报错并非是因为写文件,而是修改同一个目录属性产生的异常。再进一步,产生 Write Conflict 的不是JuiceFS 管理的数据,而是元数据,也就是 TiKV 的锁问题。最终,考虑到除了 Shuffle 场景,这样高并发的修改同一个目录的属性并不常见,我们决定为 Celeborn 部署提供单独的 JuiceFS 的 Hadoop Java SDK,这个 SDK 是单独处理的,写数据不再更新父目录的属性。
加入 JuiceFS 社区群后,我们也时常关注群内其他企业使用的问题反馈,可以帮助我们在正式上线前覆盖更多的测试案例。TiKV 的垃圾回收机制问题就是其中一个。当看到群里有其他同学反馈后,我们快速分析了该问题发生的原因,并检查补充了部署策略。TiKV 的独立服务并不会自动触发垃圾回收机制,只有同步安装 TiDB 这个组件才会正常运转。而我们在元信息服务 TiKV 部署策略中会同步安装 TiDB,不会遇到这个问题。另外,JuiceFS 1.0.4 版本开始已经新增 TiKV gc worker 后台线程适时触发垃圾回收动作。
当 HDFS 配置文件中开启了 HDFS 回收站功能(fs.trash.interval
和 fs.trash.checkpoint.interval
),只要存活的客户端实例都会检查并触发回收站中文件清理工作。但是最开始我们测试发现,清理线程总是报错提示没有文件操作权限。跟 JuiceFS 社区沟通后发现,的确存在 bug 导致过期文件没法清理,并迅速提供了 PR 修复。
正如上文中提到的,我们在架构升级过程中多次在公司开发环境进行了 Spark on YARN 和 on K8s 的性能测试对比,分别执行多次 TPC-DS SQL。以下为最终的对比结果:
上述测试是通过大数据平台 UniData 配置任务进行数据计算对比,变量包含平台调度策略的调整、Spark 版本升级等。排除其他变量,深入分析时间差异后,我们得出以下结论:
Spark 任务基于 HDFS 的on YARN 执行时长与基于 JuiceFS 的 on K8s 执行时长基本持平,性能差异较小。
JuiceFS 的数据缓存设计对数据查询存在明显的加速作用,同样的 SQL 在多次执行后,执行速度明显提升。
JuiceFS 会占用部分内存,总体而言比基于 HDFS 的任务所需内存更多。
从上述测试结果来看,已经达到了我们新架构正式上线的要求。目前这套架构已在多个公有云环境中平稳运转,接下来我们会启动现有历史 CDH 存算一体集群下线,并升级为新的存算分离新架构的动作。另外,为进一步提升Spark 执行性能,我们也在积极开展引入向量化执行引擎框架 Gluten 的测试验证工作。
在多点DMALL 从传统 Hadoop 存算一体到存算分离的升级过程中,JuiceFS 的出现填补了存储设计的空缺,推动了升级闭环。它对上保持了同样的 HDFS 协议,降低各个应用和引擎适配复杂度,对下完美对接各个云服务厂商提供的对象存储服务,提升了整体架构的升级效率。
经过整体向云原生的存算分离架构的升级,我们获得了多方面的收益:
希望这篇内容能够对你有一些帮助,如果有其他疑问欢迎加入 JuiceFS 社区与大家共同交流。