zoukankan      html  css  js  c++  java
  • 第四章

    第四章 编码与演化

    当数据格式(format)或模式(schema)发生变化时,通常需要对应用程序代码进行相应的更改(例如,为记录添加新字段,然后修改程序开始读写该字段)。但在大型应用程序中,代码变更通常不会立即完成:

    • 对于 服务端(server-side) 应用程序,可能需要执行 滚动升级 (rolling upgrade) (也称为 阶段发布(staged rollout) ),一次将新版本部署到少数几个节点,检查新版本是否运行正常,然后逐渐部完所有的节点。这样无需中断服务即可部署新版本,为频繁发布提供了可行性,从而带来更好的可演化性。
    • 对于 客户端(client-side) 应用程序,升不升级就要看用户的心情了。用户可能相当长一段时间里都不会去升级软件。

    这意味着,新旧版本的代码,以及新旧数据格式可能会在系统中同时共处。系统想要继续顺利运行,就需要保持双向兼容性:

    • 向后兼容 (backward compatibility)
      新代码可以读旧数据。
    • 向前兼容 (forward compatibility)
      旧代码可以读新数据。

    向后兼容性通常并不难实现:新代码的作者当然知道由旧代码使用的数据格式,因此可以显示地处理它(最简单的办法是,保留旧代码即可读取旧数据)。

    向前兼容性可能会更棘手,因为旧版的程序需要忽略新版数据格式中新增的部分。

    数据编码格式

    从内存中表示到字节序列的转换称为 编码(Encoding) (也称为序列化(serialization)或编组(marshalling)),反过来称为解码(Decoding)[^ii](解析(Parsing),反序列化(deserialization),反编组( unmarshalling))

    语言特定的格式

    许多编程语言都内建了将内存对象编码为字节序列的支持。但是

    • 这类编码通常与特定的编程语言深度绑定,其他语言很难读取这种数据。
    • 为了恢复相同对象类型的数据,解码过程需要实例化任意类的能力,这通常是安全问题的一个来源
    • 在这些库中,数据版本控制通常是事后才考虑的。因为它们旨在快速简便地对数据进行编码,所以往往忽略了前向后向兼容性带来的麻烦问题。
    • 效率(编码或解码所花费的CPU时间,以及编码结构的大小)往往也是事后才考虑的。 例如,Java的内置序列化由于其糟糕的性能和臃肿的编码而臭名昭着。

    JSON,XML和二进制变体

    JSON,XML和CSV是文本格式,因此具有人类可读性,但是也有一些问题

    • 数字的编码多有歧义之处。
      • XML和CSV不能区分数字和字符串(除非引用外部模式)。
      • JSON虽然区分字符串和数字,但不区分整数和浮点数,而且不能指定精度。
      • 大于253的整数不能在IEEE 754双精度浮点数中精确表示,因此在使用浮点数(例如JavaScript)的语言进行分析时,这些数字会变得不准确。
    • JSON和XML对Unicode字符串(即人类可读的文本)有很好的支持,但是它们不支持二进制数据(不带字符编码(character encoding)的字节序列)。
    • CSV没有任何模式,因此应用程序需要定义每行和每列的含义。

    尽管存在这些缺陷,但JSON,XML和CSV已经足够用于很多目的。让不同的组织达成一致的难度超过了其他大多数问题。

    二进制编码

    JSON比XML简洁,但与二进制格式一比,还是太占地方。
    大量二进制编码版本JSON & XML的出现,JSON(MessagePack,BSON,BJSON,UBJSON,BISON和Smile等)(例如WBXML和Fast Infoset)。这些格式已经被各种各样的领域所采用,但是没有一个像JSON和XML的文本版本那样被广泛采用。
    例子

    {
        "userName": "Martin",
        "favoriteNumber": 1337,
        "interests": ["daydreaming", "hacking"]
    }

    它们没有盖面JSON / XML的数据模型。特别是由于它们没有规定模式,所以它们需要在编码数据中包含所有的对象字段名称。
    二进制编码长度为66个字节,仅略小于文本JSON编码所取的81个字节

    Thrift与Protocol Buffers

    Thrift的BinaryProtocol格式:

    使用这种格式的编码只需要59个字节
    每个字段都有一个类型注释(用于指示它是一个字符串,整数,列表等),还可以根据需要指定长度(字符串的长度,列表中的项目数) 。
    最大的区别是没有字段名(userName, favoriteNumber, interest)。相反,编码数据包含字段标签,它们是数字(1, 2和3)。这些是模式定义中出现的数字。字段标记就像字段的别名 - 它们是说我们正在谈论的字段的一种紧凑的方式,而不必拼出字段名称。

    Thrift的CompactProtocol格式:

    它只将相同的信息打包成只有34个字节。它通过将字段类型和标签号打包到单个字节中,并使用可变长度整数来实现。数字1337不是使用全部八个字节,而是用两个字节编码,

    Protocol Buffers编码

    它的打包方式稍有不同,但与Thrift的CompactProtocol非常相似。 Protobuf将同样的记录塞进了33个字节中。

    需要注意的一个细节:在前面所示的模式中,每个字段被标记为必需或可选,但是这对字段如何编码没有任何影响(二进制数据中没有任何字段指示是否需要字段)。所不同的是,如果未设置该字段,则所需的运行时检查将失败,这对于捕获错误非常有用。

    字段标签和模式演变

    • 向前兼容(旧代码可以读新数据):
      您可以添加新的字段到架构,只要您给每个字段一个新的标签号码。如果旧的代码(不知道你添加的新的标签号码)试图读取新代码写入的数据,包括一个新的字段,其标签号码不能识别,它可以简单地忽略该字段。数据类型注释允许解析器确定需要跳过的字节数。这保持了前向兼容性:旧代码可以读取由新代码编写的记录。
    • 向后兼容(新代码可以读旧数据):
      只要每个字段都有一个唯一的标签号码,新的代码总是可以读取旧的数据,因为标签号码仍然具有相同的含义。

    注意:

    • 如果您要添加一个字段并将其设置为必需,那么如果新代码读取旧代码写入的数据,则该检查将失败,因为旧代码不会写入您添加的新字段。因此,为了保持向后兼容性,在模式的初始部署之后 添加的每个字段必须是可选的或具有默认值。
    • 删除一个字段就像添加一个字段,倒退和向前兼容性问题相反。这意味着您只能删除一个可选的字段(必填字段永远不能删除),而且您不能再次使用相同的标签号码(因为您可能仍然有数据写在包含旧标签号码的地方,而该字段必须被新代码忽略)。

    数据类型和模式演变

    将一个32位的整数变成一个64位的整数:
    新代码可以轻松读取旧代码写入的数据,因为解析器可以用零填充任何缺失的位。
    如果旧代码读取由新代码写入的数据,则旧代码仍使用32位变量来保存该值。如果解码的64位值不适合32位,则它将被截断。

    将可选(单值)字段更改为重复(多值)字段:
    Protobuf中:读取旧数据的新代码会看到一个包含零个或一个元素的列表(取决于该字段是否存在)。读取新数据的旧代码只能看到列表的最后一个元素。
    Thrift有一个专用的列表数据类型,它使用列表元素的数据类型进行参数化。这不允许Protocol Buffers所做的从单值到多值的相同演变,但是它具有支持嵌套列表的优点。【什么叫参数化??】

    Avro

    Apache Avro是另一种二进制编码格式。
    Avro也使用模式来指定正在编码的数据的结构。 它有两种模式语言:一种(Avro IDL)用于人工编辑,一种(基于JSON),更易于机器读取。
    Avro二进制编码只有32个字节长,这是我们所见过的所有编码中最紧凑的。

    没有什么可以识别字段或其数据类型。 编码只是由连在一起的值组成。 一个字符串只是一个长度前缀,后跟UTF-8字节,但是在被包含的数据中没有任何内容告诉你它是一个字符串。 它可以是一个整数,也可以是其他的整数。 整数使用可变长度编码(与Thrift的CompactProtocol相同)进行编码。
    为了解析二进制数据,您按照它们出现在架构中的顺序遍历这些字段,并使用架构来告诉您每个字段的数据类型。这意味着如果读取数据的代码使用与写入数据的代码完全相同的模式,则只能正确解码二进制数据。阅读器和作者之间的模式不匹配意味着错误地解码数据。

    作者模式与读者模式

    当应用程序想要编码一些数据(将其写入文件或数据库,通过网络发送等)时,它使用它知道的任何版本的模式编码数据,例如,架构可能被编译到应用程序中。这被称为作者的模式。

    当一个应用程序想要解码一些数据(从一个文件或数据库读取数据,从网络接收数据等)时,它希望数据在某个模式中,这就是读者的模式。这是应用程序代码所依赖的模式,在应用程序的构建过程中,代码可能是从该模式生成的。

    如果作者的模式和读者的模式的字段顺序不同,这是没有问题的,因为模式解析通过字段名匹配字段。如果读取数据的代码遇到出现在作者模式中但不在读者模式中的字段,则忽略它。如果读取数据的代码需要某个字段,但是作者的模式不包含该名称的字段,则使用在读者模式中声明的默认值填充。

    模式演变规则

    使用Avro,向前兼容性意味着您可以将新版本的架构作为编写器,并将旧版本的架构作为读者。相反,向后兼容意味着你可以有一个作为读者的新版本的模式和作为作者的旧版本。
    为了保持兼容性,您只能添加或删除具有默认值的字段。

    作者模式到底是什么?

    Avro使用的环境:

    • 有很多记录的大文件
    • 支持独立写入的记录的数据库
    • 通过网络连接发送记录
      具有模式版本的数据库在任何情况下都是非常有用的,因为它充当文档并为您提供了检查模式兼容性的机会。作为版本号,你可以使用一个简单的递增整数,或者你可以使用模式的散列。

    动态生成的模式

    Avro方法的一个优点是架构不包含任何标签号码。
    如果数据库模式发生变化(例如,一个表中添加了一列,删除了一列),则可以从更新的数据库模式生成新的Avro模式,并在新的Avro模式中导出数据。数据导出过程不需要注意模式的改变 - 每次运行时都可以简单地进行模式转换。
    如果您为此使用Thrift或Protocol Buffers,则字段标记可能必须手动分配:每次数据库模式更改时,管理员都必须手动更新从数据库列名到字段标签。

    代码生成和动态类型的语言

    Thrift和Protobuf依赖于代码生成:在定义了模式之后,可以使用您选择的编程语言生成实现此模式的代码。这在Java,C ++或C#等静态类型语言中很有用

    在动态类型编程语言(如JavaScript,Ruby或Python)中,生成代码没有太多意义,因为没有编译时类型检查器来满足。代码生成在这些语言中经常被忽视,因为它们避免了明确的编译步骤。

    Avro为静态类型编程语言提供了可选的代码生成功能,但是它也可以在不生成任何代码的情况下使用。这个属性特别适用于动态类型的数据处理语言如Apache Pig。

    模式的优点

    由于Protocol Buffers,Thrift和Avro实现起来更简单,使用起来也更简单,所以它们已经发展到支持相当广泛的编程语言。
    尽管JSON,XML和CSV等文本数据格式非常普遍,但基于模式的二进制编码也是一个可行的选择。他们有一些很好的属性:

    • 它们可以比各种“二进制JSON”变体更紧凑,因为它们可以省略编码数据中的字段名称。
    • 模式是一种有价值的文档形式,因为模式是解码所必需的,所以可以确定它是最新的(而手动维护的文档可能很容易偏离现实)。
    • 保留模式数据库允许您在部署任何内容之前检查模式更改的向前和向后兼容性。
    • 对于静态类型编程语言的用户来说,从模式生成代码的能力是有用的,因为它可以在编译时进行类型检查。

    数据流的类型

    数据可以通过多种方式从一个流程流向另一个流程。
    我们将探讨数据如何在流程之间流动的一些最常见的方式:

    • 通过数据库
    • 通过服务调用
    • 通过异步消息传递

    数据库中的数据流

    假设您将一个字段添加到记录模式,并且较新的代码将该新字段的值写入数据库。随后,旧版本的代码(尚不知道新字段)将读取记录,更新记录并将其写回。
    在这种情况下,理想的行为通常是旧代码保持新的领域完整,即使它不能被解释。
    但是,如果将数据库值解码为应用程序中的模型对象,稍后重新编码这些模型对象,那么未知字段可能会在该翻译过程中丢失。

    在不同的时间写入不同的值

    架构演变允许整个数据库看起来好像是用单个模式编码的,即使底层存储可能包含用模式的各种历史版本编码的记录。

    归档存储

    也许您不时为数据库创建一个快照,例如备份或加载到数据仓库(参阅“数据仓库”)。在这种情况下,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新模式进行编码。既然你正在复制数据,那么你可能会一直对数据的副本进行编码。

    服务中的数据流:REST与RPC

    最常见的安排是有两个角色:客户端和服务器。服务器通过网络公开API,并且客户端可以连接到服务器以向该API发出请求。服务器公开的API被称为服务。
    此外,服务器本身可以是另一个服务的客户端(例如,典型的Web应用服务器充当数据库的客户端)。这种方法通常用于将大型应用程序按照功能区域分解为较小的服务,这样当一个服务需要来自另一个服务的某些功能或数据时,就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为 面向服务的体系结构(service-oriented architecture,SOA) ,最近被改进和更名为 微服务架构 。
    面向服务/微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。

    Web服务

    Web服务不仅在Web上使用,而且在几个不同的环境中使用。例如:

    1. 运行在用户设备上的客户端应用程序(例如,移动设备上的本地应用程序,或使用Ajax的JavaScript web应用程序)通过HTTP向服务发出请求。这些请求通常通过公共互联网进行。
    2. 一种服务向同一组织拥有的另一项服务提出请求,这些服务通常位于同一数据中心内,作为面向服务/微型架构的一部分。 (支持这种用例的软件有时被称为 中间件(middleware)
    3. 一种服务通过互联网向不同组织所拥有的服务提出请求。这用于不同组织后端系统之间的数据交换。此类别包括由在线服务(如信用卡处理系统)提供的公共API,或用于共享访问用户数据的OAuth。

    有两种流行的Web服务方法:REST和SOAP。
    REST不是一个协议,而是一个基于HTTP原则的设计哲学。它强调简单的数据格式,使用URL来标识资源,并使用HTTP功能进行缓存控制,身份验证和内容类型协商。
    SOAP是用于制作网络API请求的基于XML的协议。虽然它最常用于HTTP,但其目的是独立于HTTP,并避免使用大多数HTTP功能。客户端可以使用本地类和方法调用(编码为XML消息并由框架再次解码)访问远程服务。这在静态类型编程语言中非常有用,但在动态类型编程语言中很少。
    SOAP缺点:

    • 而且由于SOAP消息通常是手动构建的过于复杂,所以SOAP的用户在很大程度上依赖于工具支持,代码生成和IDE。对于SOAP供应商不支持的编程语言的用户来说,与SOAP服务的集成是困难的。
    • 尽管SOAP及其各种扩展表面上是标准化的,但是不同厂商的实现之间的互操作性往往会造成问题。由于所有这些原因,尽管许多大型企业仍然使用SOAP,但在大多数小公司中已经不再受到青睐。

    远程过程调用(RPC)的问题

    RPC模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。
    尽管RPC起初看起来很方便,但这种方法根本上是有缺陷的。
    网络请求与本地函数调用非常不同:

    • 本地函数调用是可预测的,并且成功或失败,这仅取决于受您控制的参数。网络请求是不可预知的。
    • 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。
    • 如果您重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。重试将导致该操作被执行多次,除非您在协议中引入除重( 幂等(idempotence))机制。
    • 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的。
    • 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。对于较大的对象很快就会变成问题。
    • 客户端和服务可以用不同的编程语言实现,这在数据类型转换上可能会出现问题。

    RPC的当前方向

    • Finagle和Rest.li 使用futures(promises)来封装可能失败的异步操作。
    • Futures还可以简化需要并行发出多项服务的情况,并将其结果合并。
    • gRPC支持流,其中一个调用不仅包括一个请求和一个响应,还包括一系列的请求和响应。
    • 其中一些框架还提供服务发现,即允许客户端找出在哪个IP地址和端口号上可以找到特定的服务。

    使用二进制编码格式的自定义RPC协议可以实现比通用的JSON over REST更好的性能。
    但是,RESTful API还有其他一些显著的优点:对于实验和调试(只需使用Web浏览器或命令行工具curl,无需任何代码生成或软件安装即可向其请求),它是受支持的所有的主流编程语言和平台,还有大量可用的工具(服务器,缓存,负载平衡器,代理,防火墙,监控,调试工具,测试工具等)的生态系统。由于这些原因,REST似乎是公共API的主要风格。
    RPC框架的主要重点在于同一组织拥有的服务之间的请求,通常在同一数据中心内。

    数据编码与RPC的演化

    • 由于RPC经常被用于跨越组织边界的通信,所以服务的兼容性变得更加困难,因此服务的提供者经常无法控制其客户,也不能强迫他们升级。因此,需要长期保持兼容性,也许是无限期的。如果需要进行兼容性更改,则服务提供商通常会并排维护多个版本的服务API。
    • 对于RESTful API,常用的方法是在URL或HTTP Accept头中使用版本号。对于使用API密钥来标识特定客户端的服务,另一种选择是将客户端请求的API版本存储在服务器上,并允许通过单独的管理界面更新该版本选项

    消息传递中的数据流

    RPC和数据库之间的异步消息传递系统。它们与RPC类似,因为客户端的请求(通常称为消息)以低延迟传送到另一个进程。它们与数据库类似,不是通过直接的网络连接发送消息,而是通过称为消息代理(也称为消息队列或面向消息的中间件)的中介来临时存储消息。
    与直接RPC相比,使用消息代理有几个优点:

    • 如果收件人不可用或过载,可以充当缓冲区,从而提高系统的可靠性。
    • 它可以自动将消息重新发送到已经崩溃的进程,从而防止消息丢失。
    • 避免发件人需要知道收件人的IP地址和端口号(这在虚拟机经常出入的云部署中特别有用)。
    • 它允许将一条消息发送给多个收件人。
    • 将发件人与收件人逻辑分离(发件人只是发布邮件,不关心使用者)。

    差异在于:

    • 消息传递通信通常是单向的:发送者通常不期望收到其消息的回复。
    • 一个进程可能发送一个响应,但这通常是在一个单独的通道上完成的。
    • 这种通信模式是异步的:发送者不会等待消息被传递,而只是发送它,然后忘记它。

    消息掮客

    消息代理通常不会执行任何特定的数据模型 - 消息只是包含一些元数据的字节序列,因此您可以使用任何编码格式。如果编码是向后兼容的,则您可以灵活地更改发行商和消费者的独立编码,并以任意顺序进行部署。

    分布式的Actor框架

    Actor模型是单个进程中并发的编程模型。

  • 相关阅读:
    leetcode------Palindrome Number
    leetcode------Minimum Depth of Binary Tree
    leetcode------Binary Tree Level Order Traversal II
    leetcode------Plus One
    leetcode------Plus One
    leetcode------Min Stack
    leetcode------Binary Tree Level Order Traversal
    递归树与非递归树的不同实现【转载,个人感觉写的比较好的一篇,值得去思考】
    leetcode------Compare Version Numbers
    leetcode------Majority Element
  • 原文地址:https://www.cnblogs.com/aojun/p/15193551.html
Copyright © 2011-2022 走看看