Avro 1.2.0 规范

Avro 1.2.0 规范

(本文翻译自:http://hadoop.apache.org/avro/docs/current/spec.html 不足之处请指出)

介绍

本文档定义了 Avro,并将成为官方规范。Avro 的实现必须符合此规范。

模式声明

一个模式是由 JSON 表示的下列对象之一:
  • 一个 JSON 字符串,命名了一个有定义的类型。
  • 一个 JSON 对象,形式为: {"type": "typeName " ...attributes ...}。其中,typeName 是一个基本或者派生的类型名,稍后会给出定义。本文中未定义的属性可以作为元数据,但是不能影响序列化数据的结构。
  • 一个 JSON 数组,表示内嵌类型的联合。

基本类型

基本类型的集合为:
  • string : unicode character sequence
  • bytes : sequence of 8-bit bytes
  • int : 32-bit signed integer
  • long : 64-bit signed integer
  • float : single precision (32-bit) IEEE 754 floating-point number
  • double : double precision (64-bit) IEEE 754 floating-point number
  • boolean : a binary value
  • null : no value
基本类型没有指定的属性。
基本类型的名字同时也定义了类型名。例如,模式“string”和 {"type": "string"}是等效的。
复杂类型

Avro 支持六种复杂类型:记录、枚举、数据、映射(map)、联合和 fixed。

记录
记录以 “record” 作为类型名,支持两种属性:
  • name :一个JSON字符串,记录的名称(必需)。
  • fields:一个 JSON数组,列出了域(必需)。每个域都是JSON具有以下属性的对象:
    • name:JSON字符串,域名(必需)。
    • type:JSON对象,定义了一个模式,或者一个以JSON字符串命名的记录定义(必需)。
    • default:域的默认值,读取对象实例时,有些域的值未提供,使用默认值作为该域的值。允许的值依赖域的模式类型,见下表。联合域(union fields)的默认值和联合的第一个模式对应。
      avro typejson typeexample
      stringstring"foo"
      bytesstring\u00FF"
      int,longinteger1
      float,doublenumber1.1
      booleanbooleantrue
      nullnullnull
      recordobject{"a": 1}
      enumstring"FOO"
      arrayarray[1]
      mapobject{"a": 1}
      fixedstring"\u00ff"
  • order:指定了记录中的域的排列顺序(可选)。接受 "ascending"(默认值),"descending", 或者 "ignore"。更多的细节请参照之后的"排列顺序"一节。
例如,一个 64 位的链表的值可以定义为:
{
"type": "record",
"name": "LongList",
"fields" : [
{"name": "value", "type": "long"}, // each element has a long
{"name": "next", "type": ["LongList", "null"]} // optional next element
]
}

枚举
枚举以 “enum” 作为类型名,支持以下属性:
  • name:一个 JSON 字符串作为枚举的名称(必需)。
  • symbols:一个 JSON 数组,作为 JSON 字符串 列出了 symbols(必需)。
例如,一副牌的花色可以定义为:
{ "type": "enum",
"name": "Suit",
"symbols" : ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"]
}

数组
数组以 "array" 为类型名,支持单个属性:
  • items:array 条目的模式。
例如,一个字符串数据声明为:
{"type": "array", "items": "string"}
映射
映射 以 "map" 为类型名,支持一个属性:
  • values:map values 的模式。
映射的 keys 假定为字符串。
例如,一个从字符串到 long 的映射声明为:
  {"type": "map", "values": "long"}
联合

在之前提到过联合以 JSON 的数组表示。例如,["string","null"] 声明了一种模式,可以为字符串或者 null。

除了记录,固定和枚举,联合可以包含不超过一个拥有相同类型的模式。例如,不允许联合包含两个数组类型或者两个映射类型类型,但是允许两个不同名称的类型。(Names permit efficient resolution when reading and writing unions.)

联合不可以包含其他的联合

Fixed
Fixed 以 "fixed" 作为类型名,支持两种属性:
  • name:fixed 的名称(必需)。
  • size:一个整数,指定了每一个值的字节数(必需)。
比如,16-byte 的 fixed 可以声明为:
{"type": "fixed", "size": 16, "name": "md5"}

标示符

记录、域和枚举的名称必须:
  • 以[A-Za-z_]开头。
  • 后面的字符只能为[A-Za-z0-9]

数据序列化


Avro 数据通过模式来序列化。存储 Avro 数据的文件同时存储数据的模式。基于 Avro 的 RPC 系统也必须保证接收方也拥有一个写数据的模式副本。

因为用于写数据的模式可以在读取数据的时候获得,Avro 的数据本身并不会加上类型标签。用模式来分析数据。

通常,序列化和反序列化过程是对模式的深度优先,从左至右的遍历过程,当遇到基本类型的时候就对它们进行序列化。

编码

Avro指定了两种序列化的编码方法:二进制和 JSON。大多数程序会使用二进制编码,优点在于小而快。但是,对于调试和基于网络的应用,比较适合使用 JSON 编码。

二进制编码

基本类型按照以下方式进行二进制编码:
  • 一个字符串被编码为一个长整型加以 UTF-8 编码的字符数据的字符数。例如,字符串 "foo" 会被编码为长整数3(编码为hex 06)紧接'f','o','o'的 UTF-8 编码(hex 66 6f 6f):06 66 6f 6f
  • bytes 被编码为一个长整型紧接 bytes 数据。
  • int 和 long 被编码为变长的 zig-zag 编码。以下为一些例子:
    valuehex
    0 00
    -1 01
    1 02
    -2 03
    2 04
    ...
    -64 7f
    64  80 01
    ...
  • 浮点数以 4 个字节表示。浮点数利用 Jave 的 floatToIntBits 方法转化为 32 位的整数,并以小端格式表示。
  • 双精度值以 8 字节表示。利用 java 的 doubleToLongBits 方法将双精度值转化为 64 位的整型,然后以小端格式表示。
  • 布尔值以单字节表示,0 为 false,1 为 true。
复杂类型

复杂类型按照以下方式以二进制编码:

记录
记录的编码根据域的定义顺序对每个域的值进行编码。换句话说,一个记录的编码是一连串域的编码的组合。域值根据 模式进行编码。
例如,一个记录模式:
{
"type": "record",
"name": "test",
"fields" : [
{"name": "a", "type": "long"},
{"name": "b", "type": "string"}
]
}
它的一个实例的的 a 域的值为 27(编码为 hex 36),b 域的值为 "foo"(编码为hex 0c 66 6f 6f),它的编码就是这两个域的编码的连接,也就是十六进制的字符序列:36 0C 66 6f 6f
枚举

枚举以整型编码,表示模式中从零开始的位置。例如,enum:

{"type": "enum", "name": "Foo", "symbols": ["A", "B", "C", "D"] }

被编码为 0-3 之间的一个整型值,0 表示 "A",3 表示 "D"。

数组

数组被编码为一系列的块。每一个块包含一个长整型的 count 值,紧跟数组条目。一个 count 值为 0 的块表示了数组的结束。数组中的每一个条目按照自己的模式编码。

如 果一个块的 count 值为负,那么 count 值紧跟一个长整型的值,表示 block 中的字节数目。在这种情况下,实际的count值是写入的count值的绝对值(译者注:这段话有点拗口,原文为:If a block's count is negative, then the count is followed immediately by a long block size , indicating the number of bytes in the block. The actual count in this case is the absolute value of the count written.)。

比如,一个数组模式为:

{"type": "array", "items": "long"}

一个数组包含项 3 和 27,将被编码为长整型 2(hex 04)紧跟长整型 3 和 27(hex 06 36),以0结尾:04 06 36 00

因为可以在不知道数组长度的情况下读取数组块的表示法允许读写超过内存缓存长度的数组,可选项块长度允许在数据中快速定位,比如访问一个记录的子集的域。

映射
映射以一些列的块表示。每个块包含一个长整型的 count 值,紧跟 key/value 对。一个 count 值为 0 的块表示 map 的结束。每一个项根据 map 的值

模式进行编码。

If a block's count is negative, then the count is followed immediately by a long block size , indicating the number of bytes in the block. The actual count in this case is the absolute value of the count written.

因为可以在不知道映射长度的情况下读取映射,块的表示法允许读写超过内存缓存长度的映射。可选项块长度允许在数据中快速定位,比如访问一个记录的子集的域。

注意:块还未全部实现,有可能会有变化。任何大型的对象必须能很容易的读写,但是除非我们能够证明规范中的方法是有效的,在此之前,这部分规范只能作为草稿。


枚举
枚举的编码方法为首先是一个长整型的值,代表模式中从零开始的位置。枚举的值按照模式定义依次编码。
例如,一个枚举的模式为 ["string","null"] 被编码为:
  • null 为整数 1("null"在union中的序号,编码为 hex 02):02
  • 字符串 "a" 为 0("string"在union中的序号),接下来是序列化的字符串:00 02 61
Fixed

Fixed 实例的编码方法为直接使用模式中定义的字节数。

JSON 编码

JSON编码和之前所述的编码方式的不同之处在于枚举。

JSON中枚举的编码方法为:

  • 如果类型为 null,编码为 JSON null。
  • 否则按照 JSON 对象的方法编码:采用 name/value 对,name 是类型名,值是对应的编码值。对于 Avro 的命名类型(record,fixed和enum),使用用户定义的名字;其他的类型直接使用类型名。
例如,联合的模式 ["null","string","Foo"],Foo 为一个记录名,被编码为:
  • null 为 null。
  • string "a" 为{"string":"a"}
  • Foo 实例为{"Foo":{...}},{...}表示的是 Foo 实例在 JSON 中的编码。
注意一个模式仍然需要正确的处理用 JSON 编码的数据。例如,JSON 的编码并不区分 int 和 long,float 和 double,记录和映射,枚举和字符串,等。

排序

Avro 为数据定义了一个标准的排列顺序。这允许一个系统写的数据可以被其他的系统有效的排序。因为排序比较在很多时候是经常使用到的单个对象操作,这可以看做一个重要的优化。注意可以对 Avro 的二进制编码的数据直接进行排序而不需要反序列化。

只有当数据项包含相同的模式的时候,数据之间才能进行比较。成对比较按照模式中深度优先,从左至右的顺序递归的进行。找到第一个不匹配将会终止比较。

两个拥有相同的模式的项的比较方法按照以下规则进行:
  • int, long, float 按照数值比较。
  • boolean 数据的排序是 false 在 true 之前。
  • null 数据总是相等。
  • string 数据按照字典序进行比较。由于使用 UTF-8 来对 string 进行二进制编码,比较 bytes 和 characters 是等效的。
  • bytes 和 fixed 数据按照 byte 的字典序进行比较。
  • array 数据按照元素的字典序进行比较。
  • 枚举数据按照符号在枚举模式中的位置排序。比如,一个枚举的模式为 ["z","a"],那么 "z" 就排在 "a" 之前。
  • 记录数据按照域的字典序排序,如果一个域指定了以下属性:
    • "ascending",那么域值的顺序不变。
    • "descending"(译者注:原文为"ascending",应为笔误), 将域值的顺序颠倒。
    • "ignore",在排序的时候忽略域值。
  • 映射数据不可进行比较。如果对一个不在指定属性 "order":"ignore" 的 record 域中的 map 中的数据进行比较的话,将会报错。

对象容器文件

Avro 包括了一个简单的对象容器文件格式。一个文件有一个模式,所有存储在文件中的对象根据这个模式写入。对象以块存储,块可以被压缩。为了在进行 mapreduce 处理的时候有效的切分文件,在块之间采用了同步记号。

 

文件可以包含任意的用户定义的元数据。
一个文件由以下部分组成:
  • 文件头。
  • 一个或者多个块。

有两种块:普通块和元数据块。一个文件必须包含至少一个元数据块。文件以最后一个元数据块结束。任何在最后一个元数据块之后的数据都将被忽略。
文件头包括:
  • 四个字节,ASCII 'O', 'b', 'j', zero。
  • 一个 16 字节的同步记号。

元数据块包括:
  • 文件的 16 字节长的同步记号。
  • 值为 -1 的长整型,表明这是一个元数据块。
  • 标识块长度的长整型。
  • 标识块中 key/value 对数目的长整型。
  • 每一个 k/v 对的 string key 和 bytes value。
  • 标识块中字节总数的 4 字节长的大端整数。
当一个文件被正常关闭,会终止文件并允许有效的定位到元数据开始的地方。如果同步标记和文件开头的标记不匹配的话,必须扫描到文件的最后一个元数据。

以下元数据属性被保留:

  • schema 表示文件中存储的对象的模式,以 string 格式保存。
  • count 表示文件中对象的数目,以 ASCII string 格式保存。
  • codec 用于压缩块的压缩算法的名字,string 格式。当前支持的唯一值是 "null"(表明目前不支持压缩)。如果不指定 codec,默认值为 "null"。
  • sync 16 字节长的同步记号,以 byte 序列格式表示。

一个普通的块包括:
  • 文件的 16 字节同步记号。
  • 表示文件中块长度的长整型。
  • 序列化的对象。如果指定了 codec,则是利用 codec 压缩过后的对象。
注意由于支持多个元数据块,这个格式支持追加。
为了增加在遇到程序错误时的健壮性,可以定期的写入元数据,以限定为找到最后一个元数据块而需要在文件中扫描的总数。

 

协议声明

Avro 协议描述了 RPC 接口。和模式一样,也是用 JSON 文本定义的。
一个协议是一个 JSON 对象,拥有以下属性:
  • name,字符串,用以和其他协议区分。
  • namespace,字符串,限定了 name。
  • types,一个记录、枚举和错误定义列表。一个错误定义和一个数组类似,只是名称为"error"而非"record"。注意不支持对记录、枚举和错误的向前引用。
  • messages,一个 JSON 对象,keys 是消息名称,values 是对象,其属性描述如下。messages 不允许同名。

消息

一个消息拥有如下属性:

  • 请求,一个定义了名称和类型的参数模式的列表(和记录中域的定义的形式类似)。
  • 应答模式。
  • 可选的由错误模式组成的联合。
一个请求参数列表和一个匿名record是等价的。因为record域列表对读者和写者可以使不同的,请求参数对于caller和responder来讲也可以使不同的,这种不同和record中域的不同采用相同的方法解决。

协议举例

一个简单的 HelloWorld 协议可以定义为:

{
"namespace": "com.acme",
"protocol": "HelloWorld",

"types": [
{"name": "Greeting", "type": "record", "fields": [
{"name": "message", "type": "string"}]},
{"name": "Curse", "type": "error", "fields": [
{"name": "message", "type": "string"}]}
],

"messages": {
"hello": {
"request": [{"name": "greeting", "type": "Greeting" }],
"response": "Greeting",
"errors": ["Curse"]
}
}
}

线上协议格式

消息传输

消息可以通过不同的传输机制进行传输。例如,可以使用 HTTP,socket 或者 SSL 等。本文档指定了请求和响应消息的格式,但是没有指定在不同传输中的消息封装细节。
对传输层,消息是一个不透明的字节序列。
一个传输层是一个系统,它支持:
  • 请求消息的传输。
  • 响应消息的传输。
    服 务器会在收到客户端发送的请求消息后发送一个相应的响应消息。这个机制和具体的传输层相关。例如,在 HTTP 中是隐式的,因为 HTTP 直接传输请求和响应。但是在一个复用的传输层中,很多客户端线程可能同时利用一个 socket 发送消息,这时就需要为每个消息加上唯一的标示符标签。

消息分帧

Avro 的消息被分为多个帧,形成一个缓冲列表。
分帧是位于消息和传输层中的一层。它可以优化一些操作。
分帧消息的格式为:
  • 一系列的缓存,每个缓冲包括:
    • 一个4字节,大端的缓存长度
    • 缓冲数据
  • 消息以一个零长度的缓存结束。

 

分帧对于请求和响应消息格式来说是透明的。任何消息都可能被分为一个或者多个缓存。

 

分 帧允许读者有效的从不同的源获得不同的缓存,允许写者有效的将不同的缓存存入不同的目的地。特别的,它可以减少大的二进制对象被复制的次数。例如,一个 RPC 参数包含了几兆的文件数据,数据可以直接从文件描述符中复制到 socket,另一个方面,也可以直接(从socket)写到一个文件描述符,而不需要进入用户模式。

一个简单的,推荐的分帧策略是当写一个大二进制对象时,如果对象超过了输出缓存的大小,创建一个新的段。小的对象追加到缓存中,大的对象按照自己的缓存写入。当一个读者需要读入大的对象,运行时可以像处理一个完整的缓存一样处理这个读请求,而不需要复制对象。

握手

 

在发送RPC请求和响应之前需要握手。握手的目的是保证客户端和服务器端互相拥有对方的协议定义,从而客户端可以正确的反序 列化响应,服务器端可以正确的反序列化请求。客户端和服务器端都需要维护一个当前可见的协议的缓存,因此,对于大多数情况,一个握手完成后,在进行网络交 换的时候不需要再传输协议的全部文本。

 

握手过程使用如下模式:

 

{
"type": "record",
"name": "HandshakeRequest", "namespace":"org.apache.avro.ipc",
"fields": [
{"name": "clientHash",
"type": {"type": "fixed", "name": "MD5", "size": 16}},
{"name": "clientProtocol", "type": ["null", "string"]},
{"name": "serverHash", "type": "MD5"},
{"name": "meta", "type": ["null", {"type": "map", "values": "bytes"}]}
]
}
{
"type": "record",
"name": "HandshakeResponse", "namespace": "org.apache.avro.ipc",
"fields": [
{"name": "match",
"type": {"type": "enum", "name": "HandshakeMatch",
"symbols": ["BOTH", "CLIENT", "NONE"]}},
{"name": "serverProtocol",
"type": ["null", "string"]},
{"name": "serverHash",
"type": ["null", {"type": "fixed", "name": "MD5", "size": 16}]},
{"name": "meta",
"type": ["null", {"type": "map", "values": "bytes"}]}
]
}

 

  • 客户端首先在每一个请求前加入 HandshakeRequest, 包含了协议的 hash 值以及服务器端的协议(clientHash!=null, clientProtocol=null, serverHash != null),hash 值是一个对 JSON 协议文本的 128 位的 MD5 hah。如果一个客户端从未连接到 server,它发送通过猜测得到的服务器端的 hash 值,否则发送之前从服务器得到的 hash 值。
  • 服务器通过发送 HandshakeRespons 进行相应,包括:
    • match=BOTH, serverProtocol=null, serverHash=null。如果客户端发送了服务器端协议的有效 hash,并且服务器端知道客户端所发送 hash 值的对应协议。在这种情况下,请求完成,在 HandshakeResponse 之后紧接响应数据。
    • match=CLIENT, serverProtocol!=null, serverHash!=null 。如果服务器在之前知道客户端的协议,但是客户端发送来一个对服务器端的写来说是错误的 hash 值。客户端需要利用服务器端返回的协议来处理响应并缓存协议和协议的 hash 值以便将来与服务器端交互。
    • match=NONE, serverProtocol!=null, serverHash!=null。服务之前并不知道客户端的协议而且客户端想服务器发送了一个错误的 hash 值。在这种情况下,客户端必须重新提交请求,协议文本为 (clientHash!=null, clientProtocol!=null, serverHash!=null),而服务器响应一个成功的匹配( match=BOTH, serverProtocol=null, serverHash=null )。
Meta 域为将来改进握手而保留。

调用格式

一个调用由请求消息,结果响应消息或者错误消息组成。请求和响应包含可扩展的元数据,两种消息都按照之前提出的方法分帧。
调用的请求格式为:
  • 请求元数据,一个类型值的映射。
  • 消息名,一个Avro字符串,后面跟随
  • 消息参数。参数根据消息的请求定义序列化。
调用的响应格式为:
  • 响应的元数据,一个类型值的映射。
  • 一个一字节的错误标志位,后面紧跟:
    • 如果错误标志为false,响应消息,根据响应的模式序列化。
    • 如果错误标志位true,错误消息,根据消息的错误联合模式序列化。

模式解析

一个 Avro 数据的读者,不管是从RPC还是文件读,因为总可以获得模式,所以都可以解析数据。但是模式有可能不是想要的。例如,如果读数据和写数据的程序不是同一个版本,记录中增加或者删除了某些域。本小节指出了如何解决不同的模式。

 

我们把写数据的时候用到的模式称为写模式,应用所期望模式读模式。它们之间的不同为:

  • 如果两种模式不匹配,则出现错误。
    为了匹配,其中的一种必须包含:
    • 两种模式都是数组,它们的项目类型匹配。
    • 两种模式都是映射,它们的值类型匹配。
    • 两种模式都是枚举,它们的名称匹配。
    • 两种模式都是固定,它们的大小和名称匹配。
    • 两种模式都是记录,它们的名称相同。
    • 其中的一种模式是联合。
    • 两种模式都有相同的基本类型。
    • 如果按照以下步骤,写模式可以提升为读模式:
      • int 提升为 long,float 或者 double。
      • long 提升为 float 或者 double。
      • float 提升为 double。

  • 如果两种模式都是记录
    • 域的顺序可以不一样,只要域的名字都一样。
    • 两个记录中模式的域会被递归的解析。
    • 如果写记录中包含读记录中没有的域,写记录中的相应域的值被忽略。
    • 如果写记录中包含一个有默认值的域,而写记录中没有相应的域,则读记录使用相应的默认值。
    • 如果写记录中有一个没有默认值的域,二度记录中没有相应的域,则域值为不确定。
  • 如果两种都是枚举:
    如果写者的符号并没有出现在读者枚举中,枚举的值为不确定。
  • 如果两种都是数组:
    解析算法对读者和写者的数组项目的模式进行递归解析。
  • 如果两种都是映射:
    解析算法对读者和写者的值的模式进行递归解析。
  • 如果两种都是联合:
  • 读者联合的第一个匹配写者联合中选定的模式被递归的解析。如果没有匹配,发出错误信号。

  • 如果读者是一个联合,但是写者不是:
    读者联合中的第一个匹配写者联合模式的模式被递归的解析。如果没有匹配,发出错误信号。

  • 如果写者是一个联合,二读者不是
    如果读者模式匹配选定的写着模式,则递归解析。如果没有匹配,发出错误信号。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值