《数据密集型应用系统设计》 - 数据编码和演化

sjmj 《数据密集型应用系统设计》 - 数据编码和演化 前言 本章的前半部分提到的编码框架目前在GO领域如鱼得水,并且有不少成熟的产品诞生,如果是GO工作者必然会接触,如果仅仅是试图了解该领域设计的一些技术架构,这一章更多的是扫盲和拓展眼界。
本章节的后半部分讨论的RPC和SOAP,以及基于WebService服务跨语言通信服务,和RPC通信协议,但是WebService这东西现在用的人越来越少,反观微服务才是当前的主流。
虽然需要住的是虽然HTTP/2已经出来不少年头的了,但是RPC依然占有重要的比重,所以也是值得关注的,最为典型的当然是Dubbo框架。

为什么HTTP2.0都出来了,RPC还在继续发展呢?
因为HTTP2.0 刚刚发布的时候,RPC已经具备相当的发展,技术也相对成熟,另外很多项目系统架构已经完全搭建,换回HTTP2费力不讨好,本身也没有必要,因为RPC协议没有特别大的缺陷。并且拥有不错的特性 。
章节介绍 从历史的演化的角度来看,虽然过去出现过许多尝试替代HTTP协议的WEB通信框架,但是市场总是需要稳定成熟的架构,这些新兴通信协议最终都默默黯淡在历史的长河之中。
系统的演进除了数据结构和数据模型本身的演变之外,数据编码和数据之间的交互模式也在不断的进行演变,数据模式和格式改变的时候,通常需要应用程序的对应改变,而应用系统的痛点如下:
  • 新版本的部署需要滚动升级(分阶段下线节点然后有序上线),通过这样的方式可以对于大规模升级的系统可以实现不暂停升级。
  • 客户端应用程序需要依赖用户自行进行更新,或者使用强制更新手段强制升级。
这样的应用程序调整不可避免的带来关键性问题:前后兼容。
什么是前后兼容?
向后兼容:较新的代码由旧代码编写的数据。
向前兼容:比较旧的代码可以读取新编写的数据。
向后兼容不是难事,因为在原有的基础上扩展。向前兼容比较难,需要对于旧代码忽略新代码的添加。
虽然现在主流的传输结构是使用JSON,但是在这一章节将会扩展更多的数据编码格式介绍,前面两种无需过多介绍,这一章节主要介绍了后面三种针对数据编码而存在类似中间件的框架:
  • JSON
  • XML
  • Protocol Buffer
  • Thrift
  • Avro
数据编码格式
数据表现形式无非两种:
  • 内存中数据保存对象,结构体、列表、数组、哈希表和树结构等等,传统的数据结构对于CPU高效访问优化。
  • 数据写入文件通过网络发送,必须要编码为某种字节序列,但是由于一些虚拟字节比如指针的存在所以和内存的表现形式有可能不一样。
术语问题,这里的编码其实就是指的“序列化”,但是序列化在不同的结构中意义不同,所以书中用了编码解释这一概念。
语言特定格式
通常有不少的编程语言支持把内存的对象编码为字节序列码,比如经典的Java.io.Serializable,Ruby的 Marshal,Python的 picle ,还有一些第三方库比如 Kryo
但是语言的特定格式带来下面的一些问题:
  • 编码和特定语言绑定,无法完成不同编程语言互通。
  • 恢复数据的时候需要解码并且实例化对应实现类,序列化存在序列化攻击隐患,比如通过实例化异常对象的方式找到系统的漏洞攻击手段。
  • 简单快速编码在编程语言常常导致前后兼容问题。
  • JAVA的官方序列化低效被人诟病等。
