《数据密集型应用系统设计》笔记-1-基础

文章目录

    • 总览
    • 数据系统基础原则
    • 第1章 可靠、可扩展、可维护的应用系统
      • 可靠性`Reliability`
      • 可扩展性`Scalability`
      • 可维护性`Maintainability`
    • 第2章 数据模型与查询语言
      • 关系模型与文档模型
      • 关系/文档数据库比较
      • 数据查询语言
      • 图状数据模型
    • 第3章 数据存储与检索
      • 数据库核心:数据结构
        • 日志式
          • 其他索引(SSTables/LSM-Tree)
            • SSTable
            • LSM-Tree
            • 性能优化
        • B-trees
        • 对比B-tree/LSM-Tree
        • 其他索引结构
          • 在索引中存储值
          • 多列索引
          • 全文索引/模糊索引
          • 在内存中保存所有内容
      • 事务处理与分析处理
        • 数据仓库
          • OLTP数据库与数据仓库差异
        • 星型与雪花型分析模式
        • 列式存储
        • 聚合:数据立方体与物化视图
    • 第4章 数据编码与演化
      • 数据编码格式
        • 使用语言特定格式
        • 使用JSON/XML/二进制变体
        • 使用二进制编码
      • 数据流模式
        • 基于消息传递的数据流
          • 分布式Actor框架

总览

《数据密集型应用系统设计》非常好的一本书。全书分为三部分:

  1. 数据系统的一般原则
  2. 单机数据系统转向分布式系统
  3. 派生数据的系统

这里是第一部分。

数据系统基础原则

  • 可靠性
    
    • 容忍软硬件失效,人为错误
  • 可扩展性
    
    • 评测负载与性能,延迟百分位数,吞吐量
  • 可维护性
    
    • 可运维,简单与可演化性

第1章 可靠、可扩展、可维护的应用系统

问题来源:
当今应用都属于数据密集型(data-intensive),而不是计算密集型(compute-intensive)
数据应用系统常用模块:

  • 数据库
  • 高速缓存
  • 索引
  • 流式处理:持续发送消息至另一个进程
  • 批处理:定期处理大量的累积数据

本书将这个些都归类于:数据系统(data system)
Redis既可以用户数据存储,也适用于消息队列
ApacheKafka可作为消息队列,同时也具备持久化存储保证。
缓存层Memcached
全文索引服务器Elasticsearch/Solr

可靠性Reliability

简单说:即使发生某些错误,系统仍然可以正常工作。
硬件故障
添加冗余来减少系统故障率。云平台(AmazonWebServices,AWS)强调总体灵活性与弹性,不是单台机器的可靠性,所以云服务器虚拟机实例的可靠性相对较差。
软件错误
没有什么好的解决办法。
人为错误
假定人是不可靠的来设计系统.设计接口原则:使做正确的事情很简单,使做错误的事情很难。

可扩展性Scalability

描述系统应对负载增加能力的术语。
描述负载
有一个twitter的例子很好。负载多是由系统的关键业务参数决定。
描述性能
批处理系统如Hadoop,通常关心吞吐量throughout。在线系统更看重响应时间responsetime
采用百分位数(percentiles)描述响应时间。如中位数是50ms,则说明有50%的请求响应时间大于50ms。通常使用p95,p99,p999.亚马逊采用p999.描述、定义服务质量目标(ServiceLevelObjectives,SLO),服务质量协议(ServiceLevelAgreements,SLA)。
应对负载
垂直扩展:升级到更强大的机器
水平扩展:负载分布到多个更小的机器,无共享体系结构。

可维护性Maintainability

许多人不愿意维护遗留系统。

第2章 数据模型与查询语言

问题来源
数据模型可能是开发软件最重要的部分,它决定了软件的编写方式,包含业务逻辑知识。应用程序通常通过一层一层叠加数据模型来构建。每一层面临的关键问题是:如何将其用下一层来表示。
构建每一层的基本思想:每层都通过提供一个简洁的数据模型来隐藏复杂性。
这一章的内容就是数据存储和查询的通用数据模型。关系模型、文档模型、图状模型。还有数据查询语言。

关系模型与文档模型

NoSQL:最初是为了吸引眼球创造的。现意为"不仅仅是SQL".采用NoSql的驱动因素:

  • 比关系数据库更好的扩展性需求,支持超大数据集/超高写入吞吐量
  • 免费/开源
  • 关系模型不能很好支持的一些特定的查询操作
  • 关系模型具有一些限制

