基于 Paimon x Spark 采集分析半结构化 JSON 的优化实践

server/2024/12/27 7:50:00/

fef6f1baa7e9a85fa31184c89cc7a66a.gif

本文整理自 阿里巴巴 A+ 数据湖架构师康凯老师和 Paimon PMC Member 毕岩老师在11月15日 Apache Spark & Paimon Meetup,助力 Lakehouse 架构生产落地上的分享。

文章介绍了阿里巴巴 A+ 业务基于 Variant 类型的 JSON 链路优化,并从技术原理层面深入剖析了 Variant 及 Paimon 在半/非结构化的演进。

Apache Spark & Paimon 交流钉钉群:91535023202

01

A+ 业务&半结构化数据相关介绍

1.1 A+ 业务介绍

A+ 业务概括来讲就是为阿里集团内业务提供 APP、PC、Web 等跨终端采集 SDK 和高性能采集服务,为集团内 BI、搜索、推荐、广告等业务提供离线和流量数据服务,并围绕页面、事件、站点、活动、应用等分析场景,面向各 BU 提供通用的流量分析指标体系和流量分析产品。

从三个视角对 A+ 业务进行介绍。首先是整体架构视角,A+ 业务架构分为四层。最底层为采集基础设施层,此层集成了针对多种行为采集 SDK,覆盖了安卓、IOS、鸿蒙等平台。此外,这一层还提供了千万级 QPS 的数据网关,保障高效稳定的数据传输能力。

第二层是数据服务层,主要负责对采集到的信息进行生产加工。其中的数据公共层大概有一万多个节点,数据规模约400PB。再往上是采集产品层,分为两部分,一部分是基于通用指标体系和行业定制指标体系的基础版多场景预制看板,另一部分则提供自助分析、多维分析等高级版功能。

最后,架构最顶层是服务业务层,服务于阿里巴巴集团内部各业务单元(如淘宝、天猫、Lazada 等)。

32476e5d865b749c54287bd64591a45f.png

从产品视角来看,A+ 业务通过详细记录用户的交互行为,包括但不限于 PV、UV、停留时长、浏览数据、点击数据等,帮助业务部门更精准地理解用户需求与偏好,为精细化运营提供数据驱动的决策依据。

cd41a9ef8cf19c7adcd42641f540cc2f.png

站在数据视角,A+ 业务为集团各 BU 提供数据服务,ODS、DWD 与 ADS 层共同构成的公共层,应用流批一体技术为各 BU 打造了存储和计算新链路,结合 StarRocks 分析引擎,帮助各 BU 数据中台消费数据,显著提升了数据分析效率。

d2549002c0c8e95e72dace4814f491a7.png

1.2 JSON 的业务背景和挑战

事件分析是流量分析领域最重要、使用最广的分析方法。通过用户+位置+事件+参数可以捕获到用户单次的完整行为,再通过时间上进行持续采集,还可以捕获到用户前后连续的行为。下图左表就是事件模型,包含时间、设备,用户、行为参数、SPM 类信息等。随着移动端应用的普及,应用埋点等场景开始诞生,为了更好的支撑这类场景,使用半结构化 JSON 格式来存储此类数据成为趋势,以支持更加灵活的数据开发和处理流程。

然而,JSON 格式数据管理也带来了两个重大挑战。首先是存储大,主要体现在数据占比很大,仅以 ADS 层数据为例,日常数据规模约4T,大促约10T,其中半结构化数据占比达到60%;此外,由于这些数据种类繁多、结构复杂,导致压缩效率低下;加上数据量呈持续增长态势,使得存储成本显著增加。

其次,考虑到终端用户自定义埋点的情况普遍存在,并且针对这类数据的操作频率极高,比如聚合(aggregation)和过滤(filtering)操作非常频繁。然而,对于 JSON 格式的数据而言,执行查询的速度相对较慢。因此,在涉及 JSON 数据处理的场景中,实现高性能成为了极其迫切的需求。