JSON、XML以及二进制 二进制编码
目前系统较为主流的形式是JSON, 而过去XML也流行过一段时间,但是后来很快被更为轻便的JSON取代,JSON最早是出现在JS上的一种数据结构,后来被广泛采用在不同系统之间的通信格式,至今依然盛行。
XML和JSON的最大好处是使用字符串进行传输,并且JSON是JS内置的浏览器支持,具备很强的兼容性。
但是XML和JSON也暴露出不少问题:
  • 数字编码问题:JSON中无法区分数字和碰巧是数字的字符串,虽然JSON能识别出数字和字符串,但是无法区分数字的精度,也就是浮点数。
  • 针对浮点数问题,IEEE 754 双精度浮点数在JS上精度不佳。在推特中曾经有精度丢失的例子,2的53次方会导致一部分数据丢失而产生数据不准确问题,根本问题是使用了64位的数字去表示推文内容在JS中产生溢出!为了解决此问题推特最终使用了拆分小数位和整数位,以及使用字符串代替数字的方式表示一个数字,避开了JS语言的缺陷问题。
  • JSON和XML对于文本支持较好,可阅读性很强,BASE64编码之后可以解除数据传输丢失的风险,但是与此同时也会带来数据大小膨胀问题。
  • XML和JSON都有模式可选支持,通常情况下大部分的编程语言可以通用编解码方式,但是对于不使用这两种编码格式的则需要自己编写。
  • CSV没有模式,他只是介于二进制和文本之间的一种特殊状态,每一次数据改动都需要手动改动文件。
下面来讨论二进制编码问题。
二进制编码的优势在于数据体积小并且传输快,但是二进制真的和JSON文本的差异很大么?
我们可以看到下面的编码案例:
原始字符串内容如下,如果是传统的编码格式,下面的JSON字符串去掉空格需要80多个字节
《数据密集型应用系统设计》 - 数据编码和演化
文章图片

在书中的案例中,经过二进制编码的数据仅仅比JSON编码格式缩小了10几个字节,比如下面的编码格式,仅仅比上面的原始JSON缩短了10多个字节,是否意味着在较小文本传输的时候优化编码大小的性价比是很低的?
《数据密集型应用系统设计》 - 数据编码和演化
文章图片

经过二进制编码框架处理之后,可以精简到32个字节甚至更小,约等于压缩了50%甚至更高的内容。
二进制编码框架定位
为了解决二进制编码的性能远不如文本JSON的问题,在数据编码和模式出现了演进和深入研究。要更好地理解二进制编码框架,我们需要了解他们的定位。
模式框架的设计理解基本和TCP/IP协议面对的问题类似,在差异不同的应用系统之间如何完成统一格式通信,并且在不同应用系统升级之后能以最小的成本完成向前兼容。
为了更加透彻的了解Thirft以及一系列数据编码框架的设计定位,我们来看看Thrift的设计思想:
Thrift软件栈分层从下向上分别为:传输层(Transport Layer)、协议层(Protocol Layer)、处理层(Processor Layer)和服务层(Server Layer)。
  • 传输层(Transport Layer):传输层负责直接从网络中读取和写入数据,它定义了具体的网络传输协议;比如说TCP/IP传输等。
  • 协议层(Protocol Layer):协议层定义了数据传输格式,负责网络传输数据的序列化和反序列化;比如说JSON、XML、二进制数据等。
  • 处理层(Processor Layer):处理层是由具体的IDL(接口描述语言)生成的,封装了具体的底层网络传输和序列化方式,并委托给用户实现的Handler进行处理。
  • 服务层(Server Layer):整合上述组件,提供具体的网络线程/IO服务模型,形成最终的服务。