混合持久化:结合关系数据库和各种非关系数据库一起使用。

对象-关系不匹配
是指业务代码与数据库模型之间可能存在的不匹配问题。
ActiveRecord,Hibernate这样的对象-关系映射(ORM)框架减少了转换层的样板代码,但不能消除不匹配的问题。
传统SQL模型是规范化表示关系数据。之后SQL标准增加了对结构化数据/XML数据的支持。将多值数据存储在单行内,并支持在这些文档中查询和索引。例如MySQL支持JSON数据类型。
JSON的优势是JSON模型是一种“没有模式”的结构类型,能够减少阻抗失配的问题。
使用id而不是原始字符串
有很多好处,例如城市名字变更,只用更改id上值。id对人类没有任何直接意义,所以永远不需要改变。任何对人类有意义的东西都可能在将来某个时刻发生变更。
文档模型处理多对一的数据关系很弱

模型演化

  1. 层次模型。类似JSON模型。不能支持多对一,不支持联结查询。
  2. 网络模型:网络模型CODASYL模型,是层次模型的推广。层次模型一个记录只能有一个父节点,层次模型一个记录可以有多个父节点。层次/网络模型访问时都需要“遍历”。
  3. 关系模型:关系模型同网络模型有过竞争。关系模型定义所有的数据格式。关系表只是行的集合。访问记录时是有数据库自行选择“访问路径”(由查询优化器自动生成)。

文档数据库时某种方式的层次模型,但并未遵循CODASYL标准。

关系/文档数据库比较

  1. 应用代码:如果应用数据具有类似文档结构(一对多,树结构)。关系模型倾向某种数据分解,模式笨重。但文档模型也有局限性:不能直接饮用文档中的嵌套项。文档模型处理多对一的数据关系很弱。
  2. 文档模型的模式灵活性:文档模型类/关系模型 类似于编程语言中的动态/静态类型检查。文档模型是读时模式(数据结构是隐式的,只有在读取以后才解释),关系数据库传统的方式是写时模式(模式时显式的,写入/读取时都必须遵循预定的结构)。文档模型可以认为没有数据结构模式,因此改变模式非常方便。关系模式更改更复杂。大多数数据库能够在几毫秒内执行alter,但MySQL不行,MySQL会复制整张表(有辅助工具解决这个限制)
  3. 数据的存储局部性(data locality:数据视为整体存储):文档数据通常存储为JSON/XML/变体(MongoDB的BSON)连续字符串。如果频繁读取整个文档,则文档型数据库非常适用。文档数据库更新记录时,只有修改量不改变原文档大小时,才能采用原地覆盖更新,这些说明文档数据库不适用频繁更新记录的场景。
  4. 现在文档/关系数据库正在互相融合对方的功能点。如MySQL支持JSON数据。

Bigtable数据模型:用于Cassandra/HBase

数据查询语言

SQL是一种声明式的查询语言。相对的是命令式语言。声明式查询语言只需要指定所需的数据模式,而命令式语言要指定哪些操作(过程),类似于编程语言。声明式查询语言API比命令式简单,数据库引擎可自行优化查询性能。例如HTML当中指定样式,使用Style是一种声明式的,而DOM对象则是命令式的。
MapReduce查询
MapReduce是一种编程模型,用于在许多机器上批处理海量数据。一些NoSQL存储系统(MongoDB,CouchDB)支持有限的MapReduce方式在大量文档上执行只读查询。它介于声明式和命令式之间。它基于两个操作:map,reduce。map用于收集/过滤 数据集,reduce执行在数据集上的各种操作。所以通常需要手动实现map/reduce函数来表达查询需求。

图状数据模型

图状数据模型非常适合处理 多对多关系。文档数据库几乎不能处理多对多关系,关系数据库能处理简单的多对多关系。
图的构成:顶点 , 边。顶点时实体,边是实体之间的关系。例如社交网络中顶点是人,边则表示人之间的关系。
属性图:Property Graph,Neo4j/Titan/InfiniteGraph.这种图的顶点上具有属性字段,边除了有进出顶点外也有属性字段。查询时通常需要选定一个入口顶点。
三元存储模型:TripleStore,Datomic,AllegroGraph。三元存储中,所有信息都以非常简单的三部分形式存储:主谓宾。三元组(Jim,like,banana),主体Jim,谓语like,客体banana.主体相当于顶点,而客体可以是另外一个顶点,此时三元组表示一个边。
图的查询语言:声明式Cypher/SPARQL/Datalog,命令式Germlin,图处理框架Pergel

