阿里云 EMR Delta Lake 在流利说数据接入中的架构和实践

本文涉及的产品
EMR Serverless StarRocks,5000CU*H 48000GB*H
简介: 为了消灭数据孤岛,企业往往会把各个组织的数据都接入到数据湖以提供统一的查询或分析。本文将介绍流利说当前数据接入的整个过程,期间遇到的挑战,以及delta在数据接入中产生的价值。

作者简介:张宽天,毕业于华中科技大学,先后在华为、阅文集团从事数据开发方面工作,2019年加入流利说,担任数据工程师,负责数据湖相关工作。

背景

流利说目前的离线计算任务中,大部分数据源都是来自于业务 DB,业务DB数据接入的准确性、稳定性和及时性,决定着下游整个离线计算 pipeline 的准确性和及时性。同时,我们还有部分业务需求,需要对 DB 中的数据和 hive 中的数据做近实时的联合查询。
在引入阿里云 EMR Delta Lake 之前,我们通过封装 DataX 来完成业务 DB 数据的接入,采用 Master-Slave 架构,Master 维护着每日要执行的 DataX 任务的元数据信息,Worker 节点通过不断的以抢占的方式获取状态为 init 和 restryable 的 DataX 任务来执行,直到当天的所有的 DataX 任务全都执行完毕为止。

架构图大致如下:

架构图.png

Worker 处理的过程如下:

work处理流程.png

对于近实时需求,我们是直接开一个从库,配置 presto connector 去连接从库,来实现业务 BD 中的数据和 hive 中的数据做近实时的联合查询需求。

这种架构方案的优点是简单,易于实现。但是随着数据量也来越多,缺点也就逐渐暴露出来了:
性能瓶颈: 随着业务的增长,这种通过 SELECT 的方式接入数据的性能会越来越差,受 DB 性能瓶颈影响,无法通过增加 Worker 节点的方式来缓解。
规模大的表只能通过从库来拉取,造成数据接入的成本越来越高。
无法业务满足近实时的查询需求,近实时查询只能通过从库的方式查询,进一步加大了接入的成本。
为了解决这些问题,我们将目光聚焦到了 CDC实时接入的方案上。

技术方案选型

对于 CDC实时接入的方案,目前业内主要有以下几种: CDC + Merge 方案、CDC + Hudi、CDC + Delta Lake 及 CDC + Iceberg 等几种方案。其中,CDC + Merge 方案是在是在数据湖方案出现之前的做法,这种方案能节省DB从库的成本,但是无法满足业务近实时查询的需求等功能,所以最开始就 pass 掉了,而 Iceberg 在我们选型之初,还不够成熟,业界也没有可参考的案列,所以也被 pass 掉了,最后我们是在 CDC + Hudi 和 CDC + Delta Lake 之间选择。
在选型时,Hudi 和 Delta Lake 两者的功能上都是大同小异的,所以我们主要是从这几方案来考虑的: 稳定性、小文件合并、是否支持SQL、云厂商支持程度、语言支持程度等几个方面来考虑。
对比分析.jpg

基于以上指标,加上我们整个数据平台都是基于阿里云 EMR 搭建的,选择 Delta Lake 的话,会省掉大量的适配开发工作,所以我们最终选择了 CDC + Delta Lake 的方案。
整体架构
整体架构.png

总体架构图

整体的架构如上图所示。我们接入的数据会分为两部分,存量历史数据和新数据,存量历史数据使用 DataX 从 MySQL 中导出,存入 OSS 中,新数据使用 Binlog 采集存入 Delta Lake 表中。每日凌晨跑 ETL 任务前,先对历史数据和新数据做 Merge 操作,ETL 任务使用 Merge 之后的数据。

Delta Lake 数据接入
在 Binlog 实时采集方面,我们采用了开源的 Debezium ,负责从 MySQL 实时拉取 Binlog 并完成适当解析,每张表对应一个 Topic ,分库分表合并为一个 Topic 分发到 Kafka 上供下游消费。Binlog 数据接入到 Kafka 之后,我们需要创建 Kafka Source 表指向对应的 Kafka Topic 中, 表的格式为:

CREATE TABLE kafka_{db_name}_{table_name} (key BINARY, value BINARY, topic STRING, partition INT, offset BIGINT, timestamp TIMESTAMP, timestampType INT)
USING kafka
OPTIONS (
kafka.sasl.mechanism 'PLAIN',
subscribe 'cdc-{db_name}-{table_name}',
serialization.format '1',
kafka.sasl.jaas.config '*****(redacted)',
kafka.bootstrap.servers '{bootstrap-servers}',
kafka.security.protocol 'SASL_PLAINTEXT'
)
我们主要用到的字段是 value 和 offset ,其中 value 的格式如下:

{
"payload": {
"before": {
db记录变更前的schema及内容,op=c时,为null
},
"after": {
db记录变更后的schema及内容,op=d时,为null
},
"source": {
ebezium配置信息
},
"op": "c",
"ts_ms":
}
}
同时创建 Delta Lake 表,Location 指向 HDFS 或者 OSS ,表结构为:

CREATE TABLE IF NOT EXISTS delta.delta_{dbname}{table_name}(
{row_key_info},
ts_ms bigint,
json_record string,
operation_type string,
offset bigint
)
USING delta
LOCATION '------/delta/{db_name}.db/{table_name}'
其中 row_key_info 为 Delta Lake 表的唯一索引字段,对于单库单表而言,row_key_info 为 mysql 表的 primary key 字段 eg: id long,对于分库分表及分实例分库分表而言,row_key_info 为分库分表的字段和单表里primary key 字段组成,eg: 以 user_id 为分表字段,每张表里以 id 为 primary key , 那么对应的 row_key_info 为 id long, user_id long。
StreamingSQL 处理 Kafka 中的数据,我们主要是提取 Kafka Source 表中的 offset、value 字段及 value 字段中的 CDC 信息如: op、ts_ms 及 payload 的 after 和 before 字段。StreamingSQL 中,我们采用 5min 一个 mini batch,主要是考虑到 mini batch 太小会产生很多小文件,处理速度会越来越慢,也会影响读的性能,太大了又没法满足近实时查询的要求。而 Delta Lake 表,我们不将 after 或者 before 字段解析出来,主要是考虑到我们业务表 的 schema 经常变更,业务表 schema 一变更就要去修复一遍数据,成本比较大。在 StreamingSQL 处理过程中,对于 op=’c’ 的数据我们会直接 insert 操作,json_record 取 after 字段。对于 op=’u’ 或者 op=’d’ 的数据,如果 Delta Lake 表中不存在,那么执行 insert 操作, 如果存在,那么执行 update 操作;json_record 的赋值值,op=’d’,json_record 取 before 字段,op=’u’,jsonrecord 取 after 字段。保留 op=’d’ 的字段,主要是考虑到删除的数据可能在存量历史表中,如果直接删除的话,凌晨 merge 的数据中,存在存量历史表中的数据就不会被删除。
整个 StreamingSQL 的处理大致如下:
CREATE SCAN incremental
{dbname}{tablename} on kafka{dbname}{table_name} USING STREAM
OPTIONS(
startingOffsets='earliest',
maxOffsetsPerTrigger='1000000',
failOnDataLoss=false
);
CREATE STREAM job
OPTIONS(
checkpointLocation='------/delta/{db_name}.db/{table_name}checkpoint',
triggerIntervalMs='300000'
)
MERGE INTO delta.delta
{dbname}{table_name} as target
USING (
SELECT * FROM (
SELECT ts_ms, offset, operation_type, {key_column_sql}, coalesce(after_record, before_record) as after_record, row_number() OVER (PARTITION BY {key_column_partition_sql} ORDER BY ts_ms DESC, offset DESC) as rank
FROM (
SELECT ts_ms, offset, operation_type, before_record, after_record, {key_column_include_sql}
FROM ( SELECT get_json_object(string(value), '$.payload.op') as operation_type, get_json_object(string(value), '$.payload.before') as before_record,
get_json_object(string(value), '$.payload.after') as after_record, get_json_object(string(value), '$.payload.ts_ms') as tsms,
offset
FROM incremental
{dbname}{table_name}
) binlog
) binlog_wo_init ) binlog_rank where rank = 1) as source
ON {key_column_condition_sql}
WHEN MATCHED AND (source.operation_type = 'u' or source.operation_type='d') THEN
UPDATE SET {set_key_column_sql}, ts_ms=source.ts_ms, json_record=source.after_record, operation_type=source.operation_type, offset=source.offset
WHEN NOT MATCHED AND (source.operation_type='c' or source.operation_type='u' or source.operation_type='d') THEN
INSERT ({inser_key_column_sql}, ts_ms, json_record, operation_type, offset) values ({insert_key_column_value_sql}, source.ts_ms, source.after_record, source.operation_type, source.offset);
执行完 StreamingSQL 之后,就会生成如下格式的数据:

其中 part-xxxx.snappy.parquet 保存的是 DeltaLake 表的数据文件,而 _deltalog 目录下保存的是 DeltaLake 表的元数据,包括如下:
其中 xxxxxxxx 表示的是版本信息,xxxxxxxx.json 文件里保存的是有效的 parquet 文件信息,其中 add 类型的为有效的 parquet 文件, remove 为无效的 parquet 文件。
Delta Lake 是支持 Time travel 的,但是我们 CDC 数据接入的话,用不到数据回滚策略,如果多版本的数据一直保留会给我们的存储带来一定的影响,所以我们要定期删除过期版本的数据,目前是仅保留2个小时内的版本数据。同时,Delta Lake 不支持自动合并小文件的功能,所以我们还需要定期合并小文件。目前我们的做法是,每小时通过 OPTIMIZE 和 VACCUM 来做一次合并小文件操作及清理过期数据文件操作:
optimize delta
{dbname}{tablename};
set spark.databricks.delta.retentionDurationCheck.enabled = false;
VACUUM delta
{dbname}{table_name} RETAIN 1 HOURS;
由于目前 Hive 和 Presto 无法直接读取 Spark SQL 创建的 Delta Lake 表,但是监控及近实时查询需求,需要查询 Delta Lake 表,所以我们还创建了用于 Hive 和 Presto 表查询的。
Delta Lake 数据与存量数据 Merge
由于 Delta Lake 的数据我们仅接入新数据,对于存量历史数据我们是通过DataX 一次性导入的,加上 Delta Lake 表 Hive 无法直接查询,所以每日凌晨我们需要对这两部分数据做一次 merge 操作,写入到新的表中便于 Spark SQL 和 Hive 统一使用。这一模块的架构大致如下:
图片

640.png

每日凌晨0点前,调用 DeltaService API ,根据 Delta Lake 任务的配置自动生成 merge任务 的 task 信息、spark-sql 脚本及 对应的 Airflow DAG 文件。
merge 任务的 task 信息主要包括如下信息:

Dingtalk_20210113221647.jpg

自动生成 Merge 脚本,主要是从 Delta Lake 任务的配置中获取 mysql 表的schema 信息,删掉历史的 Hive 表,再根据 schema 信息重新创建 Hive 外部表,再根据新的 schema 从Delta Lake表的 json_record 字段和历史存量数据表中获取对应的字段值做 union all 操作,缺失值采用mysql 的默认值, union 之后,再根据 row_key 进行分组,按 ts_ms 排序取第一条,同时取出operation_type=’d’ 的数据。整体如下:
CREATE DATABASE IF NOT EXISTS {db_name} LOCATION '------/delta/{db_name}.db';
DROP TABLE IF EXISTS {db_name}.{table_name};
CREATE TABLE IF NOT EXISTS {db_name}.{table_name}(
{table_column_infos}
)
STORED AS PARQUET
LOCATION '------/delta/{db_name}.db/{table_name}/data_date=${{data_date}}';
INSERT OVERWRITE TABLE {db_name}.{table_name}
SELECT {table_columns}
FROM ( SELECT {table_columns}, _operation_type, row_number() OVER (PARTITION BY {row_keys} ORDER BY ts_ms DESC) as ranknum
FROM (
SELECT {delta_columns}, operation_type as _operation_type, tsms
FROM delta
{dbname}{table_name}
UNION ALL
SELECT {hive_columns}, 'c' as _operation_type, 0 as ts_ms
FROM {db_name}.{table_name}_delta_history
) union_rank
) ranked_data
WHERE ranknum=1
AND _operation_type <> 'd'
凌晨0点之后,Airflow 会根据 Airflow DAG 文件自动调度执行 merge 的Spark SQL 脚本,脚本执行成功后,更新 merge task 的状态为 succeed ,Airflow 的 ETL DAG 会根据merge task 的状态自动调度下游的 ETL 任务。
Delta Lake 数据监控
对于 Delta Lake 数据的监控,我们主要是为了两个目的:监控数据是否延迟及监控数据是否丢失,主要是在 MySQL 与 Delta Lake 表之间及 CDC 接入过来的 Kafka Topic 与 Delta Lake 表之间。
CDC 接入过来的 Kafka Topic 和 Delta Lake 表之间的延迟监控:我们是每15分钟从 Kafka 的 Topic 中获取每个 Partition 的最大 offset 对应的 mysql 的 row_key 字段内容,放入监控的 MySQL 表 delta_kafka_monitor_info 中,再从 delta_kafka_monitor_info 中获取上一周期的 row_key 字段内容,到 Delta Lake 表中查询,如果查询不到,说明数据有延迟或者丢失,发出告警。
MySQL 与 Delta Lake 之间的监控:我们有两种,一种是探针方案,每15分钟,从 MySQL 中获取最大的 id,对于分库分表,只监控一张表的,存入 delta_mysql_monitor_info 中,再从 delta_mysql_monitor_info 中获取上一周期的最大 id,到 Delta Lake 表中查询,如果查询不到,说明数据有延迟或者丢失,发出告警。另一种是直接 count(id),这种方案又分为单库单表和分库分表两种,元数据保存在 mysql 表 id_based_mysql_delta_monitor_info 中,主要包含 min_id、max_id、mysql_count 三个字段,对于单库单表,也是每隔5分钟,从 Delta Lake 表中获取 min_id 和 max_id 之间的 count 值,跟 mysql_count 对比,如果小于 mysql_count 值说明有数据丢失或者延迟,发出告警。再从 mysql 中获取 max(id) 和 max_id 与 max(id) 之间的 count 值,更新到 id_based_mysql_delta_monitor_info 表中。对于分库分表的情况,根据分库分表规则,生成每一张表对应的 id_based_mysql_delta_monitor_info 信息,每半小时执行一遍监控,规则同单库单表。