Thrift 和 Protocol Buffer
Apache Thrift 和 Protocol Buffer 基于相同原理二进制编码,而Protocol 最开始由谷歌开发, Thrift 最初由 Facbook 开发,后面被 Apach 引进并且成为顶级项目。(老接盘侠了)
Thrift是Facebook于2007年开发的跨语言的rpc服框架,提供多语言的编译功能,并提供多种服务器工作模式;用户通过Thrift的IDL(接口定义语言)来描述接口函数及数据类型,然后通过Thrift的编译环境生成各种语言类型的接口文件,用户可以根据自己的需要采用不同的语言开发客户端代码和服务器端代码。
两者的共同点是都需要使用模式进行编码,所谓模式就是指如果通过语法来描述数据结构,需要按照指定的规范。
另外经过模式定义之后两者都可以通过代码生成器生成相关的对象代码,支持多种编程语言,应用代码生成器生成的代码可以完成对应的编码和解码操作。
在Thirft 介绍一句话可以看到它最为基本的限制:
To generate the source from a thrift file run
有时候编码框架可能具备多种编码方式,比如Thrift 分为BinaryProtocol和 CompareProtocol。
实际上Thrift 还有DenseProtocol,但是因为只能支持 C++ 所以这里并没有算进去。
首先是传统的BinaryProtocol方式,最终发现需要 59个字节进行编码。
《数据密集型应用系统设计》 - 数据编码和演化
文章图片

与上面的编码方式类似的是对于字段的内容进行了ASCII编码,区别是在字段名称上的编码方式存在区别,字段名会使用类似Tag的字段给字段名进行分类,这些数字主要用于模式定义。
使用 CompareProtocol ,把相同信息缩减到34个字节完成表示,主要区别是字段类型和标签号打包到单个字节当中,并且用变长整数实现。
《数据密集型应用系统设计》 - 数据编码和演化
文章图片

Protocol Buffer 则只有一种编码方式,打包格式粗略看上去和 CompareProtocol 比较像,只使用了33个字节表示重复的记录。
这样的区别来自于两个模式对待重复字段的前后兼容的处理方式不太一样。
《数据密集型应用系统设计》 - 数据编码和演化
文章图片

需要注意前面设置的模式当中可以标记为 requiredoptional ,这种标记对于编码没任何影响,但是如果 required 字段没有填充数据,则会抛出运行时异常,这对于大型系统庞大数据系统检查数据格式是非常有帮助的。
字段标签和模式演化
了解完格式定义,接着便是编码格式的模式演化。
通常一条编码记录是一组编码字段的拼接,数据格式使用标签号+数据类型(字符串或者整数)并以此作为编码引用,编码引用不会直接引用字段名称,不能随意的更改字段标签,因为这样处理容易导致编码内容失效。
如果字段没有设置字段值,则编码记录中将会直接忽略
添加字段兼容
为了实现向前兼容性,字段字段名称可以随意更改,标签却不能随意更改。如果旧代码视图读取新代码的数据,如果程序视图读取新代码写入的数据,或者不能识别的标记代码,可以通过类型注释通知字段解析器跳过新增内容的解析。
而想要实现向后兼容性,因为新的标记号码总是可以被新代码阅读的,所以通常不会有太大问题。但是有一个细节是新增的字段不能是必填的,这有点类似给数据库新增必填字段,如果旧代码不进行改动则业务整个链路会崩溃,相信大家都有这样的体验。所以保持向后兼容性初始化部署需要塞入默认值或者直接是选填字段。
删除字段兼容
删除字段的前后兼容刚好相反,向前兼容通常不会有多少影响,但是向后兼容必须是删除非必填的字段,同时旧的标签号码需要永久废弃,因为使用完全不同的数据类型标签,新标签覆盖旧标签号码会导致程序出现奇怪现象。
字段标签改变
如果是字段的删减似乎问题并不会很大,使用标签在引用之间再套一层的方式可以解决这个问题。
但是如果是字段本身改变要如何处理?比如把一个32位的整数转为64位的整数,如果是新结构的代码可以通过填充 0 的方式让数值对齐,但是如果是旧代码读取到新结构的代码,显然会出现位截断的问题。
现在来看 ThriftProtocol Buffer是如何解决这个问题的。
Protocol Buffer:利用字段重复标记(repeated,表示可选之外的第三个选项),用于标记同一个字段标签总是重复的多次出现在记录当中。
通过设置可选字段为重复字段,读取旧代码的新代码可以看到多个元素的列表(前提是元素确实存在),新代码可以挑选符合的值处理。而读取新代码的字段则只允许读取列表的最后一个元素。
这种处理方案有点类似数据库的版本快照,
Thrift:处理方式是使用列表对于字段标签参数化,虽然没有灵活的多版本变化,但是列表可以进行嵌套可以有更多灵活组合。
这种方式是类似用句柄的方式,利用“中间层”专门管理参数化标签。
Avro 同样是 Apach 的另一个二进制编码,AvroHadoop 的一个子项目,同样通过模式指定编码的一种数据结构,主要的进攻方向有两条:
  • Avro IDL 人工编译。
