设计数据密集型应用—编码与演化(4)


新产品的推出,对需求的深入理解,或者商业环境的变化,总会伴随着功能(feature)的增增改改。在大多数情况下,修改应用程序的功能也意味着需要更改其存储的数据:可能需要使用新的字段或记录类型,或者以新方式展示现有数据。

当数据格式(format)或模式(schema)发生变化时,通常需要对应用程序进行相应的更改。但在大型应用程序中,代码变更通常不会立即完成:

  • 对于服务端的应用程序,可能需要执行滚动升级(rolling upgrade),一次将新版本部署到少数几个节点,检查新版本是否运行正常,然后逐渐部署完所有的节点。
  • 对于客户端的应用程序,升不升级就要看用户的心情了。

注:系统想要继续顺利运行,就需要保持双线性兼容性。向后兼容,新代码可以读旧数据;向前兼容,旧代码可以读新数据。

1. 编码数据的格式

程序通常(至少)使用两种形式的数据:

  • 在内存中,数据保存在对象,结构体,列表,数组,哈希表,树等中。这些数据结构针对 CPU 的高效访问和操作进行了优化。
  • 如果要将数据写入文件,或通过网络发送,则必须将其编码为某种自包含的字节序列。由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同。

1.1 语言特定的格式

许多编程语言都内建了将内存对象编码为字节序列的支持,可以用很少的额外代码实现内存对象的保存与恢复,但是它们也有一些深层次的问题:

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

注:除非临时使用,采用语言内置编码通常是一个坏主意

1.2 JSON、XML 和 二进制变体

JSON、XML 和 CSV 属于文本格式,因此具有人类可读性,它们存在一些微妙的问题:

  • 数值(numbers)的编码多有歧义之处,XML 和 CSV 不能区分数字和字符串,JSON 虽然区分字符串与数值,但不区分整数和浮点数,而且不能指定精度。
  • JSON 和 XML 对 Unicode 字符串(即人类可读的文本)有很好的支持,但是它们不支持二进制数据。二进制串是很有用的功能,人们通过使用 Base64 将二进制数据编码为文本来绕过此限制。其特有的模式通常标识着这个值应当解释为 Base64 编码的二进制数据。
  • XML 和 JSON 都有可选的模式支持。这些模式语言相当强大,所以学习和实现起来的相当复杂。
  • CSV 没有任何模式,因此每行和每列的含义完全由应用程序自行定义,如果应用程序变更添加了新的行或列,那么这种变更必须通过手工处理。

注:只要人们对格式是什么意见一致,格式要多美观或者效率有多高效就无所谓了。让不同的组织就这些东西达成一致的难度超过了绝大多数问题。

二进制编码

一旦达到 TB 级别,数据格式的选型就会产生巨大的影响。这导致大量二进制编码格式 JSON & XML 的出现,JSON(MessagePack,BSON,BJSON,UBJSON等)

1.3 Thrift 与 Protocol Buffers

Apache Thrift 和 Protocol Buffers 是基于相同原理的二进制编码库。Protocol Buffers 最初是在 Google 开发的,Thrift 最初是在 Facebook 开发的,并且都是在 2007~2008 年开源。二者都需要一个模式来编码数据,比如:

Thrift

struct Person {
    1: required string       userName,
    2: optional i64          favoriteNumber,
    3: optional list<string> interests
}

Protocol Buffers

message Person {
    required string user_name       = 1;
    optional int64  favorite_number = 2;
    repeated string interests       = 3;
}

注:二者均有一个代码生成工具,它采用类似于这里所示的模式定义,并且生成了以各种编程语言实现模式的类。

字段标签和模式演变

模式不可避免地要随着时间而改变。我们称之为模式演变。Thrift 和 Protocol Buffers 如何处理模式更改,同时保持向后兼容性?

字段标记对编码数据的含义至关重要。可以更改架构中字段的名称,因为编码的数据永远不会引用字段名称,但不能更改字段的标记,因为这会使得所有现有的编码数据无效。

  • 添加一个新的字段
    • 旧的代码试图读取新代码写入的数据,包括一个新的字段,其标签号码不能识别,它可以简单地忽略该字段。
    • 新代码总是可以读取旧的数据,因为标签号仍然具有相同的含义。