cbb14ded933d4f8f85807049924c7625.png

1.3 JSON 的业务背景和挑战

基于上述挑战,我们确实考虑过多种解决方案。

首先,我们数据入湖选型为 Spark,但无论是 Spark 还是 Paimon,都不直接支持 JSON 作为一种原生的数据类型,这给我们的数据处理带来了一定挑战。

其次,我们也探索了工程拆分方向的优化方案。例如,考虑到 JSON 对象中包含大量 Key,一种思路是通过哈希算法将这些键映射到有限数量的桶中,那就会面临单个桶 Value 巨大影响压缩效果的挑战;如果采用 Range 拆列策略,确实可以解决数据倾斜问题,但对于拥有超过一万节点的大规模集群而言,生产链路改造复杂、稳定性难以保障,而且拆列后查询链路需要路由改写,考虑到系统稳定性和客户体验,工程拆分方案也不适用。

考虑到以上两种方案都不可行,鉴于 Spark4.0 支持了更高效的 Variant 类型,可以完全提供湖架构的 String 来提升性能;另外非湖仓架构下我们广泛采用了 JSON 列化技术,我们期望将其应用于湖仓一体架构,实现基于 Spark+Paimon+StarRocks 的 Variant 的列化架构,最终实现 JSON 类型低成本存储和高性能查询。

作为探索性方案,我们在湖仓一体架构上进行了初步测试。测试基于每日约 5TB 的数据量(约400亿条记录),使用的是 Spark 4.0 版本。针对写入操作,我们分别对三种不同的场景进行了评估:直接将数据以 String 格式写入 Paimon,耗时约21分钟;而以 Variant 格式写入 Paimon 则需要大约108分钟,尽管与数仓架构相比, Variant 格式写入速度有所下降,但仍然符合生产预期;以 Variant 格式将 MaxCompute 内表转换为 Paimon 格式后写入,耗时约为100分钟。针对读操作,经过大量 SQL 查询测试,使用 Variant 类型代替 String 类型,查询速度提升了2至5倍。

02

Paimon x Spark 半结构化数据类型探索

2.1 半结构化数据 JSON

回顾一下半结构化数据,所谓半结构化即部分结构化,通常有着明确类型如 INT、String 等,半结构化数据不能完全遵循关系型表数据模型,有些 Key 可能存在于所有数据,有些 Key 可能只在某些字段出现,所以即使是同一个 Key,在不同的数据记录里可能对应不同的数据类型,因此数据 Schema 是不确定、不完整、不断演化的。基于半结构化数据的开放性和灵活性,在无法预先定义数据 Schema 的情况下,我们通常会采用半结构化数据,但是可能很难兼得高性能。

以半结构化数据 JSON 为例展开。尽管 JSON 属于半结构化数据类型,但在实际应用中我们往往采用以结构化 SQL 方式使用它,再进行访问和处理。然而,由于 JSON 固有的存储特性,诸如 ColumnProjection、FilterPushdown 等常见的结构化数据文件格式查询优化方式无法使用。

此外,在处理 JSON 数据时,我们通常会利用一系列适配的 UDF 和表达式。这些 UDF 的计算逻辑设计往往依赖于特定的 JSON 解析库,如Jackson等工具类,来对完整的JSON字符串进行全面解析,进而生成相应的对象模型,比如get_json_object。这种方法的性能开销较大,在大规模数据集上尤其成为一个瓶颈。

2.2 Spark Variant Data Type

在数仓和湖仓架构中,半结构化数据的应用日益广泛。为了应对这一趋势并提供更优的解决方案,Apache Spark4.0 Preview 版本重磅推出了一种新的数据类型——Variant。从官方发布的信息来看,Variant 是Spark 4.0四大核心特性之一,足见其重要性。