图数据库不是与前述"网络模型CODASYL模型"具有很大的不同。

第3章 数据存储与检索

这一章的内容是数据库的存储与检索。

数据库核心:数据结构

日志式

日志(log)是一个仅支持追加式更新的数据文件。日志结构写入非常快,只需要追加写在文件末尾,但是直接查询较慢,所以需要新的数据结构:索引。索引式基于原始数据派生而来的额外数据结构,作为路标,帮助定位数据记录。但是索引显然会影响写入性能,因为每次写入时都需要更新索引文件。
哈希索引
哈希索引的原理是将记录的key映射成数据文件中特定的字节偏移量。这样读取记录就只需要o(1)。索引文件通常是较小的,所以可将索引文件加载到内存中。
合并压缩
为防止用尽磁盘,可规定每一个数据文件的大小,达到限制后,新纪录写入新的文件(段文件)中。旧的段是不会改变的,所以可以对旧段进行合并,只保留每个记录最新的值。这就是日志式结构数据库的合并压缩的过程。
删除记录
删除记录采用墓碑机制,即追加一个删除记录,在合并时发现key的墓碑时,直接删除key的所有记录。
并发控制
可以使用一个写线程。
好处
日志式为什么不采用原地更新文件呢?

  • 日志式文件是“顺序写”,这比随机写入快得多(考虑旋转式的硬盘寻址过程),即使是固态硬盘也是如此。
  • 旧的段文件不可变,新的段文件是追加式的,则并发和崩溃恢复要简单得多。
  • 可以避免文件碎片化的问题,原地更新就会导致文件碎片化的问题。

缺陷

  • 哈希索引表需要全部放入内存,否则需要遍历磁盘。当哈希表扩容时代价昂贵。
  • 区间查询效率低。只能采用逐个查找的方式查询每一个记录
其他索引(SSTables/LSM-Tree)

哈希索引在某些方面有缺陷。现在介绍其他索引结构。

SSTable

在存储段文件的时候,要求key-value按key的顺序排序。这种格式为排序字符串表(SSTable)。
优点:

  • 合并段更加高效.即时文件大于可用内存仍可以合并。例如有三个段文件,采用三个指针可以高效地合并三个段文件
  • 索引文件更小,节省内存。由于段文件内的记录是按key排序的。索引文件可以是稀疏的,例如只记录段文件的头中尾三个key。
  • 区间搜索时高效。

构建维护SSTables
磁盘上维护排序结构(B-trees).内存排序有树状数据结构(红黑树/AVL树)。
于是构建SSTables的过程为:

  1. 在内存中维护排序表,当内存表大小到达某个阈值时将其作为SSTable文件写入磁盘。
  2. 读请求:先查询内存表-磁盘段文件。
  3. 存在内存表丢失的问题:保留单独的写入日志,用于崩溃时恢复。

SSTables实例:LevelDB,RocksDB.用于嵌入到其他应用程序的key-value存储引擎库。类似的存储引擎海被用于Cassandra/HBase。

LSM-Tree

LSM-Tree:Log-Structured Merge-Tree 日志结构的合并树
基于合并和压缩排序文件原理的存储引擎通常都被称为LSM存储引擎。
Lucene是Elasticsearch/Solr等全文搜索系统所使用的索引引擎,采用的是类似的方法保存词典。

性能优化

存储引擎使用额外的布隆过滤器优化“查找不存在的记录”
布隆过滤器:内存高效的数据结构,用于近似计算集合的内容,如果数据库中不存在某个key,它能够很快得出结果,从而节省大量不必要的磁盘读取。

B-trees

B-tree是目前使用最广泛的索引结构。
B-tree将数据库分解成固定大小的块/页,一般大小为4kb,页是内部读/写最小单元。每个页内可以使用地址或位置表识另外一个页,类似指针,指向的是磁盘地址。
查找
从根(某一页)开始,页内的内容是按键排序的,如果页内某位置是引用,则引用两边的key说明了引用的key范围。就这样不断缩小查询范围。查询效率很高。一个页内包含的子页引用数量称为分支因子(一般为几百)。树深度是log(n),n是记录条数。
更新
首先搜索包含该key的叶子页,更改该页的值,写回磁盘。
写入
搜索包含其key范围的页,添加到该页。如果容量不足,则该页分裂成2个半满页。并且父页页同时更新,如果父页也容量不足,重复分裂过程。
分支因子为500的4KB页的四级树存储高达256TB

