Avro
Avro是2009年发起的一个hadoop的子项目,他也是一种二进制的编码方式,但是和Thrift和Protocol Buffer不尽相同,它诞生之初就是因为Thrift在Hadoop编解码是不是很好用。他同样是用schema的形式定义编解码的数据结构,他有两种描述语言(IDL),一种给人看的,另外一种是机器更好生成和解析。
还是之前的经典,如果是Avro的IDL就长这样
人类版
record Person {
string userName;
union { null, long } favoriteNumber = null;
array<string> interests;
}
机器版
{
"type": "record",
"name": "Person",
"fields": [
{"name": "userName", "type": "string"},
{"name": "favoriteNumber", "type": ["null", "long"], "default": null},
{"name": "interests", "type": {"type": "array", "items": "string"}}
]
}
值得注意的是,Avro没有一个数字表示Field tag,之前的例子用Avro编码只有32字节,是目前讲过的最省空间的一种,具体的编码格式如Figure 4-5。如果你看编码内容,你会发现这里面没有地方标记属性名和属性类型的。字节流就是把所有内容拼在了一起。辅以一些参数,比如string的长度等等。当你需要解析的时候,你就根据schema里面字段的顺序和字段的类型依次进行解析。这就有一个要求,就是你解析时候用的schema必须和写这个数据时的schema完全一致。如果有不一致数据解码就有问题了。
写入schema和读取schema
但是Avro这种操作那数据schema如何更新呢?这就跟写入和读取的schema相关了。当写入数据的时候,应用可以用他知道的任何一个版本的schema进行编码。我们管这个称作写入schema。那在解码的时候,他需要数据按照某种格式进行解析,我们管这个叫读取schema。Avro的核心点在于读取schema和写入schema不需要相同,他们只需要兼容就可以了。当解码数据的时候,Avro库自动处理写入schema和读取schema的不同,将数据从写入schema的格式转换成读取schema。Avro文档详细讲述了这步是如何工作的。一个简单的例子如Figure 4-6。
如果是读写schema之间字段的顺序不一样,那Avro转换程序会根据名字进行映射,如果发现写入schema有一个字段但是读取的没有,那这个字段就被忽略掉了。反过来,如果发现读取schema有一个字段但是写入没有,那这个字段会赋一个默认值。
schema修改原则
对于Avro来说,向前兼容就意味着你可以用新版schema写并且用老版schema读,向后兼容就意味着你可以用老版schema写用新版schema读。为了保证兼容性,就要求只能增删有默认值的属性。例如你加了一个字段,新版的读程序在读到老数据时,因为数据中没有这个字段会给他一个默认值,这样就可以向后兼容。如果删一个字段,同样老版读程序在读到新数据时,也会给他一个默认值,就做到向前兼容。
有些程序语言NULL可以作为所有类型的默认值,但是Avro不可以。他必须用类型union { null, long, string }表示他可以是null.虽然比直接给所有类型一个默认值要麻烦,但是它可以有效的帮助你避免bug
改变数据类型也是可以的,Avro的库会自动帮你进行类型转换,另外改属性名也可以,但是有些问题。读取schema可以对属性设定一些别名,这样新老版本的数据就可以转换了。 但是他只能向后兼容而不能向前兼容了。给一个union type加一个类型也是,可以向后,但不能向前兼容。
写入schema是从哪来的?
我们之前讨论的很热闹,但是可能已经发现这个问题,读取程序从哪能够拿到写入的schema呢?我们不能把schema写入每条数据中,因为这货太大了。具体的解决方法跟Avro的使用场景有关,主要分成3种。
- 文件
Avro的使用场景一般都是一个大文件中包含上百万条数据,尤其是在hadoop中。一个文件中的数据用的都是一个schema。这个时候写入程序就需要把写入schema写在这个文件的头部就可以了。 - 数据库
数据库中往往会有多种数据版本共存的情况,这种情况解决方法是给数据一个版本号,然后把每个版本号对应的schema存下来。解码程序根据版本号去找对应的schema进行解码。 - 网络
当两个进程网络通信的时候,他们可以在连接建立之初发送自己用到的schema,在整个连接当中,schema是不会变的,如果变了就需要重起程序或重新连接。Avro RPC 就是这么工作了。
动态生成schema
Avro和Thrift 与 Protocol Buffer相比他不再需要一个数字作为属性的唯一标记。但是这么做的好处是什么呢?或者说维护一个数字和属性的映射的问题是什么呢?这个好处在于你可以动态的去生成你数据的schema,比如你有一个关系数据库,你希望把他转成二进制格式写入文件中。如果用Avro,你可以直接把数据库中每个表作为一个对象,表名就是对象名,列名作为属性名生成schema。利用这个schema把数据写到文件中。
如果你的数据库格式变了,比如加了一列,你完全不用改任何东西,还是用刚才的方法直接搞。虽然新的schema属性数量,顺序可能变了,但是读取程序使用属性名作为映射关系,所以他还是可以正常解码。但如果你用的是Protocol Buffer就麻烦了。你必须人工维护一个列名到tag的映射表,一旦数据库结构变了,你就得人工去保证他们的tag数字和之前的一样。这个就很麻烦了。虽然也可以用程序来搞,但是怎么都没有Avro方便,因为Thrift和Protocol Buffer在设计的时候就没有考虑过动态生成schema的问题。
schema的长处
有人会觉得json,xml就已经挺好的了,为什么还要有诸如Thrift,protocol Buffer这种带schema的编码格式呢?因为他们有以下优点
- 更小,因为他们不需要存储key的内容,所以编码后的内容比json,或者BJson要小
- schema是一个很好的文档,因为你要用他来解析数据,所以永远不用担心会出现老版本不更新的问题
- 新功能上线前,schema可以帮助你检查数据是否能够做到前向兼容和后向兼容
- 对于静态语言的开发者而言,它可以自动生成代码,帮助你进行类型检查,拼写检查。在json中经常出现key拼错了,或者把int写成了string这种情况。
总的来说,非结构化数据库的那些灵活性,schema也可以做到差不多。同时它还能更好的保证你的数据(数据类型一致,字段名一直),以及更方便的工具(类型检查,拼写检查)。