《数据密集型应用系统设计》 - 数据编码和演化
文章图片

  • JSON 利于机器读取
《数据密集型应用系统设计》 - 数据编码和演化
文章图片

这里再一次用到之前的案例,Avro 对于同样的内容仅仅使用32个字节的编码。
《数据密集型应用系统设计》 - 数据编码和演化
文章图片

这种二进制编码并没有显著的指示字段和数据类型,只是简单的连接列值而已,字符串仅仅为长度前缀,只有整数使用了可变长度编码。
这样的灵活度不是依靠数据结构本身支撑,而是换了一种思路,对于二进制数据的读写制定一套规则,在Avro中被叫做读写模式。
写模式和读模式
  • 写模式:指的是对于任意数据可以使用已知模式的所有版本编码,比如编译到应用程序的模式。
  • 读模式:需要根据模式解码某种数据的时候,期望数据符合某种模式。
和传统的编解码不一样,Avro 读写模式之间是可以进行相互转化的。
读写模式特点
最大的特点是读写模式不需要完全一致,只需要保持兼容即可,数据被解码读取的时候,通过对比查看读写模式,同时将写模式转为读模式进行兼容,而主要的限制是读写模式的转变需要符合Avro 的规范。
此外写模式和读模式的字段顺序不一样也是没有问题的,因为模式解析会通过字段名称对于字段进行匹配,如果读模式碰到了出现在写模式不存在读模式的字段就会执行过滤,反过来如果读模式需要字段写模式没有提供会使用默认值转化。
模式演化规则
Avro的模式演化规则意味,在向前兼容中把新版本的模式作为write,把旧版本的模式设置为reader,向后兼容则是新代码实现reader,旧版本模式为write。
这样实际上就是实现了新版本的写入会被新版本看到,但是旧版本不认识的数据就会被过滤掉。
Avro为了保持兼容性,只提供了默认值字段的增删权限,比如新增带有默认值的字段,使用新模式reader读取会使用默认值(如果读模式需要字段写模式没有提供会使用默认值转化),使用旧模式write则会直接过滤,并且只在新模式中可以看见新增默认值字段。
下面是模式演化的一个案例。
《数据密集型应用系统设计》 - 数据编码和演化
文章图片

Avro 的前后兼容实质就是利write和reader这两个模式切换,利用新旧版本屏蔽的方式兼容代码。
Avro 除了这两个模式的特点之外,还有一种非常特殊的情况,对于 null 内容的处理,这和多数编程语言不同,如果 Avro 中声明 允许为null值,必须要是联合类型。
联合类型就像是下面这样的格式:
union {null, long, string}

