【Avro介绍】
Apache Avro是hadoop中的一个子项目,也是一个数据序列化系统,其数据最终以二进制格式,采用行式存储的方式进行存储。
Avro提供了:
基于以上这些优点,avro在hadoop体系中被广泛使用。除此之外,在hudi、iceberg中也都有用到avro作为元数据信息的存储格式。
【schema】
Avro依赖"schema"(模式)来实现数据结构的定义,schema通过json对象来进行描述表示,具体表现为:
schema中的类型由原始类型(也就是基本类型)(null、boolean、int、long、float、double、bytes和string)和复杂类型(record、enum、array、map、union和fixed)组成。
1、原始类型
原始类型包括如下几种:
原始类型没有指定的属性值,原始类型的名称也就是定义的类型的名称,因此,schema中的"string"等价于{"type":"string"}。
2、复杂类型
Avro支持6种复杂类型:records、enums、arrays、maps、unions和fixed。
1)Records
reocrds使用类型名称"record",并支持以下属性
一个简单示例:
{
"type": "record",
"name": "LongList",
"aliases": ["LinkedLongs"],
"fields", [
{"name": "value", "type": "long"},
{"name": "next", "type": ["null", "LongList"]}
]
}
2)Enums
Enum使用类型名称"enum",并支持以下属性
示例:
{
"type": "enum",
"name": "Suit",
"symbols": ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"]
}
3) Arrays
一个例子:声明一个value为string的array
{
"type": "array",
"items": "string",
"default": []
}
4)Maps
一个例子:声明一个value为long类型,(key类型为string)的map
{
"type": "map",
"values": "long",
"default": {}
}
5)Unions
联合使用json数组表示,例如[null, "test"]声明一个模式,它可以是空值或字符串。
需要注意的是:当为union类型的字段指定默认值时,默认值的类型必须与union第一个元素匹配,因此,对于包含"null"的union,通常先列出"null",因为此类型的union的默认值通常为空。
另外, union不能包含多个相同类型的schema,类型为record、fixed和eum除外。
6)Fixed
Fixed使用类型名称"fixed"并支持以下属性:
例如,16字节的数可以声明为:
{
"type": "fixed",
"name": "md5",
"size": 16
}
【Avro的文件存储格式】
1、数据编码
1)原始类型
对于null类型:不写入内容,即0字节长度的内容表示;
对于boolean类型:以1字节的0或1来表示false或true;
对于int、long:以zigzag的方式编码写入
对于float:固定4字节长度,先通过floatToIntBits转换为32位整数,然后按小端编码写入。
对于double:固定8字节长度,先通过doubleToLongBits转换为64位整型,然后按小端编码写入。
对于bytes:先写入长度(采用zigzag编码写入),然后是对应长度的二进制数据内容
对于string:同样先写入长度(采用zigzag编码写入),然后再写入字符串对应utf8的二进制数据。
2)复杂类型
对于enums:只需要将enum的值所在的Index作为结果进行编码即可,例如,枚举值为["A","B","C","D"],那么0就表示”A“,3表示"D"。
对于maps:被编码为一系列的块。每个块由一个长整数的计数表示键值对的个数(采用zigzag编码写入),其后是多个键值对,计数为0的块表示map的结束。每个元素按照各自的schema类型进行编码。
对于arrays:与map类似,同样被编码为一系列的块,每个块包含一个长整数的计数,计数后跟具体的数组项内容,最后以0计数的块表示结束。数组项中的每个元素按照各自的schema类型进行编码。
对于unions:先写入long类型的计数表示每个value值的位置序号(从零开始),然后再对值按对应schema进行编码。
对于records:直接按照schema中的字段顺序来进行编码。
对于fixed:使用schema中定义的字节数对实例进行编码。
2、存储格式
在一个标准的avro文件中,同时存储了schema的信息,以及对应的数据内容。具体格式由三部分组成:
固定4字节长度,内容为字符'O','b','j',以及版本号标识,通常为1。
文件的元数据属性,包括schema、数据压缩编码方式等。整个元数据属性以一个map的形式编码存储,每个属性都以一个KV的形式存储,属性名对应key,属性值对应value,并以字节数组的形式存储。最后以一个固定16字节长度的随机字符串标识元数据的结束。
而数据内容则由一个或多个数据块构成。每个数据块的最前面是一个long型(按照zigzag编码存储)的计数表示该数据块中实际有多少条数据,后面再跟一个long型的计数表示编码后的(N条)数据的长度,随后就是按照编码进行存储的一条条数据,在每个数据块的最后都有一个16字节长度的随机字符串标识块的结束。
整体存储内容如下图所示:
3、存储格式
我们通过一个实际例子来对照分析下。
首先定义schema的内容,具体为4个字段的表,名称(字符串)、年龄(整型)、技能(数组)、其他(map类型),详细如下所示:
{
"type":"record",
"name":"person",
"fields": [
{
"name": "name",
"type": "string"
},
{
"name": "age",
"type": "int"
},
{
"name": "skill",
"type": {
"type":"array",
"items": "string"
}
},
{
"name": "other",
"type": {
"type": "map",
"values": "string"
}
}
]
}
再按照上面的schema定义两条数据(person.json):
{"name":"hncscwc","age":20,"skill":["hadoop","flink","spark","kafka"],"other":{"interests":"basketball"}}
{"name":"tom","age":18, "skill":["java","scala"],"other":{}}
通过avro-tools可以生成一个avro文件:
java -jar avro-tools-1.7.4.jar fromjson --schema-file person.avsc person.json > person.avro
通过二进制的方式查看生成的avro文件内容:
另外,对于一个已存在的文件,也可以通过avro-tools工具查看schema内容、数据内容。
[root@localhost avro]$ java -jar avro-tools-1.7.4.jar getschema ./person.avro
{
"type" : "record",
"name" : "person",
"fields" : [ {
"name" : "name",
"type" : "string"
}, {
"name" : "age",
"type" : "int"
}, {
"name" : "skill",
"type" : {
"type" : "array",
"items" : "string"
}
}, {
"name" : "other",
"type" : {
"type" : "map",
"values" : "string"
}
} ]
}
[root@localhost avro]$ java -jar avro-tools-1.7.4.jar tojson ./person.avro
{"name":"hncscwc","age":20,"skill":["hadoop","flink","spark","kafka"],"other":{"interests":"basketball"}}
{"name":"tom","age":18,"skill":["java","scala"],"other":{}}
【小结】
本文对avro的格式定义、编码方式、以及实际存储的文件格式进行了详细说明,最后也以一个实际例子进行了对照说明。另外, 在官网中还涉及rpc的使用、mapreduce的使用,这里就没有展开说明,有兴趣的可移步官网进行查阅。