注:添加的每个字段必须是可选的或具有默认值

  • 删除一个字段,意味着只能删除一个可选的字段,而且不能再次使用相同的标签号码,因为仍可能有数据写在包含旧标签号码的地方,而该字段必须被新代码忽略。

数据类型和模式演变

如何改变字段的数据类型?

答:这是可能的,但是有一个风险,即值将失去精度或被破坏。

1.4 Avro

Avro 是另一种二进制编码格式,与 Protocol Buffers 和 Thrift 不同。它作为 Hadoop 的子项目在 2009 年开始的。因为 Thrift 不适合 Hadoop 的用例。

Avro 也使用模式来指定正在编码的数据的结构。它有两种模式语言:一种是用于人工编辑——Avro IDL,一种更易于机器读取——JSON。

注:为了解析二进制数据,必须按照出现在架构中的顺序遍历这些字段,并使用架构来解析每个字段的数据类型。这意味着读取数据的代码使用与写入数据的代码完全相同的模式,则能正确解码二进制数据。读者和作者之间的模式不匹配意味着解码数据失败。

作者模式与读者模式

Avro 的关键思想是作者的模式和读者的模式不必是相同的 — 他们只需要兼容。当数据解码时,Avro 库通过并排查看作者的模式和读者的模式并将数据从作者的模式转换为读者的模式来解决差异。

模式演变规则

使用 Avro,向前兼容性意味着可以将新版本的架构作为编写器,并将旧版本的架构作为读者。相反,向后兼容意味着可以有一个作为读者的新版本的模式和作为作者的旧版本。

注:为了保持兼容性,只能添加或删除具有默认值的字段。

动态生成的模式

不同之处在于 Avro 对动态生成的模式更友善。例如有一个关系数据库,如果使用 Avro,可以很容易地从关系模式生成一个 Avro 模式,并使用该模式对数据库内容进行编码,将其全部存储到 Avro 对象容器文件中。

如果数据库模式发生变化(比如,一个表中添加一个列,删除一个列),则可以从更新的数据库模式生成新的 Avro 模式,并在新的 Avro 模式中导出数据。数据导出过程不需要注意模式的改变 — 每次运行时都可以简单地进行模式转换。任何读取数据文件的人都会看到记录的字段已经改变,但是由于字段是通过名字来标识的,所以更新的作者的模式仍然可以与旧的读者模式匹配。

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

Avro 为静态类型编程语言提供了可选的代码生成功能,但是它也可以在不生产任何代码的情况下使用。如果你有一个对象容器文件(它嵌入作者的模式),可以简单地使用 Avro 库打开它,并以与查看 JSON 文件相同的方式查看数据。

1.5 模式的优点

Thrift、Protocol Buffers 和 Avro 都使用模式来描述二进制编码格式。他们的模式语言比 XML 或 JSON 模式简单得多,它支持更详细的验证规则。

尽管 JSON、XML 和 CSV 等文本数据格式非常普遍,但基于模式的二进制编码有一些很好的属性:

  • 可以比各种「二进制 JSON」变种更紧凑,因为它们可以省略编码数据中的字段名称
  • 模式是一种有价值的文档形式,因为模式是解码所必需的,所以可以确定它是最新的
  • 保留模式数据库运行在部署任何内容之前检查模式更改的向前和向后兼容性
  • 对于静态类型编程语言的用户来说,从模式生产代码的能力是有用的,因为它可以在编译时进行类型检查

2. 数据流的类型

无论何时想要将数据发送到不共享内存的另一个进程,比如,想要通过网络发送数据或将其写入文件,就需要将它编码为一个字节序列。然后我们讨论了做这个的各种不同的编码。讨论了向前和向后的兼容性,这对可演化性来说非常重要。兼容性是编码数据的一个进程和解码它的另一个进程之间的一种关系。

数据可以通过多种方式从一个流程流向另一个流程。谁编码数据,谁解码数据?流程之间流动的一些最常见的方式:

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

2.1 数据库中的数据流

在数据库中,写入数据的过程对数据进行编码,从数据库读取的过程对数据进行解码。只有一个进程访问的数据库,可以理解为数据库中国的内容存储为向未来的自我发送的消息。

注:向后兼容性显然是必要的。否则未来的自己可能无法解密你以前写的东西。

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

数据库通常允许任何时候更新任何值。这意味着在一个单一的数据库中,可能有一些值是五毫秒前写的,而一些值是五年前写。

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