ProtocolBuffThrift 都不太一样只有当null是联合分支的时候才允许作为默认值,此外它没有默认标签或者列表维护的方式可选(由于特殊数据格式设计导致的)。
write 模式选择问题
Avro 还存在比较疑惑的问题,如何选择 reader模式如何选择write的版本?关键在于使用的上下文。
比如有很多记录的大文件:因为Hadoop中所有的记录都使用相同编码,所以在这种上下文中只需要开头包括write模式信息即可表示。
具有单独写入记录的数据库:不同的记录需要不同的模式和不同的版本处理,处理这种情况最简单的方式是每一个记录编码的开头记录一个版本号,并且在数据库中保留一个模式版本列表。
reader模式通过从记录的“数据库”中提取write模式完成对应的操作,例如Espresso就是这样工作的。
这个名字起的有点意思,翻译过来叫做浓缩咖啡,在软件领域是安卓的一款轻量级框架,详细了解具体可以看看这个网站:Espresso 基础知识|Android 开发者|Android Developers
网络连接发送记录,在建立连接的时候建立模式建立版本,然后在生命周期当中完成工作,Avro RPC的工作原理就是如此的。
动态生成模式
动态生成模式是 Avro 的另一项特点,动态生成对于模式兼容性更好,因为不带任何的标点符号,可以快速完成不同模式之间的转化。
比如如果数据库模式转为 Avro 模式,只需要根据关系模式作为中转即可快速完成转化,同时根据write和read模式的转变快速完成被改变字段的同步工作。
这意味着 Avro的模式转化似乎是其原生内容。如果使用 Thrift 或者 Protocol Buffers,则需要额外维护一套映射规则,同时维护模式生成器要特别小心错误分配标签的问题。
因为动态生成模式是 Avro 的设计目标之一,所以它在这一块表现十分出色。
代码生成和动态类型语言
传统思维上我们认为编码框架比较常用于静态语言,对于动态类型编程语言实际上并没有太多的意义,但是Avro却走了一条特殊的路。
但是对于Avro的动态生成模式,使用固定格式框架代码反而是累赘,因为本身就可以通过动态模式完成模式转化。
Avro的动态生成模式经常和动态类型数据处理语言结合使用,可以认为此编码框架本身就具备代码生成器的功能。
总之,不能带着刻板印象看待编码框架,有时候不同的设计思路,同样的赛道上会出现特殊的产品,就好比图数据库走了以前的网络编程模型的老路,却开辟一条特殊的路径。
动态类型语言是指在运行期间才去做数据类型检查的语言, 动态类型语言的数据类型不是在编译阶段决定的,而是把类型绑定延后到了运行阶段。 主要语言:Python、Ruby、Erlang、JavaScript、swift、PHP、Perl。
模式的优点
通过上面的一系列对比讨论,我们发现模式对比JSON和XML格式相比,使用独特的框架设计以及简单易懂、可维护的特点,被广泛的编程语言支持。
实际上模式框架本身的思想并不是什么新东西,ASN.l 在 1984年首次被标准化的模式定义语言中可以看到类似的影子,ASN.I 本身也被用于SSL证书的二进制编码(DER)当中。
对比模式和XML以及JSON,它们通常具备下面的特点:
  • 数据更加紧凑,甚至可以省略数据当中的字段名。
  • 模式本身具备文档化价值 ,可维护性要强于XML和JSON。
  • 模式具备前后兼容性的检查,对于大系统的升级维护这是非常有必要的。
  • 对于静态类型编程语言的用户来说,从模式生成代码的能力是有用的,它能够在编译时进行类型检查。
数据流模式 编码模式解决了不同架构之间的数据交流问题,为了实现这一目标,它需要具备简单的同时包含自动前后兼容的特征,所以归根结底模式是解决系统变更困难的问题。
流模式则讨论另一个话题,数据流动的过程,在软件系统生态架构中数据流动无非下面几种形式:
  • 通过数据库(实际上依然可以认为是中间件)。
  • 通过异步服务调用。
  • 通过异步消息传递。
基于数据库流动
写模式对数据库编码,读模式对数据库解码。数据库通常需要保证向后兼容,否则后面的版本无法读取之前的内容。
由于并发性问题不同的进程看到的数据状态可能具备差别,意味着数据库的数值可以被新版本写入,同时要兼容旧版本继续读取,说明数据库也需要向前兼容性能。
基于数据库的流动\问题和模式类似,新增一个字段容易导致数据读取的问题,理想情况下是旧版本代码保持新版本字段的不变,哪怕完全无法解释。
首先需要注意是新旧版本转化问题,有时候在应用程序读取新对象进行解码,之后在重新编码的过程中可能会遇到未知字段丢失的问题。
《数据密集型应用系统设计》 - 数据编码和演化
文章图片