B-tree写操作是使用新数据覆盖磁盘上的旧页,而日志结构索引仅追加更新文件,删除过时文件,但不修改文件。磁盘上执行这一过程是:磁头移动到正确的位置,用新的数据覆盖相应的扇区。SSD执行:SSD随机读取更快,但SSD执行重写是 擦除并重写非常大的存储芯片块(比本身要更新的块更大),也就是SSD的操作粒度比磁盘大。(想象一下用create_time/update_time作索引的区别)

由于Btree插入操作可能发生分裂,即需要覆盖多个不同的页,这就加大了崩溃以后恢复的难度。常见的机制是:预写日志write-ahead log,WAL,重做日志。WAL是一个仅支持追加修改的文件。btree必须先更新WAL。
原地更新的另一个复杂因素:如果多线程同时访问btree,需要并发控制,否则可能看到不一致的状态。而日志结构不涉及更新文件操作,无此问题。

优化

  • 一些数据库(如LMDB)不使用 覆盖页 和 维护WAL来进行崩溃恢复,使用写时复制方案。修改的页写入新的位置,父页指向新位置。“快照隔离”。
  • 保存键的缩略信息,而不是完整的键。只要key能够区分出范围即可。节省空间,有时称为B+树
  • B-tree会尝试(尽力不是保证)将相邻范围的页按顺序保存在磁盘上。
  • 添加额外的指针到页中,这样扫描时不用返回父页
  • b-tree变体如分形树,借鉴了日志结构的想法来减少磁盘寻道。

对比B-tree/LSM-Tree

简单说(都是相对而言且只考虑一般情况):B-Tree读取快,写入慢。LSM-Tree写入快,读取慢。
LSM-Tree优点
B-Tree索引必须至少写两次数据:一次WAL,一次写入页本身(还可能页分裂)。即使只更改一个字节,也要重写整个页。
写放大:由于一次数据库写入请求导致多次磁盘写。SSD只能承受有限次的擦除覆盖。
LSM-Tree写吞吐量高,有时有较低写放大。磁盘对于顺序写要比随机写快得多。
LSM-Tree更好地支持压缩,磁盘文件比B-tree小很多。B-tree有碎片化的问题。
许多SSD内部使用日志结构化算法将随机写入转换为底层存储芯片上的顺序写入(容易想见是把原数据设为墓碑,然后顺序写入)。
LSM-Tree缺点
压缩过程昂贵,容易发生读写请求等待(极有可能是发生在读取大量段文件时IO资源竞争)。B-tree相应延迟更具确定性。
如果压缩未仔细配置,而写入量很大,此时可能会产生大量未压缩合并的段文件,容易造成磁盘空间不足。比如一条记录,由于程序bug无限写入,LSM-Tree会快速扩大,而B-tree只占用一个位置。
对于事务语义:B-tree容易实现高效的事务隔离(可以使用范围锁)。

其他索引结构

在索引中存储值

LSM-Tree/B-tree都是key-value索引(value就是实际行数据/文档/顶点)。
索引中的key是查询搜索的对象,而value则可以是实际数据或者 其他地方存储的引用(堆文件)。当存在多个二级索引时,堆文件可以避免复制(二级索引的value就是堆文件的地址)。只更新值时,堆文件的方法非常高效。
但是堆文件多了一次磁盘IO,所以有了索引分类。

  • 聚集索引:索引行直接存储在索引中(不需要二次读取),MySql InnoDB表的主键始终是聚集索引,二级索引引用主键(而不是堆文件)。SQL Server可以为每个表指定一个聚集索引。
  • 非聚集索引:索引中存储数据的引用
  • 覆盖索引(包含列的索引):折中方案,只保存一些表的列值,通过索引即可回答某些简单的查询(“索引覆盖了查询”)
  • 聚集索引/覆盖索引增加了写入开销和额外的存储空间,但可以加快读取速度。
多列索引

问题:要查询2019-05-22日"充值"订单。
单列索引:先按日期索引缩小范围,再按订单类型索引。这里面日期、类型就属于单列索引。
级联索引:最常见的多列索引。将日期+类型作为索引,称级联索引。一列追加到另一列…缺点是顺序是完全指定的,不能从中间某列开始查询
多维索引:将多列数据合并成一个多维向量索引。例如经纬度场景下空间索引(R树)。多维索引可以同时缩小查询范围,而不是按索引一个一个缩小。