具体来说,Variant 是一种灵活、开放、快速的半结构化数据类型。类似于 INT、String,可以在定义表的时候明确声明并使用。此外,Variant 支持一系列相关的表达式和 UDF,还保持了 JSON schema-on-read 的特性。由于 Variant 是定义在开源 Apache Spark 框架内的数据类型,因此具备良好的开放性。具体而言,它不仅提供标准的 Variant 表达式,还提供了开放的编码/解码库,并将这些功能抽象到了一个独立的子项目中,以便于外部系统或文件格式进行集成与扩展。特别值得一提的是,Paimon 基于 Spark 4.x 版本实现了对 Variant DataType 的支持。不同于传统的以原生数据类型存储 JSON 数据的方式,Variant 采用了自定义编码方式,基于 offset 编/解码解析,实现了更高性能的读取。基于这样的结构设计,我们可以拓展更多特性进行优化,达到更好的性能表现。

96aa4bda434e35601763bf3cd07b3e44.png

Variant 语法

正如前面所说,Variant 支持以类似于 INT 的方式定义表结构,通过执行 CREATE TABLE T (variant_col Variant) 就可以创建表并定义数据类型。Variant 还提供 PARSE_JSON 等 UDF,我们可以轻松地将复杂的 JSON 字符串转换为 Variant 类型完成写入。此外,在查询阶段,Variant 还提供更直观的访问机制。我们可以直接通过 : 访问 JSON 顶层对象;通过 . 的方式访问嵌套类型,使用 :: 可以实现相关数据类型的转换。针对数组类型数据,支持使用 [index] 方式访问。此外,对于 JSON Object,可以通过 variant_explode 类型的 UDF 提取 JSON 对象中的 KV,再进行进一步处理。Variant 还提供了一些和 JSON 对齐的UDF,例如:

  • VARIANT_GET:对齐 GET_JSON_OBJECT,用于获取指定路径下的值。

  • IS_VARIANT_NULL :对齐 IS_NULL,检查给定的 VARIANT 是否为空。

  • SCHEMA_OF_VARIANT :对齐 SCHEMA_OF_JSON,返回 Variant 所代表的数据结构描述。

  • VARIANT_EXPLODE :对齐 EXPLODE,用于分解 Variant 中的复合结构。

3010a730b182f3cf6d6ebda04ad46716.png

Variant Binary Format

Variant 良好性能主要归因于它的设计,它采用 Binary 格式编码,使用 Metadata 和 Value 组合表达,基于 Offset 实现 Key 的快速访问,而且由于其编码方式,使得内存数据结构与实际存储保持一致,避免了读取过程中数据转换开销。

下图右侧是 Parquet 层面看到的实际存储,Parquet 支持具体的数据类型和抽象的复合数据如 Map、Struct 等。而 Variant 巧妙地利用了 Metadata 和 Value 组合表达来实现。举个例子:假设我们有一份包含 A、B、C 三列的数据集,A、B、C 每列都有各自对应的 Key。如果采用 Variant 进行存储,那么最终的存储结构将如下所示,后续我们将进一步探讨。

31f3d70e8f293ee6c350ef7f39824159.png

这里我们针对 Variant 的设计展开讲解,看它是如何实现数据快速访问的。JSON 数据主要由 Object、Array 两种类型组成。下面,我们将介绍这两种类型 Variant 的实现。

Metadata

  • Header: 包含版本信息。

  • Dict Size (m): 表示整个 JSON 对象中去重之后的 Key 数。

  • Key Offset (1 - m):每一个 Key 的 Offset 对应该 Key 在存储中的实际偏移量。

  • Key (1 - m):Key 的实际值,严格按照字典序排列。

Value

  • 对象类型

    • Header: 包含版本信息,还有 Type 数据,用于表示 Header 之后的字节是 Array 还是 Object,或是普通数据。

    • num fields (n): 由于元数据中对 Key 进行了去重处理,因此这里的 Key 数量可能有所不同。

    • Field ID (1 - n):每一个 Field ID 映射的是 Metadata Key 偏移量的位置,类似索引。

    • Field Offset (1 - n+1): 和 Field ID 对齐,指向 Value 实际存储的地址信息,额外 Offset(n+1)用来标记最后一个 Value 的边界

    • Value (1 - n):Value 存储的实际值。

  • 数组类型