遇到的挑战

业务表 schema 变更频繁,Delta Lake 表如果直接解析 CDC 的字段信息的话,如果不能及时发现并修复数据的话,后期修复数据的成本会较大,目前我们是不解析字段,等到凌晨 merge 的时候再解析。
随着数据量越来越大,StreamingSQL 任务的性能会越来越差。我们目前是 StreamingSQL 处理延迟,出现大量延迟告警后,将 Delta Lake 存量数据替换成昨日 merge 后的数据,再删掉 Delta Lake 表,删除 checkpoint 数据,从头开始消费 KafkaSource 表的数据。降低 Delta Lake 表数据,从而缓解StreamingSQL 的压力。
Hive 和 Presto 不能直接查询 Spark SQL 创建的 Delta Lake 表,目前我们是创建支持 Hive 和 Presto 查询的外部表来供 Hive 和 Presto 使用,但是这些表又无法通过 Spark SQL 查询。所以上层 ETL 应用无法在不更改代码的情况下,在 Hive 和 Spark SQL 及Presto 引擎之间自由切换。

带来的收益

节省了 DB 从库的成本,采用 CDC + Delta Lake 之后,我们的成本节省了近80%。
凌晨 DB 数据接入的时间成本大大降低,能够确保所有非特殊要求的 DB 数据接入都能在1个小时内跑完。

后续规划

StreamingSQL 任务随着 Delta Lake 表数据量越来越大,性能越来越差问题跟进。
推动能否解决 Spark SQL 创建的 Delta Lake 表,无法直接使用 Hive 和 Presto 查询的问题。

欢迎对阿里云 EMR Delta Lake感兴趣的朋友加入阿里云EMR钉钉群交流测试,群内会定期进行精品内容分享,测试请@扬流,钉钉群如下

阿里云钉钉群二维码.jpg