归档存储

当不时为数据库创建一个快照,例如备份或加载到数据仓库。在这种情况下,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新的模式进行编码。

2.2服务中的数据流:REST 与 RPC

通过网络进行通信时,最常见的是通过客户端和服务器。常见的客户端有 Web 浏览器,除此之外服务器本身也可以是另一个服务的客户端(微服务架构)。

注:面向服务/微服务架构的一个关键设计的目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。

Web 服务
Web 服务不仅在 web 上使用,而且可以在用户设备上的客户端应用程序上。有两种流行的 Web 服务方法:REST 和 SOAP 。

REST 不是一个协议,而是一个基于 HTTP 原则的设计哲学。它强调简单的数据格式,使用 URL 来标识资源,并使用 HTTP 功能进行缓存控制,身份验证和内容协商。与 SOAP 相比,REST 已经越来越受欢迎了。

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

RPC 模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同。但是这种方法根本上是有缺陷的:

  • 本地函数调用要么返回结果,要么抛出异常,或者永远不返回。网络请求有另一个可能的结果:由于超市,它可能会返回没有结果
  • 在重试失败的网络请求,可能会发生请求实际通过,但是响应丢失,在这种情况下,重试将导致操作被多次执行,除非该操作是幂等的。
  • 每次调用本地功能是,通常需要大致相同的时间来执行。网络请求比函数调用慢得多,而且延迟也是非常可变的。
  • 调用本地函数时,可以高效地将引用传递给本地内存中的对象。当发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。对于较大的对象这可能存在问题。

RPC 的当前方向

使用二进制编码格式的自定义 RPC 协议可以实现比通用的 JSON over REST 更好的性能。但是,RESTful API 有一些显著的优点:

  • 便于实验和调试

注:RESTful API 几乎被所有主流的编程语言和平台所支持,并且存在大量现有的工具。基于上述原因,REST 似乎是公共 API 主要的风格。RPC 框架的主要重点在于同一组织拥有的服务之间的请求,通常在同一数据中心内。

数据编码与 RPC 的演化

由于 RPC 经常被用于跨域组织边界通信,所以服务的兼容性变得更加困难,因此服务的提供者经常无法控制其用户,也不能强迫他们升级。因此,需要长期保持兼容性。

2.3 消息传递中的数据流

与直接 RPC 相比,使用消息代理有几个优点:

  • 如果收件人不可用或过载,可以充当缓存区,从而提高系统的可靠性
  • 可以自动将消息重新发送到已经崩溃的进程,从而防止消息丢失
  • 避免发件人需要知道收件人的 IP 地址和端口号
  • 允许将一条消息发送给多个收件人
  • 将收发逻辑分离

注:与 RPC 相比,消息传递通常是单向的:发送者通常不期望收到其消息的回复。

消息代理

消息代理的使用方式如下:

一个进程将消息发送到指定的队列,代理确保将消息传递给一个或多个消费者到那个队列。同一主题上可以有许多生产者和消费者。

分布式的 Actor 框架

Actor 模式是单个进程中并发的编程模式。逻辑被封装在角色中,而不是直接处理现场(以及竞争条件、锁定和死锁的相关问题)。每个角色通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。消息传送不保证:在某些错误情况下,消息将会丢失。由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度。

注:Actor 框架实质上是将消息代理和角色编程模式集成到一个框架中。此编程模型用于跨多个节点伸缩应用程序。不管发送方和接收方是在同一个节点还是在不同的节点,都使用相同的消息传递机制。

3. 碎碎念

貌似最近思考的事情太多了,文章蜕变成了记录的工具,大抵每个人的一生中都会有一段思绪纷繁复杂的时间吧,但是,别气馁啊,要加油!

  • 我跟白英,谁也不是真正的司藤,我们都只是那个叫司藤的一半,大约每个人的心里都有一个矛盾的小人,向东又想向西,拿起又想放下,因为做不到,因为这世上从来就没有不负如来不负卿的双全法,所以要克制、忍耐、内外煎熬。
  • 普通小孩热爱生活中(ps 我一直都在碰壁,在各种事情上跌倒,可是,我绝对没有逃避过,我全都接受。这就是我唯一的骄傲 .

猜你喜欢

转载自blog.csdn.net/phantom_111/article/details/117132975