一面数据原有的技术架构是在线下机房中使用 CDH 构建的大数据集群。自公司成立以来,每年都保持着高速增长,业务的增长带来了数据量的剧增。
在过去几年中,我们按照每 1 到 2 年的规划扩容硬件,但往往在半年之后就不得不再次扩容。而每次扩容都需要花费大量精力。
为了解决包括扩容周期长、计算存储资源不匹配以及高昂的运维成本等这些问题,我们决定对数据架构进行改造,并将数据迁移到云端,采用存算分离的结构。 在这个案例中,我们将为大家介绍 Hadoop 上云的架构设计、选型的思考、组件评估以及数据迁移的整个过程。
目前,基于JuiceFS 我们实现了计算和存储分离的架构,总存储量增加了2倍;性能方面的变化无明显感知,运维成本大幅降低。在案例的末尾还附上了针对阿里云 EMR 以及 JuiceFS 的一手运维经验,希望这个案例能为其他面临类似问题的同行提供有价值的参考
为了满足业务需求,一面数据抓取了国内外数百个大型网站的数据,目前数量已经超过 500 个,并积累了大量的原始数据、中间数据和结果数据。随着我们不断增加抓取的网站数量和服务的客户群,数据量也在快速增长。因此,我们着手开始进行扩容以满足需求的增长。
原有的架构是在一个线下机房使用 CDH 构建了一个大数据集群。如下图所示,我们主要使用了 Hive、Spark 和 HDFS 等组件。在 CDH 的上游有多种数据生产系统,在这里只列出了Kafka,因为与 JuiceFS 相关;除了Kafka之外,还有其他一些存储方式,包括 TiDB、HBase、MySQL 等等。
数据流向方面,我们有一个上游的业务系统和数据采集系统,数据会被采集下来后写入 Kafka。然后我们使用一个 Kafka Connect 集群,将数据同步到 HDFS。
在这个架构上方,我们使用了一个自研的数据开发平台,称为 OneWork,用于开发和管理各种任务。这些任务会通过 Airflow 下发到任务队列进行调度。
业务/数据会增长比较快,业务扩容周期长。公司在 2016 年线下机房部署了 CDH 集群,到 2021 年已存储和处理 PB 级的数据。公司自创立以来一直保持每年翻一番的高增长,而比业务量增长更快的是 Hadoop 集群的数据量。在这几年间,按 1 到 2 年规划的硬件,往往因数据增长超出预期而在半年后不得不再次扩容。每次扩容周期可达到一个月,除了花费大量精力跟进行政和技术流程,业务端也不得不安排较多人日控制数据量。如果选择购买硬盘和服务器来进行扩容,实施周期会相对较长。
存储计算耦合,容量规划难,容易错配。传统的 Hadoop 架构中,存储和计算是紧密耦合的,难以根据存储或计算的需求独立进行扩容和规划。举个例子,假设我们需要扩容存储,于是首先需要购买一批新的硬盘,同时连带着需要购买计算资源。在最初时,计算资源可能会变得过剩,因为可能实际不需要那么多的计算资源,从而一定程度上导致了超前投资。
CDH 版本比较老,不敢升级。 我们因为集群也建的比较早了,为了稳定,也就不敢升级了。
运维成本较高(全公司仅1个全职运维)公司当时有200多个人,只有一个运维,这意味着运维工作的工作量很大。因此,我们希望能够采用更稳定、更简单的架构来提供支持。
机房存在单点风险。考虑到长远的因素,所有的数据都存储在同一个机房中,这存在一定的风险。例如,如果光缆被挖断,这种情况经常发生,那么我们仅有一个机房仍然会面临单点故障的风险。
考虑到这些因素和挑战,我们决定进行一些新的改变。以下是我们考虑架构升级的一些主要维度。
最终选择的方案是使用“阿里云 EMR + JuiceFS + 阿里云 OSS” 来搭建存算分离的大数据平台,将云下数据中心的业务逐步迁移上云。
这个架构使用对象存储来替代 HDFS,并选择了 JuiceFS 作为协议层,因为JuiceFS 兼容 POSIX 和 HDFS 协议。在顶部,我们使用了云上半托管的 Hadoop 解决方案 EMR。它包含了很多 Hadoop 相关的组件,例如 Hive、Impala、Spark、Presto/Trino 等等。
首先是决定使用哪家云厂商。由于业务需求,AWS、Azure 和阿里云都有在用,综合考虑后认为阿里云最适合,有这些因素:
阿里云的 EMR 本身也有使用 JindoFS 的存算分离方案,但基于以下考虑,我们最终选择了JuiceFS:
JuiceFS 使用 Redis 和对象存储为底层存储,客户端完全是无状态的,可以在不同环境访问同一个文件系统,提高了方案的灵活性。而 JindoFS 元数据存储在 EMR 集群的本地硬盘,不便于维护、升级和迁移。
直接截取官方文档的介绍:
JuiceFS 是一款面向云原生设计的高性能共享文件系统,在 Apache 2.0 开源协议下发布。提供完备的 POSIX 兼容性,可将几乎所有对象存储接入本地作为海量本地磁盘使用,亦可同时在跨平台、跨地区的不同主机上挂载读写。
JuiceFS 采用「数据」与「元数据」分离存储的架构,从而实现文件系统的分布式设计。使用 JuiceFS 存储数据,数据本身会被持久化在对象存储(例如,Amazon S3),相对应的元数据可以按需持久化在 Redis、MySQL、TiKV、SQLite 等多种数据库中。
除了 POSIX 之外,JuiceFS 完整兼容 HDFS SDK,与对象存储结合使用可以完美替换 HDFS,实现存储和计算分离。
PoC 的目的是快速验证方案的可行性,有几个具体目标:
期间做了大量测试、文档调研、内外部(阿里云 + JuiceFS 团队)讨论、源码理解、工具适配等工作,最终决定继续推进。
我们在 2021 年 10 月开始探索 Hadoop 的上云方案;11 月做了大量调研和讨论,基本确定方案内容;12 月和 2022 年 1 月春节前做了 PoC 测试,在春节后 3 月份开始搭建正式环境并安排迁移。为了避免导致业务中断,整个迁移过程以相对较慢的节奏分阶段执行, 迁移完后,云上的 EMR 集群数据量预计会超过单副本 1 PB.
做完技术选型之后,架构设计也能很快确定下来。考虑到除了 部分业务仍然会保留在数据中心的 Hadoop 集群,所以整体实际上是个混合云的架构。
整体架构大致如上图所示:左侧是的线下机房,使用了传统的 CDH 架构和一些 Kafka 集群。右侧是部署在阿里云上的 EMR 集群。这两部分通过一条高速专线进行连接。顶部是 Airflow 和 OneWork,由于都支持支持分布式部署,因此可以轻松进行水平扩展。
挑战1: Hadoop 2 升到 Hadoop 3
我们 CDH 版本比较老,也不敢升级,但我们既然做了迁移,肯定还是希望新集群能够升级到新版本。在迁移过程中,需要注意 HDFS 2 和 3 之间的差异,接口协议和文件格式有可能会发生变化。JuiceFS 完美兼容 HDFS 2 & 3,很好地应对了这个挑战。
挑战2: Spark 2 升级到 Spark 3
Spark 的一个升级对我们影响是比较大的,因为有不少不兼容的更新。这就意味着原来在 Spark 2 上面写的代码需要完成修改才能适配到新的版本里面去。
**挑战3: Hive on Spark 不支持 Spark 3 **
在机房环境中,默认使用的是 CDH 自带的 Hive on Spark,但当时 CDH 中的 Spark 版本只有 1.6。我们在云上使用的是 Spark 3,而 Hive on Spark 并不支持 Spark 3,这导致我们无法继续使用 Hive on Spark 引擎。
经过调研和测试,我们将 Hive on Spark 改为了 Hive on Tez。这个改动相对来说还比较容易,因为 Hive 本身对于不同的计算引擎提供了抽象和适配,所以对于我们的上层代码改动较小。Hive on Tez 在性能上可能略慢于 Spark。此外,我们也关注国内网易开源的一个新计算引擎 Kyuubi,它兼容 Hive,并提供了一些新特性。
挑战4: Hive 1 升级到 Hive 3,元数据结构有变化
对于 Hive 升级来说,最主要的影响之一是元数据结构的变化,因此在迁移过程中,我们需要进行数据结构的转换。因为无法直接使用Hive来处理这种迁移,所以我们需要开发相应的程序来进行数据结构的转换。
挑战5:权限管理由 Sentry 替换为 Ranger
这是一个比较小的问题,就是我们之前使用 Sentry 做权限管理,这个社区不怎么活跃了,EMR 也没有集成,所以就替换为 Ranger。
除了技术挑战外,更大的挑战来自与业务端。
业务挑战1:涉及的业务多,不能影响交付
我们拥有多个业务,涉及不同的网站、客户和项目。由于业务交付不能中断,迁移过程必须进行分业务处理,采用渐进式迁移的方式。
迁移过程中,数据的变动会对公司的多个环节产生影响,例如 ETL 数据仓库、数据分析师、测试和产品开发等。因此,我们需要进行良好的沟通和协调,制定项目管理计划和排期。
业务挑战2: 数据表、元数据、文件、代码多
除了数据,我们在上层还有许多业务代码,包括数据仓库的代码、ETL 的代码以及一些应用程序的代码,如 BI 应用需要查询这些数据。
要迁移的数据包括两部分:Hive Metastore 元数据以及 HDFS 上的文件。由于不能中断业务,采用存量同步 + 增量同步(双写)的方式进行迁移;数据同步完后需要进行一致性校验。
存量同步
对于存量文件同步,可以使用 JuiceFS 提供的功能完整的数据同步工具 sync 子命令 来实现高效迁移。JuiceFS sync 命令支持单节点和多机并发同步,实际使用时发现单节点开多线程即可打满专线带宽,CPU 和内存占用低,性能表现非常不错。需要注意的是,同步过程中 sync 命令会在本地文件系统写缓存,因此最好挂载到 SSD 盘来提升性能。
Hive Metastore 的数据同步则相对麻烦些:
dbs
表的 DB_LOCATION_URI
和 sds
表的 LOCATION
)因此我们开发了一套脚本工具,支持表和分区粒度的数据同步,使用起来很方便。
增量同步
增量数据主要来自两个场景:Kafka Connect HDFS Sink 和 ETL 程序,我们采用了双写机制。
Kafka Connect 的 Sink 任务都复制一份即可,配置方式上文有介绍。ETL 任务统一在 OneWork 上开发,底层使用 Airflow 进行调度。通常只需要把相关的 DAG 复制一份,修改集群地址即可。实际迁移过程中,这一步遇到的问题最多,花了大量时间来解决。主要原因是 Spark、Impala、Hive 组件版本的差异导致任务出错或数据不一致,需要修改业务代码。这些问题在 PoC 和早期的迁移中没有覆盖到,算是个教训。
数据校验
为了能让业务放心的使用新的架构,数据校验必不可少。 数据同步完后需要进行一致性校验,分三层:
文件一致。在存量同步阶段做校验,通常的方式是用 checksum. 最初的 JuiceFS sync 命令不支持 checksum 机制,我们建议和讨论后,JuiceFS 团队很快就加上了该功能(issue,pull request)。除了 checksum,也可考虑使用文件属性对比的方式:确保两个文件系统里所有文件的数量、修改时间、属性一致。比 checksum 的可靠性稍弱,但更轻量快捷。
元数据一致。有两种思路:对比 Metastore 数据库的数据,或对比 Hive 的 DDL 命令的结果。
计算结果一致。即使用 Hive/Impala/Spark 跑一些查询,对比两边的结果是否一致。一些可以参考的查询:表/分区的行数、基于某个字段的排序结果、数值字段的最大/最小/平均值、业务中经常使用的统计聚合等。
数据校验的功能也封装到了脚本里,方便快速发现数据问题。
迁移完业务稳定运行后,我们开始考虑分级存储。分级存储在各种数据库或存储系统中都是一个常见问题,数据存在冷热区别,而存储介质的价格也存在差异,因此我们希望将冷数据存储在更便宜的存储介质上以控制成本。
在之前的 HDFS 中,我们已经实施了分级存储策略,购买了两种类型的硬盘,将热数据存储在高速硬盘中,将冷数据存储在低速硬盘中。
然而,JuiceFS 为了优化性能采取的数据分块模式,会对分级存储带来限制。 按照 JuiceFS 的处理,当文件存储在对象存储上时,它被逻辑上拆分为许多 chunks、slices 和 blocks,最终以 block 的形式存储在对象存储中。
因此,如果我们观察对象存储中的文件,实际上无法直接找到文件本身,而只能看到被分割成的小块。即使 OSS 提供了声明周期管理功能,但我们也无法基于表、分区或文件级别进行生命周期的配置。
后续我们通过以下这种方式来解决。
两个 bucket:标准( JuiceFS ) + 低频(OSS): 创建两个存储桶,一个存储桶用于JuiceFS,并将所有数据存储在标准存储层中。另外,我们额外创建一个低频的OSS存储桶。
基于业务逻辑,对表/分区/文件,配置存储策略表。 我们可以根据表、分区或文件来设置存储策略,并编写定时任务来扫描并执行这些策略。
用Juicesync 将低频文件从 JuiceFS 导出到 OSS 并修改 Hive 元数据。 文件从 JuiceFS 转移到 OSS 之后会从 JuiceFS 删除,并且在 OSS 上能看到完整的文件内容,我们就可以对其设置生命周期规则。转移完文件后需要及时修改 Hive 元数据,,将 Hive 表或分区的位置更改为新的OSS地址。EMR 的 Hive/Impala/Spark 等组件原生支持 OSS,因此应用层基本无感(需注意访问低频文件会带来额外开销)。
完成这个操作后,除了实现分级存储以降低成本外,还有一个额外的好处是我们可以减少JuiceFS元数据的数量。因为这些文件不再属于 JuiceFS,而是由 OSS 直接管理,这意味着JuiceFS 中的 inode 数量会减少,元数据的管理压力就会减轻,Redis请求的数量和容量也会降低。从稳定性的角度来看,这对系统会更有利。
存算分离的收益
总的存储量增长了两倍,计算资源不动,偶尔开启临时的任务节点。在我们的场景中,数据量增长非常快,但查询需求相对稳定。从 2021 年至今,数据量已增长两倍。计算资源在初始阶段至今基本没有做过太多的改动,除非出于某些业务需求需要更快的计算速度,我们会开启弹性资源和临时任务节点来加速。
性能变化
在我们的业务场景中,主要是进行大数据的批处理离线计算,总体而言对于性能的延迟并不敏感。在 PoC 期间,我们进行了一些简单的测试。然而,这些测试很难准确说明问题,因为测试过程受到了许多影响因素的影响。我们首先更换了存储系统,从 HDFS 切换到了 JuiceFS,同时进行了组件版本升级,Hive 引擎也发生了变化。此外,集群负载也无法完全一致。在我们的场景中,与之前在物理服务器上部署的 CDH 相比,集群架构的性能差异并不明显。
用性 & 稳定性
**实施复杂度 **
当评估类似架构或方案的复杂度时,有许多影响因素需要考虑。其中包括业务场景的差异,以及对延迟要求的敏感程度不同。此外,表数据量的规模也会产生影响。在我们的场景中,我们有大量的表和数据库,文件数量相对较多。此外,上层应用程序的特性、使用业务的数量以及相关程序等也会对复杂度产生影响。另一个重要的影响因素是版本迁移的逐渐差异。如果只进行平移而保持版本不变,那么组件的影响基本上可以消除。
配套工具和储备是一个重要的影响因素。在进行数仓或 ETL 任务时,有多种实现方式可供选择,例如手动编写 Hive SQL 文件、Python 或 Java 程序,或者使用常见的调度工具。但无论采用哪种方式,我们都需要复制和修改这些程序,因为双写是必要的。
我们使用自研的开发平台 OneWork,在任务配置方面非常完善。通过 OneWork 平台,用户可以在 Web 界面上配置这些任务,从而实现统一管理。Spark 任务的部署也无需登录到服务器上操作,OneWork 会自动提交到 Yarn 集群。这个平台大大简化了代码配置和修改的过程。我们编写了一个脚本将任务配置复制出来,进行一些修改,就可以实现高度的自动化程度,几乎达到百分之八九十,从而顺利运行这些任务。
后续计划大致有几个方向:
关于 IDC-阿里云专线:
能提供专线服务的供应商很多,包括 IDC、阿里云、运营商等,选择的时候主要考虑线路质量、成本、施工周期等因素,最终我们选择了IDC的方案。IDC 跟阿里云有合作,很快就完成了专线的开通。这方面如果遇到问题,可以找 IDC 和阿里云的支持。除专线租用成本,阿里云也会收取下行(从阿里云到 IDC)方向传输费用。专线两端的内网 IP 完全互通,阿里云和 IDC 两侧都需要一些路由配置。
关于 EMR Core/Task 节点类型的选择:
JuiceFS 可以使用本地硬盘做缓存,能进一步减少 OSS 带宽需求并提高 EMR 性能。更大的本地存储空间,可以提供更高的缓存命中率。
阿里云本地 SSD 实例是较高性价比的 SSD 存储方案(相对于云盘),用作缓存正合适。
JuiceFS 社区版未支持分布式缓存,意味着每一个节点都需要一个缓存池,所以应该选用尽量大的节点。
基于以上考虑和配置对比,我们决定选用 ecs.i2.16xlarge,每个节点 64 vCore、512GiB Memory、1.8T*8 SSD。
关于 EMR 版本:
软件方面,主要包括确定组件版本、开启集群、修改配置。我们机房使用的是 CDH 5.14,其中 Hadoop 版本是 2.6,阿里云上最接近的版本是 EMR 3.38. 但调研时发现该版本的 Impala 和 Ranger 不兼容(实际上我们机房使用的是 Sentry 做权限管理,但 EMR 上没有),最终经过评估对比,决定直接使用 EMR 5 的最新版,几乎所有组件的大版本都做了升级(包含 Hadoop 3、Spark 3 和 Impala 3.4)。此外,使用外部 MySQL 作为 Hive Metastore、Hue、Ranger 的数据库。
关于 JuiceFS 配置:
基本参考JuiceFS官方文档《在 Hadoop 中通过 Java 客户端访问 JuiceFS》即可完成配置。另外我们也配置了这些参数:
juicefs.cache-dir
缓存目录。这个参数支持通配符,对多个硬盘的实例环境很友好,如设置为/mnt/disk*/juicefs-cache
(需要手动创建目录,或在EMR节点初始脚本中创建),即用全部本地 SSD 作为缓存。另外也要关注 juicefs.cache-size
、juicefs.free-space
两个参数。juicefs.push-gateway
:设置一个 Prometheus Push Gateway,用于采集 JuiceFS Java 客户端的指标。juicefs.users
、juicefs.groups
:分别设置为 JuiceFS 中的一个文件(如 jfs://emr/etc/users
、jfs://emr/etc/groups
),解决多个节点 uid 和 gid 可能不统一的问题。关于 Kafka Connect 使用 JuiceFS:
经过一些测试,确认 JuiceFS 可以完美应用于 Kafka Connect 的 HDFS Sink 插件(我们把配置方式也补充到了官方文档)。相比使用 HDFS Sink 写入HDFS,写入 JuiceFS 需要增加或修改以下配置项:
将 JuiceFS Java SDK 的 JAR 包发布到 Kafka Connect 每一个节点的 HDFS Sink 插件目录。Confluent 平台的插件路径是:/usr/share/java/confluentinc-kafka-connect-hdfs/lib
编写包含 JuiceFS 配置的 core-site.xml
,发布到 Kafka Connect 每一个节点的任意目录。包括这些必须配置的项目:
fs.jfs.impl = io.juicefs.JuiceFileSystem
fs.AbstractFileSystem.jfs.impl = io.juicefs.JuiceFS
juicefs.meta = redis://:[email protected]:6379/1
请参见 JuiceFS Java SDK 的配置文档。
Kafka Connector 任务设置:
hadoop.conf.dir=
store.url=jfs:///<路径>
在整个实施过程中陆陆续续踩了一些坑,积累了一些经验,分享给大家做参考。
阿里云 EMR 和组件相关
兼容性
num_nulls=-1
的改成 num_nulls=0
. 可能需要用到 CatalogObjects.thrift 文件。Snappy: RawUncompress failed
,可能是 IMPALA-10005 导致的。规避方案是不要对 Textfile 文件使用 snappy 压缩。CONCAT_WS
函数行为有差异,老版本 CONCAT_WS('_', 'abc', NULL)
会返回 NULL
,而新版本返回 'abc'
.性能
oss://
和 jfs://
(本意是支持 JindoFS,但 JuiceFS 也默认使用 jfs 这个 scheme)设置独立的 IO 线程数。在 EMR 控制台上增加或修改 Impala 的配置项 num_oss_io_threads
.运维
/mnt/disk1/log/spark/spark-hadoop-org.apache.spark.sql.hive.thriftserver.HiveThriftServer2-1-emr-header-1.cluster-xxxxxx.out
),导致硬盘写满。解决方案有两个:配置 log rotate 或把 spark.driver.extraJavaOptions
配置清空(阿里云技术支持的建议)。JuiceFS 相关
如有帮助的话欢迎关注我们项目 Juicedata/JuiceFS 哟! (0ᴗ0✿)