全文索引/模糊索引

全文索引/模糊索引是全文搜索引擎例如Lucene支持的一种搜索技术,作用是可以搜索相近的key。Lucene基本原理是利用key字符串序列的有限状态自动机,搜索一定距离的所有记录。

在内存中保存所有内容

前面的数据结构都是为了适应磁盘限制。内存数据库是新东西。
VoltDB,MemSQL,Oracle TimesTen是具有关系模型的内存数据库。RAMCLOUD开源具有持久性key-value存储。Redis/Couchbase通过异步写入磁盘提供较弱的持久性。
内存数据库的性能
内存数据库的性能优势并不是因为它们不需要从磁盘读取(因为基于磁盘的存储引擎也会充分利用内存),而是因为它们避免使用写磁盘的格式对内存数据结构编码的开销。
内存数据库还可以提供基于磁盘索引难以实现的某些数据模型。例如:redis的优先级队列/集合。
最新发展是将最少使用的数据写入磁盘,将来访问时再写回内存。这样可以支持超过内存大小的数据量。

事务处理与分析处理

事务:是指一个逻辑单元的一组读写操作。事务不一定具有ACID属性,而是事务通常需要ACID属性。
OLTP:online transaction processing ,在线事务处理。通常是与用户交互有关的业务逻辑。
OATP:online analytic procsssing 这是与数据分析相关的逻辑。OATP数据量可能比OLTP大3个数量级(GB->TB)。
数据库通常可以同时用于OLTP和OATP,也可能单独建立数据库用于OATP,这是叫数据仓库。

数据仓库

ETL:Extract-Transform-Load,将数据导入数据仓库的过程 提取-转换-加载

OLTP数据库与数据仓库差异

数据仓库常见数据模型是 关系型。数据仓库通常都具有SQL查询接口,内部实现则差异很大,会针对不同的查询模式进行优化。
大量基于Hadoop的SQL项目,如:Apache Hive,Spark SQL,Cloudera Impala,Facebook Presto,Apache Tajo,Apache Drill,其中一些基于Google Dremel构建。

星型与雪花型分析模式

数据仓库的数据模型较少,常见的是“星型模式(维度建模)”:以一个事实表(fact_table,可理解为交易记录)为中心,对事实表的每一列都能联系到一个另外的表(维度),如产品id-产品表。这种模式产生出的就是一个中心,环绕多个表。像星型。

列式存储

星型模式的数据仓库中。维度表通常较小,而事实表则大得多。如何高效低存储和查询这些数据成为一个挑战,所以我们需要"列式存储"。

优势
分析问题通常只需要数据的某几列。
问题:有一个交易表,现在要分析一年内人们购买水果/糖果是否取决于一周中某几天。
如果是通常的OLTP数据库,则需要加载所有行记录文件。列式存储则不同,只需要加载日期、产品、数量三个文件。每个列文件以相同的顺序保存记录,位于同一索引位置的数据就是一条交易记录的数据。
列压缩
列式存储的另一个好处,例如产品交易记录可以有数亿条,而产品ID则可能只有数万种。有很多重复的内容就有了压缩的前提条件。
例如使用‘位图索引’:建立产品ID的位图索引,苹果:0000001000000…,香蕉:10000000000…,
其中每一位代表一行,0/1代表交易是否有该产品。虽然原始位图可能长达数亿位,但每一个位图都会有非常多的0,很容易压缩。
由于列数据压缩,可以高效地利用CPU L1缓存,以便进行一大段数据的操作,这种技术被称为矢量化处理。
列排序
列排序是按照某个key排序,显然这导致的问题是其他列也按照该key排序,所以在查询/压缩时可能只有第一列有很好的优化效果。C-Store引进了一种改进想法(Vertica采用):考虑到不同的查询从不同的排序中获益,那么在建议数据冗余时,可以以不同的排序方式存储数据。
写操作
由于列存储多是面向查询优化,所以列存储的写操作很困难。例如压缩的列,要想原地更新的方式插入一个数据,需要先解压缩列,插入数据,最后再压缩。解决方案是将列数据整体读到内存中维护,积累到足够的更新以后,批量写入新文件,这就避免了原地更新的诸多不便。