相比于对象类型,数组类型省略了 Field ID 部分,直接使用 Field Offset  来表示数组是第几个元素所在的偏移量,Value 则指向对应元素实际的地址信息。

ea26b4b0bb9afdef1fb490040d8487fe.png

下面结合两个例子展开介绍。第一个例子是包含三个 Key(a、b、c)及其对应值(value_a、value_b、value_c)的数据集。将其转换为 Variant 后,它会包含 Metadata 和 Value 两部分。

Metadata

  • Header: 包含版本信息。

  • Dict Size : 去重之后的 Key 数为3,即 a、b、c。

  • Key Offset:a、b、c 对应的偏移量为5、6、7 。

  • Key:Key 的实际值为 a、b、c。

Value

  • Header: 包含版本信息。

  • num fields : Key 数量为3。

  • Field ID :2、3、4 分别为 a、b、c 的偏移量即5、6、7所在的地址信息。

  • Field Offset : 8、16、24指向 Value_a、Value_b、Value_c 实际存储的地址信息,32 标记了 Value_c 的边界。

    假设一个框代表一个字节,当我们想要用 variant_get 去获取 key=c 对应值的时候,首先我们会基于 Field ID 用二分法查找取数3,发现3对应的 Key Offset 是 6,6对应的是 b 的偏移量,c 在 b 之后对应的 Key Offset 为 7,7 对应的 Field ID 4 的索引号为 2,因为 Field ID 和 Field Offset 的前 N 位是完全对齐的,将索引 2 套到 Field Offset 中可以得出 Value_c 所在的地址信息从第24个字节开始,直到下一个 Field Offset 结束。而 32 指向 Value_c 最后的位置,所以 key=c  对应的值就是 24-31,无需遍历完整的 JSON String 就能得到 value_c。

37194565e95b71babbc77bb82680d790.png

第二个例子是 Array 嵌套 Object 的场景。因为 binlog 日志中,经常遇到重复的 Key,JSON String 不做任何的编解码,就会存储多份的 key1 和 key2 。在 Variant 中存储 Array ,元数据会对 Key 进行去重处理,去重后的 Key 就是2,对应的值是 key1 和 key2,key1 和 key2 对应的偏移量为 4 和 8。且 Variant 存储 Array 不需要存储 Field ID,所以示例中 Value 的部分没有 Field ID,可以看到第一个偏移量位置是在 105 的号位置,到 104 是第一个对象的存储区域。

fb6e74a06ffacf8dba9b970d4ae04ace.png

Variant 现状与问题

社区发布了一份基于 TPC-DS 数据集的 JSON String 和 JSON Variant 两种数据类型的性能对比 Benchmark 测试报告,结果显示 JSON Variant 有8倍性能提升。

Spark 4.0 版本尚未正式发布,目前仍处于 Preview2.0 版本。Variant 也仍然存在一些需要优化的地方。我们在实际业务场景中发现,在不同的业务数据和查询 SQL 上,其读写表现差异较大。经过分析发现,对于依赖 CPU 或重数据解析的 SQL,通过 Variant 提速能带来约 2-5 倍收益,但是部分表写入过程中可能会出现数据膨胀问题,导致存储消耗大。而在重 IO 的场景下,甚至可能出现性能回退。此外,正如前面所说,JSON 基于部分表达式或者 UDF 解析比较慢,无法实现类似结构化的优化特性,同样的,Variant 编码也暂时解决不了这个问题,只能在 JSON 中快速定位 Key。

eea7e08a69a7bbfb7e1640ac645683d7.png

03

Paimon & Spark Variant 规划

最后介绍一下 Paimon 和 Spark 社区正在推进的事和未来规划。

