Apache Avro? 1.10.0 Specification
原文地址:http://avro.apache.org/docs/current/spec.html
1 Introduction
? 本文档定义了Apache Avro。它旨在成为权威规范。 Avro的实现必须遵守此文档。
2 Schema Declaration
? Schema由以下JSON之一表示:
? 1)JSON字符串,命名已定义的类型。
? 2)JSON对象,格式如下:
{"type": "typeName" ...attributes...}
? 其中typeName是基本类型或派生类型名称,如下所示。允许将本文档中未定义的属性用作元数据,但不得影响序列化数据的格式。
? 3)JSON数组,表示嵌入式类型的并集。
Primitive Types 基本类型
? 基本类型名称的集合是:
- null: 空
- boolean: 布尔值(0或1)
- int: 32位有符号整数
- long: 64位有符号整数
- float: 单精度(32位)的IEEE 754浮点数
- double: 双精度(64位)的IEEE 754浮点数
- bytes: 8位无符号字节序列
- string: 字符串
? 基本类型没有属性,基本类型的名字也就是类型的名字。因此,例如,schema “string” 等效于:
{"type": "string"}
Complex Types 混合类型
? Avro支持六种复杂类型:record, enum, array, map, union 和fixed。
Type1:Record
? Records使用类型名称“ record”,支持其它属性的设置:
? 1)name:一个JSON字符串,record类型的名字(必需)。
? 2)namespace:一个JSON字符串,命名空间(可选)。
? 3)doc:一个JSON字符串,这个类型的文档说明(可选)。
? 4)aliases:一个JSON字符串数组,record类型的别名(可选)。
? 5)fields:一个JSON对象数组,record类型中的字段(必填)。每个字段需要以下属性:
* name:字段名字(必填)* doc:字段说明文档(可选)* type:一个schema的json对象或者一个类型名字(必填)* default:该field的默认值,在读取缺少该字段的实例时使用(可选)。根据下表,允许的值取决于field的schema类型。union fields的默认值对应于union中的第一个schema。bytes和fixed field的默认值为JSON字符串,其中Unicode代码点0-255映射到无符号的8位字节值0-255。
avro type | json type | example |
---|---|---|
null | null | null |
boolean | boolean | true |
int, long | integer | 1 |
float, double | number | 1.1 |
bytes | string | “\u00FF” |
string | string | “foo” |
record | object | {“a”: 1} |
enum | string | “FOO” |
array | array | [1] |
map | object | {“a”: 1} |
fixed | string | “\u00ff” |
* order:指定此field如何影响此record的排序顺序(可选)。有效值为“ascending”(默认),“descending”或“ignore”。有关如何使用它的更多详细信息,请参见下面的 sort order部分。* aliases:一个JSON字符串数组,别名(可选)。
? 例如,定义一个元素类型是Long的链表:
{
"type": "record","name": "LongList","aliases": ["LinkedLongs"], // old name for this"fields" : [{
"name": "value", "type": "long"}, // each element has a long{
"name": "next", "type": ["null", "LongList"]} // optional next element]
}
Type2:Enum
? Enum使用类型名称“enum”,还支持其它属性的设置:
? 1)name:枚举类型的名字(必填)。
? 2)namespace:命名空间(可选)
? 3)doc:说明文档(可选)
? 4)aliases:一个JSON字符串数组,别名(可选)。
? 5)symbols:字符串数组,所有的枚举值(必填),不允许重复数据。每个符号都必须与正则表达式[A-Za-z _] [A-Za-z0-9 _] *相匹配(与names的要求相同)。
? 6)default:该enumeration的默认值,在解析器中使用,当读取器遇到来自写入器的,未在读取器schema中定义的symbol时(可选)。此处提供的值必须是symbols数组成员的JSON字符串。请参阅有关schema解析的文档以了解如何使用它。
? 一个枚举类型的例子:
{
"type": "enum","name": "Suit","symbols" : ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"]
}
Type3:Array
? Array使用类型名称“array”,并且只支持一个属性:
- items:数组元素的 schema。
? 例如,使用以下内容声明字符串数组:
{
"type": "array","items" : "string","default": []
}
Type4:Map
? Maps使用类型名称“map”,并且只支持一个属性性:
- values: map值的schema
? map keys必须是字符串。
? 一个Map例子:
{
"type": "map","items" : "long","default": {
}
}
Type5:Union
? 组合类型,表示各种类型的组合,使用数组进行组合。比如[“null”, “string”]表示类型可以为null或者string。
? 组合类型的默认值是看组合类型的第一个元素,因此如果一个组合类型包括null类型,那么null类型一般都会放在第一个位置,这样子的话这个组合类型的默认值就是null。
? 组合类型中不允许同一种类型的元素的个数超过1个,除了record,fixed和enum。比如组合类中有2个array类型或者2个map类型,这是不允许的。
? 组合类型不允许嵌套组合类型。
Type6:Fixed
? Fixed使用类型名称“fixed”,支持以下属性:
? 1)name:名字(必填)
? 2)namespace:命名空间(可选)
? 3)aliases:一个JSON字符串数组,别名(可选)
? 4)size:一个整数,指定每个值的字节数(必填)。
? 比如16个字节数的fixed类型例子如下::
{
"type": "fixed", "size": 16, "name": "md5"}
Names
Record,enums和fixed是命名类型。每个都有一个由两部分组成的全名。name和namespace。names的相等性是在fullname上定义的。
? fullname的name部分,record field names和enum symbols必须:
- 以[A-Za-z_]开头
- 随后仅包含[A-Za-z0-9_]
? namespace是此类名称的点分隔序列。空字符串也可用作namespace,以指示空namespace。name(包括field name和enum symbols)以及fullname的相等性区分大小写。
? 空namespace不能在以点分隔的名称序列中使用。所以namespace的语法是:
<empty> | <name>[(<dot><name>)*]
? 在record,enum和fixed定义中,fullname是通过以下方式之一确定的:
* name和namespace均已指定。例如,可以使用“ name”:“ X”,“ namespace”:“ org.foo”来表示org.foo.X的fullname。
* 指定了fullname。如果指定的name包含点,则假定该name为fullname,并且还将忽略所有指定的namespace。例如,使用“ name”:“ org.foo.X”表示fullname org.foo.X。
- 仅指定name,即不包含点的name。在这种情况下,namespace取自最紧密封装的schema或prootcol。例如,如果指定了“name”:“ X”,并且它发生在org.foo.Y的record定义的字段中,则fullname是org.foo.X。如果没有封闭的namespace,则使用空namespace。
? 对先前定义的name的引用与上面的后两种情况相同:如果它们包含点,则为fullname;如果它们不包含点,则namespace为封闭定义的namespace。
? Primitive type name没有namespace,并且它们的名称不能在任何namespace中定义。
? schema或protocol不得包含fullname的多个定义。此外,必须在使用name之前定义一个name(在JSON解析树的深度优先,从左到右遍历中的“之前”,其中协议的type属性始终被视为在message属性之前。
Aliases
? 命名的types和fields可能具有别名。实现可以选择使用别名将writer的schema映射到reader的schema。这既方便了模式演变,又可以处理不同的数据集。
? Aliases 通过使用reader schema中的aliases来重写writer的schema 来起作用。例如,如果writer的schema被命名为“ Foo”,而eader schema被命名为“ Bar”,并且aliases为“ Foo”,则该实现将如同读取时将“ Foo”命名为“ Bar”一样。同样,如果将数据写为具有名为“ x”的字段的record,并作为别名为“ x”的具有字段“ y”的record来读取,则读取时该实现将好像“ x”被命名为“ y”一样。
? alias的类型可以指定为完全使用名称空间限定的名称,也可以相对于其别名的名称空间来指定。例如,如果名为“ a.b”的类型的别名为“ c”和“ x.y”,则其别名的标准名称为“ a.c”和“ x.y”。
3 Data Serialization and Deserialization
二进制编码的Avro数据不包括类型信息或字段名称。好处是序列化的数据很小,但是结果是必须始终使用schema才能正确读取Avro数据。确保schema在结构上与用于写入数据的schema相同的最佳方法是使用完全相同的schema。
? Avro数据总是用它的schema来序列化。存储Avro数据的文件应该总是在同一文件中包含数据对应的schema。基于Avro的远程过程调用(RPC)系统必须保证远端接收者有一份写入数据时所用的schema。
? 通常,建议任何Avro数据读取器都应使用与用于写入数据以正确反序列化的模式相同的模式(如在针对模式的 Parsing Canonical Form for Schemas”中更完整地定义)。通过指定其他模式来完成将数据反序列化为更新的模式的操作,其结果在 Schema Resolution进行了描述。
对Avro数据序列化/反序列化时都需要对模式以深度优先(Depth-First),从左到右(Left-to-Right)的顺序来遍历schema,当遇到基本类型时直接序列化。因此,有可能(尽管不建议)使用与写入数据的模式不具有相同的解析规范形式的模式来读取Avro数据。为了使它起作用,序列化的原始值必须与反序列化模式中的项按值顺序兼容。例如,int和long总是以相同的方式序列化,因此int可以反序列化为long。由于两种模式的兼容性取决于数据和序列化格式(例如,二进制比JSON更宽容,因为JSON包含字段名,例如,太大的long会溢出int),因此它更简单,更可靠使用具有相同解析规范形式的架构。
Encodings
? Avro指定了两种序列化编码:二进制和JSON。大多数应用程序将使用二进制编码,因为它更小,更快。但是,对于调试和基于Web的应用程序,采用JSON编码有时是比较合适的。
Binary Encoding
? 二进制编码不包括字段名称,有关各个字节类型的独立信息,也不包括字段或记录分隔符。因此,读者完全依赖于对数据进行编码时使用的架构。
Primitive Types
? 基本类型以二进制编码,如下所示:
-
null 写入0字节
-
boolean写入1字节,其值为0(false)或1(true)
-
iint和long写入时使用变长的zig-zag( variable-length zig-zag )编码。例如:
value hex 0 00 -1 01 1 02 -2 03 2 04 … -64 7f 64 80 01 … -
float写入4字节。float被转换成32位整数,使用一种类似于 Java’s floatToIntBits的方法,再以little-endian格式编码
-
double写入8字节。double被转换成64位整数,使用的方法类似于java的 Java’s doubleToLongBits,然后以little-endian格式编码。
-
bytes被编码成一个long型值后面跟随多个字节的数据
-
string被编码成一个long型值后面跟随多个字节的UTF-8编码的字符数据
例如,3个字符的字符串"foo" 将被编码为long值3(编码为十六进制06)跟随UTF-8编码的f o和o(十六进制字节66 6f 6f):
06 66 6f 6f
Complex Types
复合类型的二进制编码如下:
Type1:Record
? record按照声明时的顺序对字段的值进行编码。换句话说,record的编码正是与它的字段的编码是相关联的。字段值按照各自的schema编码。
? 例如,record的schema如下:
{
"type": "record","name": "test","fields" : [{
"name": "a", "type": "long"},{
"name": "b", "type": "string"}]}
? 这个schema的一个实例,其a字段的值为27(编码为十六进制36),b字段的值为"foo"(编码为十六进制的06 66 6f 6f),实例的编码只是这些字段的级联,即十六进制字节序列:
36 06 66 6f 6f
Type2:Enum
? 枚举用一个int来编码,表示symbol在schema中的位置(位置从0开始)
? 例如,考虑如下enum:
{
"type": "enum", "name": "Foo", "symbols": ["A", "B", "C", "D"] }
? 这将由一个在0到3之间取值的int值编码,0表示A,3表示D。
Type3:Array
? 数组被编码成一系列的块。每个块包含一个long型计数值,后面跟随数组项。计数值为0的块指示数组的结束。每一项都按照数组项的schema进行编码。
? 如果块的计数是负数,则使用它的绝对值,计数后面紧跟一个long型的块大小(block size),指示块的字节数。这个块大小允许快速跳过数据,例如将record投影到它的字段的一个子集时。
? 例如,数组的schema:
{
"type": "array", "items": "long"}
? 一个包含3和27的数组可以编码为long值2(编码为十六进制04)紧跟long值3和27(编码为06 36),以0结束:
04 06 36 00
? 块形式的表示法允许读写超过内存缓冲区大小的数组,因为在不需要知道数组的完整长度的情况下就可以写入数组的项。
Type4:Map
? map被编码为一系列的块。每个块包含一个long型计数值,后面跟随计数值个key/value对。一个计数为0的块指示map的结束。每个项按照map值的schema进行编码。
? 如果块的计数值是负数,则使用它的绝对值,计数值后紧跟一个long型块大小指示块的字节数。这个块大小允许快速跳过数据,例如将record投影到它的字段的一个子集时。
? 块形式的表示法允许读写超过内存缓冲区大小的map,因为在不需要知道map的完整长度的情况下就可以写入map的项。
Type5:Union
? union被编码为:首先是一个long型值指示union值在其schema中的位置(从0开始计数)。然后根据union中指示位置处的schema编码union的值。
? 例如,union schema [“null”,“string”] 将会编码为:
- null 编码为0 (null在union中的位置):
00
- 字符串“a”编码为1(string在union中的位置,编码为十六进制02),随后是字符串的编码:
02 02 61
Type6:Fixed
? Fixed实例使用schema中声明的字节数进行编码。
JSON Encoding
? 除union外,JSON编码与用于字段默认值的编码相同。
? union值被编码为JSON如下:
-
如果它的类型是null,则它被编码为JSON null
-
否则,它被编码为一个包含一个name/value对的JSON对象,name为类型的名称,
? value是递归编码的值。对于Avro的命名类型(record fixed enum)采用用户指定的名称,
? 对于其他类型采用类型的名称,例如,union schema [“null”,“string”,“Foo”], Foo是一个record名,将会被编码为
- null 编码为null
- 字符串"a" 编码为{“string”:“a”}
- 一个Foo实例编码为{“Foo”:{…}} , {…}指示Foo实例的JSON编码
? 注意,仍然需要一个schema来正确处理JSON编码的数据。例如,JSON编码不能区分int和long,float和double,records和maps,enums和字符串等。
Single-object encoding
? 某些情况下,一个单一Avro序列化的对象需要长期存储。一个常见的例子是将Avro records储存在Apache Kafka topic中几周。
? 当一个schema发生改变后的一段时间内,这种持久化系统将包含使用不同schema编码的记录。因此需要知道编码record使用了哪个schema来支持schema的演进。大多数情况下,schema大到无法包含在消息中,因此儿进制包装格式可以更有效的支持该种情况。
Single object encoding specification
? 单一Avro对象编码如下:
? 1)一个两字节标记,C3 01,表明消息是Avro和使用该单一记录(single-record)格式(版本1)
? 2)对象schema的8字节little-endian CRC-64-AVRO
? 3)使用Avro二进制编码的Avro对象。
? 使用2字节标记的实现来确定是否是AVRO。这个检查可以帮助避免当消息不是用Avro编码时所做的无效查找----通过指纹(fingerprint)决定schema。
4 Sort Order
? Avro定义了数据的标准排序顺序。这允许由一个系统写入的数据被另一系统有效地分类。这可能是一项重要的优化,因为排序顺序比较有时是每个对象最频繁的操作。还要注意,Avro二进制编码的数据可以有效地排序,而不必反序列化为对象。
? 如果数据项具有相同的schema,则只能进行比较。成对比较通过schema的深度优先,从左到右遍历递归实现。遇到的第一个不匹配决定了项目的顺序。
? 根据以下规则比较具有相同schema的两项:
-
null data 总是相等
-
boolean data false在前,true在后
-
int, long, float and double data 按数值升序排列
-
bytes and fixed data 按字典顺序通过无符号8位值进行比较
-
string data 按字典顺序由Unicode代码点进行比较。由于UTF-8用作字符串的二进制编码,因此字节和字符串二进制数据的排序是相同的
-
array data 按字典顺序进行元素比较。
-
enum data 按符号在枚举模式中的位置排序,例如:一个enum的ymbols是 [“z”, “a”] ,则"z"排序在前,"a"排序在后
-
union data 首先由union中的分支排序,并且在其中由分支的类型排序。 例如,[“int”,“string”] union将在所有字符串值之前对所有int值进行排序,其中int和字符串本身按上面的定义排序。
-
记录数据按字段的字典顺序排序。 如果字段指定其顺序为::
- “ascending”, 然后其值的顺序不变.
- “descending”,然后其值的顺序颠倒过来
- “ignore”, 然后在排序时忽略其值
-
map data 无法比较
5 Object Container Files 对象容器文件
? 序列化后的数据需要存入文件中。Avro包含一个简单的对象容器文件,一个文件拥有一个schema,文件中所有存储的对象必须根据schema使用二进制编码写入。对象存储在可以压缩的块中,块之间使用同步机制为MapReduce处理提供高效的文件分离。文件中可能包含用户随意指定的元数据。
那么一个文件包含:
-
文件头。后面跟着
-
一个或多个文件数据块( file data block)。
其中文件头(file header)包含:
-
4个字节,分别是ASCII码的o、b、j、1。
-
包含schema的文件元数据(file metadata),文件元数据定义为如下map schema:
{"type": "map", "values": "bytes"}
- 为此文件随机生成的16字节同步器。
以“ avro”开头的所有元数据属性是保留的。当前使用以下文件元数据属性:
- avro. schema,包含存储在文件中对象的schema,如JSON数据(必须)。
- avro. codec,编解码器名称,其编码器用来压缩诸如字符串的数据块。需要实现支持"null"和"deflate"编解码器,如果没有编解码器,那假设为"null"。
因此,文件头由以下schema描述:
{
"type": "record", "name": "org.apache.avro.file.Header","fields" : [{
"name": "magic", "type": {
"type": "fixed", "name": "Magic", "size": 4}},{
"name": "meta", "type": {
"type": "map", "values": "bytes"}},{
"name": "sync", "type": {
"type": "fixed", "name": "Sync", "size": 16}},]
}
文件数据块( file data block)包含:
- 一个长整型,用于指示块中对象数目。
- 一个长整型,用于表示使用编解码器后,所在块中序列化对象的字节大小。
- 序列化对象,如果编解码器是指定的,则用它进行压缩对象。
- 16字节的文件同步器。
? 这样,即使不用反序列化,也可以高效获得或跳过每个块的二进制数据。这种块的大小、对象数目和同步器的结合可以检测出坏的块并且帮助保持数据的完整性。
? 下图表示了对象容器文件的具体格式:
Required Codecs 必需编解码器
null
“null”‘编解码器仅通过未经压缩的数据。
deflate
“ deflate”编解码器使用RFC 1951中指定的deflate算法写入数据块,通常使用zlib库来实现。请注意,此格式(与RFC 1950中的“ zlib格式”不同)没有校验和。
Optional Codecs 可选编解码器
bzip2
"bzip2"编解码器使用 bzip2 压缩库
snappy
"snappy"编解码器使用谷歌 Snappy压缩库
xz
“xz” 编解码器使用 XZ 压缩库
zstandard
“zstandard” 编解码器使用 Facebook Zstandard 压缩库
6 Protocol Declaration 协议声明
? 当Avro用于RPC时,Avro使用协议描述远程过程调用RPC接口。和schema一样,它们是用JSON文本来定义的。
? 协议是带有以下属性的JSON对象:
- protocol,协议名称的字符串(必须)。
- namespace,限定名称的可选字符串。
- doc,描述协议的可选字符串。
- types,指定类型(记录、枚举、固定型和错误)定义的可选列表。错误的定义和记录一样,只不过错误使用"error"而记录使用"record",要注意不允许对指定类型的向前引用。
- messages,一个可选的JSON对象,其键是消息名称,值是对象,任意两个消息不能拥有相同的名称。
? 模式中定义的名称和命名空间规则也同样适用于协议。
Messages
message具有以下属性:
- doc,消息的可选描述。
- request,指定的类型化的参数模式列表(这和记录声明中的字段有相同的形式)。
- response,响应模式。
- error union,所声明的错误模式的联合(可选)。有效的联合会在声明的联合前面加上"string",允许传递未声明的“系统”错误。例如,如果声明的错误联合是[“AccessError”],那么有效的联合是[“string”,“AcessError”]。如果没有错误声明,那么有效的错误联合是[“string”]。使用有效联合错误可以序列化,且协议的JSON声明只能包含声明过的联合。
- one-way,布尔参数(可选)。
? 处理请求参数列表相当于处理没有名称的记录。既然读取的记录字段列表和写入的记录字段列表可以不同,那么调用者和响应者的请求参数也可以不同,这种区别的解决方法与记录字段间差异的解决方式相同。只有当回应的类型是“null”并且没有错误列出的时候,one-way参数才为真。
Sample Protocol
下面来举一个简单的HelloWorld协议的例子,它可以定义为:
{"namespace": "com.acme","protocol": "HelloWorld","doc": "Protocol Greetings","types": [{"name": "Greeting", "type": "record", "fields": [{"name": "message", "type": "string"}]},{"name": "Curse", "type": "error", "fields": [{"name": "message", "type": "string"}]}],"messages": {"hello": {"doc": "Say hello.","request": [{"name": "greeting", "type": "Greeting" }],"response": "Greeting","errors": ["Curse"]}}
}