为了解决上面提到的向前兼容问题,数据往往采用的方式是把磁盘编码的所有数据填充空数值。
注意一些文档数据库本身会利用模式来完成向前兼容,比如 Linkedln 的文档数据库Espresso使用,Avro进行存储,并支持的Avro的模式过渡规则。
归档存储
所谓的归档存储指的是对于数据库存储快照,由于使用快照对于数据进行恢复,所以需要对于数据副本进行统一编码。
像Avro对象容器文件这样的对象容器文件十分合适,因为没有额外的模式字段维护,只需要利用框架本身的模式完成转化。
归档存储在本书第十章“批处理系统”有更多讨论。
基于服务数据流:REST和RPC
REST和RPC的概念
在系统应用中WEB应用是最多的,而关于WEB的传输API包括(HTTP、URL、SSL/TLS、HTML)等,这些协议在过去受到广泛认可,现在已经成为大多人同意的标准。
通常情况下HTTP可以用作传输协议 ,但是在顶层实现的API是特定于应用程序的,客户端和服务器需要就API的细节达成一致。
RPC的概念通常和微服务做比较,现代的系统设计更加倾向于细化分工和服务职责拆分,就算是简单的系统也会按照分模块的方式进行职权拆分,独立部署和快速演化是微服务的目标。
实际微服务也诞生这样的问题,不同的团队持有不同的微服务模块,这带来了API兼容以及数据编码的问题,这也是为什么编码框架和异步通信框架的诞生。
网络服务
针对WEB服务有两种流行的处理方法:REST 和 SOAP,这两个都不算是新东西。REST是基于HTTP协议的设计而改造的另一种概念 和强化,SOAP是基于XML的协议。
【《数据密集型应用系统设计》 - 数据编码和演化】REST 的概念是利用URL标识资源,通过HTTP协议本身完成缓存控制,身份验证和内容类型协商。不同的是为资源定义更为明显的标记和界限。REST原则所设计的API称为RESTful Api。
SOAP用于发送API请求,但是由于庞大复杂的多重相关标准,这几年逐渐被REST简单风格替换。SOAP WEB服务的API叫做WSDL。支持代码生成和访问远程服务,但是同样针对动态编程语言的生成效果很弱。
尽管SOAP及其各种扩展表面上是标准化的,但是不同厂商的实现之间的交互操作性往往存在一些问题,SOAP虽然依然被一些大厂商使用,但是针对小公司来说已经不再受到欢迎,而到了现在整个WebService的使用范围也在不断缩小。
需要注意这些讨论都是基于作者是外国人对于国外编程环境的探讨,到了国内则是完全不同的另一番景象。
最后,ResultFul 的API生成工具目前较为主流的是使用 Swagger,Swagger组件也是目前对外文档的一种优秀格式,虽然注解和文本描述会让接口变得“复杂”,但是确实十分好用。
远程调用RPC
在过去许多的编程语言的远程方法调用大肆宣扬,但是它们多少都存在缺陷或者一些明显的短板,比如:
  • JAVA的EJB远程方法调用仅限于JAVA;
  • 分布式组件对象模型 DCOM 适用于微软平台;
  • 请求代理体系 CORBA 缺乏前后兼容被放弃;
远程方法调用的思想从上世纪70年代就已经出现了,RPC起初看起来很方便,但这种方法在根本上有明显缺陷, 网络请求与本地函数调用的巨大差别:
  • 本地函数调用可控可维护。
  • 本地函数调用的结果基本可以预知,比如超时和进程崩溃都可以通过各种手段排查。
  • 每一次重试失败需要花费相同的时间继续重试,如果一个任务总是在将要完成的时候崩溃,不仅占用资源还容易导致系统的各种复杂情况。
  • 本地函数可以借用内存完成对象的之间的高速传递。
  • 本地和远程调用端用不同语言实现,所以中间需要进行转化,或者借助编码框架完成前后兼容。
