《Designing Data-intensive Application》读书笔记 – 编码

编码数据的格式


程序中有两种格式的数据,一是内存中的数据对象,如果需要持久化在文件中,或者通过网络发送,就需要转换成字节序列。从数据对象变成字节序列,称为编码(Encoding),反过来称为解码(Decoding)

语言特定的格式

许多语言有内置的实现,比如java的serializable,python的pickle,但是会存在以下问题:

  • 编码与特定语言绑定,无法跨语言
  • 解码过程中的实例话对象,可能会导致安全问题
  • 向前向后兼容的问题
  • 性能低下

JSON, XML, Binary

json/xml等格式,方便人们阅读,但是也会存在各种问题

  • 数字的编码歧义,比如xml无法区分字符串和数字,json无法区分整数和浮点数
  • javascript处理大数时,会缺失精度
  • 仅支持unicode字符,无法支持二进制数据
  • csv没有schema,增删列会比较麻烦

后续出现了一些新的格式,来支持二进制数据,比如BSON、MessagePack等。

Thrift & Protocol Buffers

Thrift和protobuf都需要schema来编码数据,它们可以通过IDL(接口定义语言)来描述schema,同时也提供了工具包来生成指定语言的代码,来进行数据的编码和解码。Thrift有两种编码格式,BinaryProtocol和CompactProtocol。protobuf的方式和CompactProtocol的方式比较类似。

BinaryProtocol

每个字段有一个类型注释(1 byte),在必要的时候还需要指定长度(比如字符串的长度以及list的长度)。为来节省空间,编码后的结果并没有存储字段名,只存储了schema中定义的tag(2 bytes)。

CompactProtocol

将字段类型和tag压缩到单字节中,同时通过varint的方式节省整数的存储空间。CompactProtocol的具体分析可以参见此文档

变长整数以节省空间

向前向后兼容性

  • 编码后的数据强依赖于schema中的tag,而不是字段名。所以schema中的tag不能随意变更
  • 如果新增字段,那么旧代码识别不了新的tag,会自动略过,所以新增字段保证向前的兼容性
  • 但是如果要保证向后的兼容性,让新代码读取旧的数据,新增字段必须设置为optional
  • 如果要删除字段,为了保证兼容性,只能删除optional的字段,且后续不能再使用删除的tag

Avro

Avro的编码相比于CompactProtocol更加紧凑,因为所有的数据类型和Tag信息都存储在schema中,不需要存储在编码后的结果中。所以读取的时候,只需要按照schema的顺序依次读取,就能正确的进行解码。

Avro官方文档描述了其与Thrift/protobuf的不同之处

  • Dynamic Typing:不需要提前生成编码解码的code,因为数据中总会保留着其对应的schema
  • Untagged Data:由于读取数据的时候会提供schema,则数据中不需要存储类型等信息
  • No manually-assigned field IDs:当schema发生变更的时候,新旧schema都会保留,根据fieldName进行映射,因此也不需要提供field id

向前向后兼容性

为了保证兼容性,只能添加或者删除具有默认值的字段

Avro的典型场景

  • 有很多记录的大文件:Avro最常见的用途,就是用于处理hadoop环境的大数据量文件。且这些文件的schema完全一致,只需要每个文件开头保存schema即可。
  • 支持写入的数据库:在数据库中,不同时间点可能会使用不同的schema进行写入。可以在数据库中存储多个版本的schema,并建立记录与版本的映射关系,从而可以在读取的时候获取版本,再根据版本获取schema。比如Espresso
  • 通过网络发送记录:当通过网络传输数据时,可以在连接设置时协商双方使用的schema,然后在连接的生命周期里都使用这个schema。比如Avro RPC

Schema的优势

  • 编码结果会变得更加紧凑
  • 解码的时候必须依赖schema,因为可以保证schema是最新的
  • 保留schema数据库,可以检查向前向后的兼容性
  • 对于静态类型编程语言,schema可以让用户在编译的时候进行类型检查

数据流的类型


数据库中的数据流

  • 当旧版本的应用代码,更新新版本代码生成的数据时,可能会造成数据丢失
更新导致的数据丢失
  • 在不同的时间可以写入不同的值,比如上文提到的Espresso
  • 归档存储。即使源数据库中包含不同schema,同步到数据仓库后,可以转换成最新schema

REST和RPC

Web服务

有两种流行的Web服务方法:REST和SOAP

  • Rest
    • REST不是一种协议,而是一个基于HTTP原则的设计哲学
    • 使用URL来标识资源,使用HTTP进行缓存控制、安全、身份验证等
  • SOAP
    • SOAP是一种用于网络请求的基于XML的协议
    • 使用WSDL来描述API,支持代码生成
  • The main difference between SOAP and REST is the degree of coupling between client and server implementations

A SOAP client works like a custom desktop application, tightly coupled to the server. There’s a rigid contract between client and server, and everything is expected to break if either side changes anything. You need constant updates following any change, but it’s easier to ascertain if the contract is being followed.

A REST client is more like a browser. It’s a generic client that knows how to use a protocol and standardized methods, and an application has to fit inside that.

https://stackoverflow.com/a/19884975

RPC的问题

RPC的思想,就是向远程的服务端发起请求,看起来和本地调用函数或方法相同。但是网络调用和本地调用仍然存在着某些不同:

  • 本地函数调用的结果是可预知的,取决于输入参数,而网络调用由于存在超时等原因,结果不可预知
  • 本地函数调用要么返回结果,要么异常,或者死循环不返回。网络请求有可能超时导致没有返回
  • 如果因为超时进行重试,需要引入幂等机制
  • 本地调用可以引用其他的对象,对于网络请求,参数需要序列化后传输过去

虽然会有这样的问题,但是RPC使用还是越来越广泛,比如Avro RPC,gRPC等。有些实现会通过future来封装可能失败的异步操作。

消息传递数据流

通过消息传递数据,相比于直接RPC调用,有以下几点好处:

  • 充当消息的缓冲区,避免接收方不可用或者过载,而无法及时处理消息
  • 自动重试,保证消息不会丢失
  • 避免发送方知晓接收方的地址
  • 消息可以发送给多个接收方
  • 发送方和接收方的逻辑分离

Message Broker

比较流行的有RabbitMQ、ActiveMQ、Apache Kafka。

一个进程将消息发送到指定的队列或者主题,消息代码负责将消息传递给订阅这个主题的多个消费者。一个主题上可以有多个生产者和消费者。

一个主题只允许消息的单向流动,消费者可以将消息发布到另一个主题,或者发送给原始消息生产者使用的回复队列。

分布式的Actor框架

在Actor模型中,所有的逻辑都封装在角色中,每个角色通过发送和接受异步消息,和其他角色做通信,但是消息传递是不保证的。

这些角色不管是在单个节点还是多个节点,消息传递机制是相同的,因此可以很方便地扩展。

发表评论