Apache Avro 是 Hadoop 中的一个子项目,也是一个数据序列化系统,其数据最终以二进制格式,采用行式存储的方式进行存储。
Avro提供了:
基于以上这些优点,Avro 在 Hadoop 体系中被广泛使用。除此之外,在 Hudi、Iceberg 中也都有用到 Avro 作为元数据信息的存储格式。
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
)组成。
原始类型包括如下几种:
null
:没有值boolean
:布尔类型的值int
:
32
32
32 位整形long
:
64
64
64 位整形float
:
32
32
32 位浮点double
:
64
64
64 位浮点bytes
:
8
8
8 位无符号类型string
:unicode
字符集序列原始类型没有指定的属性值,原始类型的名称也就是定义的类型的名称,因此,schema
中的 "string"
等价于 {"type":"string"}
。
Avro 支持 6 种复杂类型:records
、enums
、arrays
、maps
、unions
和 fixed
。
reocrds
使用类型名称 "record"
,并支持以下属性
name
:提供记录名称的 json
字符串(必选)namespace
:限定名称的 json
字符串doc
:一个 json
字符串,为用户提供该模式的说明(可选)aliases
:字符串的 json
数组,为该记录提供备用名称fields
:一个 json
数组,罗列所有字段(必选),每个字段又都是一个 json
对象,并包含如下属性:
name
:字段的名称(必选)doc
:字段的描述(可选)type
:一个 schema
,定义如上default
:字段的默认值order
:指定字段如何影响记录的排序顺序,有效值为 "ascending"
(默认值)、"descending"
和 "ignore"
。aliases
:别名一个简单示例:
{
"type": "record",
"name": "LongList",
"aliases": ["LinkedLongs"],
"fields", [
{"name": "value", "type": "long"},
{"name": "next", "type": ["null", "LongList"]}
]
}
Enum
使用类型名称 enum
,并支持以下属性
name
:提供记录名称的 json
字符串(必选)namespace
:限定名称的 json
字符串aliases
:字符串的 json
数组,为该记录提供备用名称doc
:一个 json
字符串,为用户提供该模式的说明(可选)symbols
:一个 json
数组,以 json
字符串的形式列出符号。在枚举中每个符号必须唯一,不能重复,每个符号都必须匹配正则表达式 "[A-Za-z_][A-Za-z0-9_]*"
。default
:该枚举的默认值。示例:
{
"type": "enum",
"name": "Suit",
"symbols": ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"]
}
item
:数组中元素的 schema
一个例子:声明一个 value
为 string
的 array
{
"type": "array",
"items": "string",
"default": []
}
values
:map
的值(value
)的 schema
,其 key
被假定为字符串一个例子:声明一个 value
为 long
类型,(key
类型为 string
)的 map
{
"type": "map",
"values": "long",
"default": {}
}
union
使用 json
数组表示,例如 [null, "test"]
声明一个模式,它可以是空值或字符串。
需要注意的是:当为 union
类型的字段指定默认值时,默认值的类型必须与 union
第一个元素匹配,因此,对于包含 "null"
的 union
,通常先列出 "null"
,因为此类型的 union
的默认值通常为空。
另外,union
不能包含多个相同类型的 schema
,类型为 record
、fixed
和 enum
除外。
Fixed
使用类型名称 "fixed"
并支持以下属性:
name
:提供记录名称的 json
字符串(必选)namespace
:限定名称的 json
字符串aliases
:字符串的 json
数组,为该记录提供备用名称doc
:一个 json
字符串,为用户提供该模式的说明(可选)size
:一个整数,指定每个值的字节数(必须)例如,16 字节的数可以声明为:
{
"type": "fixed",
"name": "md5",
"size": 16
}
null
类型:不写入内容,即 0 字节长度的内容表示。boolean
类型:以 1 字节的 0 或 1 来表示 false
或 true
。int
、long
:以 zigzag
的方式编码写入。float
:固定 4 字节长度,先通过 floatToIntBits
转换 32 位整数,然后按小端编码写入。double
:固定 8 字节长度,先通过 doubleToLongBits
转换为 64 位整型,然后按小端编码写入。bytes
:先写入长度(采用 zigzag
编码写入),然后是对应长度的二进制数据内容。string
:同样先写入长度(采用 zigzag
编码写入),然后再写入字符串对应 utf8
的二进制数据。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
中定义的字节数对实例进行编码。在一个标准的 avro
文件中,同时存储了 schema
的信息,以及对应的数据内容。具体格式由三部分组成:
魔数:固定 4 字节长度,内容为字符 'O'
,'b'
,'j'
,以及版本号标识,通常为
1
1
1。
元数据信息:文件的元数据属性,包括 schema
、数据压缩编码方式等。整个元数据属性以一个 map
的形式编码存储,每个属性都以一个 KV 的形式存储,属性名对应 key
,属性值对应 value
,并以字节数组的形式存储。最后以一个固定 16 字节长度的随机字符串标识元数据的结束。
数据内容:而数据内容则由一个或多个数据块构成。每个数据块的最前面是一个 long
型(按照 zigzag
编码存储)的计数表示该数据块中实际有多少条数据,后面再跟一个 long
型的计数表示编码后的(
N
N
N 条)数据的长度,随后就是按照编码进行存储的一条条数据,在每个数据块的最后都有一个 16 字节长度的随机字符串标识块的结束。
整体存储内容如下图所示:
我们通过一个实际例子来对照分析下。
首先定义 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
的使用,这里就没有展开说明,有兴趣的可移步官网进行查阅。