RPC发展
个人接触微服务比较多,对于RPC了解不是很足,目前的看法是不温不火但是并没有完全消失。Thrift和Avro带有RPC支持, gRPC是使用 Protocol Buffers的RPC实现, Finagle也使用 Thrift , RestFul 使用 HTTP上 的JSON。
RPC框架还在继续发展,新一代框架更加明确RPC和本地函数调用。
  • Finagle 和 RestFul 使用 Futures 封装失败异步操作。
  • Futres 简化多项服务结果合并。
  • gRPC支持流。
此外二进制编码格式也支持自定义的RPC协议,对于一些REST和JSON的协议具有更好的性能。RESTFUL的设计风格现在看来反而有点脱裤子放屁,因为不过是包装了一层HTTP协议而已,似乎SOAP的设计才是符合RPC的定义,这个话题也经常被放上来进行讨论。
RPC 的数据编码和演化
由于是远程调用,涉及不同服务之间的通信,必然涉及到编码演进和前后兼容问题,而针对前后兼容问题,RPC出现制定了下面一些方案:
  • Thrift 、 gRPC (Protocol Buffers )和Avro RPC可以根据各自编码格式的兼容性规则处理。
  • SIAO XML 虽然是可以演化的,但是有陷阱。
  • RESTFul 使用JSON格式保持兼容性。
此外对于RESTful API ,常用的是在URL或HTTP Accept头中使用 版本号限定调用和兼容性保持。另一种选择是客户端请求的API版本存储服务器,同时提供多版本的接口管理调用功能。
异步消息
RPC 和数据库之间的异步数据消息传递,是本章的最后一个话题,和RPC调用类似,客户端的请求同样低延迟推送到另一个服务进程。消息队列通过暂存消息的方式,嫁接生成者和消费者。
和RPC相比的消息队列有下面几个特点:
  • 消息队列可以充当缓冲照顾双方的处理能力。
  • 避免发送方需要知道接收方IP和地址的问题。
  • 支持一个消息发给多个接收方。
  • 逻辑上的发送方和接收方分离。
消息队列比较显著的问题是消息传递是单向的,同时并不在意消费方是否进行回应。发送者发送之后通常会忘记它的存在。
消息队列
消息队列最早是由一些商用收费软件控制,之后才出现各种开源流行软件kafka、activeMQ、HornetQ、RabbitMQ等。
同一个主题上可以绑定多个生产者和消费者,消息队列不会强制任何数据类型,消息传递的元数据都是一些字节数据。
此外,主题通常只指定单向流,但是消息本身会发给另一个主题和可能存在的多个消费者绑定。
消息队列的另一显著优势是前后兼容很容易实现,最大灵活的调整双方即可。消息队列的内容在12章会继续进行阐述。
分布式Actor框架
Actor模型是1973年提出的一个分布式并发编程模式,在Erlang语言中得到广泛支持和应用,Erlang是啥这里读者有可能忘了,简单关联一下:JVM实现的主要编程语言。
Actor是基于单进程的并发编程模型,所有的逻辑被封装到Actor而不是现成当中,每个Actor代表客户端的一个实体,也就是可以把每一个线程等同于一个进程看待。由于是单进程的设计,不需要线程问题,每个Actor都可以自由调度。
Actor 模型的计算方式与传统面向对象编程模型(Object-Oriented Programming,OOP)类似,一个对象接收到一个方法的调用请求(类似于一个消息),从而去执行该方法。
Actor的最大特点是可以编程模型可以跨越多个节点扩展应用程序,无论发送和接收方是否在一个节点。换种说法是在不同的节点上消息被透明封装为字节序列并且通过网络传递,同时在另一端解码。
分布式Actor实际上就是把消息队列和Actor的编程模型绑定到单进程当中,可以简单看作是特殊版本的消息队列,这样有一个好处是屏蔽了复杂性,但是坏处是程序无法细粒度的控制编程模型和函数,所有的规则都被Actor牢牢控制,此外Actor的明显缺陷是具备前后兼容性的问题,因为新版本的节点可能被送到旧节点可能无法正常工作。(这和JAVA的版本一样存在问题)
下面是Actor处理消息编码的方式实际应用:
  1. 使用Akka抽象让JAVA内置序列化,可以利用Protocol Buffers完成前后兼容。
  2. Orleans 使用自定义编码格式,需要部署新版本应用程序,同样可以支持序列化插件。
  3. 在Erlang OTP 当中,但是很难对于记录模式更改。