首先还是聚焦于性能,我们发现 Variant 元数据和 Value 解析上存在进一步优化的空间,比如在同一条记录中抽取多个 Key 时可以复用已经加载的元数据信息。

另外一个主要的优化就是通过 Shredding,实现部分字段单独列化存储。这种方式能够支持 Stats 的统计,也可以支持列裁和行级过滤,在有些 case 下是可以拿到和结构化存储同样的性能的。

从开放性上来讲,Variant 目前定义在 Spark 引擎里,没办法实现跨引擎的复用。所以 Spark 和 Parquet 社区目前正在推动在 Parquet 层面定义 Variant 类型,以实现更好的通用性。

值得一提的是,由于 Paimon 社区较早就跟进了 Variant 功能,已经率先支持与 Spark4.0 集成,并且从阿里巴巴内部实际业务场景中收集到了宝贵的反馈信息。基于这些实践经验,我们计划在 Paimon 1.0 版本中逐步推出 Variant 功能,便于用户使用。

c916562b98c0900c2e5ff3387a4cd281.png

 12bc3674787b21e782b228ff65161f8e.gif  点击跳转至 Apache Paimon 官网~


http://www.ppmy.cn/server/153559.html

相关文章

质数分解,用sqrt缩小范围

题目:scanf一个整数,int32范围内,分解为质数序列输出 例如: 12分解为2 2 3 技巧就一个:用sqrt缩小范围 因为uint32(4,294,967,295)(接近43亿个数)范围内有2亿个左右质数,所以,一般不会用缓存去…

DevExpress WPF中文教程:Grid - 如何移动和调整列大小?(二)

DevExpress WPF拥有120个控件和库,将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress WPF能创建有着强大互动功能的XAML基础应用程序,这些应用程序专注于当代客户的需求和构建未来新一代支持触摸的解决方案。 无论是Office办公软件…

DP动态规划+贪心题目汇总

文章目录 背包01背包416. 分割等和子集 完全背包279. 完全平方数322. 零钱兑换 两个字符串DPLCR 095. 最长公共子序列139. 单词拆分 单个数组字符串DP5. 最长回文子串300. 最长递增子序列53.最大子数组和152. 乘积最大子数组198. 打家劫舍 三角形120. 三角形最小路径和 贪心121…

【EtherCATBasics】- KRTS C++示例精讲(2)

EtherCATBasics示例讲解 目录 EtherCATBasics示例讲解结构说明代码讲解 项目打开请查看【BaseFunction精讲】。 结构说明 EtherCATBasics:应用层程序,主要用于人机交互、数据显示、内核层数据交互等; EtherCATBasics.h : 数据定义…

Linux系统升级OpenSSH 9.8流程

参考链接: openssh最新版本下载地址:Index of /pub/OpenBSD/OpenSSH/portable/ 注意:openssh9.8需要依赖openssl,版本至少为1.1.1。 一、简介 Openssh存在远程代码执行漏洞(CVE-2024-6387),攻击者可以成功利用该漏…

深入理解Nginx工作原理及优化技巧

NGINX以高性能的负载均衡器,缓存,和web服务器闻名,驱动了全球超过 40% 最繁忙的网站。在大多数场景下,默认的 NGINX 和 Linux 设置可以很好的工作,但要达到最佳性能,有些时候必须做些调整。 NGINX被广泛应…

视频的音乐怎么提取为MP3格式?

MP3是一种广泛使用的音频压缩格式,以其高效的压缩率和良好的音质表现,成为了数字音频领域中的佼佼者,广泛应用于音乐存储、传输和播放。在日常生活中,我们经常遇到需要从视频中提取音频并将其转换为MP3格式的情况。视频的音乐怎么…

Unity自定义Inspector属性名特性以及特性自定义布局问题

前言: 在Unity中编辑属性的适合,一般都是显示属性的英文,如果想要改成中文的话又不能改变属性名,那么自定义特性是很好的选择。 一、自定以特性 这一块没有什么要多说的,就是自定义特性 using UnityEngine; #if UNI…