相关实践学习
基于EMR Serverless StarRocks一键玩转世界杯
基于StarRocks构建极速统一OLAP平台
快速掌握阿里云 E-MapReduce
E-MapReduce 是构建于阿里云 ECS 弹性虚拟机之上,利用开源大数据生态系统,包括 Hadoop、Spark、HBase,为用户提供集群、作业、数据等管理的一站式大数据处理分析服务。 本课程主要介绍阿里云 E-MapReduce 的使用方法。
相关文章
|
5天前
|
存储 分布式计算 数据挖掘
数据架构 ODPS 是什么?
数据架构 ODPS 是什么?
50 7
|
5天前
|
数据采集 搜索推荐 数据管理
数据架构 CDP 是什么?
数据架构 CDP 是什么?
19 2
|
4天前
|
Cloud Native 安全 API
云原生架构下的微服务治理策略与实践####
—透过云原生的棱镜,探索微服务架构下的挑战与应对之道 本文旨在探讨云原生环境下,微服务架构所面临的关键挑战及有效的治理策略。随着云计算技术的深入发展,越来越多的企业选择采用云原生架构来构建和部署其应用程序,以期获得更高的灵活性、可扩展性和效率。然而,微服务架构的复杂性也带来了服务发现、负载均衡、故障恢复等一系列治理难题。本文将深入分析这些问题,并提出一套基于云原生技术栈的微服务治理框架,包括服务网格的应用、API网关的集成、以及动态配置管理等关键方面,旨在为企业实现高效、稳定的微服务架构提供参考路径。 ####
24 5
|
7天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
5天前
|
负载均衡 监控 Cloud Native
云原生架构下的微服务治理策略与实践####
在数字化转型浪潮中,企业纷纷拥抱云计算,而云原生架构作为其核心技术支撑,正引领着一场深刻的技术变革。本文聚焦于云原生环境下微服务架构的治理策略与实践,探讨如何通过精细化的服务管理、动态的流量调度、高效的故障恢复机制以及持续的监控优化,构建弹性、可靠且易于维护的分布式系统。我们将深入剖析微服务治理的核心要素,结合具体案例,揭示其在提升系统稳定性、扩展性和敏捷性方面的关键作用,为读者提供一套切实可行的云原生微服务治理指南。 ####
|
5天前
|
消息中间件 缓存 Cloud Native
云原生架构下的性能优化实践与挑战####
随着企业数字化转型的加速,云原生架构以其高度解耦、弹性伸缩和快速迭代的特性,成为现代软件开发的首选模式。本文深入探讨了云原生环境下性能优化的关键策略与面临的主要挑战,通过案例分析,揭示了如何有效利用容器化、微服务、动态调度等技术手段提升应用性能,同时指出了在复杂云环境中确保系统稳定性和高效性的难题,为开发者和架构师提供了实战指南。 ####
18 3
|
6天前
|
运维 Kubernetes Cloud Native
深入理解云原生架构:从理论到实践
【10月更文挑战第38天】本文将引导读者深入探索云原生技术的核心概念,以及如何将这些概念应用于实际的软件开发和运维中。我们将从云原生的基本定义出发,逐步展开其背后的设计哲学、关键技术组件,并以一个具体的代码示例来演示云原生应用的构建过程。无论你是云原生技术的初学者,还是希望深化理解的开发者,这篇文章都将为你提供有价值的见解和实操指南。
|
6天前
|
Kubernetes Cloud Native 持续交付
云原生技术在现代应用架构中的实践与思考
【10月更文挑战第38天】随着云计算的不断成熟和演进,云原生(Cloud-Native)已成为推动企业数字化转型的重要力量。本文从云原生的基本概念出发,深入探讨了其在现代应用架构中的实际应用,并结合代码示例,展示了云原生技术如何优化资源管理、提升系统弹性和加速开发流程。通过分析云原生的优势与面临的挑战,本文旨在为读者提供一份云原生转型的指南和启示。
19 3
|
5天前
|
运维 Kubernetes Cloud Native
云原生技术在现代应用架构中的实践与挑战####
本文深入探讨了云原生技术的核心概念、关键技术组件及其在实际项目中的应用案例,分析了企业在向云原生转型过程中面临的主要挑战及应对策略。不同于传统摘要的概述性质,本摘要强调通过具体实例揭示云原生技术如何促进应用的灵活性、可扩展性和高效运维,同时指出实践中需注意的技术债务、安全合规等问题,为读者提供一幅云原生技术实践的全景视图。 ####
|
11天前
|
人工智能 运维 网络架构
阿里云引领智算集群网络架构的新一轮变革
11月8日至10日,CCF ChinaNet(中国网络大会)在江苏张家港召开,众多院士、教授和技术领袖共聚一堂,探讨网络未来发展方向。阿里云研发副总裁蔡德忠发表主题演讲,展望智算技术发展趋势,提出智算网络架构变革的新思路,发布高通量以太网协议和ENode+超节点系统规划,引起广泛关注。阿里云HPN7.0引领智算以太网生态蓬勃发展,成为业界标杆。未来,X10规模的智算集群将面临新的挑战,Ethernet将成为主流方案,推动Scale up与Scale out的融合架构,提升整体系统性能。