前言
前面几篇讲过,元数据侧重于配置【驱动】编程的思想,通过建立统一的数据资产,进一步【驱动】企业数字化升级。
但数字化升级是个战略性的目标,短期内效果并不明显,甚至很多时候容易引发空谈数字化转型,而达不到实际意义的用途。或者落地之后,产生的效果比预期会低很多。
而数据质量往往却是企业当下需要解决的问题。比元数据管理来得更迫切一些。并且元数据是需要数据质量来引导的。
1. 元数据与数据质量的关系是什么呢?
元数据是蓝图,就像建筑商建房子一样,先要有整体规划及配套的周边设施。而数据质量则是执行过程中的技术标准,让你了解整个蓝图过程中需要面对的所有问题,以及追踪反映相应的实际情况。
所以两者是相辅相成的关系,彼此相互促进与发展。
但是,我们可能没有建立元数据系统,这并不代表没有元数据,只是元数据没有规整成好的蓝图。就像程序员写垃圾代码一样可以完成功能而已,只是对后期带来了很多潜在的隐患。并且元数据系统的从视野上来看非常广泛,可能需要增量构建,这样数据质量的必要性在这种形式下是很容易提上议程的。
对于已有的系统,数据质量相关的问题往往更迫切,这时候可能顾不上数字化蓝图。在这种形式下,数据质量系统的建的重要度极高,在国情下更是如此。随着数据质量的建立,后期往往会促发元数据的管理。毕竟数据质量的定义本身就是一种元数据【元数据的实体】,而高级数据质量的建立需要于推断功能【元数据的关系】,这个就是元数据体系的强大之处了。
所以,在笔者看来,数据质量能解决当下问题更加容易落地生根。而随着元数据体系的建立,数据质量可以依据元数据更加强大。而元数据可以应运而生,转动它的轮轴,【逐步驱动】一个新的数字化世界的到来。
2. 数据质量的发展现状
当前企业的现状是,数据质量规则或多或少总会有的。毕竟数据质量问题直接引发的就是系统的故障感知度低,进而运维成本极高,对业务的稳定性带来极大的挑战性。
在这种形式下,数据质量是在有限的形式下展开的。所以,这种情况下的数据质量往往是【事后】数据质量。就像上车了再买票一样,它没有花过多精力在前期的准备事项中,而是等事情发生了,然后去【亡羊补牢】。毕竟有句古话说的好,相同的错误我允许你犯一遍,犯第二遍,但是不允许你犯第三遍。所以这种低成本,好附加的方式得到了普遍应用。
虽然这样【事后检验】有效地【显示】了数据质量问题,进而反馈调节,但是它并没有从根源上解决【数据质量】问题。系统的复杂度反而随着【数据质量】的建立,而越发凸显,毕竟缝缝补补的日子是无法持久的,总会有人跳出来的。
这时候就有出现了【数据标准】的概念,觉得数据应该是从【源头】就要抓起。然而它们所谓的【数据标准】是建立在流程之上,通过人力来解决并规范化,对于老的系统要逐步修正,对于新的系统要强行审批。
而【数据标准】往往与【事后数据质量】结合得很紧密,如果【数据质量指标】提高了,说明【数据标准】得到了效果。但是这往往是只是改善了情况,还是有大量数据质量问题并不能得到解决,为什么呢?因为从源头到结局,涉及到了太多环节跟因素,完全靠人跟流程制度的力量是不太可取的,但是对于企业高管来说却很喜欢,因为这样会显示他的价值,这里就不过多展开讨论了。
3. 数据质量的表现形式
前面讲过,【事后数据质量】得到了很大的普及,所以ebay开源了apache griffin。基本思想就是抽象成一层dsl,然后生成sql去检验规则,方便使用。当然还有一些大企业会把【事后规则】渗透到etl任务中,可以进行阻塞作业的执行,但是这个架构对于etl性能有很大影响,很难扩展开来。进一步,也有系统将数据规则组成多级规则,一旦发生错误,进一步诊断子规则,并收集规则相应的信息,便于提升系统的故障感知度。
当然,【事后数据质量】并没有从根源上解决问题,只是【穷举检查后显现】问题。所以企业迫切需要从【事后数据质量】转移到【事前数据质量】的思维转变。但是【事前数据质量】由于具有非常大的开放性,非常需要启发性思维,大部分都在探索期,不太容易有太多成功案例的【借鉴】。所以【事前数据质量】还属于新生阶段,大部分企业提供出了另外一个子系统概念【数据探查】,既然无法直接【感知】问题,那么我就提供相应的数据工具,让其快速了解数据,便于快速【分析及诊断】问题。对于数据源头有了一定的【探查】之后,再对数据进行相应的进行【约束】与【诊断】,促进【事前数据质量】体系的建立与完善。
本文不太过多关注【事后数据质量】,本人觉得真正的【数据质量】必定是在源头控制,进行【约束】,然后扩展到后期的【故障感知】,【事前数据质量】才是数据质量体系的一盘大期。
常见的事前数据质量又分为几部分:提供数据源头数据有效性检测的【数据模式】系统, 提供数据统计的【数据探查】系统,基于统计信息与实体关系进行启发式推断的【数据防火墙】系统。
那么现在可以进入本文的正文:【数据模式】系统
4. avro构建强大的数据模式系统
由于大部分对于avro数据模式了解得不多,先给予一定的介绍
a. avro 历史背景
对于数据模式来说,历史上有两个霸主,一个是google的protobuf,一个是facebook的thrift,它们主要用于rpc数据通讯的数据模式,当然跨语言是必备条件。
但是它们有一个强大的致命弱点,不支持动态模式。也就是说它们的模式必须要生成代码变成类或者语言的数据结构。对于每一个数据模式都得生成相应的代码进行规则约束。当然最新版本的protobuf提供了DynamicMessage动态反射,但是avro已经强大到不再是那个少年。
这个时候hadoop之父迫切需要一种数据格式,能够支持数据的【物理约束】与【逻辑约束】,支持数据的【模式演化】,支持数据的【切块压缩】存储。这个时候avro横空出世,一统了大数据江湖。
这个avro厉害到了什么程度了。
hive非常早期的版本就已经核心支持avro,支持模式推断高级功能。
linkedin一直以avro作为核心,建立的kafka schema-registry, kafka connect, kafka sql更是以avro为基础。由于hadoop之父不给力,老是要搞兼容,不愿意引进其它库的avro新特性,linkedin甚至自立门户搞了一套阉割版本的Pegasus,然后基于这玩意,搞了一套rest.li元数据驱动的微服务架构,以及datahub的元数据管理系统。
flink也是最优先支持avro数据格式。
什么,你说apache parquet, apache arrow文件格式更厉害?
avro有四种数据形式[json, datum, container, trevni]。trevni就是parquet的前身,面向列存储,也继承了trevni的使命。
如果说avro datum面向于通讯及实时, avro container面向于数据交换存储, avro trevni就是面向于数据计算。
前面说过hadoop之父很墨迹,毕竟兼容性对他来说是头等大事,所以apache parquet成功继承avro trevni,致力于作为数据计算的标准存储格式,目前也被apache flink/apache spark作为批量计算的核心格式,当然实时计算被avro牢牢把控。
既然apache parquet想做为标准数据计算格式,所以它要无缝对接转换所有的数据格式。前期它就这么干了,但是随着google新一代的flatbuffer内存数据格式的出现,它的性能指票显然不是特别友好的。
所以批量计算存储分切成了两块,apache parquet与apache arrow。apache parquet是物理存储形式,apache arrow是基于flatbuffer的内存数据格式,无缝迅猛对接各大平台的数据格式转换器。
为什么它快呢?如果你读过flatbuffer的文档就会知道,它的解序列化开销为零,当然apache arrow不是本文的重点,这里不作过多介绍。
所以,目前大数据的生态,基本上就是: apache avro(实时,数据交换) + apache parquet(批量) + apache arrow(内存)的三大格局。当然还有我没有提到的ORC存储,本质上是apache parquet的可选方案,发展似乎更加缓慢一点。
b. avro底层原理。
大部分人对json数据格式都有较强的认知。但是json的缺点是很明显的,它的schema每条记录都要带上,数据是文本形式很浪费空间,无法强制约束数据格式,数据类型扩展性较弱,对于数据模式检验有一定的json schema标准支持。
那么,如果我们把schema单独提出来,对数据类型进行高效编码,那么每条数据是不是空间最省性能最高呢?居然还真是。。。
参考链接: https://github.com/mtth/avsc/wiki/Benchmarks
编码规则在官方文档里面写得很清晰了:
https://avro.apache.org/docs/1.9.2/spec.html
我这里简单介绍一下:
对于int, long其实本质上无差别,都是采用通用的zigzag变长编码,比如如果你的整数数值范围比较少,可能一个字节就够存了,不用搞int的四个字节的标准。
对于bytes跟string其实无差别,就是长度+内容。
null就是用单个零字节表示。
enum转换成数字编码。
record就是按记录模式名称依次按字段名称顺序一个个写入值(无模式)
map, array就是先写记录条数,然后再写内容。当然这里面有个高级用法。可以拆分成多个记录条数+内容(即子map/array),最后以null零字节表示结束。map的key由于是动态的,要同时写入key(有模式)/value,适用于无法提前感知的扩展字段场景。
union就是最强大的类型了,就是表示可以有多个字段类型。
这里面有一点要注意的是,union不允许相同类型出现,除非是命名类型。命名类型有三种,record, enum, fixed。简单来说,recordname_b是允许在union里面出现的。不同名称的record认为是不同的类型。
union是最强大的特性,也是各个库实现不一致的地方。
union编码逻辑非常简单,先写入类型的索引值,然后写入具体编码值即可。
对于hadoop早期版本来说,每个union类型进行json编码的时候都要加入类型前缀。默认每个字段类型是不能为null的,所以可以通过union类型来解决type: ["null", "string"]。如果在java版本里面你必须要写"field": {"string", "aaa"}或者"field": null。注意"null"类型带有引号, null值没有引号。
但是后来大部分其它语言的库,包括linkedin的改造版本,觉得null是太常见且必要的特性了,如果有个null,就得套一层数据格式就显得太麻烦了。所以对于一个值如果是null或者本身两个元素的时候做了优化,可以直接写{"field":null}, {"field": "aaa"}了。。。
一些java第三方库也做了增强。
当然,就算加了null类型支持,仅仅是说这个值可以为null, 也是一定要填的。如果想让这个值可以missing,那么就得给default值,当然这里关联到模式演化的内容,将于后续介绍。
c. avro的文件格式类型及应用场景。
上一节讲了,我把schema提取出来,接着对单条记录整个数据结构按照schema进行编码,然后就得到了datum,有着强大的效率与size缩减效果。
所以,我们认识了第一种数据格式datum(在avro-tools里面叫做frag,有些库叫做avro_value)。
这种格式不带模式,并且是单条记录。适用于数据量小的单条消息处理场景。比如rpc通讯,实时计算。对于rpc通讯,模式是双方约定好了的,对于实时计算模式需要单独关联,kafka schema-registry是专门管理这个的。对于kafka avro的每条消息,前面有五个字节表示对应于kafka schema-registry管理的schema id,然后本地有个缓存,每个schema id只需要去同步获取一次即可。
当然对于大批量的数据格式,显然是需要压缩分块的,所以avro container文件格式出现了。
它的算法很简单:
单个header(avro文件标识+压缩编码+数据模式) + [多个block(avro_datum记录数+ avro_datum压缩内容)],每个header及block后面接16位随机生成sync mark用于hadoop分割文件使用。hive中对应的avro格式就是avro container格式,它是包含了avro_datum头部的,可以直接下载下来使用avro-tools工具查看。
对于列式存储avro trevni,主要由apache parquet接任了,后续可能会有专门的篇章进行介绍。
d. avro的类型约束以及idl模式
avro的类型分为物理类型,逻辑类型及引用类型。
先看物理类型约束:
默认对于每个字段来说是必填不可空的,如果需要为空,需要走union ["null", type],如果需要可缺失, 则可加上default值。
所以这里严格定义了类型的可空性与可缺失性。对于字典值,有enum类型,对于每个enum类型会生成相应的整数值。这对于数据质量也是重大的好功能。
对于一个数据可能存大多种格式的情况,比如同一个接口可能返回有不同的类型及值,那么就有union大法了,定义不同的named record就可以解决了。
对于扩展字段来说,由于模式不可知,可采用map结构来处理。其它已知模式字段必需严格按照记录类型处理。
逻辑类型约束:
逻辑类型约束是强大的功能,比如Decimal, uuid, date, time相关的类型,都是建立在已有的物理类型上,建立自己的逻辑规则。通过添加logicalType属生来分别。
对于每个企业都可以自己自己的逻辑类型,定制自己的业务值约束。而标准库仅提供了上面的通用逻辑类型。
引用类型约束 :
对于每一个命名类型,尤其是record类型,是可以引用的。
也就是说,不可能存在两个相同名称的record类型,如果另一个地方有相同的record类型名称,它就不需要再定义具体结构,直接引用过来复用了结构定义。
如果确实想要相同名称的record类型,那么可以添加namespace进行区分。
在相同的namespace下面不能有相同的命名类型出现。如果需要复用,可以定义共用的namespace进行引用,或者直接引用其它人的namespace。如果不需要复用,可以定义各自namespace下的数据结构。默认不定义都在相同的namespace下面。
IDL文件
avro虽然引入了引用数据类型,对于需要复用的类型可以引用过来,但是随着模式的复杂度,json表达形式维护性会比较困难。这时候可以引用idl文件,进行拆解,idl接口格式可以进行import以及独立的类型定义。然后组合转换生成单个avsc模式文件
e. avro的模式演化
前面虽然讲过json的很多坏话,但是json有很强的灵活性,比如值缺失,新增不影响代码的正常运行。avro不止这些基础功能,它更加强大。
- 字段增删改
avro可以用老的代码读新的数据,也可以用新的代码读老的数据。
它是怎么做到的呢?
如果一个模式字段新增,那么它必需要默认值,如果一个字段删除当然不受影响。
比如老的模式字段有a, b, c, 新的模式字段有a, c, d。
当新的模式依次按字段读取老的数据的时候,发现没有d,但是它有默府值它所以是没有问题的。
当老的模式读取新的数据的时候,发现没有b,如果对于老的模式有b的默认值也是可以运行正常的。
所以,老的模式如果想兼容新数据,它就必须对它以后想要删除数据提供默认值。新的模式想兼容老数据,那么它就必须对新增的字段提供默认值。
当然各种兼容模式可以按需选择。
-
值的类型转换
除了字段的增删,另一个问题就是字段的可空性问题了。比如同样的字段,在有的系统可以为空,有的系统不为空。我们可以分别读取成各自特写的外部表,也可以通过通用的行为读取成通用的外部表。
当然空值是跟union的模式演化有关系的。
关于模式演化的规则有兴趣可以参见问题进一步了解。
模式演化的过程。
对于一个已经序列化过的数据,新的模式是无非读取的。所以模式演化分为两部分,这也是avro重度依赖于schema的一个原因。首先reader通过writer schema(写入时schema)读取值出来,然后通过reader schema进行resolve转换成新的数据格式。
f: avro的编码格式及应用
前面所说的avro datum格式是binary编码的,当然它也有对应的json编码。json编码一般用于转换json数据至binary编码,或者binary编码转换成外部json数据。本质是就是根据schema进行遍历,读取或者输出对应的json或者binary格式.
当然avro也可以把文本文件的每一行当作是bytes模式,进行切块压缩成container文件,便于可切割压缩形式的文件处理格式。
那么我们来看一下avro-tools给我们提供的功能
源码路径:
https://github.com/apache/avro/tree/master/lang/java/tools
larry@larrys-MBP-2 ~ % avro-tools
Version 1.9.1
of Apache Avro
Copyright 2010-2015 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (https://www.apache.org/).
----------------
Available tools:
canonical Converts an Avro Schema to its canonical form
cat Extracts samples from files
compile Generates Java code for the given schema.
concat Concatenates avro files without re-compressing.
fingerprint Returns the fingerprint for the schemas.
fragtojson Renders a binary-encoded Avro datum as JSON.
fromjson Reads JSON records and writes an Avro data file.
fromtext Imports a text file into an avro data file.
getmeta Prints out the metadata of an Avro data file.
getschema Prints out schema of an Avro data file.
idl Generates a JSON schema from an Avro IDL file
idl2schemata Extract JSON schemata of the types from an Avro IDL file
induce Induce schema/protocol from Java class/interface via reflection.
jsontofrag Renders a JSON-encoded Avro datum as binary.
random Creates a file with randomly generated instances of a schema.
recodec Alters the codec of a data file.
repair Recovers data from a corrupt Avro Data file
rpcprotocol Output the protocol of a RPC service
rpcreceive Opens an RPC Server and listens for one message.
rpcsend Sends a single RPC message.
tether Run a tethered mapreduce job.
tojson Dumps an Avro data file as JSON, record per line or pretty.
totext Converts an Avro data file to a text file.
totrevni Converts an Avro data file to a Trevni file.
trevni_meta Dumps a Trevni file's metadata as JSON.
trevni_random Create a Trevni file filled with random instances of a schema.
trevni_tojson Dumps a Trevni file as JSON.
- 通过json与avro datum格式互转
avro-tools jsontofrag --schema schema/XXX.avsc sample/XXX.json
cat xxx.avro | avro-tools fragtojson --schema-file schema/XXX.avsc -
- 通过json与avro container格式互转
avro-tools fromjson --codec snappy --schema-file schema/XXX.avsc
avro-tools tojson xxx.avro
avro-tools getschema xxx.avro
- 编码text文件转换成avro压缩格式
avro-tools fromtext ...
avro-tools totext ...
- avro idl生成avsc模式文件
avro-tools idl ...
g. avro的各个语言版本
官方版本清单:
https://github.com/apache/avro/tree/master/lang
rust版(本人参照改造增强,可交流):
https://github.com/flavray/avro-rs
java增强版(本人参照改造增强,可交流):
https://github.com/Celos/avro-json-decoder
https://github.com/zolyfarkas/avro
js增强版:
https://github.com/flavray/avro-rs/
c扩展版(本人参照改造增强,可交流):
https://github.com/grisha/json2avro
avro手机版(本人参照改造增强,可交流):
https://github.com/flurry/avro-mobile/