作者:王华峰(花名继儒),Hologres研发
近年来,随着移动端应用的普及,应用埋点、用户标签计算等场景开始诞生,为了更好的支撑这类场景,越来越多的大数据系统开始使用半结构化JSON格式来存储此类数据,以获得更加灵活的开发和处理。Hologres是阿里云自研的云原生一站式实时数仓,支持PB级数据多维分析(OLAP)以及高并发低延迟的在线数据服务(Serving),在对半结构化数据分析场景,Hologres持续优化技术能力,从最开始支持JSONB类型,到支持JSONB GIN索引,再到1.3版本支持JSONB列存,在不牺牲使用灵活性的前提下,提升JSONB数据的写入和查询性能,同时也降低存储成本。JSONB列存也在阿里集团内部多个核心业务使用,其中稳定支撑搜索事业部2022年双11大促,历经生产考验,查询性能提升400%,存储下降50%!
点击查看阿里巴巴搜索事业部双11JSONB实践>>升级JSONB列式存储,Hologres助力淘宝搜索2022双11降本增效!
通过本文,我们将会揭秘Hologres JSONB半结构化数据的技术原理,实现JSON半结构数据的极致分析性能。
什么是半结构化数据
介绍什么是半结构数据之前,我们首先明确下什么是结构化数据。结构化数据可以理解成在关系型数据库(RDBMS)中的一张表,每张表都有明确严格的结构定义,比如包含哪些列,每列的数据类型是怎样的,存储的数据必须严格遵循表结构的定义。
相对应的,半结构化数据就是非固定结构的、经常变化的,且一般是自描述的,数据的结构和内容混杂在一起,最典型的例子就是JSON格式数据。JSON有标准的格式定义,其主要由对象(Object)和数组构成(Array),对象中存储的是键值对,其中键只能是字符串,值可以是字符串、数组、布尔值、Null值、对象或者数组,数组中可以存放任意多个值。
以下就是一个简单的JSON实例,相信大家都很熟悉:
{"user_name": "Adam", "age": 18, "phone_number": [123456, 567890]}
Hologres当前正是通过支持JSON数据类型来提供半结构化数据的能力,为了兼容Postgres生态,我们支持Postgres的JSON/JSONB这两种原生类型,其中JSON类型实际以TEXT格式进行存储,而JSONB类型存储的是解析过后的二进制,因为查询时不需要再解析,所以JSONB在处理时会快很多,下文提到的Hologres半结构化数据方案的很多内部优化都是依托JSONB类型完成的。
我们为什么需要半结构化数据?
半结构化数据得益于其本身的易用性以及强大的表达能力,使得半结构化数据的使用场景非常广泛。
对于数仓来说,每当上游的数据格式有变更时,比如变更数据类型、增删字段,数仓中的强Schema格式的表,必须进行相应的表结构演进(Schema Evoluation)来适配上游的数据,比如需要执行DDL进行加列或者删列,甚至中间的实时数据ETL作业也需要进行适配改动并重新上线。
在有频繁Schema Evoluation的场景的时候,如何保证数据的质量是个很大的挑战,同时维护和管理表结构,对于数据开发人员来说也是一项琐碎且麻烦的工作。
而半结构化数据则天然支持Schema Evoluation,上游业务的变更,只需要在JSON列数据中进行增删相应的字段,无需对数仓中的表做任何DDL就能完成,也能对中间的ETL作业做到透明,这样就能大大降低维护和管理表结构的成本。
传统数仓的半结构化数据解决方案
数仓在处理半结构化数据的时候,衡量一个解决方案好坏的核心考量主要有两点:
- 能否保持半结构化数据的易用性和灵活性
- 能否实现高效的查询性能
而传统的解决方案常常是顾此失彼,没法做到“熊掌”与“鱼”的兼得。常见的JSON数据处理方式有2种:
以下方案都以JSON数据为例,假设我们有如下JSON数据:
{“user_id”:1001, “user_name”: “Adam”, “gender”: “Male”, “age”: 16} |
---|
{“user_id”:1002, “user_name”: “Bob”, “gender”: “Male”, “age”: 41} |
{“user_id”:1003, “user_name”: “Clair”, “gender”: “Female”, “age”: 21} |
方案1: 数仓直接存储原始JSON数据
一种最直观的方案就是将原始JSON数据存成单独的一列,以Hive为例:
在存储层,这张Hive表的数据也是以一个完整的JSON值作为最小的存储粒度在磁盘上连续存储:
之后使用相关的JSON函数进行查询,比如查询所有年龄大于20的用户数:
SELECT COUNT(1) FROM tbl WHERE cast(get_json_object(json_data, '$.age') as int) > 20;
抽象成下面的流程:
上游直接写入JSON类型到Hologres,中间不经过处理,应用层查询时,再去解析需要的数据。
这种处理方式:
- 优点是:JSON则天然支持Schema Evoluation,上游业务的变更,只需要在JSON列数据中进行增删相应的字段,无需对数仓中的表做任何DDL就能完成,也能对中间的ETL作业做到透明,最大程度地保留了半结构化数据的易用性和灵活性,能大大降低维护和管理表结构的成本。
- 缺点是:应用端查询时需要选择合适的处理函数和方法,才能解析到需要的数据,开发较为复杂,如果JSON较复杂,同时查询性能会有退化,因为每次JSON列的数据参与计算的时候,都需要对JSON数据完整的解析一遍,比如需要抽取出整个JSON中某个字段,那么查询引擎执行的时候就要读出每一行JSON,解析一遍,取出需要的字段再返回。这中间会涉及大量的IO和计算,而需要的可能只是JSON数据成百上千字段当中的一个字段,这中间的大量IO和计算都是浪费的。
方案2: 加工成宽表
既然JSON查询时的解析开销很大,那就把解析前置在数据加工链路中,于是另外一种做法就是把JSON拍平成了一张宽表:
相应的抽象出来的流程如下:
上游是JSON格式,在导入时,将JSON进行解析,比如常见的通过Flink的JSON_VALUE函数解析,然后打宽成一张大宽表,再写入至Hologres,对于上层应用,直接查询Hologres中已经解析好的列。
对于这种处理方法:
- 优点是:写入Hologres时,因为是普通列写入,所以写入性能会更好,同时在查询侧,不需要对JSON数据进行解析,查询性能也会更好。
- 缺点是:每当上游的数据格式有变更时,比如变更数据类型、增删字段、执行DDL进行加列或者删列,中间的实时数据ETL作业也需要进行适配改动并重新上线,使用非常不灵活,也会额外增加运维和开发负担。