序言
此书帮助我们在飞速变化的数据处理和数据存储技术大观园中找到方向。
我们将看到一些成功数据系统的样例:许多流行应用每天都要在生产中会满足可扩展性、性能、以及可靠性的要求,而这些技术构成了这些应用的基础。
阅读本书后,我们能很好地决定哪种技术适合哪种用途,并了解如何将工具组合起来,为一个良好应用架构奠定基础。我们将获得对系统底层发生事情的敏锐直觉,这样你就有能力推理它们的行为,做出优秀的设计决策,并追踪任何可能出现的问题。
本书的目的:
- 了解如何使数据系统可扩展,例如,支持拥有数百万用户的Web或移动应用。
- 提高应用程序的可用性(最大限度地减少停机时间),保持稳定运行。
- 寻找使系统在长期运行过程易于维护的方法,即使系统规模增长,需求与技术也发生变化。
- 希望知道一些主流网站和在线服务背后发生的事情。这本书打破了各种数据库和数据处理系统的内幕,探索这些系统设计中的智慧是非常有趣的。
第一部分 数据密集型应用所赖的基本思想
第一章 可靠性,可扩展性和可维护性
现今很多应用程序都是 数据密集型(data-intensive) 的
数据密集型应用通常由标准组件构建而成,标准组件提供了很多通用的功能
例如,许多应用程序都需要:
- 存储数据,以便自己或其他应用程序之后能再次找到 (数据库(database) )
- 记住开销昂贵操作的结果,加快读取速度(缓存(cache) )
- 允许用户按关键字搜索数据,或以各种方式对数据进行过滤(搜索索引(search indexes) )
- 向其他进程发送消息,进行异步处理(流处理(stream processing) )
- 定期处理累积的大批量数据(批处理(batch processing) )
越来越多的应用程序有着各种严格而广泛的要求,单个工具不足以满足所有的数据处理和存储需求。取而代之的是,总体工作被拆分成一系列能被单个工具高效完成的任务,并通过应用代码将它们缝合起来。
此书着重讨论三个问题:
- 可靠性:
- 可扩展性
- 可维护性
1 可靠性
1.1 什么是可靠性
可靠性:即使出现问题也能继续正确工作
- 应用程序表现出用户所期望的功能。
- 允许用户犯错,允许用户以出乎意料的方式使用软件。
- 在预期的负载和数据量下,性能满足要求。
- 系统能防止未经授权的访问和滥用。
造成错误的原因叫故障(fault) ,能预料并应对故障的系统特性可称为容错(fault-tolerant) 。故障不等于失效(failure) 。
- 故障:系统的一部分状态偏离其标准
- 失效:系统作为一个整体停止向用户提供服务
1.2 故障的类型
硬件故障:一旦你拥有很多机器,这些事情总会发生
- 增加单个硬件的冗余度。例:磁盘组件RAID、服务器双路电源和热插拔CPU、备用电源等
软件错误:内部的系统性错误,这类错误难以预料
- 仔细考虑系统中的假设和交互
- 彻底的测试
- 进程隔离
- 允许进程崩溃并重启;测量
- 监控并分析生产环境中的系统行为
人为错误:运维配置错误是导致服务中断的首要原因
- 以最小化犯错机会的方式设计系统(精心设计的抽象、API和管理后台)
- 将人们最容易犯错的地方与可能导致失效的地方解耦(decouple)
- 在各个层次进行彻底的测试
- 允许从人为错误中简单快速地恢复(回滚配置变更、分批发布新代码、提供数据重算工具)
- 配置详细和明确的监控,比如性能指标和错误率(即遥测)
- 良好的管理实践与充分的培训
2 可扩展性
2.1 什么是可扩展性
可扩展性(Scalability) :用来描述系统应对负载增长能力的术语。讨论“如果系统以特定方式增长,有什么选项可以应对增长?”和“如何增加计算资源来处理额外的负载?”的问题
描述负载:负载可以用一些称为 负载参数(load parameters) 的数字来描述。参数的最佳选择取决于系统架构。
负载增加时会发生什么:系统的负载被描述好后才可以讨论负载增加时会发生什么
- 增加负载参数并保持系统资源(CPU、内存、网络带宽等)不变时,系统性能将受到什么影响?
- 增加负载参数并希望保持性能不变时,需要增加多少系统资源?
使用百分位点(percentiles)可以更好的展示典型(typical)响应时间,可以用中位数和尾部延迟(tail latencies,即高百分点位的响应时间,95%、99.9%等)来判断响应速度。尾部延迟直接影响着用户体验,因为请求响应更慢的客户数据更多所以往往更有价值。
排队延迟(queueing delay)占了尾部延迟中的很大一部分,由于需要等待先前请求完成,客户端最终看到的是缓慢的总体响应时间。
2.2 应对负载
跨多台机器分配负载也称为“无共享(shared-nothing) ”架构。运行在单台机器上的系统更加简单,但由于资金问题,密集的负载无法避免地需要横向扩展。而优秀架构需要将这两种方法务实地结合,因为使用几台强大的机器可能比使用大量的小型虚拟机更简单也更便宜。
普通的系统需要手动扩展系统资源,但当负载极难预测时,可以使用弹性(elastic)系统,当检测到负载增加时会自动增加计算资源。
跨多台机器部署无服务状态(stateless services)非常简单,但将带状态的数据系统从单节点变为分布式配置有些困难,所以应该将数据库放在单个节点上(纵向扩展)直到机器不得不用分布式。
一个良好适配应用的可扩展架构,是围绕假设(assumption)建立的:哪些操作是常见的?哪些操作是罕见的?这就是所谓负载参数。但如果这些假设是错误的,那么为扩展所做的工程投入就白费了,甚至会适得其反。所以在早期创业公司或非正式产品中,通常支持产品快速迭代的能力,要比可扩展至未来的假想负载要重要得多
3 可维护性
3.1 什么是可维护性
软件的大部分开销不在开发阶段而在持续维护阶段,包括修复漏洞、保持系统正常运行、调查失效、适配新的平台、为新的场景进行修改、偿还技术债、添加新的功能等。
由于公司和员工都不喜欢修复遗留问题,所以设计之初就应该尽量考虑尽可能减少维护期间的痛苦,从而避免自己的软件系统变成遗留系统。
三个设计原则:
- 可操控性:便于运维团队保持系统平稳运行
- 简单性:消除尽可能多的复杂度,使新的工程师也能轻松理解系统
- 可演化性:使系统能更轻松的进行更改,当需求变化时为新应用场景做适配,也称为可扩展性、可修改性、可塑性。
3.2 如何实现
可操作性
-
运维团队对于保持软件系统顺利运行至关重要,一个优秀的运营团队有以下职责:
- 监控系统运行情况,在服务状态不佳时快速恢复服务
- 跟踪问题的原因,例如系统故障或性能下降
- 及时更新软件和平台,比如安全补丁
- 了解系统间的相互作用,以便在异常变更造成损失前进行规避
- 预测未来的问题,在问题出现前解决(例如容量规划)
- 建立部署、配置、管理方面的良好实践,编写相应工具
- 执行复杂的维护任务,例如将应用程序从一个平台迁移到另一个平台
- 当配置变更时,维持系统的安全性
- 定义工作流程,使运维操作可预测,并保持生产环境稳定
- 铁打的营盘流水的兵,维持组织对系统的了解
-
更好的数据系统可以使日常任务更轻松:
- 通过良好的监控,提供对系统内部状态和运行时行为的可见性(visibility)
- 为自动化提供良好支持,将系统与标准化工具相集成
- 避免依赖单台机器(在整个系统继续不间断运行的情况下允许机器停机维护)
- 提供良好的文档和易于理解的操作模型(“如果做X,会发生Y”)
- 提供良好的默认行为,但需要时也允许管理员自由覆盖默认值
- 有条件时进行自我修复,但需要时也允许管理员手动控制系统状态
- 行为可预测,最大限度减少意外
简单性:管理复杂度
随着项目越来越大,代码往往变得非常复杂且难以理解,慢慢的变成屎山。
因为复杂度导致维护困难时,预算和时间安排通常会超支,在复杂的软件中进行变更,引入错误的风险也更大。当开发人员难以理解系统时,隐藏的假设、无意的后果和意外的交互就更容易被忽略。
复杂度(complexity)有各种可能的症状,例如:状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的Hack、需要绕开的特例等等
简化系统不一定是减少功能,也可以是消除额外复杂度。额外复杂度是由具体实现中涌现而非问题本身固有的复杂度。
消除额外复杂度最好的工具是抽象。一个好的抽象隐藏实现细节且外观简单易懂,并且能广泛用于各类的不同应用。
可演化性:拥抱变化
系统的需求通常处于常态的变化中,例如:出现意想不到的应用场景、业务优先级发生变化、用户要求新功能、新平台取代旧平台、法律或监管要求发生变化、系统增长迫使架构变化等。
简单易懂的系统通常比复杂的系统更容易修改,这与简单性和抽象性密切相关
在组织流程方面,敏捷工作模式为适应变化提供了一个框架,敏捷社区开发了对频繁变化的环境中开发软件很有帮助的技术工具和模式,例如测试驱动开发(TDD)和重构(refactoring)。这些技术可以用于小规模的代码(同一个应用中几个代码文件)中。
4 小结
一个应用必须满足各种需求才称得上有用。有一些功能需求(functional requirements) (它应该做什么,比如允许以各种方式存储,检索,搜索和处理数据)以及一些非功能性需求(nonfunctional ) (通用属性,例如安全性,可靠性,合规性,可扩展性,兼容性和可维护性)。在本章详细讨论了可靠性,可扩展性和可维护性。
第二章 数据模型与查询语言
1 关系模型与文档模型
关系型数据库源于商业数据处理,以前用于商业数据的事务处理和批处理,现在广泛用于各种软件
NoSQL的意思是Not Only SQL,诞生了许多有趣的数据库,背后的驱动因素包括:
- 需要比关系型数据库更好的可扩展性,包括非常大的数据集或非常高的写入吞吐量
- 关系模型不能很好地支持一些特殊的查询操作
- 受挫于关系模型的限制性,需要一种更具多态性和表现力的数据模型
对象关系不匹配:面向对象编程语言连接SQL数据模型时需要一个笨拙的转换层,模型之间的不连贯有时称为阻抗不匹配,而对象关系映射(ORM)框架可以减少这个转换层所需代码的数量,但不能完全隐藏两个模型之间的差异
文档数据库通常储存JSON格式的文档,且更适合用于描述一对多的关系,JSON表示比多表模式具有更好的局部性(locality),所有相关信息都在同一个地方,一个查询就足够了。
随着时间的推移,关系数据库和文档数据库似乎变得越来越相似,这是一件好事:数据模型相互补充,如果一个数据库能够处理类似文档的数据,并能够对其执行关系查询,那么应用程序就可以使用最符合其需求的功能组合。
2 数据查询语言
在声明式查询语言中,我们只需要指定所需的数据的模式以及结果必须符合的条件,即可获取数据库中想要的信息。数据库系统的查询优化器决定使用哪些索引和哪些连接方法,以及何种顺序执行
声明式查询语言比命令式API更简洁容易,并且它还隐藏了数据库引擎的实现细节,这使得数据库系统可以在无需对查询做任何更改的情况下,进行性能的提升。声明式语言往往适合并行执行。
MapReduce是一个由Google推广的编程模型,用于在多台机器上批量处理大规模的数据。一些NoSQL数据存储(包括MongoDB和CouchDB)支持有限形式的MapReduce,作为在多个文档中执行只读查询的机制。
3 图数据模型
多对多关系是不同数据模型之间具有区别性的重要特征。关系模型可以简单处理多对多关系,但是随着连接变得更加复杂,就应该将数据建模为图形
一个图由两种对象组成:顶点(vertices)(也称为节点(nodes) 或实体(entities)),和边(edges)( 也称为关系(relationships)或弧 (arcs) )
例子:
- 社交图谱:顶点是人,边指示哪些人彼此认识
- 网络图片:顶点是网页,边缘表示只想其他页面的链接
- 公路或铁路网:顶点是交叉路口,边代表之间的路线
当我们将数据建模成模型时,一些算法就可以运用在这些图上。
3.1 属性图
在属性图模型中,每个顶点(vertex) 包括:
- 唯一的标识符
- 一组 出边(outgoing edges)
- 一组 入边(ingoing edges)
- 一组属性(键值对)
每条 边(edge) 包括:
- 唯一标识符
- 边的起点/尾部顶点(tail vertex)
- 边的终点/头部顶点(head vertex)
- 描述两个顶点之间关系类型的标签
- 一组属性(键值对)
可以将图存储看作由两个关系表组成:一个存储顶点,另一个存储边。头部和尾部顶点用来存储每条边;如果你想要一组顶点的输入或输出边,你可以分别通过head_vertex或tail_vertex来查询edges表。
由于SQL中的图查询比较困难,诞生了Cypher这种属性图的声明式查询语言,避免了SQL繁琐的递归进行图查询。
3.2 三元组储存和SPARQL
三元组储存大体上与属性图模型相同,用不同的词来描述相同的想法。
在三元组储存中,所有信息都以简单的三部分表示形式存储(主语、谓语、宾语)
三元组的主语相当于图中的一个顶点。而宾语是下面两者之一:
- 原始数据类型中的值,例如字符串或数字。在这种情况下,三元组的谓语和宾语相当于主语顶点上的属性的键和值。例如,
(lucy, age, 33)就像属性{“age”:33}的顶点lucy。 - 图中的另一个顶点。在这种情况下,谓语是图中的一条边,主语是其尾部顶点,而宾语是其头部顶点。例如,在
(lucy, marriedTo, alain)中主语和宾语lucy和alain都是顶点,并且谓语marriedTo是连接他们的边的标签。
SPARQL是一种用于三元组储存的面向RDF数据模型的查询语言,与Cypher看起来很相似。
4 小结
在历史上,数据最开始被表示为一棵大树(层次数据模型),但是这不利于表示多对多的关系,所以发明了关系模型来解决这个问题。最近,开发人员发现一些应用程序也不适合采用关系模型。新的非关系型“NoSQL”数据存储在两个主要方向上存在分歧:
- 文档数据库的应用场景是:数据通常是自我包含的,而且文档之间的关系非常稀少。
- 图形数据库用于相反的场景:任意事物都可能与任何事物相关联。
这三种模型(文档,关系和图形)在今天都被广泛使用,并且在各自的领域都发挥很好。一个模型可以用另一个模型来模拟 — 例如,图数据可以在关系数据库中表示 — 但结果往往是糟糕的。这就是为什么我们有着针对不同目的的不同系统,而不是一个单一的万能解决方案。
第三章 储存与检索
1 驱动数据库的数据结构
一个最简单的数据库只需要一个get和一个set,用于读取或写入键值对。
set对于简单的场景性能很高,因为只是普通的在尾部追加写入。因此许多数据库使用的日志也是仅追加文件,同时实现了回收磁盘空间。
但是当数据量变大时,普通的get性能很低,数据库使用索引进行高效查找。索引是从主数据衍生的附加(additional)结构,删除索引只会影响查询性能。维护索引会导致写入时的开销,因为索引使得数据库不是简单的追加写入。
1.1 索引的类型
(简单介绍,具体结构自行了解 )
哈希索引:将key和位置的哈希映射放在内存中以实现快速索引
SSTables和LSM树:排序字符串表,键值对按照键排序,且每个键只在每个合并的段文件中出现一次。LSM树在内存中组织这些数据,保持Key在内存中有序
B - Tree:B树页保持了按键排序的键值对,同时B树将数据库分解成固定大小的块或页面,并且一次只能读入一个页,这样对硬件更友好。同时每个页面都可以使用地址或位置来标识,这允许一个页面可以在磁盘中引用另一个页面。B树通常会有重做日志(redo log)
其他索引结构:
- 将值存储在索引中
- 多列索引
- 全文搜索和模糊索引
- 在内存中存储一切
2 事务处理还是分析?
业务数据处理的早期事务不一定具有ACID属性,只是进行低延迟的读写而非批处理(批处理是定期运行的,例如一天一次)
随着需求的不断上升,诞生了OLTP用于与数据库交互,OLAP用于分析数据。
| 属性 | 在线事务处理 OLTP | 在线分析处理 OLAP |
|---|---|---|
| 主要读取模式 | 查询少量记录,按键读取 | 在大批量记录上聚合 |
| 主要写入模式 | 随机访问,写入要求低延时 | 批量导入(ETL),事件流 |
| 主要用户 | 终端用户,通过Web应用 | 内部数据分析师,决策支持 |
| 处理的数据 | 数据的最新状态(当前时间点) | 随时间推移的历史事件 |
| 数据集尺寸 | GB ~ TB | TB ~ PB |
OLTP系统通常面向用户,为了处理大量请求,程序通常只访问每个查询中的少部分记录。程序使用某种键来请求记录,存储引擎使用索引来查找所请求的键的数据。磁盘寻道时间往往是其瓶颈。
OLAP主要由业务分析人员使用,它的查询量少但是每个查询的开销都高昂,需要在短时间内扫描数百万条记录。磁盘带宽往往是瓶颈,列式存储是这种工作负载越来越流行的解决方案。
2.1 数据仓库
由于企业规模扩大,所需要的OLTP变多且要求高可用与低延迟,同时分析人员运行OLAP会造成巨大开销而妨碍事务性能,所以渐渐引入了数据仓库。
数据仓库是一个独立的数据库,分析人员可以查询他们想要的内容同时不影响OLTP操作。数据仓库可以针对分析访问模式进行优化,索引算法在OLTP上能很好的工作,但回答分析查询不是很好。
小型企业不需要数据仓库,少量的数据可以在传统SQL数据库中查询。
2.2 OLTP数据库与数据仓库之间的分歧
数据仓库的数据模型通常是关系型的,因为SQL很适合分析查询。一个数据仓库与一个关系OLTP数据库看起来相似是因为他们都有SQL查询接口,然而他们的内部完全不同,它们针对非常不同的查询模式进行了优化。现在许多数据库供应商都将重点放在支持事务处理或分析工作负载上,而不是两者都支持。
例如Microsoft SQL Server和SAP HANA,支持在同一产品中进行事务处理和数据仓库。但是,它们正在日益成为两个独立的存储和查询引擎,这些引擎正好可以通过一个通用的SQL接口访问。
2.3 分析的模式:星型模式和雪花模式
星型模式即当关系可视化时,事实表在中间由维表包围,这些表的连接就像星星的光芒。事实被视为单独的事件(例如交易记录表),事实表的列中有很多对其他表(称为维表)的外键引用,这样的表被称为星型模式。
雪花模式是星星模式的变体,维表被进一步分解,即维表中也有许多外键引用。雪花模式比星形模式更规范化,但是星形模式通常是首选,因为分析师使用它更简单
3 列存储
在某些特定的情况下,只需要获取库表几百列中的几列,这时典型的数据仓库查询需要将每一行读入内存而带来不必要的开销。
面向列存储不将所有来自一行的值存在一起,而是将每一列的所有值存储在一起,从而节省上述情况时的工作时间。
3.1 列压缩
面向列的存储通常很适合压缩,通过压缩数据可以进一步降低对磁盘吞吐量的需求。
3.2 内存带宽和向量处理
读取大量数据时,从磁盘获取数据到内存的带宽是一个巨大的瓶颈。
分析数据库的开发人员需要有效利用主存储器带宽到CPU缓存中的带宽,避免CPU指令处理流水线中的分支错误预测和泡沫
面向列的存储布局可以有效利用CPU周期,比如查询引擎可以将大量压缩的列数据放在CPU的L1缓存中,然后再紧密的循环中循环。
3.3 列存储中的排序顺序
在列存储中,存储行的顺序不一定重要,且每列独自排序是没有意义的。
排序顺序的好处是可以帮助压缩列,当一个值连续重复多次时可以进行长度编码的压缩。
4 小结
在本章中,我们试图深入了解数据库如何处理存储和检索。将数据存储在数据库中会发生什么,以及稍后再次查询数据时数据库会做什么?
在高层次上,我们看到存储引擎分为两大类:优化 事务处理(OLTP) 或 在线分析(OLAP) 。
在OLTP方面,我们能看到两派主流的存储引擎:
日志结构学派
只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 Bitcask,SSTables,LSM树,LevelDB,Cassandra,HBase,Lucene等都属于这个类别。
就地更新学派
将磁盘视为一组可以覆写的固定大小的页面。 B树是这种哲学的典范,用在所有主要的关系数据库中和许多非关系型数据库。
第四章 编码与演化
应用程序更改时可能会改变数据库,此时使用滚动升级(阶段升级)时新旧数据格式会在系统中同时共处,系统想要顺利运行就需要保证双向兼容性
向前兼容:旧代码可以读取新数据
向后兼容:新代码可以读取旧数据
向前兼容比较棘手,因为需要忽略掉新数据格式中新增的那部分。
1 编码数据的格式
若要将数据写入文件或者由网络发送,则必须将其编码(Encoding)为某种子包含的字节序列(例如JSON),而从字节序列到内存中表示的过程称为解码(Decoding)
1.1 语言特定的格式
许多编程语言都内建了将内存对象编码为字节序列的支持,例如java的java.id.Serializable。但是这会导致这类编码与编程语言的深度绑定,并且解码过程中实例化任意类的能力会导致安全问题,效率也低,所以通常不使用语言内置的编码。
1.2 JSON,XML和二进制变体
JSON、XML和CSV是文本格式,具有人类可读性,他们作为数据交换格式非常受欢迎,但是他们都有各自的问题:
- XML太过冗长和复杂
- XML和CSB不能区分数字和字符串,JSON不能区分整数和浮点数且不能指定精度
- JSON和XML对Unicode字符串有很好的支持,但是他们不支持二进制数据。
- CSV没有任何模式,因此程序需要定义每行和每列的含义。
二进制编码更紧凑且解析更快,数据量大时会发挥更大的优势。这导致了二进制编码版本的JSON与XML的出现(BSON、BJSON、WBXML等)。
1.3 Thrift与Protocol Buffers
Apache Thirft 与 protobuf 是基于相同原理的二进制编码库。
他们都带有一个代码生成工具,生成了以各种编程语言实现模式的类,我们的代码可以调用生成的代码对模式的记录进行编码和解码。
编码的记录就是其编码字段的拼接,每个字段由其标签号码标识并用数据类型注释。如果没有设置字段值,则简单的从编码记录中省略。由此,字段标记对编码数据的含义至关重要,字段名可以随意更改因为编码数据永远不会引用字段名,但是更改字段标记会使所有现有的编码数据无效。
模式演变
模式演变需要我们保证向前&向后兼容性
当我们进行模式演变时,可以添加新的标签号码从而添加新的字段到架构中。如果旧的代码试图读取新代码写入的数据,其标签号码不能识别,所以就被忽略了。同时数据类型的注释允许解析器确定需要跳过的字节数,这就保持了向前兼容性。
只要每个字段都有一个唯一的标签号码,那么新代码就总是可以读取到旧的数据,因为标签号码仍然具有相同的含义。但是当我们将一个新添加的字段设置为必须,那么当新代码读入旧代码的数据时将读不到。因此,后来增加的每个字段必须是非必须的的或具有默认值。
删除字段时,我们只能删除一个非必填的字段,并且不能再次使用相同的标签号码。
当改变字段的数据类型时,值有可能失去精度或被扼杀(例如32变64位)
1.4 Avro
Apache Avro是另一种二进制编码格式,它也使用模式来指定正在编码的数据的结构,它有两种模式语言:Avro IDL用于人工编辑,另一种基于JSON更易于机器读取。
Avro的字节序列中没有可识别字段和数据类型,编码只是由连在一起的值组成,这使其占用空间更小。解析时,需要按照它们在架构中的顺序遍历这些字段,并告诉架构我每个字段的数据类型,这意味着阅读器与作者之间的模式不匹配会导致解码错误。
作者模式与读者模式
编码的就是作者,解码的就是读者。
作者和读者的模式不必相同,他们只需要兼容即可。例如作者和读者的模式的字段顺序不同是被允许的,因为模式解析通过字段名解析字段。作者模式中有读者模式里没有的字段会被忽略,反之则用默认值填充。
模式演变
向前兼容性意味着可以将新版本的架构作为编写器,并将旧版本的架构作为读者。相反,向后兼容意味着可以有一个作为读者的新版本的模式和作为作者的旧版本。
为了保持兼容性,我们只能添加或删除具有默认值的字段,使用新模式的阅读器读取旧模式写入的记录时,才不会缺少默认值,才能保持向后兼容性。当我们删除没有默认值的字段时,旧阅读器将无法读取新作者写的数据,而打破向前兼容性。
Avro中,null不是所有变量都能接受的默认值。当我们要使用null作为默认值时必须使用联合类型,例如 union{null,string}表示该字段可以是null或者字符串。
当我们改变数据类型时读者的旧模式还是能匹配作家的心模式,所以字段不向前兼容。同样,向联合类型添加分支也不能向前兼容。
动态生成的模式
与其他二进制方法相比,Avro方法的一个优点是架构不包含标签号码,这导致了Avro对动态生成的模式更加友善。
这使得Avro能更容易的从关系模式(关系型数据库)生成一个Avro模式(在我们之前看到的JSON表示中),并使用该模式对数据库内容进行编码,并将其全部转储到Avro对象容器文件中。
当数据库模式发生变化后,则可以从更新的数据库模式生成新的Avro模式并导出数据,数据导出过程中不需要注意模式的改变因为每次运行时都可以简单的进行模式转换。由于字段是通过名字来标识的,所以更新的作者和模式仍然可以和旧的读者模式匹配。
代码生成和动态类型的语言
Thrift和Protobuf依赖代码生成,这在静态语言中很有用,它允许将高效的内存中结构用于解码的数据,并且编写访问数据结构的程序时允许在IDE中进行类型检查和自动完成。
在动态类型的编程语言中,生成代码没有意义,因为没有编译时的类型检查器来满足。
Avro可以代码生成,同时也能在不生成代码时使用。当我们有一个嵌入了作者模式的对象容器文件时就可以哟Avro库打开它,这样就能以查看JSON文件相同的方式查看该数据。这个属性非常适用于动态类型的数据处理语言(如Apache pig)
1.5 模式的优点
尽管JSON、XML和CSV等文本数据格式非常普遍,但基于模式的二进制代码也是一个可行的选择,它有以下优点:
- 更紧凑,因为它们可以省略编码数据中的字段名称。
- 模式是一种有价值的文档形式,因为模式是解码所必需的,所以可以确定它是最新的(而手动维护的文档可能很容易偏离现实)。
- 保留模式数据库允许您在部署任何内容之前检查模式更改的向前和向后兼容性。
- 对于静态类型编程语言的用户来说,从模式生成代码的能力是有用的,因为它可以在编译时进行类型检查。
2 数据流的类型
2.1 数据库中的数据流
在数据库中,写入数据库的过程对数据进行编码,从数据库读取的过程对数据进行解码。
向后兼容性显然是必要的。而当版本滚动升级时数据库中一个值可能被新版本的代码写入,然后被旧版本读出,因此数据库一般也需要向前兼容。
归档存储
定期为数据库创建一个快照用于备份,此时即使数据库中包含不同时代的模式的数据,数据转储也通常使用最新模式进行编码。
由于数据转存是一次写入且不改变的,此时用Avro对象容器文件等格式非常合适。
2.2 服务中的数据流:REST与RPC
服务器通过网络公开API,并且客户端可以连接到服务器以向该API发出请求。服务器公开的API被称为服务。
客户端向Web服务器发出请求时GET请求下载HTML等,并向POST请求提交数据到服务器,而在移动端时通常只传递编码数据(如JSON)。尽管HTTP可能被用作传输协议,但顶层实现的API是特定于应用程序的,客户端和服务器需要就该API的细节达成一致。
服务器本身是另一个服务的客户端,这个使得大型应用程序能按照功能区域分解为较小的服务,当服务需要来自另一个服务的某些功能或数据时,就会向另一个服务发出请求,这种构建应用的方式称为面向服务的体系架构(SOA) ,即微服务架构
某些方面,服务类似于数据库,他们允许客户端的提交和查询数据,但是它做了一定程度的封装,于是服务可以对客户能做什么和不能做什么施加细粒度的限制。
微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。团队发布新版本服务时不必与其他团队协调。
Web服务
当服务使用HTTP作为底层通信协议时,可称之为Web服务。Web服务不仅在Web上使用,而且能在几个不同的环境中使用。
- 运行在用户设备上的客户端应用程序,通过HTTP发送请求。
- 一种服务向同一组织拥有的另一项服务提出请求,这些服务通常位于同一数据中心内,作为面向服务/微型架构的一部分。 (支持这种用例的软件有时被称为 中间件(middleware) )
- 一种服务通过互联网向不同组织所拥有的服务提出请求。这用于不同组织后端系统之间的数据交换。此类别包括由在线服务(如信用卡处理系统)提供的公共API,或用于共享访问用户数据的OAuth。
REST和SOAP:
- REST:REST不是一个协议,而是一个基于HTTP原则的设计哲学。强调简单的数据格式,使用RTL来标识资源,使用HTTP功能进行缓存控制,身份验证和内容类型协商。目前越来越受欢迎。根据REST原则设计的API称为RESTful。
- SOAP:SOAP是用于制作网络API请求的基于XML的协议(SOAP和SOA不一样,SOA是构建系统的一般方法)。它常用且独立于于HTTP,有很多相关标准增加了许多功能。
SOAP Web服务的API使用称为Web服务描述语言(WSDL)的基于XML的语言来描述。 WSDL支持代码生成,客户端可以使用本地类和方法调用(编码为XML消息并由框架再次解码)访问远程服务。这在静态类型编程语言中非常有用,但在动态类型编程语言中很少
RPC的问题
基于远程过程调用(RPC)思想,RPC模型试图向远程网络服务发出请求,让本地函数调用远程函数就像调用通过进程中的函数一样(这种抽象称为位置透明)。
然而,网络请求和本地函数调用其实非常不同:
- 网络问题会影响调用
- 网络请求可能超时,此时本地无法获取具体情况
- 发送的对象较大会造成网络压力
RPC的方向
新一代的RPC框架更加明确了远程请求和本地调用函数的不同。其中一些框架还提供了服务发现。
二进制编码格式的自定义RPC协议比JSON over REST性能好,但是RESTful API受支持于更多的编程语言和平台,且拥有大量可用工具。
数据编码于RPC的演化
可独立更改和部署的RPC客户端和服务器对于可演化性来讲至关重要。
RPC的演化只需要在请求上有向后兼容性,对响应有向前兼容性。
RPC经常被用于跨越组织边界的通信,所以服务的兼容性很难完成,服务的提供者无法强迫客户升级,所以需要长期保持兼容性。因此,当需要进行兼容性更改时,服务提供商通常会并排维护多个版本的服务API。
API的版本化没有一个公认的标准。常见的是在URL或HTTP Accept头中使用版本号。
2.3 消息传递中的数据流
与RPC相比,消息队列的优点:
- 如果收件人不可用或过载,可用充当缓冲区,提高系统可靠性
- 可用自动将消息重新发送到已经崩溃的进程,防止消息丢失
- 避免发件人需要知道收件人的IP地址和端口号
- 允许一条消息发给多个收件人(发布-订阅模式)
- 将发件人与收件人逻辑分离
同时,消息队列是单向的,发送者不会等待消息被传递,而只是发送它,然后忘记它。
消息代理 / 消息掮客(message broker)
掮客[qián kè]
消息代理通过在正式消息传递协议之间转换消息来使得应用、系统和服务之间能相互通信。消息代理是消息传递中间件或面向消息的中间件 (MOM) 解决方案中的软件模块。
一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上,因此可以将它们链接在一起,或者发送给原始消息的发送者使用的回复队列。
消息代理通常不会执行任何特定的数据模型 - 消息只是包含一些元数据的字节序列,因此您可以使用任何编码格式。如果编码是向后兼容的,则您可以灵活地更改发行商和消费者的独立编码,并以任意顺序进行部署。
分布式的Actor框架
Actor模型是一个通用的并发编程模型。每个Actor实例封装字节相关的状态,并且和其它Actor物理隔离。一个Actor就是一个工人,与进程或线程一样能工作或处理任务。
Actor模型用于跨越多个节点来扩展应用程序。多个Actor通过发消息进行交流但是不会产生数据竞争。
位置透明在Actor模型中比在RPC中效果更好,因为它已经假定消息可能丢失。尽管网络上的延迟可能比同一个进程中的延迟更高,但是在使用参与者模型时,本地和远程通信之间的基本不匹配是较少的。
分布式的Actor框架实质上是将消息代理和角色编程模型集成到一个框架中。但是,滚动升级时消息可能会从运行新版本的节点发送到运行旧版本的节点,所以需要担心先前向后兼容性的问题。
3 小结
编码的细节不仅影响其效率,更重要的是应用程序的体系结构和部署它们的选项。
本章讨论了几种数据编码格式及其兼容性属性,包括文本格式还有二进制格式。还讨论了数据流的几种模式,包括数据库、RPC和REST API、异步消息传递,说明了数据编码是重要的不同场景。