聚合:数据立方体与物化视图

聚合操作:sum,count,avg,min,max.
创建聚合操作结果的缓存的一种方式是物化视图(显然它会影响OLTP数据库的写入性能)。
物化视图可以加速某些查询操作,缺点也很明显,灵活性不高。

第4章 数据编码与演化

这一章讨论的问题是数据如何应对变化。关系型数据库通过ALTER语句改变模式,而文档型数据库存储json数据时,无模式,应对变化无需改变。
变化时通常需要考虑双向的兼容性:
向后兼容
新代码可以读取旧代码写入的数据
向前兼容
旧代码可以读取新代码写入的数据
向后兼容通常比较容易,只需要在编写新代码时注意即可。而向前兼容则比较棘手,编写旧代码时不会知道将来的变化,通常能做的事尽量考虑到数据模式的演变,让旧代码能够有一定的适应变化的能力。

数据编码格式

程序通常的使用的两种不同的数据表示形式:
1.在内存中,数据保存在对象,结构,列表,数组等数据结构中(使用指针寻址)
2.将数据写入文件/通过网络发送时,必须将其编码为某种自包含的字节序列(如JSON文档).
因此数据需要在1,2两种情形之间转化,1->2称为编码(序列化),2->1解码(解析,反序列化).请注意加密不是编码,属于不同的范畴。

使用语言特定格式

许多编程语言内置支持了序列化工具,如java.io.serializable,第三方库Java Kryo.
问题:

  • 编码和特定语言绑定,另一种语言想要访问几乎不可能。
  • 解码过程会实例化类,存在安全隐患
  • 通常都忽略了兼容问题
  • 性能效率也很糟糕
    因此使用语言内置的编码方案通常是不好的。

使用JSON/XML/二进制变体

这里方案具有一定的通用性,如JSON/XML/CSV,但也有一些问题:

  • 编码有模糊之处,XML/CSV无法区分数字和数字字符串。JSON能区分数字和数字字符串,但无法区分整数和浮点数,并且没有精度。在处理大数字时问题很大,大于 2 53 2^{53} 253在双精度浮点数中不能精确表示,在使用浮点数语言(JavaScript)不准确
  • JSON/XML支持Unicode字符(人类可读文本),但不支持二进制字符串(没有字符编码的字节序列,如如一个图片文件)。通常使用Base64将二进制数据编码为文本解决这个问题(数据大小增加了33%)。
  • XML/JSON都有可选的模式支持,功能强大但学习和实现复杂。XML模式比较常见。
  • CSV没有模式
    虽然有这些问题,但这类编码方式之所以能够流行,是因为在数据交换过程中,最重要最困难的问题是双方就数据格式达成一致。格式的美观或者高效多属于次要问题。JSON也具有非常好的(前后)兼容性,模式演变非常容易。

使用二进制编码

二进制编码较适合在组织内部使用。二进制的解析是一个问题,因此出现了一些二进制编码工具包,用以支持JSON(如MessagePack,BSON,BJSON,UBJSON,BISON,Smile)和XML(如WBXML,FastInfoset)。
这类工具包的原理都是将JSON以一定的格式(模式)编码成二进制,解析端需要使用同样的工具解析,因此限制颇多只适合在组织内部使用。
Apache Thrift/protocol Buffers是基于相同原理的两种二进制编码库。他们都需要模式来编码任意数据:在读取数据时,将按照模式规定的顺序遍历每一个字段,这意味着读取模式和写入模式必须完全一致,否则将会导致读取失败。

Apache Avro是另一种二进制编码格式,它是Hadoop的子项目。Avro也需要指定数据的模式。但Avro不是严格依赖静态模式,Avro同时支持动态模式(类似用map/json来表示对象,map/json具有高度的通用性)
Avro支持模式演变:Avro在读取时,不必要求读模式与写时模式完全一致。关键点在于Avro内部在读取时提供了模式转换,Avro会比较读时模式和写时模式,忽略两者的差异字段;按照字段名字匹配,这样字段的顺序也不要求严格一致。实现的一个关键点是Avro会将写模式直接记录在数据文件开头。
Avro对特定问题进行了优化,特别是记录了数据所用的上下文,例如:

  • 有很多记录的大文件:Avro会在文件开头记录Writer模式信息
  • 具有单独写入记录的数据库:在每次更改数据模式以后,都记录版本,这样在编码记录开始时就能判断该数据的写入模式
  • 通过网络发送:建立连接时协商数据模式版本
    这一点可以学习:在传输数据时记录版本信息。

