加速和扩大洞察|如何做好半结构化数据分析
背景
数据仓库擅长对结构化数据进行管理、分析。针对半结构化数据,不少数据仓库产品虽然支持了 struct、map、array 类型来加强对半结构化数据类型的支持,但是这些类型都需要预定义 schema,在数据导入、schema 变更时有比较大的使用成本。
而 Json 类型由于其灵活多变,不需要预定义 schema,为数据采集、数据分析、schema 变更提供了极大的灵活性。在业务变动频繁的互联网,尤其是游戏行业,Json 类型被广泛采用。其灵活性可以快速满足业务上对字段的增删要求。此外,Json 作为跨语言的协议,也比较好地被各种大数据平台、中间件所支持。
今天,我们邀请质变科技 AI-ready 数据云团队布道师文军与大家分享话题《如何做好半结构化数据分析》。
挑战
Json 类型应用起来比较灵活是因为 Json 逐行保存了 schema 信息。以 github events 数据集(https://www.gharchive.org)为例,每行数据都会重复存储 key 字段(id、type、actor、payload 等)。Json 的这种特性对数据仓库来说会带来如下三方面的挑战:
1)Json 类型的每行数据都会反复存储 key 字段信息,导致较大的存储空间
2)通过数据仓库分析 Json 类型的数据时,需要逐行解析 Json 结构,占用大量的 CPU
3)Json 类型作为一个数据仓库表的一个列,作为 IO 的最小逻辑单元,无法在分析时按需加载 Json 子字段来减少 IO,同时 Json 子字段不具备索引、裁剪的能力。
github events 数据数据样例:
{"id":"2489651045","type":"CreateEvent","actor":{"id":665991,"login":"petroav","gravatar_id":"","url":"https://api.github.com/users/petroav","avatar_url":"https://avatars.githubusercontent.com/u/665991?"},"repo":{"id":28688495,"name":"petroav/6.828","url":"https://api.github.com/repos/petroav/6.828"},"payload":{"ref":"master","ref_type":"branch","master_branch":"master","description":"Solution to homework and assignments from MIT's 6.828 (Operating Systems Engineering). Done in my spare time.","pusher_type":"user"},"public":true,"created_at":"2015-01-01T15:00:00Z"}
面对此类问题,simdjson(https://github.com/simdjson/simdjson)被广泛应用在 Json 解析加速。相比于 naive 的实现,simdjson 往往能带来 10~20x 的解析提升。但是,从端到端的视角来看,Json 分析查询的耗时主要由 Scan + Projection 算子组成。simdjson 一定程度上解决了 projection 算子的性能问题,当 Json 内容较大时,巨大的 IO 开销让查询的延迟变得不可接受。
Relyt JsonB 类型
为了提升 Json 类型的分析性能、存储密度,Relyt AI-ready Data Cloud 新增了对 JsonB 类型的支持。相比较于 Json 类型,Relyt 在写入 JsonB 类型时,会对 Json 字符串进行解析,自动识别 schema,并对数据进行结构化存储。从而,用户对 Json 子字段进行分析时,可以达到 scalar 类型接近的性能。用户在写入、分析 JsonB 类型的数据时,完全兼容 Json 类型的行为。
使用介绍
-- 创建包含 JsonB 字段的表
CREATE TABLE json_test (f0 json)
WITH(json_expand=on);
-- 写入 Json 数据
INSERT INTO json_test
VALUES
('{"key1": "value1", "key2": 1234, "key3": true} ');
-- 查询 Json 的子字段
-- ->>操作符
select f0->>'key1' from json_test;
-- json_extract_text 函数
select json_extract_text(f0, '$.[key1]') from json_test;
实现原理
JsonB 结构化存储
Relyt 在处理 Json 类型的数据时,会对实际写入的 Json 数据内容进行统计,统计 Json 中 Json key 的出现频率、Json value 的类型。在数据持久化到磁盘之前,Relyt 会根据统计信息动态决策,哪些 Json key 需要结构化存储以及 Json value 的存储类型。Json key 以列存文件的 schema 信息,不会在文件的数据区存储。Json value 按照如下的类型转换关系映射成 Relyt 的类型:
对于多行 Json 数据,相同 Json key 所对应的 Json value 类型不一致的情况。Json value 的类型统一归类为 Json 类型。对于动态决策后不需要结构化存储的所有 Json key,Relyt 会将其合并成一个字飞来段,Json value 的类型归类为 Json 类型。
样例一:
写入的 Json 数据
由于第一行 Json 数据的 key3 的 Json value 为 Json boolean 类型,第二行 Json 数据的 key3 的 Json value 为 Json string 类型。因而,结构化存储时,key3 的类型映射为 Json 类型。
编辑
图一
样例二:
写入的 Json 数据
由于 Json key "key3"的存在率未超过 50%,因而 key3 并不进行结构化存储。
编辑
图二
JsonB 查询优化
由于 JsonB 类型已经被结构化存储,Relyt 在查询 JsonB 的数据时,可以只加载查询涉及的 Json key 数据。与此同时,也可以在 Relyt 存储类型与 json_extract 函数的返回类型一致的情况下,也可以进一步节省掉 projection 算子部分的计算消耗。下面分别从 IO 裁剪、projection 视角来介绍 Relyt 在查询阶段的优化。
IO 裁剪
以上述 SQL 为例,Relyt 在生成执行计划的时候,会识别->>操作符的有操作数。由于 column f0 已经被结构化存储了。因而,在生成执行计划的时候,仅仅 Scan 算子仅仅需要扫描,'key1'子字段。而'key1'、'key2'、'key3'这些子字段,Relyt 是用列存来存储的,因而扫描的时候,可以做到只扫描'key1'子字段,从而做到纵向维度的 IO 裁剪。
此外,'key1'对于列存文件来说与普通的列没有区别。该列上的统计信息、索引等能力,Relyt 在查询的时候,依然可以使用,从而达到横向维度的 IO 裁剪。
编辑图三
Projection pushdown
如图三所示,针对 JsonB 的查询,Projection 算子的json_extract_text
函数优化掉了。因为'key1'的存储类型为 Varchar,而json_extract_text
的返回类型也是 Varchar。不需要再进行额外的 Projection 计算。而如果'key1'存储的类型为 bigint,与json_extract_text
的返回类型 Varchar 不一致的情况下,还是需要额外的 cast 操作来对类型进行转换。这部分 cast 操作,我们需要 pushdown 到 Scan 算子中进行,因为不同的列存文件之间的 JsonB 字段的 schema 可能是不一致的。所以,我们需要文件粒度去决策是否要进行 cast 操作的计算。
编辑
图四
查询效果评估
JsonB 类型在 Relyt 的核心客户上得到了很好的应用,下面的性能数据来自于该客户业务场景下的应用情况。在相同业务数据情况下,分别采用大宽表、JsonB、Json 的方案的性能结果。
编辑
图五
业界相关工作
Doris、Clickhouse 都支持了 variant 类型来解决 json 数据格式的动态类型的问题,在内存中结构化表示 json 数据,避免了对 json 子字段访问的开销。磁盘上都通过高密度的列存来结构化存储 json 数据。
而 spark 社区目前也支持了 variant 类型,同时也通过 variant shredding 功能来做到列存的方式结构化存储 json 的子字段从而进一步提升 IO 裁剪能力。
未来工作
Relyt 当前仅支持 Json 第一级子字段的结构化存储,由于多级嵌套子字段在业务中还是比较场景的,这部分的支持我们还在进行中。
随着 Spark 和数据湖生态对 Variant 类型支持的完善,Relyt 也会在目前湖仓一体的架构下做好对数据湖生态的 Variant 类型的支持,进一步强化数据湖的非结构化数据处理能力。
相关引用
https://doris.incubator.apache.org/zh-CN/docs/dev/sql-manual/sql-data-types/semi-structured/VARIANT
https://docs.databricks.com/en/semi-structured/variant-json-diff.html
https://www.databricks.com/blog/introducing-open-variant-data-type-delta-lake-and-apache-spark
https://github.com/delta-io/delta/blob/master/protocol_rfcs/variant-type.md
https://github.com/simdjson/simdjson
https://mp.weixin.qq.com/s/KVNcd25gngbBESU1QCYxbg
https://github.com/apache/spark/blob/master/common/variant/shredding.md
https://github.com/delta-io/delta/blob/master/protocol_rfcs/variant-type.md#variant-data-in-parquet
评论