Akka:
对并行程序的简单的高层的抽象;异步非阻塞、高性能的事件驱动的编程模型;非常轻量的事件驱动处理(1G内存可容纳270万个Actors)。

小结 本章内容量比较庞大。第一个维度是讨论了数据格式的编码问题,以此产生了内存结构转为网络或磁盘字节流的方法,第二个维度是基于数据格式编码的前后兼容问题而诞生的不同框架对比:Thirft、Protocol Buffer、Avro,其中Avro花了很多的篇幅讲述,显然是本章的重点之一。
而最后一个维度则换到了另一面,从数据流动的方式看问题,和前面维度不同的是它如果把前两个看作设计一艘好船(数据格式)能停到不同的港口(服务),而数据流则是载着这些数据以何种形式流动,数据流的正常流动是目前的核心,现代是高可用为王。
在编码的细节内部可以看到哪怕是一个字节的变动都有可能带来性能影响,同时不同的设计理念直接影响系统的部署方式。
在许多服务需要滚动升级的情况下,新版本需要依次部署到几个节点,滚动升级是在不损害旧版本正常运行下“不停机”升级系统版本的通用手段,同时有效降低部署上线的风险。
滚动升级需要考虑最大问题是数据格式的前后兼容问题,在微服务和模块更加细化的今天,这样的情况更加频繁出现,哪怕是小项目也可以实现分布式部署,这样也带来了编码框架的前后兼容影响。
至于Avro,这一章作者对它他吹特吹,但是个人并没有使用过类似的编码框架经验,关于各种体验笔记也只有个大概印象。
接着本章讨论了下面这些问题,首先是关于编码问题的讨论:
  • 特定语言只在特定的领域适用,虽然JVM的野心是统合所有的编程语言,但是显然还有漫漫长路要走。
  • JSON、XML是经典的通用兼容模式语言,但是因为广泛使用的JSON诞生于JS在数字类型上存在明显纰漏。
  • Thirft、Protocol Buffers 和 Avro 遵循二进制编码的原则,对于数据进行前后兼容和高效编码,静态编程语言对于这样的框架十分受用,得到广泛的编程语言的认可和支持。尤其是在GO的领域大放异彩。
之后是数据流的讨论,数据流目前已经非常成熟:
  • 数据库,因为存在“旧版本”数据读取的场景,通常使用特殊方式对于数据进行编解码,保证数据向前兼容读取。
  • RPC以及RESTFUL,RPC依然在蓬勃发展。
  • 消息队列,高可用和高性能的代表产物,也是现代架构设计的三大马车之一(缓存、消息队列、数据库),现代项目的三马车则是微服务、消息队列和定时任务。
  • 分布式Actor框架,大数据中重要框架,如果是接触大数据的相关人员,Actor框架显然占有重要的一席之地。
写在最后 这一章节均为理论和视野拓展,个人来看并不算是非常重要的章节(主要是没有实际接触),当然也写不出多少东西,感兴趣可以针对某一个话题深入了解。
参考资料 Actor 分布式并行计算模型: The Actor Model for Concurrent Computation - 腾讯云开发者社区-腾讯云 (tencent.com)
深入解析actor 模型(一): actor 介绍及在游戏行业应用 - 知乎 (zhihu.com)
聊聊日常开发中,如何对接WebService协议? - 掘金 (juejin.cn)

    推荐阅读