为什么要使用二进制编码?
二进制编码相比JSON/XML及其变体的最大优势: 数据更紧凑,由于模式的存在,编码时可以省略字段名称。

数据流模式

探讨最常见的进程间数据流动方式(数据库,服务调用REST/RPC,异步消息传递)涉及的数据编码解码问题。
有两种流行的WEB服务方法:REST/SOAP.
REST/SOAP
REST不是一种协议,而是一种基于HTTP原则的设计理念,使用URL标示资源,使用Http功能进行缓存控制、身份验证等。SOAP是基于XML的协议,最常用HTTP但避免使用大多数HTTP功能,SOAP自身实现这些功能,因此SOAP协议非常复杂。
REST比较简单,可以利用工具如swagger生成代码和文档

RPC(remote procedure call远程过程调用)是一种思想,该思想试图向远程网络服务发出请求看起来与在同一进程中调用方法相同(这种抽象称为位置透明).JAVA的远程方法调用(RMI)和JavaBeans(EJB)是该思想的实现。
RPC的思想是有根本缺陷的,与本地调用有区别:

  • 本地调用行为可预测,RPC影响因素众多。RPC可能由于网络问题超时,超时时完全不知道结果,此时无论是忽略还是重试都可能导致不可预知的错误。RPC执行时间浮动可能很大。本地调用可以直接将大对象的引用传入,而RPC则要通过网络传递较大的对象。
  • RPC客服端和服务端可以用不同的语言来实现。这看起来是一个优点,但不同语言的差异,实际可能带来很多问题,如JavaScript大数字问题。用单一语言编写的单个进程不存在此问题。

这意味着RPC和本地调用是根本不同的东西,强行使之看起来像是无意义的。因此RPC的优点不是像本地调用,RPC的优点在于自定义的RPC协议比通用的JSON协议有更好的性能。
REST公共API的主流风格,而RPC框架则侧重于组织内多项服务之间的请求,通常发生在同一数据中心。

基于消息传递的数据流

可以认为异步消息传递系统是位于RPC和数据库之间的消息传递系统。它与RPC相似之处:客户端请求(消息)以低延迟传递到另一个进程。与数据库相似之处:不是通过直接网络连接发送消息,而是以消息代理为中介,中介会暂存消息。消息代理只负责传递数据,而不是像RPC在整个网络过程中还执行业务逻辑。
相比RPC,消息代理的优点:

  • 如果接收方不可用活过载,它可以充当缓冲区,提高系统可靠性
  • 消息代理可以有重试机制,从而防止消息丢失。(因为消息代理只负责传递数据,网络的影响比RPC小。试想A通过RPC执行B,任务执行失败了,A重试或者忽略都很有问题。如果是消息代理,A只需要确认将消息发送给消息代理即可。)
  • 它避免了发送方对接收方IP地址端口号及网络情况的依赖。
  • 支持将一条消息发送给多个接收方
  • 发送与接收方逻辑分离(有时候是好有时候不好,根据业务确定)

消息代理是异步处理模式,RPC是同步。

常见消息代理:RabbitMQ,ActiveMQ,HornetQ,NATS,Apache Kafka
消息代理常见的使用方式:一个业务进程向指定的队列或主题发送消息,代理确保消息被传递给队列或主题的一个(或多个)消费者(订阅者)。
消息是包含一些元数据的字节序列,因此可以使用任何编码格式,只要消费者和生产者能够协商好。

分布式Actor框架

Actor模型是用于单进程中并发的编程模型。逻辑封装在Actor中,而不是直接处理线程(及竞争、锁等问题)。Actor与Actor之间只通过消息代理通信。
现在将Actor模型扩展为分布式Actor模型,能够跨越多个节点来扩展应用程序,其实质就是将消息代理和Actor编程模型集成到单个框架中。
三种流行的Actor框架处理消息编码的方式如下:

  • Akka使用Java的内置序列化,因此不提供向前或向后兼容性。但同时它也允许自定义序列化插件
  • Orleans默认使用不支持滚动升级的自定义数据编码格式,它也允许自定义序列化插件
  • Erlang OTP很难更改记录模式。

总结:编码解码的关键问题是向后/向前兼容性和滚动升级。结论是这两点都是可以实现的。

你可能感兴趣的:(Java)