【Avro介绍】
Apache Avro是hadoop中的一个子项目,也是一个数据序列化系统,其数据最终以二进制格式,采用行式存储的方式进行存储。
Avro提供了:
- 丰富的数据结构
- 可压缩、快速的二进制数据格式
- 一个用来存储持久化数据的容器文件
- 远程过程调用
- 与动态语言的简单集成,代码生成不需要读取或写入数据文件,也不需要使用或实现RPC协议。代码生成是一种可选的优化,只值得在静态类型语言中实现。
基于以上这些优点,avro在hadoop体系中被广泛使用。除此之外,在hudi、iceberg中也都有用到avro作为元数据信息的存储格式。
【schema】
Avro依赖"schema"(模式)来实现数据结构的定义,schema通过json对象来进行描述表示,具体表现为:
- 一个json字符串命名一个定义的类型
- 一个json对象,其格式为`{"type":"typeName" ...attributes...}`,其中`typeName`为原始类型名称或复杂类型名称。
- 一个json数组,表示嵌入类型的联合
schema中的类型由原始类型(也就是基本类型)(null、boolean、int、long、float、double、bytes和string)和复杂类型(record、enum、array、map、union和fixed)组成。
1、原始类型
原始类型包括如下几种:
- null:没有值
- boolean:布尔类型的值
- int:32位整形
- long:64位整形
- float:32位浮点
- double:64位浮点
- bytes:8位无符号类型
- string:unicode字符集序列
原始类型没有指定的属性值,原始类型的名称也就是定义的类型的名称,因此,schema中的"string"等价于{"type":"string"}。
2、复杂类型
Avro支持6种复杂类型:records、enums、arrays、maps、unions和fixed。
1)Records
reocrds使用类型名称"record",并支持以下属性
- name:提供记录名称的json字符串(必选)
- namespace:限定名称的json字符串
- doc:一个json字符串,为用户提供该模式的说明(可选)
- aliases:字符串的json数组,为该记录提供备用名称
- fields:一个json数组,罗列所有字段(必选),每个字段又都是一个json对象,并包含如下属性:
- name:字段的名称(必选)
- doc:字段的描述(可选)
- type:一个schema,定义如上
- default:字段的默认值
- order:指定字段如何影响记录的排序顺序,有效值为`"ascending"`(默认值)、"descending"和"ignore"。
- aliases:别名
一个简单示例:
代码语言:javascript复制{
"type": "record",
"name": "LongList",
"aliases": ["LinkedLongs"],
"fields", [
{"name": "value", "type": "long"},
{"name": "next", "type": ["null", "LongList"]}
]
}
2)Enums
Enum使用类型名称"enum",并支持以下属性
- name:提供记录名称的json字符串(必选)
- namespace:限定名称的json字符串
- aliases:字符串的json数组,为该记录提供备用名称
- doc:一个json字符串,为用户提供该模式的说明(可选)
- symbols:一个json数组,以json字符串的形式列出符号。在枚举中每个符号必须唯一,不能重复,每个符号都必须匹配正则表达式"[A-Za-z_][A-Za-z0-9_]*"。
- default:该枚举的默认值。
示例:
代码语言:javascript复制{
"type": "enum",
"name": "Suit",
"symbols": ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"]
}
3) Arrays
- item:数组中元素的schema
一个例子:声明一个value为string的array
代码语言:javascript复制{
"type": "array",
"items": "string",
"default": []
}
4)Maps
- values:map的值(value)的schema,其key被假定为字符串
一个例子:声明一个value为long类型,(key类型为string)的map
代码语言:javascript复制{
"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"并支持以下属性:
- name:提供记录名称的json字符串(必选)
- namespace:限定名称的json字符串
- aliases:字符串的json数组,为该记录提供备用名称
- doc:一个json字符串,为用户提供该模式的说明(可选)
- size:一个整数,指定每个值的字节数(必须)
例如,16字节的数可以声明为:
代码语言:javascript复制{
"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类型),详细如下所示:
代码语言:javascript复制{
"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):
代码语言:javascript复制{"name":"hncscwc","age":20,"skill":["hadoop","flink","spark","kafka"],"other":{"interests":"basketball"}}
{"name":"tom","age":18, "skill":["java","scala"],"other":{}}
通过avro-tools可以生成一个avro文件:
代码语言:javascript复制java -jar avro-tools-1.7.4.jar fromjson --schema-file person.avsc person.json > person.avro
通过二进制的方式查看生成的avro文件内容:
另外,对于一个已存在的文件,也可以通过avro-tools工具查看schema内容、数据内容。
代码语言:javascript复制[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的使用,这里就没有展开说明,有兴趣的可移步官网进行查阅。