网站首页 > 文章精选 正文
程序通常使用(至少)两种不同的表示形式处理数据:
- 在内存中,数据保存在对象,结构,列表,数组,哈希表,树等中。对这些数据结构进行了优化,以实现CPU的有效访问和操纵(通常使用指针)。
- 当您要将数据写入文件或通过网络发送数据时,必须将其编码为某种独立的字节序列(例如JSON文档)。由于指针对其他任何进程都没有意义,因此这种字节顺序表示看起来与内存中通常使用的数据结构完全不同。
因此,我们需要在两种表示形式之间进行某种转换。从内存中的表示形式到字节序列的转换称为编码(也称为序列化或编组),而相反的称为解码(解析,反序列化,解组)
由于这是一个常见问题,因此有许多不同的库和编码格式可供选择。让我们做一个简短的概述。
语言特定的格式
许多编程语言都带有内置支持,可将内存中的对象编码为字节序列。例如,Java具有java.io.Serializable,Ruby具有Marshal,Python具有pickle,依此类推。还存在许多第三方库,例如Kryo for Java。
这些编码库非常方便,因此他们允许内存中的对象,可以用最少的额外代码实现存储和恢复。然而他们也有一些严重的问题:
- 通常编码是绑定于一个特殊的编程语言的,通过另一种语言来读取数据困难的。如果你想以这样的编码存储或传输数据,则可能会使您长时间使用当前的编程语言,并且会阻止将您的系统与其他组织(可能使用不同语言)的系统集成。
- 为了恢复相同对象类型的数据,解码过程需要能够实例化任意类。这通常是安全问题的根源:如果攻击者可以拿到您的应用程序从而解码任意字节序列,则他们可以实例化任意类,这反过来通常使他们可以执行可怕的事情,例如远程执行任意代码。
- 在这些库中,对数据进行版本控制通常是事后的想法:由于它们旨在快速而轻松地对数据进行编码,因此它们常常忽略了向前和向后兼容性的不便之处。
- 效率(编码或解码所花费的CPU时间以及编码结构的大小)通常也是事后的想法。例如,Java的内置序列化因其糟糕的性能和过大的编码而臭名昭著。
由于这些原因,除了非常短暂的目的,使用你的当前编程语言的内置编码通常是个坏主意。
JSOM,XML以及二进制变量
转向可以由许多编程语言编写和读取的标准化编码时,JSON和XML是显而易见的竞争者。它们广为人知,得到广泛支持,并且几乎也同样不那么受欢迎。 XML经常因过于冗长和不必要的复杂性而受到批评。 JSON的流行主要是由于它在Web浏览器中的内置支持(由于是JavaScript的子集)以及相对于XML的简单性。 CSV是另一种与语言无关的流行格式,尽管功能较弱。
JSON,XML和CSV是文本格式,因此在某种程度上是人可读的(尽管其语法是一个争论的热门话题)。除了表面的句法问题外,它们还存在一些细微的问题:
- 关于数字编码有很多歧义。在XML和CSV中,您无法区分数字和恰好由数字组成的字符串(除非引用外部模式)。JSON区分字符串和数字,但不区分整数和浮点数,并且不指定精度。在处理庞大数字时,这是一个问题。例如,大于2^53的整数不能在IEEE 754双精度浮点数中精确表示,因此当使用使用浮点数的语言(例如JavaScript)进行解析时,此类数字将变得不准确。大于2^53的数字示例发生在Twitter上,该示例使用64位数字标识每个推文。 Twitter API返回的JSON包含两次推特ID,一次是JSON数字,一次是十进制字符串,以解决JavaScript应用程序未正确解析数字的事实。
- JSON和XML对Unicode字符串(即,人类可读的文本)有很好的支持,但它们不支持二进制字符串(无字符编码的字节序列)。二进制字符串是一个有用的功能,因此人们可以通过使用Base64将二进制数据编码为文本来解决此限制。然后使用模式指示该值应解释为Base64编码。这是可行的,但有点黑科技,数据量大小增加了33%。
- XML和JSON都具有可选的模式schema支持。这些模式语言非常强大,因此学习和实现起来非常复杂。 XML模式的使用相当普遍,但是许多基于JSON的工具都不会使用模式。由于对数据(例如数字和二进制字符串)的正确解释取决于模式中的信息,因此,不使用XML / JSON架构的应用程序可能需要对相应的编码/解码逻辑进行硬编码。
- CSV没有任何模式,因此由应用程序来定义每行和每列的含义。如果应用程序的更改添加了新的行或列,则必须手动处理该更改。 CSV也是一种模糊的格式(如果值包含逗号或换行符,会发生什么?)。尽管已正式指定其转义规则,但并非所有解析器都能正确实现它们。
尽存在上述的问题,但JSOM、XML和CSV对于多数用途来说都已经足够了。它们可能会继续流行,尤其是作为数据交换格式(即,用于将数据从一个组织发送到另一个组织)的情况下。在这种情况下,只要人们对格式是什么达成共识,格式的美观或有效程度通常并不重要。因为让不同的组织就任何事情达成共识的困难超过了其他大多数问题。
二进制编码
对于仅在组织内部使用的数据,使用一种大家都接受的编码格式的压力较小。例如,您可以选择一种更紧凑或更快解析的格式。对于较小的数据集,增益可以忽略不计,但是一旦达到TB,数据格式的选择可能会产生很大的影响。
JSON不如XML冗长,但与二进制格式相比,两者仍然占用大量空间。这也导致大量的二进制编码的开发用于JSON(例如MessagePack,BSON,BJSON,UBJSON,BISON和Smile)和XML(例如,WBXML和Fast Infoset)。这些格式已在各个领域被采用,但没有一个像JSON和XML的文本版本那样被广泛采用。
其中一些格式扩展了数据类型集(例如,区分整数和浮点数,或增加了对二进制字符串的支持),但在其他方面,它们并没有使JSON / XML数据模型发生改变。特别是,由于它们没有规定模式,因此需要在编码数据中包括所有对象字段名称。也就是说,在对下面示例中的JSON文档的二进制编码中,它们将需要在某处包含字符串userName,favoriteNumber和interest。
让我们以MessagePack(一种对JSON的二进制编码)作为例子,上图展示了如果你想用MessagePack编码JSON文档而获得的文档中的字节序列。前几个字节如下:
- 第一个字节,0x83,表示一个对象含有三个字段,高4位0x80表示一个对象,低4位0x30表示有三个字段。(以防万一您想知道如果一个对象具有超过15个字段,以致字段数不能容纳四位,那么我们会得到一个不一样的类型指代,字段的数量会被编码成2个或4个字节)。
- 第二个字节,0xa8,表示紧跟着的是一个字符串(0xa0),并且长度为8个字节(0x08)
- 接下来的八个字节是ASCII码形式的字段名userName,长度之前已经指示了,所以没有必要用任何的标记表明字符串在哪里结束。
- 接下来的七个字节是前缀0xa6,和对六个字母的字符串值Martin的编码,依此类推。
二进制编码一共是66个字节长,比起上图中的文本JSON的81个字节少了一点。所有的对JSON二进制编码方式都与这个类似。但尚不清楚这么小的减少(也许数据解析更快了)是否值得以丢失可读性作为代价。
下面我们会看到更好的方法,编码上图中相同的内容只用了32个字节。
Thrift和Protocol Buffers
Apache Thrift和Protocol Buffers(protobuf)是基于相同原理的二进制编码库。 Protocol Buffers最初是由Google开发的,Thrift最初是由Facebook开发的,两者均已在2007年8月开源。
Thrift和protobuf都要求了数据编码的模式。为了编码之前图中的JSON数据,在Thrift中,您将使用Thrift接口定义语言(IDL)来描述模式,如下所示:
而在protobuf中相等模式定义如下,看上去很像:
Thrift和protocol都会通过一个编码生成工具将上面的模式定义转换成各种不同形式的编程语言的类。你的应用这个通过调用这些生成的代码来对模式中每条内容进行编码或解码。
用这个模式编码的数据是怎么样的呢?令人困惑的是,Thrift有两个不同的二进制编码格式,分别叫做BinaryProtocol和CompactProtocol。让我们首先来看一下BinaryProtocol。这种格式下对于之前文本JSON的编码大小是59个字节。如下图所示:
与Message相似,每个字段都有一个类型注释(指示它是否是字符串,整数,列表等),并在需要时提供长度指示(字符串的长度,列表中的项目数) 。类似于之前,出现在数据中的字符串(“马丁”,“白日梦”,“黑客”)也被编码为ASCII(或更确切地说是UTF-8)。
与Message相比,最大的不同是没有字段名称(userName,favoriteNumber,interest)。而是,编码的数据包含字段标记,这些字段标记是数字(1,2和3)。这些是出现在模式定义中的数字。字段标签就像字段的别名一样,它们是一种紧凑的方式,可以表明我们要的字段,而不必拼写出字段名称。
Thrift CompactProtocol编码在语义上等效于BinaryProtocol,但是如下图所示,它将相同的信息打包为34个字节。它通过将字段类型和标记号打包到一个字节中并使用可变长度的整数来做到这一点。它没有使用完整的八个字节来表示数字1337,而是将其编码为两个字节,每个字节的最高位用于指示是否还有更多字节要来。这意味着–64和63之间的数字被编码为一个字节,–8192和8191之间的数字被编码为两个字节,依此类推。更大的数字使用更多的字节。
protobuf,只有一种二进制编码形式,如下图所示,它对bit的打包稍微有点不同,但在其他方面与Thrift's CompactProtocol很类似。protobuf编码上述JSON文本的大小也是33个字节。
这里需要注意的一个细节是:在前面显示的模式中,每个字段都标记为required或optional,但这与字段的编码方式没有区别(二进制数据中没有任何内容表明是否需要一个字段)。区别仅在于,required启用了运行时检查,如果未设置该字段,则运行检查失败,这对于捕获错误很有用。
字段的标签和模式的演化
模式不可避免地需要随着时间而改变。我们称这种模式为进化。 那么Thrift和Protocol Buffer如何处理架构更改,同时保持向后和向前的兼容性?
从上面的例子中可以看到,编码记录只是其编码字段的串联。每个字段均由其标签号(示例模式中的数字1、2、3)标识,并以数据类型(例如字符串或整数)进行注释。如果未设置字段值,则将其从编码记录中省略。由此可见,字段标签对于编码数据的意义至关重要。您可以更改模式中的字段名称,因为编码数据永远不会引用字段名称,但是您无法更改字段的标记,因为这会使所有现有的编码数据无效。
您可以向模式中添加新字段,前提是您要给每个字段一个新的标签号。如果旧代码(不知道您添加的新标签号)尝试读取由新代码写入的数据,包括带有标签号的新字段,无法识别,只需要简单的忽略该字段。数据类型批注允许解析器确定需要跳过多少字节。这样可以保持向前兼容性:旧代码可以读取由新代码编写的记录。
向后兼容性呢?只要每个字段都有唯一的标签号,新代码就可以始终读取旧数据,因为标签号仍然具有相同的含义。唯一的细节是,如果添加新字段,则不能将其设为required字段。如果要添加字段并将其设为required字段,则如果新代码读取了由旧代码编写的数据,则该检查将失败,因为旧代码不会写入您添加的新字段。因此,为了保持向后兼容性,在初始部署模式后添加的每个字段都必须是optional的或具有默认值。
删除字段就像添加字段一样。这意味着您只能删除一个option字段(required字段永远不能删除),并且永远不能再使用相同的标签号(因为您可能仍然在包含旧标签号的地方写入了该数据,并且该字段必须由新代码忽略)。
数据类型和模式的演化
如何更改字段的数据类型?这是有可能的(请查阅相关文档以获取详细信息),但存在值丢失精度或被截断的风险。例如,假设您将32位整数更改为64位整数。新代码可以轻松读取旧代码写入的数据,因为解析器可以用零填充任何丢失的位。但是,如果旧代码读取由新代码写入的数据,则旧代码仍将使用32位变量来保存该值。如果解码后的64位值不能容纳32位,则会被截断。
protobuf的一个令人好奇的细节是它没有列表或数组数据类型,而是具有repeated的字段标记(这是除了required和optional的第三个选项)。如您在之前的图中所见,repeated字段的编码是:同一个字段标签在记录中只是多次出现。这样可以产生很好的效果,可以将可选(单值)字段更改为重复(多值)字段。读取旧数据的新代码将看到一个包含零个或一个元素的列表(取决于该字段是否存在);读取新数据的旧代码只能看到列表的最后一个元素。
Thrift具有专用的列表数据类型,使用列表元素的数据类型进行参数化。这不允许像protobuf那样从单值到多值的演变,但是它具有支持嵌套列表的优点。
Avro
Apache Avro是另一种二进制编码格式,与protobuf和Thrift不同。它从2009年作为Hadoop的子项目开始,原因是Thrift不适合Hadoop的用例。
Avro也使用一种模式来指定要编码的数据的结构。它有两种模式语言:一种(Avro IDL)用于人工编辑,另一种(基于JSON)更易于机器阅读。
根据之前JSON文本的内容,根据Avro IDL,如下所示:
该模式的等效JSON表示如下:
首先,请注意,模式中没有标签号。如果我们使用这种模式对之前JSON文本进行编码,Avro二进制编码只有32个字节长,这是我们所看到的所有编码中最紧凑的一种。编码字节序列的细分如下图所示。
如果检查字节序列,可以看到没有什么可标识字段或其数据类型的。编码仅由串联在一起的值组成。字符串只是一个长度前缀,后跟UTF-8字节,但是编码数据中没有任何内容告诉您这是一个字符串。它也可以是整数,也可以是其他整数。整数使用可变长度编码(与Thrift的CompactProtocol相同)进行编码。
要解析二进制数据,你需要按照字段在模式中出现的顺序浏览各个字段,然后使用该模式告诉您每个字段的数据类型。这意味着,仅当读取数据的代码使用与写入数据的代码完全相同的模式时,才能正确解码二进制数据。读取器和写入器之间的模式中的任何不匹配都将意味着解码数据不正确。
那么,Avro如何支持模式演进?
写入器模式和读取器模式
使用Avro,当应用程序想要对某些数据进行编码(将其写入文件或数据库,通过网络发送等)时,它将使用其知道的任何模式版本对数据进行编码,例如,模式可以编译到应用程序中。这就是写模式。
当应用程序要解码某些数据(从文件或数据库读取数据,从网络接收数据等)时,它期望数据处于某种模式下,这称为读取器模式。这就是应用程序代码所依赖的模式-代码可能是在应用程序的构建过程中从该模式生成的。
Avro的关键思想是写入器模式和读取器模式不必相同,它们只需要兼容即可。解码(读取)数据时,Avro库通过并排查看写入器模式和读取器模式并将数据从写入器模式转换为读取器模式来解决差异。 Avro规范精确定义了该分辨率的工作原理,如下图所示。
例如,如果写入器模式和读取器模式的字段顺序不同,这没问题,因为模式解析会按字段名称匹配字段。如果读取数据的代码遇到的字段出现在写入器的模式中,但没有出现在读取器的模式中,则将其忽略。如果读取数据的代码需要某个字段,但是写入器的模式中不包含该名称的字段,则使用在读取器的模式中声明的默认值填充该字段。
模式演化的规则
使用Avro,向前兼容性意味着您可以将模式的新版本用作写入器,而将旧版本的模式用作读取器。相反,向后兼容意味着您可以将模式的新版本作为读取器,并将旧版本作为写入器。
为了保持兼容性,您只能添加或删除具有默认值的字段。 (在我们的Avro模式中,favoriteNumber的默认值为null。)。例如,假设您添加了一个带有默认值的字段,因此此新字段存在于新模式中,而不存在于旧模式中。当使用新模式的读取器读取使用旧模式写入的记录时,将为缺少的字段填充默认值。
如果您要添加一个没有默认值的字段,那么新的读取器将无法读取由旧的写入器写入的数据,因此会破坏向后兼容性。如果要删除没有默认值的字段,则旧的读取器将无法读取新写入器写入的数据,因此会破坏兼容性。
在某些编程语言中,null是任何变量的可接受默认值,但Avro并非如此:如果要允许字段为null,则必须使用联合类型。例如,union {null,long,string}字段;表示字段可以是数字,字符串或null。你只能在null之union的一个分支的情况下,使用null。 这比默认情况下使所有内容都可以为空更为冗长,但是它通过明确可以为空和不能为空的内容来帮助防止错误。
因此,Avro没有与Protocol Buffers和Thrift相同的optional标记和required标记(而是具有union类型和默认值)。
只要Avro可以转换类型,就可以更改字段的数据类型。更改字段的名称是可能的,但是有些棘手:读取器的模式可以包含字段名称的别名,因此可以将旧写入器的模式字段名称与别名进行匹配。这意味着更改字段名称是向后兼容但不向前兼容。类似地,将分支添加到并集类型是向后兼容的,但不是前向兼容的。
什么是写入者模式?
现在,我们需要解决一个重要的问题:读取器如何知道写入器对特定数据进行编码的模式?我们不能在每条记录中包含整个模式,因为该模式可能比编码数据大得多,从而使二进制编码节省的所有空间都付之一炬。
答案取决于使用Avro的环境。举几个例子:
- 有许多记录的大文件:Avro的常见用途(尤其是在Hadoop上下文中)是用于存储包含数百万条记录的大文件,所有记录均使用相同的模式编码。在这种情况下,文件的写入者只需要在文件开头包含一次写入器的模式即可。 Avro会指定一种文件格式(对象容器文件)来执行此操作。
- 具有不同类型的写入数据的数据库:在数据库中,不同的记录可能在不同的时间点使用不同的写入器的模式进行写入-您不能假设所有记录都具有相同的模式。最简单的解决方案是在每个编码记录的开头都包含一个版本号,并在数据库中保留模式版本的列表。读者可以获取一条记录,提取版本号,然后从数据库中获取该版本号的写入器模式。使用该写入器的模式,它可以解码其余记录。 (例如,Expresso就是这样工作的。)
- 通过网络连接发送记录:当两个进程通过双向网络连接进行通信时,它们可以在连接建立时协商模式版本,然后在连接的生存期内使用该模式。 Avro RPC协议的工作原理就是这样的。
在任何情况下,模式版本数据库都是有用的东西,因为它充当文档,并为您提供了检查模式兼容性的机会。而版本号,可以使用简单的递增整数,也可以使用模式的哈希。
动态生成的模式
与protobuf和Thrift相比,Avro方法的优势之一是该模式不包含任何标签号。但是为什么这很重要?在模式中保留几个数字有什么问题吗?
区别在于,Avro对动态生成的模式更友好。例如,假设您有一个关系数据库,您要将其内容转储到文件中,并且想要使用二进制格式来避免上述文本格式(JSON,CSV,SQL)的问题。如果使用Avro,则可以很容易地从关系模式生成Avro模式(就像我们前面看到的JSON表示),并使用该模式对数据库内容进行编码,然后将其全部转储到Avro对象容器文件中。您为每个数据库表生成一个读取器模式,并且每个列都成为该记录中的一个字段。数据库中的列名称映射为Avro中的字段名称。
现在,如果数据库模式发生了更改(例如,一个表添加了一个列,而删除了一个列),则只需从更新的数据库模式中生成一个新的Avro模式,并将数据导出到新的Avro模式中。数据导出过程不需要对模式更改进行任何关注,它可以在每次运行时简单地进行模式转换。读取新数据文件的任何人都将看到记录的字段已更改,但是由于这些字段是通过名称标识的,因此更新的读取器的模式仍然可以与旧的读取器的模式进行匹配。
相比之下,如果您为此目的使用Thrift或Protocol Buffer,则可能必须手动分配字段标签:每次更改数据库架构时,管理员都必须手动更新从数据库列名到字段标签的映射。 (可能可以自动执行此操作,但是模式生成器必须非常小心,不要分配以前使用的字段标签。)这种动态生成的模式根本不是Thrift或Protocol Buffer的设计目标。
代码生成和动态类型化的语言
Thrift和Protocol Buffer依赖于代码生成:定义了模式后,您可以生成以您选择的编程语言实现该模式的代码。这在Java,C或C等静态类型语言中很有用,因为它允许用高效的内存结构来解码数据,并且在编写访问数据结构时允许在IDE中进行类型检查和自动补全。
在动态类型的编程语言(例如JavaScript,Ruby或Python)中,生成代码没有太多意义,因为没有编译时类型检查器可以满足。这些语言通常不赞成使用代码生成,因为否则,它们干嘛避免显式的编译步骤。此外,对于动态生成的模式(例如从数据库表生成的Avro架构),代码生成不是获取数据的必要障碍。
Avro为静态类型的编程语言提供了可选的代码生成,但是无需任何代码生成,也可以使用它。如果您有一个对象容器文件(嵌入了写入器的模式),则可以使用Avro库打开它,并以与查看JSON文件相同的方式查看数据。该文件是自描述的,因为它包含所有必需的元数据。
与动态类型的数据处理语言(例如Apache Pig)结合使用时,此属性特别有用。在Pig中,您只需打开一些Avro文件,开始分析它们,然后将派生的数据集写成Avro格式的输出文件,甚至无需考虑架构。
模式的优点
如我们所见,Protocol Buffers,Thrift和Avro都使用模式描述二进制编码格式。他们的模式语言比XML Schema或JSON Schema简单得多,后者支持更详细的验证规则(例如,“此字段的字符串值必须匹配此正则表达式”或“此字段的整数值必须在0到100之间” ”)。由于Protocol Buffers,Thrift和Avro更易于实现和使用,因此它们已经发展为支持相当广泛的编程语言。
这些编码所基于的思想绝非新鲜事物。例如,它们与ASN.1有?很多共同点,ASN.1是1984年首次标准化的模式定义语言。它用于定义各种网络协议及其例如,二进制编码(DER)仍用于编码SSL证书(X.509)。 ASN.1支持使用标签号进行模式演化,类似于protobuf和Thrift 。但是,它也非常复杂且文档记录很差,因此ASN.1可能不是新应用程序的理想选择。
许多数据系统还为其数据实现某种专有的二进制编码。例如,大多数关系数据库都具有网络协议,通过该网络协议,您可以将查询发送到数据库并获取响应。这些协议通常是特定于特定数据库的,并且数据库供应商提供了一个驱动程序(例如,使用ODBC或JDBC API),该驱动程序将来自数据库网络协议的响应解码为内存中的数据结构。
因此,我们可以看到,尽管文本数据格式(例如JSON,XML和CSV)已经广泛使用,但基于模式的二进制编码也是可行的选择。它们具有许多不错的属性:
- 它们可以比各种“二进制JSON”变体紧凑得多,因为它们可以从编码数据中省略字段名称。
- 模式是文档的一种宝贵形式,并且由于解码需要模式,因此您可以确保它是最新的(而手动维护的文档可能很容易与实际情况有所出入)
- 通过保留模式数据库,可以在部署任何内容之前检查模式更改的向前和向后兼容性。
- 对于使用静态类型的编程语言的用户,从架构生成代码的功能很有用,因为它可以在编译时进行类型检查。
总而言之,模式演化所提供的灵活性与无模式/无读取模式的JSON文档数据库所提供的灵活性相同,同时还对数据的提供了更好的保证和更好的工具。
猜你喜欢
- 2024-12-31 面试须知:通常都要知道的TCP、HTTP知识点
- 2024-12-31 excel函数——常用的字符串函数(二)
- 2024-12-31 小小的字符串,在PLC编程中不容小觑,到底有何特别 ?
- 2024-12-31 玩转Python—字符串使用教程
- 2024-12-31 vlookup的高阶用法——数据提取,不是很简单,但是很实用
- 2024-12-31 替换函数Substitute,用法大全,值得收藏备用
- 2024-12-31 C++基础算法:统计字符数
- 2024-12-31 Java基础面试:一文看懂String类中的常用方法
- 2024-12-31 老司机归纳-经典SQL语句(二)
- 2024-12-31 32767、8192、255在Excel中这三个数有什么含义?
- 最近发表
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 计算机网络的拓扑结构是指() (45)
- 稳压管的稳压区是工作在什么区 (45)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)