mirror of
https://github.com/Vonng/ddia.git
synced 2024-10-07 15:10:07 +08:00
chapter 4 in progress
This commit is contained in:
parent
106172808d
commit
a234a4e881
98
ch4.md
98
ch4.md
@ -78,7 +78,7 @@ JSON,XML和CSV属于文本格式,因此具有人类可读性(尽管它们
|
||||
|
||||
* **数值(numbers)** 的编码多有歧义之处。XML和CSV不能区分数字和字符串(除非引用一个外部模式)。 JSON虽然区分字符串与数值,但不区分整数和浮点数,而且不能指定精度。
|
||||
* 当处理更大的数值时,这个问题显得尤为严重。例如大于$2^{53}$的整数无法使用IEEE 754双精度浮点数精确表示,因此在使用浮点数(例如JavaScript)的语言进行分析时,这些数字会变得不准确。 Twitter有一个关于大于$2^{53}$的数字的例子,它使用64位整数来标识每条推文。 Twitter API返回的JSON包含了两种推特ID,一种是JSON数值,另一种是十进制字符串,以避免JavaScript程序无法正确解析数字的问题【10】。
|
||||
* JSON和XML对Unicode字符串(即人类可读的文本)有很好的支持,但是它们不支持二进制数据(即不带 **字符编码(character encoding)** 的字节序列)。二进制串是很有用的功能,人们通过使用Base64将二进制数据编码为文本来绕过此限制。其特有的模式通常标识着这个值应当被解释为Base64编码的二进制数据。这种方案虽然管用,但比较Hacky,并且会增加三分之一的数据大小。
|
||||
* JSON和XML对Unicode字符串(即人类可读的文本)有很好的支持,但是它们不支持二进制数据(即不带 **字符编码(character encoding)** 的字节序列)。二进制串是很有用的功能,人们通过使用Base64将二进制数据编码为文本来绕过此限制。其特有的模式标识着这个值应当被解释为Base64编码的二进制数据。这种方案虽然管用,但比较Hacky,并且会增加三分之一的数据大小。
|
||||
* XML 【11】和 JSON 【12】都有可选的模式支持。这些模式语言相当强大,所以学习和实现起来都相当复杂。 XML模式的使用相当普遍,但许多基于JSON的工具才不会去折腾模式。对数据的正确解读(例如区分数值与二进制串)取决于模式中的信息,因此不使用XML/JSON模式的应用程序可能需要对相应的编码/解码逻辑进行硬编码。
|
||||
* CSV没有任何模式,因此每行和每列的含义完全由应用程序自行定义。如果应用程序变更添加了新的行或列,那么这种变更必须通过手工处理。 CSV也是一个相当模糊的格式(如果一个值包含逗号或换行符,会发生什么?)。尽管其转义规则已经被正式指定【13】,但并不是所有的解析器都正确的实现了标准。
|
||||
|
||||
@ -88,7 +88,7 @@ JSON,XML和CSV属于文本格式,因此具有人类可读性(尽管它们
|
||||
|
||||
对于仅在组织内部使用的数据,使用最小公约数式的编码格式压力较小。例如,可以选择更紧凑或更快的解析格式。虽然对小数据集来说,收益可以忽略不计;但一旦达到TB级别,数据格式的选型就会产生巨大的影响。
|
||||
|
||||
JSON比XML简洁,但与二进制格式相比还是太占空间。这一事实导致大量二进制编码版本JSON & XML的出现,JSON(MessagePack,BSON,BJSON,UBJSON,BISON和Smile等)(例如WBXML和Fast Infoset)。这些格式已经在各种各样的领域中采用,但是没有一个能像文本版JSON和XML那样被广泛采用。
|
||||
JSON比XML简洁,但与二进制格式相比还是太占空间。这一事实导致大量二进制编码版本JSON(MessagePack,BSON,BJSON,UBJSON,BISON和Smile等) 和 XML(例如WBXML和Fast Infoset)的出现。这些格式已经在各种各样的领域中采用,但是没有一个能像文本版JSON和XML那样被广泛采用。
|
||||
|
||||
这些格式中的一些扩展了一组数据类型(例如,区分整数和浮点数,或者增加对二进制字符串的支持),另一方面,它们没有改变JSON / XML的数据模型。特别是由于它们没有规定模式,所以它们需要在编码数据中包含所有的对象字段名称。也就是说,在[例4-1]()中的JSON文档的二进制编码中,需要在某处包含字符串`userName`,`favoriteNumber`和`interest`。
|
||||
|
||||
@ -105,7 +105,7 @@ JSON比XML简洁,但与二进制格式相比还是太占空间。这一事实
|
||||
我们来看一个MessagePack的例子,它是一个JSON的二进制编码。图4-1显示了如果使用MessagePack 【14】对[例4-1]()中的JSON文档进行编码,则得到的字节序列。前几个字节如下:
|
||||
|
||||
1. 第一个字节`0x83`表示接下来是**3**个字段(低四位= `0x03`)的**对象 object**(高四位= `0x80`)。 (如果想知道如果一个对象有15个以上的字段会发生什么情况,字段的数量塞不进4个bit里,那么它会用另一个不同的类型标识符,字段的数量被编码两个或四个字节)。
|
||||
2. 第二个字节`0xa8`表示接下来是**8**字节长的字符串(最高四位= 0x08)。
|
||||
2. 第二个字节`0xa8`表示接下来是**8**字节长(低四位=0x08)的字符串(高四位= 0x0a)。
|
||||
3. 接下来八个字节是ASCII字符串形式的字段名称`userName`。由于之前已经指明长度,不需要任何标记来标识字符串的结束位置(或者任何转义)。
|
||||
4. 接下来的七个字节对前缀为`0xa6`的六个字母的字符串值`Martin`进行编码,依此类推。
|
||||
|
||||
@ -148,7 +148,7 @@ Thrift和Protocol Buffers每一个都带有一个代码生成工具,它采用
|
||||
|
||||
**图4-2 使用Thrift二进制协议编码的记录**
|
||||
|
||||
[^iii]: 实际上,Thrift有三种二进制协议:CompactProtocol和DenseProtocol,尽管DenseProtocol只支持C ++实现,所以不算作跨语言[18]。 除此之外,它还有两种不同的基于JSON的编码格式【19】。 真逗!
|
||||
[^iii]: 实际上,Thrift有三种二进制协议:BinaryProtocol、CompactProtocol和DenseProtocol,尽管DenseProtocol只支持C ++实现,所以不算作跨语言[18]。 除此之外,它还有两种不同的基于JSON的编码格式【19】。 真逗!
|
||||
|
||||
与[图4-1](Img/fig4-1.png)类似,每个字段都有一个类型注释(用于指示它是一个字符串,整数,列表等),还可以根据需要指定长度(字符串的长度,列表中的项目数) 。出现在数据中的字符串`(“Martin”, “daydreaming”, “hacking”)`也被编码为ASCII(或者说,UTF-8),与之前类似。
|
||||
|
||||
@ -176,17 +176,17 @@ Thrift CompactProtocol编码在语义上等同于BinaryProtocol,但是如[图4
|
||||
|
||||
您可以添加新的字段到架构,只要您给每个字段一个新的标签号码。如果旧的代码(不知道你添加的新的标签号码)试图读取新代码写入的数据,包括一个新的字段,其标签号码不能识别,它可以简单地忽略该字段。数据类型注释允许解析器确定需要跳过的字节数。这保持了前向兼容性:旧代码可以读取由新代码编写的记录。
|
||||
|
||||
向后兼容性呢?只要每个字段都有一个唯一的标签号码,新的代码总是可以读取旧的数据,因为标签号码仍然具有相同的含义。唯一的细节是,如果你添加一个新的领域,你不能要求。如果您要添加一个字段并将其设置为必需,那么如果新代码读取旧代码写入的数据,则该检查将失败,因为旧代码不会写入您添加的新字段。因此,为了保持向后兼容性,在模式的初始部署之后 **添加的每个字段必须是可选的或具有默认值**。
|
||||
向后兼容性呢?只要每个字段都有一个唯一的标签号码,新的代码总是可以读取旧的数据,因为标签号码仍然具有相同的含义。唯一的细节是,如果你添加一个新的字段,你不能设置为必需。如果您要添加一个字段并将其设置为必需,那么如果新代码读取旧代码写入的数据,则该检查将失败,因为旧代码不会写入您添加的新字段。因此,为了保持向后兼容性,在模式的初始部署之后 **添加的每个字段必须是可选的或具有默认值**。
|
||||
|
||||
删除一个字段就像添加一个字段,倒退和向前兼容性问题相反。这意味着您只能删除一个可选的字段(必填字段永远不能删除),而且您不能再次使用相同的标签号码(因为您可能仍然有数据写在包含旧标签号码的地方,而该字段必须被新代码忽略)。
|
||||
删除一个字段就像添加一个字段,只是这回要考虑的是向前兼容性。这意味着您只能删除一个可选的字段(必需字段永远不能删除),而且您不能再次使用相同的标签号码(因为您可能仍然有数据写在包含旧标签号码的地方,而该字段必须被新代码忽略)。
|
||||
|
||||
#### 数据类型和模式演变
|
||||
|
||||
如何改变字段的数据类型?这可能是可能的——检查文件的细节——但是有一个风险,值将失去精度或被破坏。例如,假设你将一个32位的整数变成一个64位的整数。新代码可以轻松读取旧代码写入的数据,因为解析器可以用零填充任何缺失的位。但是,如果旧代码读取由新代码写入的数据,则旧代码仍使用32位变量来保存该值。如果解码的64位值不适合32位,则它将被截断。
|
||||
如何改变字段的数据类型?这也许是可能的——详细信息请查阅相关的文档——但是有一个风险,值将失去精度或被截断。例如,假设你将一个32位的整数变成一个64位的整数。新代码可以轻松读取旧代码写入的数据,因为解析器可以用零填充任何缺失的位。但是,如果旧代码读取由新代码写入的数据,则旧代码仍使用32位变量来保存该值。如果解码的64位值不适合32位,则它将被截断。
|
||||
|
||||
Protobuf的一个奇怪的细节是,它没有列表或数组数据类型,而是有一个字段的重复标记(这是第三个选项旁边必要和可选)。如[图4-4](img/fig4-4.png)所示,重复字段的编码正如它所说的那样:同一个字段标记只是简单地出现在记录中。这具有很好的效果,可以将可选(单值)字段更改为重复(多值)字段。读取旧数据的新代码会看到一个包含零个或一个元素的列表(取决于该字段是否存在)。读取新数据的旧代码只能看到列表的最后一个元素。
|
||||
Protobuf的一个奇怪的细节是,它没有列表或数组数据类型,而是有一个字段的重复标记(这是除必需和可选之外的第三个选项)。如[图4-4](img/fig4-4.png)所示,重复字段的编码正如它所说的那样:同一个字段标记只是简单地出现在记录中。这具有很好的效果,可以将可选(单值)字段更改为重复(多值)字段。读取旧数据的新代码会看到一个包含零个或一个元素的列表(取决于该字段是否存在)。读取新数据的旧代码只能看到列表的最后一个元素。
|
||||
|
||||
Thrift有一个专用的列表数据类型,它使用列表元素的数据类型进行参数化。这不允许Protocol Buffers所做的从单值到多值的相同演变,但是它具有支持嵌套列表的优点。
|
||||
Thrift有一个专用的列表数据类型,它使用列表元素的数据类型进行参数化。这不允许Protocol Buffers所做的从单值到多值的演变,但是它具有支持嵌套列表的优点。
|
||||
|
||||
### Avro
|
||||
|
||||
@ -218,7 +218,7 @@ record Person {
|
||||
}
|
||||
```
|
||||
|
||||
首先,请注意架构中没有标签号码。 如果我们使用这个模式编码我们的例子记录([例4-1]()),Avro二进制编码只有32个字节长,这是我们所见过的所有编码中最紧凑的。 编码字节序列的分解如[图4-5](img/fig4-5.png)所示。
|
||||
首先,请注意模式中没有标签号码。 如果我们使用这个模式编码我们的例子记录([例4-1]()),Avro二进制编码只有32个字节长,这是我们所见过的所有编码中最紧凑的。 编码字节序列的分解如[图4-5](img/fig4-5.png)所示。
|
||||
|
||||
如果您检查字节序列,您可以看到没有什么可以识别字段或其数据类型。 编码只是由连在一起的值组成。 一个字符串只是一个长度前缀,后跟UTF-8字节,但是在被包含的数据中没有任何内容告诉你它是一个字符串。 它可以是一个整数,也可以是其他的整数。 整数使用可变长度编码(与Thrift的CompactProtocol相同)进行编码。
|
||||
|
||||
@ -226,19 +226,19 @@ record Person {
|
||||
|
||||
**图4-5 使用Avro编码的记录**
|
||||
|
||||
为了解析二进制数据,您按照它们出现在架构中的顺序遍历这些字段,并使用架构来告诉您每个字段的数据类型。这意味着如果读取数据的代码使用与写入数据的代码完全相同的模式,则只能正确解码二进制数据。读者和作者之间的模式不匹配意味着错误地解码数据。
|
||||
为了解析二进制数据,您按照它们出现在模式中的顺序遍历这些字段,并使用模式来告诉您每个字段的数据类型。这意味着如果读取数据的代码使用与写入数据的代码完全相同的模式,才能正确解码二进制数据。Reader和Writer之间的模式不匹配意味着错误地解码数据。
|
||||
|
||||
那么,Avro如何支持模式演变呢?
|
||||
|
||||
#### 作者模式与读者模式
|
||||
#### Writer模式与Reader模式
|
||||
|
||||
有了Avro,当应用程序想要编码一些数据(将其写入文件或数据库,通过网络发送等)时,它使用它知道的任何版本的模式编码数据,例如,架构可能被编译到应用程序中。这被称为作者的模式。
|
||||
有了Avro,当应用程序想要编码一些数据(将其写入文件或数据库,通过网络发送等)时,它使用它知道的任何版本的模式编码数据,例如,模式可能被编译到应用程序中。这被称为Writer模式。
|
||||
|
||||
当一个应用程序想要解码一些数据(从一个文件或数据库读取数据,从网络接收数据等)时,它希望数据在某个模式中,这就是读者的模式。这是应用程序代码所依赖的模式,在应用程序的构建过程中,代码可能是从该模式生成的。
|
||||
当一个应用程序想要解码一些数据(从一个文件或数据库读取数据,从网络接收数据等)时,它希望数据在某个模式中,这就是Reader模式。这是应用程序代码所依赖的模式,在应用程序的构建过程中,代码可能已经从该模式生成。
|
||||
|
||||
Avro的关键思想是作者的模式和读者的模式不必是相同的 - 他们只需要兼容。当数据解码(读取)时,Avro库通过并排查看作者的模式和读者的模式并将数据从作者的模式转换到读者的模式来解决差异。 Avro规范【20】确切地定义了这种解析的工作原理,如[图4-6](img/fig4-6.png)所示。
|
||||
Avro的关键思想是Writer模式和Reader模式不必是相同的 - 他们只需要兼容。当数据解码(读取)时,Avro库通过并排查看Writer模式和Reader模式并将数据从Writer模式转换到Reader模式来解决差异。 Avro规范【20】确切地定义了这种解析的工作原理,如[图4-6](img/fig4-6.png)所示。
|
||||
|
||||
例如,如果作者的模式和读者的模式的字段顺序不同,这是没有问题的,因为模式解析通过字段名匹配字段。如果读取数据的代码遇到出现在作者模式中但不在读者模式中的字段,则忽略它。如果读取数据的代码需要某个字段,但是作者的模式不包含该名称的字段,则使用在读者模式中声明的默认值填充。
|
||||
例如,如果Writer模式和Reader模式的字段顺序不同,这是没有问题的,因为模式解析通过字段名匹配字段。如果读取数据的代码遇到出现在Writer模式中但不在Reader模式中的字段,则忽略它。如果读取数据的代码需要某个字段,但是Writer模式不包含该名称的字段,则使用在Reader模式中声明的默认值填充。
|
||||
|
||||
![](img/fig4-6.png)
|
||||
|
||||
@ -246,34 +246,35 @@ Avro的关键思想是作者的模式和读者的模式不必是相同的 - 他
|
||||
|
||||
#### 模式演变规则
|
||||
|
||||
使用Avro,向前兼容性意味着您可以将新版本的架构作为编写器,并将旧版本的架构作为读者。相反,向后兼容意味着你可以有一个作为读者的新版本的模式和作为作者的旧版本。
|
||||
使用Avro,向前兼容性意味着您可以将新版本的模式作为Writer,并将旧版本的模式作为Reader。相反,向后兼容意味着你可以有一个作为Reader的新版本模式和作为Writer的旧版本模式。
|
||||
|
||||
为了保持兼容性,您只能添加或删除具有默认值的字段。 (我们的Avro模式中的字段`favourNumber`的默认值为`null`)。例如,假设您添加一个默认值的字段,所以这个新的字段存在于新的模式中,而不是旧的。当使用新模式的阅读器读取使用旧模式写入的记录时,将为缺少的字段填充默认值。
|
||||
为了保持兼容性,您只能添加或删除具有默认值的字段。 (我们的Avro模式中的字段`favourNumber`的默认值为`null`)。例如,假设您添加了一个有默认值的字段,这个新的字段将存在于新模式而不是旧模式中。当使用新模式的Reader读取使用旧模式写入的记录时,将为缺少的字段填充默认值。
|
||||
|
||||
如果你要添加一个没有默认值的字段,新的阅读器将无法读取旧作者写的数据,所以你会破坏向后兼容性。如果您要删除没有默认值的字段,旧的阅读器将无法读取新作者写入的数据,因此您会打破兼容性。在一些编程语言中,null是任何变量可以接受的默认值,但在Avro中并不是这样:如果要允许一个字段为`null`,则必须使用联合类型。例如,`union {null,long,string}`字段;表示该字段可以是数字或字符串,也可以是`null`。如果它是union的分支之一,那么只能使用null作为默认值[^iv]。这比默认情况下可以为`null`是更加冗长的,但是通过明确什么可以和不可以是什么,有助于防止错误的`null` 【22】。
|
||||
如果你要添加一个没有默认值的字段,新的Reader将无法读取旧Writer写的数据,所以你会破坏向后兼容性。如果您要删除没有默认值的字段,旧的Reader将无法读取新Writer写入的数据,因此您会打破向前兼容性。在一些编程语言中,null是任何变量可以接受的默认值,但在Avro中并不是这样:如果要允许一个字段为`null`,则必须使用联合类型。例如,`union {null,long,string} field;`表示field可以是数字或字符串,也可以是`null`。如果要将null作为默认值,则它必须是union的分支之一[^iv]。这样的写法比默认情况下就允许任何变量是`null`显得更加冗长,但是通过明确什么可以和什么不可以是`null`,有助于防止出错【22】。
|
||||
|
||||
[^iv]: 确切地说,默认值必须是联合的第一个分支的类型,尽管这是Avro的特定限制,而不是联合类型的一般特征。
|
||||
|
||||
因此,Avro没有像Protocol Buffers和Thrift那样的`optional`和`required`标记(它有联合类型和默认值)。
|
||||
因此,Avro没有像Protocol Buffers和Thrift那样的`optional`和`required`标记(但它有联合类型和默认值)。
|
||||
|
||||
只要Avro可以转换类型,就可以改变字段的数据类型。更改字段的名称是可能的,但有点棘手:读者的模式可以包含字段名称的别名,所以它可以匹配旧作家的模式字段名称与别名。这意味着更改字段名称是向后兼容的,但不能向前兼容。同样,向联合类型添加分支也是向后兼容的,但不能向前兼容。
|
||||
只要Avro可以支持相应的类型转换,就可以改变字段的数据类型。更改字段的名称也是可能的,但有点棘手:Reader模式可以包含字段名称的别名,所以它可以匹配旧Writer的模式字段名称与别名。这意味着更改字段名称是向后兼容的,但不能向前兼容。同样,向联合类型添加分支也是向后兼容的,但不能向前兼容。
|
||||
|
||||
##### 但作者模式到底是什么?
|
||||
##### 但Writer模式到底是什么?
|
||||
|
||||
到目前为止,我们一直跳过了一个重要的问题:对于一段特定的编码数据,Reader如何知道其Writer模式?我们不能只将整个模式包括在每个记录中,因为模式可能比编码的数据大得多,从而使二进制编码节省的所有空间都是徒劳的。
|
||||
|
||||
到目前为止,我们已经讨论了一个重要的问题:读者如何知道作者的模式是哪一部分数据被编码的?我们不能只将整个模式包括在每个记录中,因为模式可能比编码的数据大得多,从而使二进制编码节省的所有空间都是徒劳的。
|
||||
答案取决于Avro使用的上下文。举几个例子:
|
||||
|
||||
* 有很多记录的大文件
|
||||
|
||||
Avro的一个常见用途 - 尤其是在Hadoop环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码。 (我们将在[第10章](ch10.md)讨论这种情况。)在这种情况下,该文件的作者可以在文件的开头只包含一次作者的模式。 Avro指定一个文件格式(对象容器文件)来做到这一点。
|
||||
Avro的一个常见用途 - 尤其是在Hadoop环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码。 (我们将在[第10章](ch10.md)讨论这种情况。)在这种情况下,该文件的作者可以在文件的开头只包含一次Writer模式。 Avro指定了一个文件格式(对象容器文件)来做到这一点。
|
||||
|
||||
* 支持独立写入的记录的数据库
|
||||
|
||||
在一个数据库中,不同的记录可能会在不同的时间点使用不同的作者的模式编写 - 你不能假定所有的记录都有相同的模式。最简单的解决方案是在每个编码记录的开始处包含一个版本号,并在数据库中保留一个模式版本列表。读者可以获取记录,提取版本号,然后从数据库中获取该版本号的作者模式。使用该作者的模式,它可以解码记录的其余部分。 (例如Espresso 【23】就是这样工作的。)
|
||||
在一个数据库中,不同的记录可能会在不同的时间点使用不同的Writer模式来写入 - 你不能假定所有的记录都有相同的模式。最简单的解决方案是在每个编码记录的开始处包含一个版本号,并在数据库中保留一个模式版本列表。Reader可以获取记录,提取版本号,然后从数据库中获取该版本号的Writer模式。使用该Writer模式,它可以解码记录的其余部分。(例如Espresso 【23】就是这样工作的。)
|
||||
|
||||
* 通过网络连接发送记录
|
||||
|
||||
当两个进程通过双向网络连接进行通信时,他们可以在连接设置上协商模式版本,然后在连接的生命周期中使用该模式。 Avro RPC协议(参阅“[通过服务的数据流:REST和RPC](#通过服务的数据流:REST和RPC)”)如此工作。
|
||||
当两个进程通过双向网络连接进行通信时,他们可以在连接设置上协商模式版本,然后在连接的生命周期中使用该模式。 Avro RPC协议(参阅“[通过服务的数据流:REST和RPC](#通过服务的数据流:REST和RPC)”)就是这样工作的。
|
||||
|
||||
具有模式版本的数据库在任何情况下都是非常有用的,因为它充当文档并为您提供了检查模式兼容性的机会【24】。作为版本号,你可以使用一个简单的递增整数,或者你可以使用模式的散列。
|
||||
|
||||
@ -283,49 +284,50 @@ Avro的关键思想是作者的模式和读者的模式不必是相同的 - 他
|
||||
|
||||
不同之处在于Avro对动态生成的模式更友善。例如,假如你有一个关系数据库,你想要把它的内容转储到一个文件中,并且你想使用二进制格式来避免前面提到的文本格式(JSON,CSV,SQL)的问题。如果你使用Avro,你可以很容易地从关系模式生成一个Avro模式(在我们之前看到的JSON表示中),并使用该模式对数据库内容进行编码,并将其全部转储到Avro对象容器文件【25】中。您为每个数据库表生成一个记录模式,每个列成为该记录中的一个字段。数据库中的列名称映射到Avro中的字段名称。
|
||||
|
||||
现在,如果数据库模式发生变化(例如,一个表中添加了一列,删除了一列),则可以从更新的数据库模式生成新的Avro模式,并在新的Avro模式中导出数据。数据导出过程不需要注意模式的改变 - 每次运行时都可以简单地进行模式转换。任何读取新数据文件的人都会看到记录的字段已经改变,但是由于字段是通过名字来标识的,所以更新的作者的模式仍然可以与旧的读者模式匹配。
|
||||
现在,如果数据库模式发生变化(例如,一个表中添加了一列,删除了一列),则可以从更新的数据库模式生成新的Avro模式,并在新的Avro模式中导出数据。数据导出过程不需要注意模式的改变 - 每次运行时都可以简单地进行模式转换。任何读取新数据文件的人都会看到记录的字段已经改变,但是由于字段是通过名字来标识的,所以更新的Writer模式仍然可以与旧的Reader模式匹配。
|
||||
|
||||
相比之下,如果您为此使用Thrift或Protocol Buffers,则字段标记可能必须手动分配:每次数据库模式更改时,管理员都必须手动更新从数据库列名到字段标签。 (这可能会自动化,但模式生成器必须非常小心,不要分配以前使用的字段标记。)这种动态生成的模式根本不是Thrift或Protocol Buffers的设计目标,而是为Avro。
|
||||
相比之下,如果您为此使用Thrift或Protocol Buffers,则字段标签可能必须手动分配:每次数据库模式更改时,管理员都必须手动更新从数据库列名到字段标签的映射。(这可能会自动化,但模式生成器必须非常小心,不要分配以前使用的字段标签。)这种动态生成的模式根本不是Thrift或Protocol Buffers的设计目标,而是Avro的。
|
||||
|
||||
#### 代码生成和动态类型的语言
|
||||
|
||||
Thrift和Protobuf依赖于代码生成:在定义了模式之后,可以使用您选择的编程语言生成实现此模式的代码。这在Java,C ++或C#等静态类型语言中很有用,因为它允许将高效的内存中结构用于解码的数据,并且在编写访问数据结构的程序时允许在IDE中进行类型检查和自动完成。
|
||||
|
||||
在动态类型编程语言(如JavaScript,Ruby或Python)中,生成代码没有太多意义,因为没有编译时类型检查器来满足。代码生成在这些语言中经常被忽视,因为它们避免了明确的编译步骤。而且,对于动态生成的模式(例如从数据库表生成的Avro模式),代码生成对获取数据是一个不必要的障碍。
|
||||
在动态类型编程语言(如JavaScript,Ruby或Python)中,生成代码没有太多意义,因为没有编译时类型检查器来满足。代码生成在这些语言中经常被忽视,因为它们避免了显示的编译步骤。而且,对于动态生成的模式(例如从数据库表生成的Avro模式),代码生成对获取数据是一个不必要的障碍。
|
||||
|
||||
Avro为静态类型编程语言提供了可选的代码生成功能,但是它也可以在不生成任何代码的情况下使用。如果你有一个对象容器文件(它嵌入了作者的模式),你可以简单地使用Avro库打开它,并以与查看JSON文件相同的方式查看数据。该文件是自描述的,因为它包含所有必要的元数据。
|
||||
Avro为静态类型编程语言提供了可选的代码生成功能,但是它也可以在不生成任何代码的情况下使用。如果你有一个对象容器文件(它嵌入了Writer模式),你可以简单地使用Avro库打开它,并以与查看JSON文件相同的方式查看数据。该文件是自描述的,因为它包含所有必要的元数据。
|
||||
|
||||
这个属性特别适用于动态类型的数据处理语言如Apache Pig 【26】。在Pig中,您可以打开一些Avro文件,开始分析它们,并编写派生数据集以Avro格式输出文件,而无需考虑模式。
|
||||
|
||||
### 模式的优点
|
||||
|
||||
正如我们所看到的,Protocol Buffers,Thrift和Avro都使用模式来描述二进制编码格式。他们的模式语言比XML模式或者JSON模式简单得多,它支持更详细的验证规则(例如,“该字段的字符串值必须与该正则表达式匹配”或“该字段的整数值必须在0和100之间“)。由于Protocol Buffers,Thrift和Avro实现起来更简单,使用起来也更简单,所以它们已经发展到支持相当广泛的编程语言。
|
||||
正如我们所看到的,Protocol Buffers,Thrift和Avro都使用模式来描述二进制编码格式。他们的模式语言比XML模式或者JSON模式简单得多,而后者支持更详细的验证规则(例如,“该字段的字符串值必须与该正则表达式匹配”或“该字段的整数值必须在0和100之间“)。由于Protocol Buffers,Thrift和Avro实现起来更简单,使用起来也更简单,所以它们已经发展到支持相当广泛的编程语言。
|
||||
|
||||
这些编码所基于的想法绝不是新的。例如,它们与ASN.1有很多相似之处,它是1984年首次被标准化的模式定义语言【27】。它被用来定义各种网络协议,其二进制编码(DER)仍然被用于编码SSL证书(X.509),例如【28】。 ASN.1支持使用标签号码的模式演进,类似于Protocol Buffers和Thrift 【29】。然而,这也是非常复杂和严重的文件记录,所以ASN.1可能不是新应用程序的好选择。
|
||||
这些编码所基于的想法绝不是新的。例如,它们与ASN.1有很多相似之处,它是1984年首次被标准化的模式定义语言【27】。它被用来定义各种网络协议,例如其二进制编码(DER)仍然被用于编码SSL证书(X.509)【28】。 ASN.1支持使用标签号码的模式演进,类似于Protocol Buffers和Thrift 【29】。然而,它也非常复杂,而且没有好的配套文档,所以ASN.1可能不是新应用程序的好选择。
|
||||
|
||||
许多数据系统也为其数据实现某种专有的二进制编码。例如,大多数关系数据库都有一个网络协议,您可以通过该协议向数据库发送查询并获取响应。这些协议通常特定于特定的数据库,并且数据库供应商提供将来自数据库的网络协议的响应解码为内存数据结构的驱动程序(例如使用ODBC或JDBC API)。
|
||||
许多数据系统也为其数据实现了某种专有的二进制编码。例如,大多数关系数据库都有一个网络协议,您可以通过该协议向数据库发送查询并获取响应。这些协议通常特定于特定的数据库,并且数据库供应商提供将来自数据库的网络协议的响应解码为内存数据结构的驱动程序(例如使用ODBC或JDBC API)。
|
||||
|
||||
所以,我们可以看到,尽管JSON,XML和CSV等文本数据格式非常普遍,但基于模式的二进制编码也是一个可行的选择。他们有一些很好的属性:
|
||||
|
||||
* 它们可以比各种“二进制JSON”变体更紧凑,因为它们可以省略编码数据中的字段名称。
|
||||
* 模式是一种有价值的文档形式,因为模式是解码所必需的,所以可以确定它是最新的(而手动维护的文档可能很容易偏离现实)。
|
||||
* 保留模式数据库允许您在部署任何内容之前检查模式更改的向前和向后兼容性。
|
||||
* 维护一个模式的数据库允许您在部署任何内容之前检查模式更改的向前和向后兼容性。
|
||||
* 对于静态类型编程语言的用户来说,从模式生成代码的能力是有用的,因为它可以在编译时进行类型检查。
|
||||
|
||||
总而言之,模式进化允许与JSON数据库提供的无模式/模式读取相同的灵活性(请参阅第39页的“文档模型中的模式灵活性”),同时还可以更好地保证数据和更好的工具。
|
||||
总而言之,模式进化允许与JSON数据库提供的无模式/读时模式相同的灵活性(请参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”),同时还可以更好地保证数据和更好的工具。
|
||||
|
||||
|
||||
|
||||
## 数据流的类型
|
||||
|
||||
在本章的开始部分,我们曾经说过,无论何时您想要将某些数据发送到不共享内存的另一个进程,例如,只要您想通过网络发送数据或将其写入文件,就需要将它编码为一个字节序列。然后我们讨论了做这个的各种不同的编码。
|
||||
|
||||
我们讨论了向前和向后的兼容性,这对于可演化性来说非常重要(通过允许您独立升级系统的不同部分,而不必一次改变所有内容,可以轻松地进行更改)。兼容性是编码数据的一个进程和解码它的另一个进程之间的一种关系。
|
||||
|
||||
这是一个相当抽象的概念 - 数据可以通过多种方式从一个流程流向另一个流程。谁编码数据,谁解码?在本章的其余部分中,我们将探讨数据如何在流程之间流动的一些最常见的方式:
|
||||
|
||||
* 通过数据库(参阅“[通过数据库的数据流](#通过数据库的数据流)”)
|
||||
* 通过服务调用(参阅“[通过服务传输数据流:REST和RPC](#通过服务传输数据流:REST和RPC)”)
|
||||
* 通过异步消息传递(参阅“[消息传递数据流](#消息传递数据流)”)
|
||||
* 通过数据库(参阅“[数据库中的数据流](#数据库中的数据流)”)
|
||||
* 通过服务调用(参阅“[服务中的数据流:REST和RPC](#服务中的数据流:REST和RPC)”)
|
||||
* 通过异步消息传递(参阅“[消息传递中的数据流](#消息传递中的数据流)”)
|
||||
|
||||
|
||||
|
||||
@ -335,15 +337,13 @@ Avro为静态类型编程语言提供了可选的代码生成功能,但是它
|
||||
|
||||
向后兼容性显然是必要的。否则你未来的自己将无法解码你以前写的东西。
|
||||
|
||||
一般来说,几个不同的进程同时访问数据库是很常见的。这些进程可能是几个不同的应用程序或服务,或者它们可能只是几个相同服务的实例(为了可伸缩性或容错性而并行运行)。无论哪种方式,在应用程序发生变化的环境中,访问数据库的某些进程可能会运行较新的代码,有些进程可能会运行较旧的代码,例如,因为新版本当前正在部署在滚动升级,所以有些实例已经更新,而其他实例尚未更新。
|
||||
一般来说,几个不同的进程同时访问数据库是很常见的。这些进程可能是几个不同的应用程序或服务,或者它们可能只是几个相同服务的实例(为了可伸缩性或容错性而并行运行)。无论哪种方式,在应用程序发生变化的环境中,访问数据库的某些进程可能会运行较新的代码,有些进程可能会运行较旧的代码,例如,因为新版本当前正在部署滚动升级,所以有些实例已经更新,而其他实例尚未更新。
|
||||
|
||||
这意味着数据库中的一个值可能会被更新版本的代码写入,然后被仍旧运行的旧版本的代码读取。因此,数据库也经常需要向前兼容。
|
||||
|
||||
但是,还有一个额外的障碍。假设您将一个字段添加到记录模式,并且较新的代码将该新字段的值写入数据库。随后,旧版本的代码(尚不知道新字段)将读取记录,更新记录并将其写回。在这种情况下,理想的行为通常是旧代码保持新的领域完整,即使它不能被解释。
|
||||
但是,还有一个额外的障碍。假设您将一个字段添加到记录模式,并且较新的代码将该新字段的值写入数据库。随后,旧版本的代码(尚不知道新字段)将读取记录,更新记录并将其写回。在这种情况下,理想的行为通常是旧代码保持新的字段不变,即使它不能被解释。
|
||||
|
||||
前面讨论的编码格式支持未知域的保存,但是有时候需要在应用程序层面保持谨慎,如图4-7所示。例如,如果将数据库值解码为应用程序中的模型对象,稍后重新编码这些模型对象,那么未知字段可能会在该翻译过程中丢失。
|
||||
|
||||
解决这个问题不是一个难题,你只需要意识到它。
|
||||
前面讨论的编码格式支持未知字段的保存,但是有时候需要在应用程序层面保持谨慎,如图4-7所示。例如,如果将数据库值解码为应用程序中的模型对象,稍后重新编码这些模型对象,那么未知字段可能会在该翻译过程中丢失。解决这个问题不是一个难题,你只需要意识到它。
|
||||
|
||||
![](img/fig4-7.png)
|
||||
|
||||
@ -355,21 +355,21 @@ Avro为静态类型编程语言提供了可选的代码生成功能,但是它
|
||||
|
||||
在部署应用程序的新版本时,也许用不了几分钟就可以将所有的旧版本替换为新版本(至少服务器端应用程序是这样的)。但数据库内容并非如此:对于五年前的数据来说,除非对其进行显式重写,否则它仍然会以原始编码形式存在。这种现象有时被概括为:数据的生命周期超出代码的生命周期。
|
||||
|
||||
将数据重写(迁移)到一个新的模式当然是可能的,但是在一个大数据集上执行是一个昂贵的事情,所以大多数数据库如果可能的话就避免它。大多数关系数据库都允许简单的模式更改,例如添加一个默认值为空的新列,而不重写现有数据[^v]读取旧行时,数据库将填充编码数据中缺少的任何列的空值在磁盘上。 LinkedIn的文档数据库Espresso使用Avro存储,允许它使用Avro的模式演变规则【23】。
|
||||
将数据重写(迁移)到一个新的模式当然是可能的,但是在一个大数据集上执行是一个昂贵的事情,所以大多数数据库如果可能的话就避免它。大多数关系数据库都允许简单的模式更改,例如添加一个默认值为空的新列,而不重写现有数据[^v]读取旧行时,对于磁盘上的编码数据缺少的任何列,数据库将填充空值。 LinkedIn的文档数据库Espresso使用Avro存储,允许它使用Avro的模式演变规则【23】。
|
||||
|
||||
因此,架构演变允许整个数据库看起来好像是用单个模式编码的,即使底层存储可能包含用模式的各种历史版本编码的记录。
|
||||
因此,模式演变允许整个数据库看起来好像是用单个模式编码的,即使底层存储可能包含用各种历史版本的模式编码的记录。
|
||||
|
||||
[^v]: 除了MySQL,即使并非真的必要,它也经常会重写整个表,正如“[文档模型中的架构灵活性](ch3.md#文档模型中的灵活性)”中所提到的。
|
||||
[^v]: 除了MySQL,即使并非真的必要,它也经常会重写整个表,正如“[文档模型中的模式灵活性](ch3.md#文档模型中的模式灵活性)”中所提到的。
|
||||
|
||||
|
||||
|
||||
#### 归档存储
|
||||
|
||||
也许您不时为数据库创建一个快照,例如备份或加载到数据仓库(参阅“[数据仓库](ch3.md#数据仓库)”)。在这种情况下,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新模式进行编码。既然你正在复制数据,那么你可能会一直对数据的副本进行编码。
|
||||
也许您不时为数据库创建一个快照,例如备份或加载到数据仓库(参阅“[数据仓库](ch3.md#数据仓库)”)。在这种情况下,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新模式进行编码。既然你不管怎样都要拷贝数据,那么你可以对这个数据拷贝进行一致的编码。
|
||||
|
||||
由于数据转储是一次写入的,而且以后是不可变的,所以Avro对象容器文件等格式非常适合。这也是一个很好的机会,可以将数据编码为面向分析的列式格式,例如Parquet(请参阅第97页的“[列压缩](ch3.md#列压缩)”)。
|
||||
由于数据转储是一次写入的,而且以后是不可变的,所以Avro对象容器文件等格式非常适合。这也是一个很好的机会,可以将数据编码为面向分析的列式格式,例如Parquet(请参阅“[列压缩](ch3.md#列压缩)”)。
|
||||
|
||||
在[第10章](ch10.md)中,我们将详细讨论在档案存储中使用数据。
|
||||
在[第10章](ch10.md)中,我们将详细讨论使用档案存储中的数据。
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user